feat(site): prerender every locale and publish structured data, llms.txt and sitemaps
Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Successful in 53s
ci / 모바일 린트·타입·Jest (push) Successful in 45s
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 21s
ci / .NET API 서버 테스트 (push) Successful in 13s
deploy-site / deploy (push) Failing after 27s
ci / 워크스페이스 빌드 검증 (push) Successful in 36s
Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Successful in 53s
ci / 모바일 린트·타입·Jest (push) Successful in 45s
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 21s
ci / .NET API 서버 테스트 (push) Successful in 13s
deploy-site / deploy (push) Failing after 27s
ci / 워크스페이스 빌드 검증 (push) Successful in 36s
Crawlers received an empty client-rendered shell (107 characters of text); most AI crawlers do not run JavaScript, so the product was invisible to them. - Build renders each locale to static HTML (/ for Korean, /en/ … /vi/) with src/entry-server.tsx + scripts/prerender.mjs; the browser hydrates the same locale. The language menu links to those pages instead of switching in place. - Per-locale head: title, description, canonical, hreflang (+ x-default /en/), Open Graph and X cards with a per-locale 1200x630 image. - JSON-LD graph: Organization, WebSite, WebPage, SoftwareApplication (KRW offers, version, release date, download URL from the canonical sources), HowTo and FAQPage. No invented ratings. - robots.txt (search, ai-input and ai-train allowed; /app, /api, invites excluded), sitemap.xml with language alternates, llms.txt and llms-full.txt generated from the same translations and canonical values. - IndexNow key and a post-deploy ping (Bing, Naver, Yandex). - A factual "at a glance" section and two FAQ answers (recognised languages, where data is stored) in all 10 languages. - Icons, apple-touch-icon and web manifest; asset base is now absolute so sub-path pages load the same bundle. Verified: 3.6k-7.7k characters of text per page, no hydration warnings, no overflow at 320/1440 in six locales, axe 0, Lighthouse mobile SEO, accessibility and best practices 100.
This commit is contained in:
parent
52e364e578
commit
a0abda7a5c
45 changed files with 852 additions and 79 deletions
40
site/scripts/prerender.mjs
Normal file
40
site/scripts/prerender.mjs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// site/scripts/prerender.mjs
|
||||
// `vite build`(브라우저용)와 `vite build --ssr`(서버용) 뒤에 실행한다.
|
||||
// 언어마다 본문이 채워진 HTML(dist/index.html, dist/<lang>/index.html)과
|
||||
// sitemap.xml, robots.txt, llms.txt, llms-full.txt 를 만든다.
|
||||
// 크롤러(검색·AI)는 JavaScript 를 실행하지 않아도 전체 내용을 읽을 수 있다.
|
||||
|
||||
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
const siteRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const dist = join(siteRoot, 'dist')
|
||||
const ssrDir = join(siteRoot, 'dist-ssr')
|
||||
|
||||
const server = await import(pathToFileURL(join(ssrDir, 'entry-server.js')).href)
|
||||
const template = readFileSync(join(dist, 'index.html'), 'utf8')
|
||||
|
||||
for (const marker of ['<!--app-head-->', '<!--app-html-->', '<html lang="ko">']) {
|
||||
if (!template.includes(marker)) throw new Error(`prerender: index.html 에 ${marker} 가 없다`)
|
||||
}
|
||||
|
||||
for (const { code } of server.LOCALES) {
|
||||
const html = template
|
||||
.replace('<html lang="ko">', `<html lang="${code}">`)
|
||||
.replace('<!--app-head-->', server.buildHead(code))
|
||||
.replace('<!--app-html-->', server.render(code))
|
||||
const out = join(dist, server.localePath(code), 'index.html')
|
||||
mkdirSync(dirname(out), { recursive: true })
|
||||
writeFileSync(out, html)
|
||||
}
|
||||
|
||||
const lastmod = new Date().toISOString().slice(0, 10)
|
||||
writeFileSync(join(dist, 'sitemap.xml'), server.buildSitemap(lastmod))
|
||||
writeFileSync(join(dist, 'robots.txt'), server.buildRobots())
|
||||
writeFileSync(join(dist, 'llms.txt'), server.buildLlms())
|
||||
writeFileSync(join(dist, 'llms-full.txt'), server.buildLlmsFull())
|
||||
writeFileSync(join(dist, 'indexnow-urls.json'), JSON.stringify(server.sitemapUrls(), null, 2))
|
||||
|
||||
rmSync(ssrDir, { recursive: true, force: true })
|
||||
console.log(`[prerender] ${server.LOCALES.length} locales + sitemap.xml, robots.txt, llms.txt, llms-full.txt`)
|
||||
Loading…
Add table
Add a link
Reference in a new issue