Skip to main content

Implementation Guide

Prerendering for Next.js: Setup Guide

Production prerendering for Next.js App Router: bot detection middleware, reverse proxy routing, WAF whitelist, validation.

8 min readUpdated
Prerendering for Next.js: Setup Guide

Article

Implementing prerendering for a Next.js application requires four components: bot detection middleware, reverse proxy routing, WAF whitelist configuration, and snapshot validation. This guide covers all four with production-ready code for Next.js App Router (v13+) and ostr.io as the prerendering service.

ostr.io is a managed prerendering service that delivers deterministic HTML snapshots to search crawlers and AI retrieval systems through proxy-level middleware, without requiring framework rewrites. The middleware patterns below work with any prerendering service; ostr.io-specific configuration is noted where applicable.

Step 1: Identify Which Routes Need Prerendering

Not every route in a Next.js App Router application needs prerendering. Server Components, Static Pages (with generateStaticParams), and ISR pages are already crawler-friendly. Prerendering adds value for:

  • Routes using 'use client' components with data fetching (useEffect, React Query, SWR)
  • Routes with significant client-side state that populates after mount
  • Routes using Web Components or Shadow DOM content
  • Any route where View Page Source shows significantly less content than the rendered DOM

Check each route category:

bash
# Find all client component files
grep -r "'use client'" ./app --include="*.tsx" -l
# Check which routes use client-side data fetching
grep -r "useEffect\|useSWR\|useQuery" ./app --include="*.tsx" -l

Routes that are purely Server Components with no client-side data fetching can skip prerendering. Routes with the patterns above need it.

Raster technical flow diagram for Prerendering Implementation Guide for Next.js: Setup, Routing, and Validation — delivery paths, caching, and crawler-facing HTML.

Step 2: Implement Bot Detection Middleware

Next.js middleware runs on every request at the Edge. The bot detection logic intercepts crawler requests and routes them to the prerendering service.

typescript
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
const PRERENDERING_SERVICE_URL = process.env.PRERENDERING_SERVICE_URL ?? 'https://ostr.io'
const PRERENDERING_TOKEN = process.env.PRERENDERING_API_TOKEN ?? ''
// Bot User-Agent patterns — keep updated as new AI crawlers launch
const BOT_UA_REGEX =
/googlebot|bingbot|yandexbot|duckduckbot|slurp|baiduspider|sogou|exabot|facebot|ia_archiver|gptbot|claudebot|applebot|anthropic-ai|perplexitybot|bytespider/i
// Routes to exclude from prerendering
const EXCLUDED_PATHS = [
/^\/_next\//, // Next.js internal assets
/^\/api\//, // API routes
/^\/static\//, // Static files
/\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$/i, // Assets
]
function isBot(userAgent: string): boolean {
return BOT_UA_REGEX.test(userAgent)
}
function isExcluded(pathname: string): boolean {
return EXCLUDED_PATHS.some((pattern) => pattern.test(pathname))
}
export function middleware(req: NextRequest) {
const ua = req.headers.get('user-agent') ?? ''
const { pathname } = req.nextUrl
// Skip non-bot traffic and excluded paths
if (!isBot(ua) || isExcluded(pathname)) {
return NextResponse.next()
}
// Build the prerendering request URL
const targetUrl = `${PRERENDERING_SERVICE_URL}/render/${req.url}`
// Forward to prerendering service with auth header
return NextResponse.rewrite(targetUrl, {
headers: {
'X-Prerender-Token': PRERENDERING_TOKEN,
'X-Forwarded-For': req.headers.get('x-forwarded-for') ?? req.ip ?? '',
},
})
}
export const config = {
matcher: [
// Match all routes except Next.js internals and static assets
'/((?!_next/static|_next/image|favicon.ico).*)',
],
}

Step 3: Configure Nginx Reverse Proxy (Alternative to Middleware)

If your deployment uses Nginx as a reverse proxy (common for non-Vercel deployments), configure bot routing at the Nginx layer:

