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

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:
Yun Chan 2026-09-27 10:45:05 +09:00
parent 52e364e578
commit a0abda7a5c
45 changed files with 852 additions and 79 deletions

View file

@ -0,0 +1,23 @@
// scripts/ci/ping-indexnow.mjs
// 사이트 배포 뒤 IndexNow(Bing·네이버·Yandex 등 공유)에 바뀐 주소를 알린다. 계정이 필요 없고,
// 소유 확인은 사이트 루트의 키 파일(site/public/<key>.txt)로 한다. 주소 목록은 prerender 가 만든
// site/dist/indexnow-urls.json(sitemap 과 같은 목록)이다.
import { readdirSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
const HOST = 'd3ro.chanpaca.net'
const publicDir = join(process.cwd(), 'site', 'public')
const keyFile = readdirSync(publicDir).find((name) => /^[0-9a-f]{32}\.txt$/.test(name))
if (!keyFile) throw new Error('indexnow: site/public/<32 hex>.txt 키 파일이 없다')
const key = readFileSync(join(publicDir, keyFile), 'utf8').trim()
const urlList = JSON.parse(readFileSync(join(process.cwd(), 'site', 'dist', 'indexnow-urls.json'), 'utf8'))
const response = await fetch('https://api.indexnow.org/indexnow', {
method: 'POST',
headers: { 'content-type': 'application/json; charset=utf-8' },
body: JSON.stringify({ host: HOST, key, keyLocation: `https://${HOST}/${keyFile}`, urlList }),
})
// 200/202 = 접수. 429 등은 배포 실패로 보지 않고 경고만 남긴다.
console.log(`[indexnow] ${response.status} ${response.statusText} (${urlList.length} urls)`)
if (response.status >= 400 && response.status !== 429) process.exitCode = 1