initial site
This commit is contained in:
commit
da5b259d06
167 files changed
+34157
No files matched your search
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Пререндер лендинга в статический 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);
|
||||
}
|
||||
Reference in new issue
Block a user