/** * SSG — Static Site Generation (Memory-Served) * * Pre-renders pages at startup and stores the HTML in the `builtAssets` map. * Pages are served from memory (zero syscalls) instead of re-running SSR on every request. * * Features: * - Pre-render all static (non-dynamic) routes at startup * - Store rendered HTML in memory-served `builtAssets` * - Skip dynamic routes ([id]) unless explicitly configured * - Dev mode: bypass cache for fresh renders * - Page exports `ssg = true` or `ssg = { revalidate: 60 }` to opt in * * @example * ```tsx * // app/about/page.tsx * export const ssg = true; // Pre-render at startup * * export default function AboutPage() { * return

About Us

; * } * ``` */ import { builtAssets } from "./build"; import { ssgMeasure } from "./measure"; import type { Route } from "./router"; // ─── Pre-rendered Page Cache ──────────────────────────────────────────────────── export interface SSGEntry { html: string; renderedAt: number; revalidateMs?: number; // optional TTL for stale-while-revalidate } const ssgCache = new Map(); /** * Check if a pre-rendered page exists and is still fresh. * Returns the HTML string if cached and valid, otherwise null. */ export function getPrerendered(pathname: string): string | null { const entry = ssgCache.get(pathname); if (!entry) return null; // Check TTL if configured if (entry.revalidateMs) { const age = Date.now() - entry.renderedAt; if (age > entry.revalidateMs) { ssgCache.delete(pathname); return null; } } return entry.html; } /** * Store a pre-rendered page in memory. */ export function setPrerendered( pathname: string, html: string, revalidateMs?: number, ): void { ssgCache.set(pathname, { html, renderedAt: Date.now(), revalidateMs, }); // Also store in builtAssets for direct serving from the asset pipeline const content = new TextEncoder().encode(html).buffer as ArrayBuffer; builtAssets[`__ssg:${pathname}`] = { content, contentType: "text/html" }; } /** * Pre-render all eligible routes at startup. * Call this with the discovered routes and a render function. * * @param routes - Discovered routes from discoverRoutes() * @param renderRoute - Function that renders a route to HTML (provided by app-router) * @returns Number of pages pre-rendered */ export async function prerender( routes: Route[], renderRoute: (route: Route) => Promise, ): Promise { return await ssgMeasure.measure("SSG pre-render", async () => { let count = 0; for (const route of routes) { // Skip API routes if (route.type === "api") continue; // Skip dynamic routes (have parameters) if (route.paramNames.length > 0) continue; // Check if page opts in to SSG try { const mod = await import(route.filePath); const ssgConfig = mod.ssg; if (!ssgConfig) continue; // Page must export `ssg = true` or `ssg = { revalidate: N }` const revalidateMs = typeof ssgConfig === "object" ? (ssgConfig.revalidate ?? 0) * 1000 : undefined; const html = await ssgMeasure.measure(`Route: ${route.pattern}`, () => renderRoute(route), ); if (html) { setPrerendered(route.pattern, html, revalidateMs); count++; } } catch (e: any) { console.warn(` ⚠ Failed to pre-render ${route.pattern}:`, e.message); } } return count; }); } /** * Clear all pre-rendered pages (useful for dev mode or cache invalidation). */ export function clearSSGCache(): void { for (const [key] of ssgCache) { delete builtAssets[`__ssg:${key}`]; } ssgCache.clear(); }