All Systems Operational
Powered By
profound-logo
profound-logoProfound CMS
⌘K
Admin

Static Rendering With Edit Mode Support

explains how the docs site achieves fast static page loads while maintaining CMS edit mode functionality

Continue Reading
Previous‹Setup Admin Panel ProxyNextScripting In Template Builder›

Hybrid

Setup Hybrid CMS ProjectParametric Pages with ComponentsTypes of ComponentsSetup server sent events (SSE) content refetchSetup admin panel proxyStatic Rendering with Edit Mode SupportScripting in Template BuilderGetting Started Hybrid

Headless

Quick startSplit Screen JSON Component Builder with LLMComponent Zod Pull

MCP

MCP with Claude Code / Codex

Feature

Documentation Site TemplateFeature Template BuilderTranslation ServiceOrganizations & Website Heirarchy

Motivation

Our Approch

Terminology

'Hybrid' vs 'Headless'

This guide explains how the docs site achieves fast static page loads while maintaining CMS edit mode functionality.

Dual Route Architecture

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.

How It Works

Production Path (Default)

// 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
  • Pages are pre-rendered at build time
  • Served instantly from CDN edge cache
  • Revalidated every 60 seconds (ISR) as a safety net

Edit Mode Path

// 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
  • Rendered on every request
  • Can read ?edit_mode=true from URL
  • Renders with CMS editable wrappers and block outlines

The Proxy Rewrite

// src/proxy.ts
export const proxy = async (request: NextRequest) => {
  const { pathname, searchParams } = request.nextUrl;

  const editMode = searchParams.get('edit_mode');

Performance Comparison

ScenarioResponse TimeRendering
Production page (cached)~50-100msStatic from CDN
Production page (stale)~50-100ms + background refreshStatic, then ISR
Edit mode~500-1500msDynamic SSR

Understanding ISR (Incremental Static Regeneration)

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:

  • Page cached at 10:00 AM
  • No visitors until 3:00 PM
  • First visitor at 3:00 PM gets stale cache instantly
  • Background refresh happens, next visitor gets fresh content

The 60-second window is a safety net. Ideally, we'd use on-demand revalidation via webhooks when CMS content changes.

When to Use Each Route

Use CaseURLRoute Used
Normal browsing/en/headless/quickstartStatic
CMS template builder/en/headless/quickstart?edit_mode=truePreview (via rewrite)
AI block preview/en/headless/quickstart?ai_preview=1Preview (via rewrite)

Why Not Just Use Dynamic Rendering?

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:

  • Cold starts on every page
  • No CDN edge caching benefit
  • 1-2 second load times vs milliseconds

The dual-route architecture gives us the best of both worlds: static speed for readers, dynamic functionality for editors.

<
ParametricRoutePage
params
={
params
} />;
}
}
searchParams
={
searchParams
} />;
}
const
aiPreview
=
searchParams.
get
(
'ai_preview'
);
// Rewrite to preview route if edit_mode or ai_preview is present
if ((editMode === 'true' || editMode === '1' || aiPreview) && !pathname.startsWith('/preview')) {
const url = request.nextUrl.clone();
url.pathname = `/preview${pathname}`;
return NextResponse.rewrite(url);
}
return NextResponse.next();
};