Managing Draft vs Published Content States in Frontend
Draft state leaks into production when a frontend fails to isolate unpublished content from its caches — usually through shared query keys, missing cache-control directives, or sloppy webhook filtering. This page shows how to keep draft and published as separate execution contexts with distinct caching lifecycles, using token-gated routing and cache-key stratification.
Where leakage comes from
The common failure modes are shared query keys, absent cache-control directives, and indiscriminate webhook handling. When a CMS emits a draft-to-published webhook, many Jamstack apps trigger blanket ISR revalidation or full-site rebuilds. That creates race conditions: draft content briefly surfaces on production routes, or published updates stall until the next cache expiry.
The CMS is rarely at fault. The problem is the frontend not holding a hard boundary between ephemeral preview contexts and immutable production ones. Without routing guards, token validation, and conditional data-fetching, draft payloads bleed into static generation.
Two parallel pipelines
Split content resolution. The production pipeline serves published content via SSG or ISR with aggressive CDN caching for low latency. The preview pipeline runs on demand, bypasses edge caches, and uses short-lived tokens to fetch draft variants straight from the CMS API. Aligning both with Preview & Draft Workflow Patterns keeps transitions predictable and gives editors accurate real-time feedback without risking production.
The two pipelines diverge at the cookie set by the server-side token exchange:
flowchart TD
A["Request /posts/my-article"] --> B{"preview_mode cookie set?"}
B -->|no| C["Production pipeline"]
C --> D["SSG / ISR, status=published"]
D --> E["public, s-maxage CDN cache"]
B -->|yes| F["Preview pipeline"]
F --> G["Validate token server-side"]
G --> H["Fetch draft, append status=draft"]
H --> I["private, no-store (bypass edge)"]
Enforce three invariants:
- Draft routes are never statically generated at build time.
- Preview tokens are validated server-side before any data resolution.
- Cache keys include state identifiers (
draft=truevspublished=true) to prevent collisions.
Token-gated preview routing
Client-side routing guards are trivially bypassed and offer zero protection against cache poisoning. Use a server-side token exchange that sets an httpOnly, Secure cookie with a short expiry. That cookie is the state flag for downstream fetches.
// app/api/preview/route.ts
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const token = searchParams.get('token');
const slug = searchParams.get('slug');
const secret = process.env.PREVIEW_SECRET;
// Validate against environment-stored secret
if (token !== secret || !slug) {
return NextResponse.json({ error: 'Invalid preview request' }, { status: 401 });
}
const cookieStore = await cookies();
cookieStore.set('preview_mode', 'true', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
path: '/',
maxAge: 60 * 30, // 30 minutes
sameSite: 'lax'
});
// Redirect to the requested slug in preview mode
return NextResponse.redirect(new URL(slug, request.url));
}
Server components and route handlers then inspect the cookie before querying. When preview_mode is set, the data layer switches to draft endpoints, appends ?status=draft, and disables response caching — the server-side-first approach the Next.js Draft Mode docs recommend over client-side routing.
Cache-key stratification
Key collisions are the primary leakage vector. A CDN typically hashes path plus query parameters; if draft and published requests hash identically, the first cached response serves both. Stratify explicitly:
- Production:
GET /posts/my-article→public, max-age=3600, s-maxage=86400 - Preview:
GET /posts/my-article?preview=true→private, no-store, max-age=0
Configure the CDN to Vary on the auth cookie. Enforcing Draft State Management at the data-fetching layer keeps draft payloads out of shared cache buckets.
Scope webhook filtering precisely too. Instead of a blanket revalidatePath('/'), parse the payload for the affected document ID and slug and call targeted revalidation. That shrinks the invalidation blast radius and removes race conditions during high-frequency updates.
Operational guardrails
Provision content teams with deterministic preview URLs generated by the CMS rather than hand-appended query parameters. Add lint rules that flag client-side fetches targeting draft endpoints in production builds.
For accessibility compliance, keep preview routes structurally identical to published ones — same DOM, same ARIA landmarks. State toggling must never alter semantic markup, since screen readers depend on a consistent hierarchy. During legacy decoupling, map legacy draft states onto the token-gated pipeline through a middleware translation layer to keep backward compatibility.
Token validation, explicit cache stratification, and targeted webhook routing together isolate ephemeral draft state from immutable production caches — eliminating leakage, accelerating editorial workflows, and preserving Jamstack performance guarantees.