import { createServer } from 'http'; import { readFileSync, writeFileSync, existsSync, statSync } from 'fs'; import { join, extname } from 'path'; import { launch } from 'puppeteer'; const DIST_DIR = join(import.meta.dirname, '..', 'dist'); const INDEX_PATH = join(DIST_DIR, 'index.html'); const MIME_TYPES = { '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml', }; function startServer() { return new Promise((resolve) => { const server = createServer((req, res) => { const url = new URL(req.url, 'http://localhost'); let filePath = join(DIST_DIR, url.pathname === '/' ? 'index.html' : url.pathname); if (!existsSync(filePath) || !statSync(filePath).isFile()) { // SPA fallback filePath = INDEX_PATH; } const ext = extname(filePath); const mime = MIME_TYPES[ext] || 'application/octet-stream'; const content = readFileSync(filePath); res.writeHead(200, { 'Content-Type': mime }); res.end(content); }); server.listen(0, '127.0.0.1', () => { const port = server.address().port; resolve({ server, port }); }); }); } async function prerender() { console.log('Starting prerender...'); const { server, port } = await startServer(); console.log(`Static server on port ${port}`); const browser = await launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'], }); try { const page = await browser.newPage(); // Intercept API requests to prevent real fetches and retry loops await page.setRequestInterception(true); page.on('request', (req) => { const url = req.url(); if (url.includes('/api/features')) { req.respond({ status: 200, contentType: 'application/json', body: JSON.stringify({ groups: [] }), }); } else if (url.includes('/api/poi-categories')) { req.respond({ status: 200, contentType: 'application/json', body: JSON.stringify({ groups: [] }), }); } else if (url.includes('/api/')) { req.respond({ status: 200, contentType: 'application/json', body: '{}', }); } else { req.continue(); } }); await page.goto(`http://127.0.0.1:${port}/`, { waitUntil: 'networkidle0', timeout: 30000, }); // Wait for the home page heading to render await page.waitForSelector('h1', { timeout: 10000 }); // Extract and clean the rendered HTML const html = await page.evaluate(() => { const root = document.getElementById('root'); if (!root) return ''; // Strip fade-in-visible classes (added by IntersectionObserver effects) root.querySelectorAll('.fade-in-visible').forEach((el) => { el.classList.remove('fade-in-visible'); }); // Clean canvas elements (dimensions set by ResizeObserver effect) root.querySelectorAll('canvas').forEach((canvas) => { canvas.removeAttribute('width'); canvas.removeAttribute('height'); canvas.style.removeProperty('width'); canvas.style.removeProperty('height'); }); return root.innerHTML; }); if (!html || html.length < 100) { throw new Error('Prerender produced too little HTML — something went wrong'); } // Inject into dist/index.html const indexHtml = readFileSync(INDEX_PATH, 'utf-8'); const updated = indexHtml.replace( '
', `
${html}
` ); if (updated === indexHtml) { throw new Error('Could not find
in index.html'); } writeFileSync(INDEX_PATH, updated); console.log(`Prerendered ${html.length} chars into dist/index.html`); } finally { await browser.close(); server.close(); } } prerender().catch((err) => { console.error('Prerender failed:', err); process.exit(1); });