A Nuxt 4 module that replaces the standard Nitro SSG preset with a custom runtime that pre-renders pages on demand via a lightweight HTTP API. Instead of generating all static files at build time, pages are rendered and written to disk at runtime by calling a REST endpoint — enabling JIT (just-in-time) pre-rendering.
NOTE: The current implementation does not work properly behind CDNs (unless the cache is purged manually), as the build ID is not updated in the static files.
⚠️ WARNING: This repository is under heavy development.
When added as a Nuxt module, nuxt-jit-prerender:
- Injects a custom Nitro preset (
src/nitro-preset) that extends the standardnode-serverpreset. - Replaces the default server entry with a minimal Node.js HTTP server exposing a REST API.
- Renders pages on demand by calling Nitro's internal
localFetchand writing the HTML (and payload JSON) to.output/public. - Tracks dependencies via a tag-based
CacheRegistrypersisted to.output/.cache-manifest.json, allowing for targeted re-renders.
Routes are processed in configurable-concurrency batches. Any additional routes discovered via the x-nitro-prerender response header are automatically queued and rendered too.
| Method | Path | Description |
|---|---|---|
GET |
/api/health |
Health check — returns { status: "ok", timestamp } |
POST |
/api/generate |
Pre-render a list of routes and write them to disk |
POST |
/api/invalidate |
Re-render routes based on their associated cache tags |
DELETE |
/api/route |
Purge a list of routes from the registry and delete their files from disk |
Triggers on-demand generation for a specific list of routes.
Request body:
{
"routes": ["/", "/about"]
}Response:
{
"success": true,
"summary": {
"requested": 2,
"generated": 2,
"discovered": 2,
"total": 4
},
"results": [
{ "route": "/", "success": true, "cacheTags": ["page:index"], "discoveredRoutes": ["/_payload.json"] }
]
}Triggers re-generation for all routes associated with one or more tags, or for every route known to the registry.
Request body (tag-based):
{
"tags": ["product:123", "category:electronics"]
}Request body (all):
{
"all": true
}Permanently remove a list of routes from the cache manifest and physically delete their corresponding .html and _payload.json files from disk.
Request body:
{
"routes": ["/old-page", "/temporary-promo"]
}Security Guards:
- Path Traversal Protection: Rejects any route that attempts to escape the public output directory using
... _nuxtProtection: Rejects any route targeting or containing the/_nuxtdirectory to prevent deletion of core assets.- Cleanup Safety: When removing empty folders, the base output directory is always preserved.
- ⚡ JIT Pre-rendering — Render pages at runtime, not at build time
- 🏷️ Tag-based Invalidation — Precision re-rendering of specific pages when data changes
- 🔄 Auto-discovery — Automatically follows the
x-nitro-prerenderresponse header to render linked assets like_payload.json - 📦 Payload extraction — Co-renders metadata alongside each HTML page for SPA hydration/navigation
- 🏎️ Concurrent batch processing — Configurable concurrency for parallel route generation
- 💾 Persistent Registry — Tracks route-to-tag mappings in a
.cache-manifest.jsonfile - 🏥 Health check endpoint — Built-in liveness probe at
GET /api/health - 🪵 Structured logging — Request-scoped logging with
consola; emits JSON logs in CI environments
To use tag-based invalidation, your pages must be associated with one or more tags.
In your Nuxt pages or components, use the auto-imported useCacheTags composable to associate the current page with specific tags.
<script setup>
// Tags can be a string or an array of strings
useCacheTags(['product:123', 'category:electronics'])
</script>If you are not using the composable (e.g., in a Nitro server route or plugin), you can manually set or append the x-jit-prerender-cache-tags header.
// server/plugins/cache-tags.ts
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('render:response', (response, { event }) => {
if (event.path.startsWith('/products/')) {
appendHeader(event, 'x-jit-prerender-cache-tags', 'all-products, product:123')
}
})
})| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Port the HTTP server listens on |
HOST |
0.0.0.0 |
Host the HTTP server binds to |
NUXT_JIT_PRERENDER_CONCURRENCY |
10 |
Max routes rendered in parallel per batch |
NUXT_JIT_PRERENDER_OUTPUT_DIR |
.output |
Root directory for output (contains /server, /public, nitro.json and .cache-manifest.json) |
NUXT_JIT_PRERENDER_CI |
— | Set to "true" for structured JSON log output |
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['nuxt-jit-prerender']
})After building your app (nuxi build), start the server and trigger pre-rendering via the API:
# Start the pre-render server
node .output/server/index.mjs
# Trigger pre-rendering for specific routes
curl -X POST http://localhost:3000/api/generate \
-H 'Content-Type: application/json' \
-d '{"routes": ["/", "/about"]}'
# Invalidate a specific product tag
curl -X POST http://localhost:3000/api/invalidate \
-H 'Content-Type: application/json' \
-d '{"tags": ["product:123"]}'
# Permanently delete a route from disk and registry
curl -X DELETE http://localhost:3000/api/route \
-H 'Content-Type: application/json' \
-d '{"routes": ["/old-page"]}'Local development
# Install dependencies
pnpm install
# Generate type stubs and prepare the playground
pnpm dev:prepare
# Develop with the playground
pnpm dev
# Build the playground
pnpm dev:build
# Start the pre-render server against the built playground
pnpm dev:server
# Lint
pnpm lint
# Run tests
pnpm test
pnpm test:watch
# Type-check
pnpm test:types