70 lines
3.0 KiB
TypeScript
70 lines
3.0 KiB
TypeScript
/**
|
|
* Пререндер лендинга в статический HTML (SEO).
|
|
*
|
|
* Работает БЕЗ браузера: react-dom/server рендерит <App /> в Node, готовый
|
|
* DOM вставляется между маркерами <!--SEO:FALLBACK_*--> в dist/index.html.
|
|
* useEffect в SSR не исполняется, поэтому вызовов /api/* в снапшоте нет —
|
|
* страница полностью статична; стоп-лист подмешивается уже на клиенте.
|
|
*
|
|
* Снапшот заменяется React (createRoot) при монтировании — SPA работает
|
|
* как раньше. Повторные запуски идемпотентны (маркеры сохраняются).
|
|
*
|
|
* Ошибка рендера не ломает сборку: остаётся статический fallback-блок
|
|
* (H1 + SEO-абзац), в консоль пишется предупреждение.
|
|
*
|
|
* Запуск: npm run build (vite build && tsx scripts/prerender.tsx)
|
|
*/
|
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import * as React from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import App from "../src/App";
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const distIndex = join(here, "..", "dist", "index.html");
|
|
|
|
// флаг для src/lib/prerender.ts (секции рендерятся «видимыми») — до рендера
|
|
(globalThis as { __NEON_PRERENDER__?: boolean }).__NEON_PRERENDER__ = true;
|
|
// App читает document.title в useRef — минимальный полифилл для Node
|
|
if (typeof (globalThis as { document?: unknown }).document === "undefined") {
|
|
(globalThis as { document?: unknown }).document = { title: "" };
|
|
}
|
|
|
|
const START = "<!--SEO:FALLBACK_START-->";
|
|
const END = "<!--SEO:FALLBACK_END-->";
|
|
|
|
if (!existsSync(distIndex)) {
|
|
console.error("[prerender] dist/index.html не найден — сначала vite build");
|
|
process.exit(1);
|
|
}
|
|
|
|
const html = readFileSync(distIndex, "utf-8");
|
|
if (!html.includes(START) || !html.includes(END)) {
|
|
console.warn("[prerender] маркеры SEO:FALLBACK_* не найдены — снапшот не вставлен");
|
|
process.exit(0);
|
|
}
|
|
|
|
try {
|
|
const snapshot = renderToStaticMarkup(<App />);
|
|
|
|
if (!snapshot.includes("<h1")) {
|
|
throw new Error("в снапшоте нет <h1> — похоже, отрендерилась не главная");
|
|
}
|
|
|
|
const updated = html.replace(
|
|
new RegExp(`${START}[\\s\\S]*?${END}`),
|
|
`${START}${snapshot}${END}`,
|
|
);
|
|
writeFileSync(distIndex, updated, "utf-8");
|
|
console.log(
|
|
`[prerender] снапшот вставлен: ${Math.round(snapshot.length / 1024)} KB внутрь #root`,
|
|
);
|
|
} catch (error) {
|
|
console.warn(
|
|
"[prerender] рендер не удался, в dist остался статический fallback:",
|
|
error instanceof Error ? error.message : error,
|
|
);
|
|
process.exit(0);
|
|
}
|