Aimpress-site/scripts/prerender.mjs
Vadym Samoilenko 4fc85ef99e Enable SPA prerendering in build pipeline
- Add puppeteer + mime-types to devDependencies
- Integrate prerender.mjs into build script (runs after vite build)
- Add Linux/Docker-safe Chrome flags (--disable-setuid-sandbox, --disable-dev-shm-usage, --disable-gpu)
- Fix static server to strip query strings from URLs

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

93 lines
2.8 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) => {
const urlPath = req.url.split('?')[0];
let filePath = join(distDir, urlPath === '/' ? '/index.html' : urlPath);
// 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',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
],
});
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);