@revijs/core
v1.0.1
Published
Local-first SPA prerender CLI — convert React/Vite apps into SEO-friendly static HTML
Maintainers
Readme
ReviJs
The SPA prerender toolkit that actually works. Local-first, zero cloud, built for developers who are tired of their React app being invisible to Google.
The problem
Your React app looks great in a browser. But when Googlebot, GPTBot, or any other crawler visits it — they see an empty <div id="root"></div>. No content. No metadata. Nothing to index.
That is why your SEO is suffering. ReviJs fixes this.
How it works
ReviJs opens your app in a real headless Chromium browser, waits for all your data to load, captures the full rendered HTML, and either saves it as static files or serves it live to bots on demand — while your real users continue to get the normal React experience.
Two modes. You pick what fits your setup.
Quick start
npm install @revijs/core
npx revijs init
npm run build
npx revijsYour prerendered HTML files are now in dist-prerendered/. Deploy them alongside your app and bots will see full content on every page.
Mode 1 — Static prerender (run once, deploy)
Best for static hosts like Netlify, Vercel, Cloudflare Pages.
npx revijsReviJs starts a local server, opens every route in a headless browser, waits for data to load, and writes the full HTML to disk.
dist-prerendered/
├── index.html
├── about/
│ └── index.html
└── blog/
└── post-1/
└── index.htmlDeploy this folder. Done.
Mode 2 — Live middleware (real-time, always fresh)
Best for VPS, DigitalOcean, any Node.js server.
Add two lines to your existing server:
import express from 'express'
import { createLiveMiddleware } from '@revijs/core'
const app = express()
app.use(createLiveMiddleware({
distDir: 'dist',
ttl: 30, // cache each page for 30 minutes
debug: false,
}))
app.use(express.static('dist'))
app.listen(3000)Now whenever a bot hits any route on your server — ReviJs renders that page in real time, serves the full HTML to the bot, caches it for 30 minutes, then renders fresh again next time. Your content is always up to date. No manual step. No cron job. Nothing to remember.
Real users hit the same server and get the normal React app — the middleware only intercepts bot requests.
Configuration
Create revi.config.js in your project root, or run npx revijs init to generate one.
export default {
// Routes to prerender
routes: ['/', '/about', '/blog/post-1'],
// Or read routes from a sitemap (supports sitemap-index.xml)
sitemapInput: null, // './sitemap.xml' or 'https://yoursite.com/sitemap-index.xml'
// Or let ReviJs find routes automatically from your dist/ folder
autoDiscover: false,
// Or define routes dynamically from your own API
// routes: async () => {
// const posts = await fetch('/api/posts').then(r => r.json())
// return posts.map(p => `/blog/${p.slug}`)
// },
engine: 'browser', // 'browser' | 'advanced' | 'ssr'
outputDir: 'dist-prerendered',
distDir: 'dist',
// Smart wait — pick one strategy
waitFor: 1200, // wait X ms after network idle (default)
readyFlag: false, // wait for window.__REVI_READY__ = true in your app
waitForSelector: null, // wait for a CSS selector, e.g. '#app[data-loaded]'
// Output
injectMeta: true, // auto-inject missing og:title, og:description, og:image
injectHead: null, // inject custom HTML into <head> of every page
sitemap: false, // generate sitemap.xml after render. Set to your base URL for absolute URLs
score: true, // print SEO score for each page after rendering
report: true, // save revi-report.json after each run
cache: false, // skip unchanged routes on next run (uses content hash)
retries: 2, // retry failed routes N times before giving up
parallel: 1, // number of routes to render at the same time
headless: true,
port: 4173,
debug: false,
};All options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| routes | string[] \| async fn | ['/'] | Routes to prerender, or async function that returns routes |
| sitemapInput | string | null | Path or URL to sitemap.xml or sitemap-index.xml |
| autoDiscover | boolean | false | Scan dist/ and discover routes automatically |
| engine | string | 'browser' | Rendering engine: browser, advanced, ssr |
| outputDir | string | 'dist-prerendered' | Where to write HTML files |
| distDir | string | 'dist' | Your built SPA directory |
| waitFor | number | 1200 | ms to wait after network idle |
| readyFlag | boolean | false | Wait for window.__REVI_READY__ = true |
| waitForSelector | string | null | Wait for a CSS selector to appear in DOM |
| injectMeta | boolean | true | Auto-inject missing og: and twitter: meta tags |
| injectHead | string | null | Custom HTML to inject into <head> on every page |
| sitemap | boolean\|string | false | Generate sitemap.xml. Set to base URL for absolute URLs |
| score | boolean | true | Print SEO score report in terminal |
| report | boolean | true | Save revi-report.json with full results |
| cache | boolean | false | Skip routes whose content hasn't changed since last run |
| retries | number | 2 | Retry failed routes before marking them as failed |
| parallel | number | 1 | Render multiple routes simultaneously |
| headless | boolean | true | Run browser in background |
| port | number | 4173 | Local server port |
| debug | boolean | false | Verbose logging |
CLI commands
# Prerender all routes
npx revijs
# Generate a starter config file
npx revijs init
# Watch dist/ for changes and re-render automatically
npx revijs watch
# Open the dashboard in your browser
npx revijs ui
# Print the last prerender report
npx revijs report
# Deploy your output to a hosting provider
npx revijs deploy --target=netlify
npx revijs deploy --target=vercel
npx revijs deploy --target=cloudflare
# Force re-render everything, ignore cache
npx revijs --no-cacheSmart wait strategy
The default waitFor: 1200 is just a timer — it waits 1.2 seconds and hopes your data has loaded. For production use, tell ReviJs exactly when your app is ready.
Option 1 — Ready flag (recommended)
In your React app, set a flag when everything is loaded:
useEffect(() => {
Promise.all([fetchPosts(), fetchUser()]).then(() => {
window.__REVI_READY__ = true
})
}, [])In your config:
readyFlag: true,
waitFor: 0,ReviJs will wait for that flag instead of guessing.
Option 2 — Wait for a DOM element
waitForSelector: '#app[data-loaded="true"]',ReviJs waits for that element to appear before capturing HTML.
Cache layer
On large sites with many routes, re-rendering everything on every run is slow. Enable caching:
cache: true,ReviJs hashes the rendered HTML for each route. On the next run, if the hash matches — it skips that route entirely. Only changed pages get re-rendered. A site with 500 routes where 10 changed will only render those 10.
The cache is stored in dist-prerendered/.revi-cache.json.
SEO scoring
After each render, ReviJs scores every page and prints a report:
SEO Scores
────────────────────────────────────────────
100% /
80% /about
60% /blog/post-1
missing: og:image, meta description
────────────────────────────────────────────
avg 80%Checks performed on each page: title tag, meta description, og:title, og:description, og:image, h1 tag, image alt attributes, canonical link, lang attribute.
Prerender report
After each run, a revi-report.json file is saved to your output directory:
{
"generatedAt": "2026-03-20T11:00:00.000Z",
"summary": {
"total": 5,
"ok": 5,
"failed": 0,
"avgScore": 84
},
"routes": [
{
"route": "/",
"ms": 1823,
"score": 100,
"passed": ["Has <title>", "Has og:title", "Has h1"],
"failed": []
}
],
"errors": []
}View the last report anytime:
npx revijs reportMeta tag injection
ReviJs automatically adds missing meta tags to each rendered page based on the page content. If your page has a <title> and an <h1> but no og: tags, ReviJs will inject them:
<meta property="og:title" content="Your Page Title" />
<meta property="og:description" content="First heading content" />
<meta property="og:image" content="/first-image-found.png" />
<meta name="twitter:card" content="summary_large_image" />You can also inject custom HTML into every page's <head>:
injectHead: '<link rel="preconnect" href="https://fonts.googleapis.com" />',Sitemap support
Generate a sitemap after prerendering:
sitemap: 'https://yoursite.com',Creates dist-prerendered/sitemap.xml with all rendered routes.
Read routes from an existing sitemap:
sitemapInput: 'https://yoursite.com/sitemap-index.xml',Supports both regular sitemap.xml and sitemap-index.xml. ReviJs fetches all child sitemaps from the index and extracts every route automatically.
Vite plugin
Auto-prerender after every npm run build:
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { reviPlugin } from '@revijs/core'
export default defineConfig({
plugins: [
react(),
reviPlugin(),
],
})After this, running npm run build will automatically prerender your app. No separate command needed.
Dashboard
npx revijs uiOpens a local web dashboard showing render status, time per route, SEO scores per page, and any errors. You can trigger a new prerender run directly from the dashboard.
Static middleware
If you are running a Node.js server and want to serve prerendered files to bots from disk:
import express from 'express'
import { createMiddleware } from '@revijs/core'
const app = express()
app.use(createMiddleware({ prerenderedDir: 'dist-prerendered' }))
app.use(express.static('dist'))
app.listen(3000)Live middleware
If you want real-time on-demand rendering with no pre-build step:
import express from 'express'
import { createLiveMiddleware } from '@revijs/core'
const app = express()
app.use(createLiveMiddleware({
distDir: 'dist',
ttl: 30, // cache rendered HTML for 30 minutes
maxPages: 200, // max number of pages to keep in cache
waitFor: 1200,
injectMeta: true,
debug: false,
}))
app.use(express.static('dist'))
app.listen(3000)| Option | Default | Description |
|--------|---------|-------------|
| distDir | 'dist' | Your built SPA directory |
| ttl | 30 | Minutes to cache each rendered page |
| maxPages | 200 | Max pages in memory cache |
| waitFor | 1200 | ms to wait after network idle |
| readyFlag | false | Wait for window.__REVI_READY__ |
| waitForSelector | null | Wait for CSS selector |
| injectMeta | true | Inject missing og: tags |
| debug | false | Log every bot request and cache hit |
Programmatic API
import { prerender } from '@revijs/core'
await prerender({
routes: ['/', '/about', '/contact'],
outputDir: 'dist-prerendered',
injectMeta: true,
score: true,
cache: true,
retries: 2,
})Full API:
import {
prerender,
loadConfig,
renderAllPages,
renderPage,
saveHTML,
createMiddleware,
createLiveMiddleware,
isBot,
detectBot,
generateSitemap,
routesFromSitemap,
discoverRoutes,
injectMeta,
scoreHTML,
deploy,
reviPlugin,
watchMode,
} from '@revijs/core'Supported bots
ReviJs detects 30+ crawlers out of the box.
Most common
| Bot | Company | Type |
|-----|---------|------|
| googlebot | Google | Search engine |
| bingbot | Microsoft | Search engine |
| gptbot | OpenAI | AI crawler |
| claudebot | Anthropic | AI crawler |
| perplexitybot | Perplexity AI | AI crawler |
| facebookexternalhit | Meta | Social preview |
| twitterbot | X | Social preview |
| linkedinbot | LinkedIn | Social preview |
| yandexbot | Yandex | Search engine |
| baiduspider | Baidu | Search engine |
Search engines
googlebot google-inspectiontool bingbot yandexbot baiduspider duckduckbot slurp sogou exabot rogerbot mj12bot dotbot ia_archiver
AI crawlers
gptbot chatgpt-user claudebot claude-web anthropic-ai perplexitybot cohere-ai amazonbot applebot
SEO tools
ahrefsbot semrushbot
Social and preview
facebookexternalhit twitterbot linkedinbot slackbot discordbot telegrambot whatsapp vkshare pinterest tumblr flipboard
Supported bot list in code
const BOT_PATTERNS = [
// Search engines
'googlebot', 'google-inspectiontool', 'bingbot', 'slurp', 'duckduckbot',
'baiduspider', 'yandexbot', 'sogou', 'exabot', 'facebot', 'ia_archiver',
'mj12bot', 'dotbot', 'ahrefsbot', 'semrushbot', 'rogerbot',
// AI / LLM crawlers
'gptbot', 'chatgpt-user', 'claudebot', 'claude-web', 'perplexitybot',
'cohere-ai', 'amazonbot', 'applebot', 'anthropic-ai',
// Social / preview
'facebookexternalhit', 'twitterbot', 'linkedinbot', 'slackbot', 'discordbot',
'telegrambot', 'whatsapp', 'vkshare', 'pinterest', 'tumblr', 'flipboard',
// Generic
'spider', 'crawler', 'scraper', 'bot/', '+http',
]Hosting compatibility
| Platform | Static mode | Live middleware | |----------|-------------|-----------------| | VPS / DigitalOcean | Yes | Yes | | Netlify | Yes | No | | Vercel | Yes | No | | Cloudflare Pages | Yes | No | | AWS EC2 | Yes | Yes | | Shared hosting (cPanel) | No | No | | AWS Lambda / serverless | No | No |
Live middleware requires a persistent Node.js process. Static mode works everywhere you can upload files.
Rendering engines
browser — Standard headless Chromium. Works for almost every React and Vite app. This is what you should use unless you have a specific reason not to.
advanced — Same as browser but also simulates page scroll to trigger lazy-loaded components. Injects a <meta name="x-prerendered-by" content="revijs"> tag into the output.
ssr — Minimal-wait mode for apps that already have server-side rendering. Skips the waitFor delay.
FAQ
Does it work with Vue, Svelte, or other frameworks? Yes. ReviJs renders whatever the browser renders. It does not care about your framework.
Does my app need any changes?
No. ReviJs works with your existing built output. Optionally you can add window.__REVI_READY__ = true for smarter wait detection, but it is not required.
Will it slow down my users? No. The live middleware only intercepts requests from known bots. Real users are never affected.
Is the browser engine exposed to users? No. Playwright Chromium is an internal implementation detail. It installs automatically and is never exposed in the API.
Why is my SEO score not 100? The score checks for meta description, canonical link, image alt attributes and other tags that your app may not have. These are real SEO issues worth fixing in your app, not ReviJs bugs.
Team
| Name | Role | |------|------| | Utkarsh Dubey | Core Developer | | Adarsh | Core Developer |
Telegram: @thealphabotz · @alphabotzchat
MIT License — free to use, modify, and distribute.
