- 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>
93 lines
2.8 KiB
JavaScript
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);
|