npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@revijs/core

v1.0.1

Published

Local-first SPA prerender CLI — convert React/Vite apps into SEO-friendly static HTML

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.

npm downloads license node GitHub stars


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 revijs

Your 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 revijs

ReviJs 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.html

Deploy 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-cache

Smart 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 report

Meta 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 ui

Opens 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.