From 34e5932fbcbd6c94abd88d6d8eaf72401ef2fe31 Mon Sep 17 00:00:00 2001 From: Mina Choi Date: Mon, 7 Sep 2026 09:07:36 +0900 Subject: [PATCH] =?UTF-8?q?[fix]=20solution/frontend,site:=20=EB=9E=9C?= =?UTF-8?q?=EB=94=A9=20=EC=83=89=EC=9D=B8=20=ED=95=B4=EC=A0=9C=20+=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=ED=8A=B8=EB=A7=B5=20URL=20=EC=9D=84=20canoni?= =?UTF-8?q?cal=20=EA=B3=BC=20=EC=9D=BC=EC=B9=98=20=E2=80=94=20=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=EC=9D=98=20=EC=9C=A0=EC=9D=BC=ED=95=9C=20?= =?UTF-8?q?=EB=AC=B8=EC=9D=84=20=EC=97=B0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이 호스트에는 들어오는 링크가 없다. 크롤러가 발행 사이트를 찾는 경로는 사이트맵 하나뿐인데 그 문이 두 군데서 막혀 있었다. - 랜딩('/')에 `noindex, nofollow` 가 박혀 있었다. 관리자 화면이라고 보고 걸어 둔 것인데(7a6fee7 로 '/' 관문이 사라져 이제는 랜딩이다), nofollow 때문에 랜딩→발행 사이트로 이어지는 발견 경로까지 함께 죽어 있었다. - 사이트맵이 `/s//` 를 담는데 페이지 canonical 은 `/s/` 다 (shared/lib/slug.ts publishUrl). 서치콘솔은 제출 URL 을 전부 "대체 페이지"로 분류한다 — 색인은 되는데 제출분 0건으로 보이는, 조용히 틀리는 종류다. - frontend/index.html: robots 를 index,follow 로. description·canonical·og 추가. 호스트는 적지 않고 Vite 가 빌드 때 `%VITE_PUBLISH_HOST%` 를 치환한다 (compose 가 루트 SITE_PUBLIC_HOST 를 흘려보낸다) — 두 곳에 적으면 갈라진다 - site/scripts/prerender.ts: 사이트맵 loc 의 끝 슬래시 제거 + 오리진 루트를 첫 항목으로 남은 것: 랜딩 body 가 빈 SPA 셸이라 렌더링에 기댄다. 랜딩 프리렌더는 별건이다. 검증: tsc(solution/site) 통과 · vite build 로 %VITE_PUBLISH_HOST% 치환 확인 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_019fteiJNvAEbTnUKq8fSqoj --- solution/frontend/index.html | 21 +++++++++++++++++++-- solution/site/scripts/prerender.ts | 15 ++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/solution/frontend/index.html b/solution/frontend/index.html index f5ab125..6c3f519 100644 --- a/solution/frontend/index.html +++ b/solution/frontend/index.html @@ -3,8 +3,25 @@ - - + + + + + + + + + + + + diff --git a/solution/site/scripts/prerender.ts b/solution/site/scripts/prerender.ts index a42c424..8c6df86 100644 --- a/solution/site/scripts/prerender.ts +++ b/solution/site/scripts/prerender.ts @@ -26,6 +26,7 @@ import { renderLlmsTxt, renderRootRobotsTxt, renderSiteUrlset, + type SiteEntry, verifyGeo, verifyJsonLd, } from '@/seo'; @@ -509,21 +510,29 @@ function writeRootMachineFiles(outRoot: string, origin: string) { const sitesDir = join(outRoot, SITE_DIR); if (!existsSync(sitesDir)) return; - const entries = readdirSync(sitesDir, {withFileTypes: true}) + const entries: SiteEntry[] = readdirSync(sitesDir, {withFileTypes: true}) .filter((entry) => entry.isDirectory()) .map((entry) => ({slug: entry.name, file: join(sitesDir, entry.name, 'index.html')})) // index.html 이 없으면 발행이 끝나지 않은(또는 실패한) 디렉토리다. 사이트맵에 넣지 않는다. .filter((entry) => existsSync(entry.file)) .map((entry) => ({ - loc: joinUrl(origin, SITE_DIR, entry.slug) + '/', + // ★ 끝 슬래시를 붙이지 않는다. 페이지의 canonical 은 `/s/` 다(shared/lib/slug.ts + // publishUrl). 사이트맵이 `/s//` 로 어긋나 있던 동안 서치콘솔은 제출한 URL 을 + // 전부 "대체 페이지(적절한 표준 태그가 있음)" 로 분류했다 — 색인은 되는데 제출분은 + // 0건으로 보이는, 눈으로 원인을 못 찾는 종류다. + loc: joinUrl(origin, SITE_DIR, entry.slug), // 페이지는 그 사이트를 구울 때마다 다시 쓰인다 — 파일 mtime 이 곧 마지막 발행 시각이다. lastmod: statSync(entry.file).mtime.toISOString(), })) .sort((a, b) => a.loc.localeCompare(b.loc)); + // ★ 오리진 루트(랜딩)도 담는다. 이 호스트에 들어오는 링크가 없어 크롤러의 유일한 문이 + // 사이트맵인데, 정작 그 문을 여는 첫 페이지가 빠져 있었다. + entries.unshift({loc: origin + '/'}); + writeFileSync(join(outRoot, 'robots.txt'), renderRootRobotsTxt(origin), 'utf-8'); writeFileSync(join(outRoot, 'sitemap.xml'), renderSiteUrlset(entries), 'utf-8'); - console.log(` ✓ 루트 robots.txt · sitemap.xml (사이트 ${entries.length}개)`); + console.log(` ✓ 루트 robots.txt · sitemap.xml (랜딩 + 사이트 ${entries.length - 1}개)`); writeIndexNowKey(outRoot); }