Aimpress_site/scripts/prerender.mjs
Vadym Samoilenko 272839d7a9 Add post-build SPA prerendering script with Puppeteer
Manual script (node scripts/prerender.mjs) that starts a local static
server, visits each route with Puppeteer, waits 3s for React to render,
and saves full HTML to dist/. Covers all static routes + blog posts.

Run after vite build when prerendering is needed. Requires puppeteer
installed (npm i -D puppeteer mime-types).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 21:37:34 +00:00

85 lines
2.6 KiB
JavaScript

/**
* Post-build prerendering script.
* Run after `vite build` to generate static HTML for each route.
* Requires: npm install -D puppeteer serve
*
* Usage: node scripts/prerender.mjs
*/
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, '..');
const distDir = resolve(root, 'dist');
function getBlogRoutes() {
try {
const posts = JSON.parse(readFileSync(resolve(root, 'public/blog/posts.json'), 'utf-8'));
return posts.map(p => `/blog/${p.slug}`);
} catch { return []; }
}
const routes = [
'/',
'/about',
'/services',
'/pricing',
'/blog',
'/privacy-policy',
'/terms-of-use',
...getBlogRoutes(),
];
async function prerender() {
// Dynamic imports to avoid breaking build if puppeteer not installed
const { default: puppeteer } = await import('puppeteer');
const { createServer } = await import('node:http');
const { createReadStream, existsSync } = await import('node:fs');
const { extname, join } = await import('node:path');
const { lookup } = await import('mime-types');
// Simple static file server for dist/
const PORT = 5199;
const server = createServer((req, res) => {
let filePath = join(distDir, req.url === '/' ? '/index.html' : req.url);
// For SPA routes, serve index.html
if (!existsSync(filePath) || !extname(filePath)) {
filePath = join(distDir, 'index.html');
}
const mime = lookup(filePath) || 'text/html';
res.setHeader('Content-Type', mime);
createReadStream(filePath).pipe(res);
});
await new Promise(r => server.listen(PORT, r));
console.log(`Static server running on http://localhost:${PORT}`);
const browser = await puppeteer.launch({ args: ['--no-sandbox'] });
for (const route of routes) {
const url = `http://localhost:${PORT}${route}`;
const page = await browser.newPage();
try {
await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 });
// Wait for React to render
await new Promise(r => setTimeout(r, 3000));
const html = await page.content();
const routeDir = route === '/' ? distDir : join(distDir, route);
mkdirSync(routeDir, { recursive: true });
writeFileSync(join(routeDir, 'index.html'), html, 'utf-8');
console.log(`Prerendered: ${route}`);
} catch (err) {
console.error(`Failed to prerender ${route}:`, err.message);
} finally {
await page.close();
}
}
await browser.close();
server.close();
console.log('Prerendering complete!');
}
prerender().catch(console.error);