Local-first SPA prerender CLI — convert your React/Vite app into SEO-friendly static HTML. Zero cloud. Zero backend.
React apps are invisible to search engines because they render HTML using JavaScript — and most bots don't run JavaScript. ReviJs fixes this by opening your app in a real headless browser, waiting for everything to load, and saving the final HTML to disk.
That saved HTML gets served to bots like Googlebot, GPTBot, and ClaudeBot — while real users continue to get your normal React app. No code changes needed in your app.
npm run build as normalnpx revijsrevi.config.jsindex.html file in dist-prerendered/prerender() in your own build scripts.npx revijs init to generate a starter config instantly.Set up ReviJs in your existing project in under 30 seconds.
dist/ build outputInstall ReviJs. Playwright Chromium installs automatically — nothing extra needed.
npm install @revijs/core
playwright install manually.Auto-generate a config file:
npx revijs init
Or create revi.config.js manually:
export default { routes: ['/', '/about', '/blog/post-1'], engine: 'browser', outputDir: 'dist-prerendered', distDir: 'dist', waitFor: 1200, headless: true, };
npm run build
npx revijs
Expected output:
⚡ ReviJs — SPA Prerenderer
Rendering / ... ✔
Rendering /about ... ✔
Rendering /blog/post-1 ... ✔
3 rendered → dist-prerendered/
npm run build before npx revijs. ReviJs renders your built output, not your source files.Each route maps to a nested index.html:
/ → dist-prerendered/index.html /about → dist-prerendered/about/index.html /blog/post-1 → dist-prerendered/blog/post-1/index.html
All options for revi.config.js. Run npx revijs init to generate one automatically.
export default { routes: ['/', '/about', '/blog/post-1'], engine: 'browser', outputDir: 'dist-prerendered', distDir: 'dist', waitFor: 1200, headless: true, port: 4173, debug: false, };
| Option | Type | Default | Description |
|---|---|---|---|
| routes | string[] | [] | Route paths to prerender. Required. |
| engine | string | 'browser' | Rendering engine. See below. |
| outputDir | string | 'dist-prerendered' | Where to write prerendered HTML files. |
| distDir | string | 'dist' | Your built SPA directory. Must exist before running. |
| waitFor | number | 1200 | ms to wait after network idle. Increase if data loads slowly. |
| headless | boolean | true | Run browser hidden. Set false to watch it (useful for debugging). |
| port | number | 4173 | Local server port. Auto-increments if taken. |
| debug | boolean | false | Enable verbose logging. Also available as --debug CLI flag. |
Standard headless Chromium. Works for 99% of React/Vite apps. Navigates to each route, waits for network idle, captures the DOM. Use this unless you have a specific reason not to.
Like browser, but also simulates page scroll to trigger lazy-loaded components. Also injects a <meta name="x-prerendered-by" content="revijs"> tag. Use this if your components only load when scrolled into view.
Minimal-wait mode for apps that already do server-side rendering. Skips the waitFor delay since content is already in the initial HTML.
| Flag | Description |
|---|---|
| --config <path> | Path to config file. Default: revi.config.js |
| --output <dir> | Override outputDir from config |
| --debug | Enable verbose logging |
Use ReviJs in your own Node.js scripts instead of the CLI. Useful for custom build pipelines and CI workflows.
The main entry point. Renders all routes and writes static HTML to disk. Equivalent of running npx revijs.
import { prerender } from '@revijs/core'; await prerender({ routes: ['/', '/about', '/contact'], outputDir: 'dist-prerendered', distDir: 'dist', engine: 'browser', waitFor: 1500, });
Lower-level function. Accepts a fully resolved config, starts the server, boots the engine, renders all routes, tears down.
import { renderAllPages, loadConfig } from '@revijs/core'; const config = await loadConfig('revi.config.js'); await renderAllPages(config);
Loads and validates revi.config.js, merges with defaults. Returns a fully resolved config object.
import { loadConfig } from '@revijs/core'; const config = await loadConfig('revi.config.js', { debug: true, });
Check if a user-agent string belongs to a known bot. Used internally by the middleware.
import { isBot, detectBot } from '@revijs/core'; isBot('Googlebot/2.1'); // true isBot('Mozilla/5.0 ...'); // false detectBot('GPTBot/1.0'); // 'gptbot'
Maps a route path to a file and writes the HTML string to disk.
import { saveHTML } from '@revijs/core'; // /about → dist-prerendered/about/index.html await saveHTML('/about', htmlString, 'dist-prerendered');
Optional middleware that serves prerendered HTML to bots while passing all other requests through normally. Works with Express, Polka, and any Connect-compatible framework.
User-Agent header against 30+ known bot patternsdist-prerendered/ and returns itnext() and your app handles it normallyimport express from 'express'; import { createMiddleware } from '@revijs/core'; const app = express(); // register BEFORE your static/SPA handler app.use(createMiddleware({ prerenderedDir: 'dist-prerendered', debug: false, })); app.use(express.static('dist')); app.listen(3000);
import polka from 'polka'; import sirv from 'sirv'; import { createMiddleware } from '@revijs/core'; polka() .use(createMiddleware({ prerenderedDir: 'dist-prerendered' })) .use(sirv('dist', { single: true })) .listen(3000);
| Option | Type | Default | Description |
|---|---|---|---|
| prerenderedDir | string | 'dist-prerendered' | Folder with your prerendered HTML files. |
| debug | boolean | false | Log which bot matched and which file was served. |
Real-world configs for common use cases. Copy and adjust for your project.
export default { routes: ['/', '/blog', '/blog/hello-world', '/blog/react-tips', '/about'], engine: 'browser', waitFor: 2000, // extra time for content to load };
export default { routes: ['/', '/products', '/products/sneaker-x1', '/categories/shoes'], // skip /cart /checkout /account — auth-protected routes engine: 'browser', waitFor: 1500, };
export default { routes: ['/', '/pricing', '/features', '/changelog'], // /app/* and /dashboard/* intentionally skipped waitFor: 1200, };
// package.json
{
"scripts": {
"build": "vite build",
"prerender": "npm run build && npx revijs",
"deploy": "npm run prerender && netlify deploy --dir=dist-prerendered"
}
}
If content isn't appearing in the prerendered HTML, your app needs more time. Increase waitFor:
waitFor: 1200, // fast APIs waitFor: 3000, // slow APIs or heavy data waitFor: 5000, // very slow — use sparingly
Set headless: false to watch the browser open and --debug for verbose logs:
export default { headless: false, // opens a visible browser window routes: ['/problem-route'], waitFor: 3000, };
npx revijs --debug