nginx
# /etc/nginx/conf.d/prerendering.conf
mapmap $http_user_agent $is_bot {
default 0;
"~*googlebot" 1;
"~*bingbot" 1;
"~*yandexbot" 1;
"~*gptbot" 1;
"~*claudebot" 1;
"~*applebot" 1;
"~*anthropic-ai" 1;
"~*perplexitybot" 1;
}
serverserver {
listenlisten 443 ssl;
server_nameserver_name yourdomain.com;
locationlocation / {
# Route bots to prerendering service
ifif ($is_bot) {
proxy_passproxy_pass https://ostr.io/render/$scheme://$host$request_uri;
proxy_set_header X-Prerender-Token $PRERENDERING_TOKEN;
proxy_set_header X-Forwarded-For $remote_addr;
breakbreak;
}
# Route users to Next.js app
proxy_passproxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

Step 4: Add ostr.io IP Ranges to WAF Whitelist

Before prerendering traffic can reach your origin, ostr.io's IP ranges must be allowed in your WAF.

Cloudflare:

  1. Go to Security → WAF → Tools → IP Access Rules
  2. Add each ostr.io IP range with action: Allow
  3. Note: Enter each CIDR block separately (Cloudflare does not support bulk import in the UI — use the API for large range lists)
bash
# Cloudflare API bulk allowlist
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/firewall/access_rules/rules" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mode":"whitelist","configuration":{"target":"ip_range","value":"<ostr.io CIDR>"},"notes":"ostr.io prerendering"}'

AWS WAF:

hcl
# Terraform: AWS WAF IP set for ostr.io
resource "aws_wafv2_ip_set" "ostrio" {
name = "ostr-io-prerendering"
scope = "CLOUDFRONT"
ip_address_version = "IPV4"
addresses = [/* ostr.io IP ranges */]
}
resource "aws_wafv2_web_acl_rule" "allow_ostrio" {
name = "AllowOstrio"
priority = 1
action { allow {} }
statement {
ip_set_reference_statement {
arn = aws_wafv2_ip_set.ostrio.arn
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AllowOstrio"
sampled_requests_enabled = true
}
}

Raster comparison panel summarizing architectural tradeoffs discussed in Prerendering Implementation Guide for Next.js: Setup, Routing, and Validation.

Step 5: Configure Cache TTL per Route Type

Different content types have different freshness requirements. Configure TTL per URL pattern in ostr.io's dashboard or via API:

typescript
// ostr.io TTL configuration via API
const ttlConfig = {
patterns: [
{ match: '/blog/**', ttl: 3600 }, // Blog posts: 1 hour
{ match: '/product/**', ttl: 900 }, // Products: 15 min (pricing)
{ match: '/category/**', ttl: 1800 }, // Categories: 30 min
{ match: '/docs/**', ttl: 86400 }, // Docs: 24 hours
{ match: '/', ttl: 3600 }, // Homepage: 1 hour
]
}

Rule of thumb by content type:

  • Prices, inventory, real-time data: 10–15 minutes
  • Product pages, listing pages: 15–30 minutes
  • Blog posts, static content: 1–4 hours
  • Documentation, legal pages: 24 hours

Step 6: Set Up Cache Warming API Integration

Trigger snapshot warming on content publish events. For Next.js, the natural hook is after revalidatePath():

typescript
// lib/prerendering.ts
export async function warmSnapshot(url: string, priority: 'high' | 'normal' = 'normal') {
if (!process.env.OSTRIO_API_KEY) return
try {
await fetch('https://ostr.io/api/cache-warm', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.OSTRIO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ url, priority }),
})
} catch {
// Non-critical — log but don't throw
console.warn(`Cache warm failed for ${url}`)
}
}
// In your CMS webhook handler or Server Action
import { revalidatePath } from 'next/cache'
import { warmSnapshot } from '@/lib/prerendering'
export async function publishContent(slug: string) {
const url = `https://yourdomain.com/blog/${slug}`
// Revalidate ISR cache
revalidatePath(`/blog/${slug}`)
// Warm prerendering snapshot
await warmSnapshot(url, 'high')
}

Step 7: Test the Prerendering Configuration

Before going live, verify that:

1. Bots receive prerendered HTML:

bash
# Simulate Googlebot request
curl -A "Googlebot" https://yourdomain.com/blog/your-post -I
# Expected: X-Prerender-Status: hit or X-Prerender-Status: rendered header
# If missing: middleware is not routing bot traffic correctly

2. Users receive the live Next.js application:

bash
# Simulate user request (no bot UA)
curl https://yourdomain.com/blog/your-post -I
# Expected: no prerender headers, normal Next.js response

3. WAF is not blocking prerendering traffic: Check ostr.io dashboard for WAF-blocked render attempts. Check your WAF logs for blocked requests from ostr.io IP ranges.

4. DOM Consistency Score is above 95%: Use ostr.io's dashboard to compare the rendered snapshot for a representative product page against the live page. DOM Consistency Score should be 95%+.

5. Prerendering.info Checker: Submit your domain to the Prerender Checker and verify that the tool can retrieve a complete prerendered snapshot for your most important pages.

Step 8: Validate JSON-LD and Structured Data

Structured data (JSON-LD) must appear in the prerendered HTML, not be injected by client-side JavaScript. Verify with View Page Source (not DevTools, which shows the rendered DOM):

bash
# Check JSON-LD in prerendered output
curl -A "Googlebot" https://yourdomain.com/product/your-product | grep -o 'application/ld+json' | wc -l
# Should return 1 or more

If JSON-LD blocks are missing from the prerendered HTML, they are being generated client-side. Move them to Server Components.

Step 9: Monitor with Google Search Console

After going live, monitor these signals in Google Search Console:

  • Crawl Stats → Render type: Should shift from "JavaScript" to "HTML" rendering
  • Coverage → Not indexed: Should decrease over 4–8 weeks as snapshots reach Googlebot
  • URL Inspection: For any page still not indexed, use URL Inspection to fetch and render — verify the prerendered output is complete
FAQ

Frequently Asked Questions

Yes. Next.js middleware runs on Vercel's Edge Runtime by default. The `NextResponse.rewrite()` call routes bot requests to ostr.io at the edge, before reaching the Next.js application origin. This is the recommended Vercel deployment pattern — it adds zero latency for user traffic.

Exclude authenticated routes from prerendering. If the prerendering service attempts to render a page behind authentication, it receives a login redirect — not the page content. In your middleware, add authenticated route paths to the `EXCLUDED_PATHS` array.

No. Server Components are rendered server-side by Next.js for all requests. For Server Component pages, prerendering adds a caching layer on top of the server-rendered output — the output should be identical. The primary value of prerendering for App Router sites is for pages with Client Components that fetch data after mount.

Run the ostr.io service locally or use the hosted service with a tunnel (ngrok, Cloudflare Tunnel) to expose your local Next.js server. Test using `curl -A "Googlebot" https://your-tunnel-url/your-page`. The prerendering pre-launch checklist covers the full local testing workflow. !Raster matrix diagram of operational levers, risks, and validation checks for Prerendering Implementation Guide for Next.js: Setup, Routing, and Validation.

Editorial trust

Written by prerender Editorial · Engineering Team. We build and run pre-rendering infrastructure for more than 200 engineering teams, which is where the numbers and code samples on this page come from.

Last updated . Editorial scope and review policy: About prerender.info.