diff --git a/AGENTS.md b/AGENTS.md index 8d169a8..7bbb7de 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,6 +34,12 @@ → **`out/assets` 에서 파일을 지우는 코드는 `out/s/**` 의 HTML 이 참조하는 것을 먼저 뺀다** (`prerender.ts` `referencedAssets`). 보관 기간으로는 못 막는다 — 기간이 지나면 같은 일이 난다. → 목업을 다루는 작업은 `out/s/` 를 먼저 열어 **payload 가 없는 디렉토리가 무엇인지** 본다. + → ★ **목업 슬러그는 프리렌더가 굽지 않는다** (`prerender.ts` `PROTECTED_SLUGS`, 기본 + `stay,stay2,stay3` · `PRERENDER_PROTECTED_SLUGS` 로 덮어쓴다). "payload 가 없으면 안 굽는다" + 는 보호가 못 된다 — **payload 가 생기는 순간** 덮인다. 실측(2026-09-15): 누가 빌더에서 + 슬러그 `stay` 로 발행해 `payloads/stay.json` 이 생기자 프리렌더가 `/s/stay` 를 그 payload 로 + 구워 목업을 통째로 날렸다(캐치프레이즈 100개·미니 플레이어·날씨 문구·주입분 전부). + 그 payload 는 `solution/site/payloads-mockup-hold/` 로 옮긴다 — 지우면 재발행 때 또 온다. - **번들 파일명은 콘텐츠 해시다.** HTML 은 `/assets/index-DvNTmLhy.css` 를 **루트 절대경로**로 가리킨다. 경로는 프리렌더가 `dist/client/.vite/manifest.json` 에서 읽어 박는다 diff --git a/solution/site/scripts/prerender.ts b/solution/site/scripts/prerender.ts index 204c0c3..da38513 100644 --- a/solution/site/scripts/prerender.ts +++ b/solution/site/scripts/prerender.ts @@ -75,6 +75,29 @@ import {MOONLIGHT_STAY_PAYLOAD} from '@site/fixtures/moonlight-stay'; /** 사이트별 산출물이 놓이는 디렉터리. 발행 주소 `/s/` 와 같은 모양이다. */ const SITE_DIR = 's'; +/** + * 손으로 만든 시연본(목업)이 차지한 슬러그. **이 슬러그는 굽지 않는다.** + * + * ★ 왜 (2026-09-15, 실측) + * `/s/stay` 는 payload 가 없는 목업이고 그 index.html 이 **유일본**이다(AGENTS.md 함정 1). + * 그런데 누군가 빌더에서 슬러그 `stay` 로 발행하자 `payloads/stay.json` 이 생겼고, 프리렌더가 + * 기동하며 그 payload 로 같은 자리를 구워 **목업을 통째로 덮었다** — 캐치프레이즈 100개 · + * 미니 플레이어 · 날씨 문구 · 주입분이 전부 사라졌다. 파일을 백업에서 되돌려야 했다. + * "payload 가 없으면 안 굽는다" 는 목업을 지켜 주지 못한다. **payload 가 생기는 순간** 덮인다. + * + * ★ 슬러그를 막는 것이지 발행을 막는 것이 아니다. 백엔드는 발행에 성공했다고 보고, 여기서 + * 실패 보고서를 써 준다 — 사장님 화면에 사유가 뜨고, 목업은 그대로 남는다. + * + * ★ 목업을 늘리거나 걷어낼 때는 `PRERENDER_PROTECTED_SLUGS`(쉼표) 로 덮어쓴다. 값을 비우면 + * 보호가 전부 풀린다 — 목업을 정식 사이트로 넘길 때만 그렇게 한다. + */ +const PROTECTED_SLUGS = new Set( + (process.env.PRERENDER_PROTECTED_SLUGS ?? 'stay,stay2,stay3') + .split(',') + .map((slug) => slug.trim()) + .filter(Boolean), +); + const HERE = dirname(fileURLToPath(import.meta.url)); const SITE_ROOT = resolve(HERE, '..', '..'); // dist/prerender → site/ // 백엔드가 노래 파일을 떨구는 자리. payload 디렉토리와 나란히 둔다(한 디렉토리 약속). @@ -883,6 +906,15 @@ function main() { continue; } + // ★ 목업이 쓰는 슬러그는 굽지 않는다 — 구우면 손으로 만든 유일본을 덮는다(PROTECTED_SLUGS). + if (PROTECTED_SLUGS.has(entry.payload.site.slug)) { + fail( + entry, + `슬러그 '${entry.payload.site.slug}' 는 시연본이 쓰고 있어 발행할 수 없습니다. 다른 슬러그로 발행해 주세요.`, + ); + continue; + } + try { const result = prerenderSite(entry.payload, args.out, assets, referenced); const payload = result.payload;