93 lines
3.8 KiB
JavaScript
93 lines
3.8 KiB
JavaScript
// 빌드 후 실행: 라우트별 완성 HTML(<head> + 본문)을 dist 에 생성하는 사전 렌더링(SSG) 스크립트
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
import { DEFAULT_PATH } from '../src/config.js';
|
|
import { getPrerenderPaths } from './routes.js';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const root = path.resolve(__dirname, '..');
|
|
const distDir = path.join(root, 'dist');
|
|
const ssrEntry = path.join(root, 'dist-ssr', 'entry-server.js');
|
|
|
|
const HEAD_PLACEHOLDER = '<!--app-head-->';
|
|
const HTML_PLACEHOLDER = '<!--app-html-->';
|
|
|
|
const template = fs.readFileSync(path.join(distDir, 'index.html'), 'utf-8');
|
|
if (!template.includes(HEAD_PLACEHOLDER) || !template.includes(HTML_PLACEHOLDER)) {
|
|
throw new Error('dist/index.html 에서 <!--app-head--> / <!--app-html--> 자리표시자를 찾지 못했습니다.');
|
|
}
|
|
// Cloudflare Workers (_worker.js) 생성용으로 원본 템플릿 보존
|
|
fs.writeFileSync(path.join(distDir, '_template.html'), template, 'utf-8');
|
|
|
|
const { render } = await import(pathToFileURL(ssrEntry).href);
|
|
|
|
const writeFile = (file, content) => {
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
fs.writeFileSync(file, content, 'utf-8');
|
|
};
|
|
|
|
// url -> 출력 파일 경로
|
|
const popularPaths = [
|
|
DEFAULT_PATH,
|
|
'/incheon/yeonsu-gu/songdo-dong/middle/math',
|
|
'/daegu/suseong-gu/hwanggeum-dong/middle/math',
|
|
'/daegu/suseong-gu/hwanggeum-dong/high/korean',
|
|
'/seoul/gangnam-gu/daechi-dong/high/math',
|
|
'/gyeonggi/seongnam-si-bundang-gu/sunae-dong/middle/english',
|
|
'/busan/haeundae-gu/u-dong/middle/english'
|
|
];
|
|
const allLandingPaths = Array.from(new Set([...popularPaths, ...getPrerenderPaths()]));
|
|
|
|
const targets = [
|
|
{ url: '/', file: path.join(distDir, 'index.html'), kind: 'home' }, // 메인 홈 화면 (지역·학년·과목 허브)
|
|
{ url: '/regions', file: path.join(distDir, 'regions', 'index.html'), kind: 'page' },
|
|
{ url: '/admin', file: path.join(distDir, 'admin', 'index.html'), kind: 'noindex' },
|
|
{ url: '/__not-found__', file: path.join(distDir, '404.html'), kind: 'noindex' },
|
|
...allLandingPaths.map((p) => ({
|
|
url: p,
|
|
file: path.join(distDir, ...p.split('/').filter(Boolean), 'index.html'),
|
|
kind: 'landing'
|
|
}))
|
|
];
|
|
|
|
const problems = [];
|
|
let count = 0;
|
|
|
|
for (const t of targets) {
|
|
const { html, head, routeName } = await render(t.url);
|
|
|
|
let out = template;
|
|
if (head) {
|
|
out = out.replace(/<title\s+data-default[\s\S]*?<\/title>\s*/i, '');
|
|
out = out.replace(/<[^>]+data-default[^>]*>\s*/gi, '');
|
|
out = out.replace(HEAD_PLACEHOLDER, head);
|
|
} else {
|
|
out = out.replace(HEAD_PLACEHOLDER, '');
|
|
}
|
|
out = out.replace(HTML_PLACEHOLDER, html);
|
|
|
|
// 품질 검증: 색인 대상 페이지는 h1 1개, title, canonical, JSON-LD 가 반드시 있어야 함
|
|
if (t.kind === 'landing' || t.kind === 'home' || t.kind === 'page') {
|
|
const h1 = (html.match(/<h1[\s>]/g) || []).length;
|
|
if (h1 !== 1) problems.push(`${t.url}: h1 ${h1}개`);
|
|
if (!/<title data-head>[^<]+<\/title>/.test(head)) problems.push(`${t.url}: title 없음`);
|
|
if (!/rel="canonical"/.test(head)) problems.push(`${t.url}: canonical 없음`);
|
|
if (!/application\/ld\+json/.test(head) && t.kind !== 'page') problems.push(`${t.url}: JSON-LD 없음`);
|
|
}
|
|
if (t.kind === 'landing' && routeName !== 'LocationSubjectFullLanding') {
|
|
problems.push(`${t.url}: 랜딩 라우트에 매칭되지 않음 (${String(routeName)})`);
|
|
}
|
|
|
|
writeFile(t.file, out);
|
|
count++;
|
|
}
|
|
|
|
if (problems.length > 0) {
|
|
console.error('[FAIL] 사전 렌더링 검증 실패:\n - ' + problems.join('\n - '));
|
|
process.exit(1);
|
|
}
|
|
|
|
// SSR 번들은 배포 대상이 아니므로 정리하지 않고 dist-ssr 에 남겨둠 (.gitignore 대상)
|
|
console.log(`[OK] ${count}개 페이지 사전 렌더링 완료 -> dist/`);
|