Turn a running Single Page Application into per-route static HTML for SEO — hydration-safe prerendering built entirely on top of aux4/browser.
React (and other client-rendered) SPAs ship an empty <div id="root"></div> shell and render everything in the browser. Crawlers and social scrapers that do not run JavaScript therefore see no content, no per-page title, and no meta tags. aux4/prerender fixes that: it drives a headless browser to each route of your locally-served build, waits for the client render to finish, and writes the fully-rendered HTML back into your dist — while keeping the <script>/asset tags intact so the SPA still boots and hydrates for real visitors.
It works well with static hosts (e.g. S3 + CloudFront) where /route/index.html files are served directly and unknown paths fall back to the SPA — so trailing-slash <route>/index.html snapshots drop straight in.
aux4 aux4 pkger install aux4/prerender
This also pulls in its dependency, aux4/browser.
Build your SPA, serve the build locally, then prerender every route in its sitemap:
# 1. build your SPA (however your project does it), producing ./dist
# 2. serve the build locally on some port, e.g. http://localhost:4173
# 3. prerender it in place
aux4 prerender site \
--baseUrl http://localhost:4173 \
--routes dist/sitemap.xml \
--output dist
Each route is written to dist/<route>/index.html, replacing the empty shell with rendered content.
Visit every route of a locally-served SPA and write each rendered page to <output>/<route>/index.html.
aux4 prerender site --baseUrl <url> --routes <sitemap|list> --output <dir> [--waitUntil <strategy>] [--settle <ms>] [--concurrency <n>]
| Flag | Description | Default | |------|-------------|---------| | --baseUrl | Base URL of the built SPA served locally, e.g. http://localhost:4173 | (required) | | --routes | A sitemap.xml (local path OR URL) whose <loc> entries are the routes, OR an explicit comma/newline separated list of paths (e.g. "/,/about,/pricing") | (required) | | --output | Directory to write <route>/index.html into, typically the built dist | (required) | | --waitUntil | Navigation wait strategy: domcontentloaded, load, networkidle, or settle | networkidle | | --settle | Extra delay in milliseconds after network idle, before capturing the HTML | 500 | | --concurrency | Number of routes to prerender in parallel, one browser session each | 4 |
For each route, prerender site:
aux4/browser daemon (once, for the whole run).--concurrency browser sessions and pulls routes from a queue.<baseUrl><path> with the --waitUntil strategy, does a best-effort network-idle wait, then waits --settle milliseconds.document.documentElement.outerHTML, prefixes a <!doctype html> when missing, and writes it to <output>/<path>/index.html (/ maps to index.html).The captured HTML keeps every <script> and asset tag, so hydration works. A route that fails is logged and skipped, but the command exits non-zero if any route failed — so a broken prerender fails CI.
--routes accepts, in order of detection:
http:// / https://) — fetched; if the body is a sitemap, its <loc> entries become the routes.<loc> entries, otherwise the file is treated as a comma/newline separated list of paths."/,/about,/pricing". A single bare route such as "/" is a one-item list — it is only treated as a file when it ends in .xml or is an existing readable file, so --routes "/" prerenders just the home page.Sitemap <loc> values may be absolute URLs (the origin is stripped to derive the path) or bare paths.
| Route | Output file | |-------|-------------| | / | <output>/index.html | | /about | <output>/about/index.html | | /for/cto/ | <output>/for/cto/index.html |
Prerender any live site by pointing --baseUrl at it and giving --routes a one-line sitemap (or an explicit route list). For example, capture the rendered home page of example.com into ./out:
printf '/\n' > routes.txt
aux4 prerender site \
--baseUrl https://example.com \
--routes routes.txt \
--output out
Prerendering 1 route(s) from https://example.com into out
ok / -> out/index.html
Prerendered 1/1 route(s)
The same works with a single inline route — --routes "/" prerenders just the home page — or a comma-separated list, e.g. --routes "/,/about".
A typical CI flow prerenders by building the SPA, serving the built dist locally, and pointing prerender site at that local server with the built sitemap:
# 1. build the SPA to dist (includes dist/sitemap.xml)
npm run build
# 2. serve dist locally with SPA fallback, backgrounded, on a fixed port
npx serve -s dist -l 4173 &
# 3. prerender every route in the sitemap back into dist, in place
aux4 prerender site \
--baseUrl http://localhost:4173 \
--routes dist/sitemap.xml \
--output dist
# 4. deploy dist (unchanged — prerender never deploys)
Because the routes come from dist/sitemap.xml served locally, CI never depends on any external site being reachable.
aux4/prerender only prerenders. It does not deploy, sync to S3, or invalidate a CDN — that stays in your existing deploy step. Run it right after your build, then deploy dist as you already do.
dist.vite preview, npx serve, or a tiny Node static server), backgrounded.aux4 prerender site --baseUrl http://localhost:<port> --routes dist/sitemap.xml --output dist.dist.The browser daemon runs headless by default. Ensure the browser engine used by aux4/browser is installed on the runner (see the aux4/browser docs).
aux4/browser — the headless browser daemon that provides every primitive used here.Apache-2.0