Skip to content

Commit 60284f8

Browse files
raifdmuellerclaude
andcommitted
feat: add pre-rendering for SEO with Puppeteer
Added build-time pre-rendering to generate static HTML snapshots for search engine indexing. Implementation: - scripts/prerender.js — Puppeteer-based renderer - Starts local server (serve-handler on port 8765) - Waits for React hydration - Captures fully rendered HTML - Replaces <body> in dist/index.html Build process updated: - npm run build now executes: vite build && node scripts/prerender.js Benefits: - Search engines immediately see full content (no JS execution needed) - Faster initial indexing by Google/Bing - Better crawl budget efficiency - Fallback for bots without JS support Dependencies added: - puppeteer@^24.37.3 - serve-handler@^6.1.6 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent c778295 commit 60284f8

File tree

2 files changed

+80
-1
lines changed

2 files changed

+80
-1
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"dev": "vite",
8-
"build": "vite build",
8+
"build": "vite build && node scripts/prerender.js",
99
"preview": "vite preview",
1010
"docs": "asciidoctor -a icons=font -a source-highlighter=highlight.js -a linkcss -a stylesheet=asciidoctor.css -D dist/docs docs/risk-radar.adoc docs/risk-radar-en.adoc",
1111
"prepare": "husky"
@@ -26,6 +26,8 @@
2626
"husky": "^9.1.7",
2727
"lint-staged": "^16.2.7",
2828
"prettier": "^3.8.1",
29+
"puppeteer": "^24.37.3",
30+
"serve-handler": "^6.1.6",
2931
"vite": "^6.0.0"
3032
},
3133
"lint-staged": {

scripts/prerender.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env node
2+
/* global process */
3+
import puppeteer from "puppeteer";
4+
import { readFileSync, writeFileSync } from "fs";
5+
import { fileURLToPath } from "url";
6+
import { dirname, join } from "path";
7+
import { createServer } from "http";
8+
import handler from "serve-handler";
9+
10+
const __dirname = dirname(fileURLToPath(import.meta.url));
11+
const distPath = join(__dirname, "../dist");
12+
const distIndexPath = join(distPath, "index.html");
13+
14+
async function prerender() {
15+
console.log("🚀 Starting prerender...");
16+
17+
// Read the built index.html
18+
const originalHtml = readFileSync(distIndexPath, "utf-8");
19+
20+
// Start a local server for dist/
21+
const server = createServer((req, res) => {
22+
return handler(req, res, { public: distPath });
23+
});
24+
25+
await new Promise((resolve) => {
26+
server.listen(8765, () => {
27+
console.log("📡 Local server started on http://localhost:8765");
28+
resolve();
29+
});
30+
});
31+
32+
// Launch headless browser
33+
const browser = await puppeteer.launch({
34+
headless: true,
35+
args: ["--no-sandbox", "--disable-setuid-sandbox"],
36+
});
37+
38+
const page = await browser.newPage();
39+
40+
// Navigate to local server
41+
await page.goto("http://localhost:8765", {
42+
waitUntil: "networkidle0",
43+
timeout: 30000,
44+
});
45+
46+
// Wait for React to render
47+
await page.waitForSelector("#root > *", { timeout: 10000 });
48+
49+
// Give React a moment to fully hydrate
50+
await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 1000)));
51+
52+
// Get the rendered HTML
53+
const renderedHtml = await page.content();
54+
55+
await browser.close();
56+
server.close();
57+
58+
// Extract only the <body> content from rendered page
59+
const bodyMatch = renderedHtml.match(/<body[^>]*>([\s\S]*)<\/body>/i);
60+
if (!bodyMatch) {
61+
throw new Error("Could not extract body content from rendered page");
62+
}
63+
const renderedBody = bodyMatch[1];
64+
65+
// Replace the original body content with rendered content
66+
const prerenderedHtml = originalHtml.replace(/<body[^>]*>[\s\S]*<\/body>/i, `<body>${renderedBody}</body>`);
67+
68+
// Write back to dist/index.html
69+
writeFileSync(distIndexPath, prerenderedHtml, "utf-8");
70+
71+
console.log("✅ Prerendering complete. Updated dist/index.html");
72+
}
73+
74+
prerender().catch((err) => {
75+
console.error("❌ Prerendering failed:", err);
76+
process.exit(1);
77+
});

0 commit comments

Comments
 (0)