explains how the docs site achieves fast static page loads while maintaining CMS edit mode functionality
This guide explains how the docs site achieves fast static page loads while maintaining CMS edit mode functionality.
We use two routes for the same content:
app/
├── [...slug]/page.tsx # Production: force-static (fast)
└── preview/[...slug]/page.tsx # Edit mode: force-dynamic (reads searchParams)
A proxy middleware transparently rewrites edit mode requests:
User requests: /en/headless/quickstart?edit_mode=true
↓
Proxy detects ?edit_mode=true
↓
Internal rewrite to: /preview/en/headless/quickstart?edit_mode=true
↓
Dynamic route renders with edit wrappers
The user never sees /preview in their URL - it's an internal rewrite.
// app/[...slug]/page.tsx
export const dynamic = 'force-static';
export const revalidate = 60;
export default async function Page({ params }) {
// No searchParams - page is fully static
return
// app/preview/[...slug]/page.tsx
export const dynamic = 'force-dynamic';
export default async function PreviewPage({ params, searchParams }) {
// searchParams available - can detect edit_mode
return <ParametricRoutePage params={params
?edit_mode=true from URL// src/proxy.ts
export const proxy = async (request: NextRequest) => {
const { pathname, searchParams } = request.nextUrl;
const editMode = searchParams.get('edit_mode');
| Scenario | Response Time | Rendering |
|---|---|---|
| Production page (cached) | ~50-100ms | Static from CDN |
| Production page (stale) | ~50-100ms + background refresh | Static, then ISR |
| Edit mode | ~500-1500ms | Dynamic SSR |
The revalidate = 60 setting enables ISR. This does NOT mean pages recompute every 60 seconds.
ISR uses a stale-while-revalidate pattern:
Request comes in:
→ Is cached page < 60s old? → Serve from cache (instant)
→ Is cached page > 60s old? → Serve stale cache (instant)
+ revalidate in background for next request
No requests = No recomputation. Ever.
Example:
The 60-second window is a safety net. Ideally, we'd use on-demand revalidation via webhooks when CMS content changes.
| Use Case | URL | Route Used |
|---|---|---|
| Normal browsing | /en/headless/quickstart | Static |
| CMS template builder | /en/headless/quickstart?edit_mode=true | Preview (via rewrite) |
| AI block preview | /en/headless/quickstart?ai_preview=1 | Preview (via rewrite) |
The simple approach (no force-static, no preview route) works but is slow:
// Simple approach - works but ~1-2s per request
export default async function Page({ params, searchParams }) {
return <ParametricRoutePage params={params} searchParams={searchParams} />;
}
This renders dynamically on every request. For a documentation site with many pages, this means:
The dual-route architecture gives us the best of both worlds: static speed for readers, dynamic functionality for editors.