Draft vs published - preview host routing and caching
Draft vs published: preview host routing and caching
TL;DR
- Use a Preview Token (against the preview host) for draft content and a Delivery Token (against the CDN host) for published content -- never mix them.
- The safest architecture is two separate deployments (production and preview) with identical codebases but different environment variables.
- Preview caching must be explicitly disabled at every layer (CDN, browser, framework fetch cache) or editors will see stale drafts.
- The Live Preview SDK auto-detects iframe context and redirects SDK calls to the preview API, including the real-time live_preview hash.
Every content entry in Contentstack exists in one of two states: saved as a draft, or published to an environment. The entire preview infrastructure exists to answer one question reliably: can an editor see exactly what the page will look like before they hit publish? Getting that right requires deliberate separation of how draft content and published content are fetched, routed, and cached.
As covered in Lesson 4.1.1, preview and production operate on different delivery planes. This lesson focuses on the concrete mechanics: how tokens control access to draft versus published content, how hosting architecture routes requests to the correct plane, and why caching strategy must differ between the two.
Preview Token vs Delivery Token
Contentstack uses two distinct token types to separate draft and published access at the API level.
A Delivery Token is scoped to a specific environment (for example, production or staging). When you make a request to the Content Delivery API at cdn.contentstack.io using a Delivery Token, the response only includes entries that have been published to that environment. Unpublished drafts, in-progress edits, and entries awaiting approval are invisible. This is exactly what you want for your live site.
A Preview Token grants access to the latest saved version of every entry, regardless of publish state. Requests go to your region's REST Preview host (for example, rest-preview.contentstack.com for AWS NA, eu-rest-preview.contentstack.com for AWS EU). Every region has its own preview host -- see the full endpoint table in Lesson 3.1.2 for all seven regions. If an editor has saved changes but not yet published, the Preview Token returns those unsaved changes. If an entry has never been published, the Preview Token still returns it.
You generate both tokens under Settings > Tokens in the Contentstack dashboard. The critical difference is behavioral:
import { getContentstackEndpoints } from "@timbenniks/contentstack-endpoints";
const endpoints = getContentstackEndpoints(process.env.NEXT_PUBLIC_CONTENTSTACK_REGION || "na");
// Production request - only returns published content
const productionHeaders = {
api_key: process.env.NEXT_PUBLIC_CONTENTSTACK_API_KEY,
access_token: process.env.NEXT_PUBLIC_CONTENTSTACK_DELIVERY_TOKEN,
environment: "production",
};
const productionUrl = `${endpoints.contentDelivery}/v3/content_types/page/entries`;
// Preview request - returns latest draft, including unpublished changes
const previewHeaders = {
api_key: process.env.NEXT_PUBLIC_CONTENTSTACK_API_KEY,
preview_token: process.env.NEXT_PUBLIC_CONTENTSTACK_PREVIEW_TOKEN,
environment: "production",
};
const previewUrl = `${endpoints.preview}/v3/content_types/page/entries`;The @timbenniks/contentstack-endpoints package resolves the correct base URLs for any region string. It is a zero-dependency helper that stays in sync with Contentstack's official regions data. You could just as easily look up the hosts from the endpoint documentation and set them manually -- the package simply removes the risk of hardcoding the wrong host for your region.
flowchart LR
subgraph Production
A[Frontend] -->|Delivery Token| B[CDA host for your region]
B --> C[Published content only]
end
subgraph Preview
D[Preview Frontend] -->|Preview Token| E[REST Preview host for your region]
E --> F[Latest draft + unpublished]
endNotice that both requests specify the same environment. The Preview Token does not bypass environments - it still respects the environment scope. What it bypasses is the publish gate. The entry does not need to be published to that environment for the Preview Token to return it.
Common pitfall:
If you accidentally use a Delivery Token in your preview environment, editors will only see already-published content -- and the bug often goes unnoticed during setup because testing happens with already-published entries.
This distinction has a direct architectural implication: your preview deployment must use a different token and a different API host than your production deployment.
Two hosting approaches
There are two primary architectures for separating preview from production traffic.
Approach 1: separate preview host
The most common and most reliable approach uses two distinct deployments:
- www.example.com - the production deployment, using the Delivery Token against cdn.contentstack.io
- preview.example.com - the preview deployment, using the Preview Token against your region's preview host (for example, rest-preview.contentstack.com in AWS NA)
Both deployments run the same frontend application codebase. The difference is entirely in environment configuration. The preview deployment reads NEXT_PUBLIC_CONTENTSTACK_PREVIEW_TOKEN instead of NEXT_PUBLIC_CONTENTSTACK_DELIVERY_TOKEN, and targets the preview API host.
flowchart TD
subgraph Same Codebase
A[Git Repository]
end
A --> B[Production Deploy
www.example.com]
A --> C[Preview Deploy
preview.example.com]
B -->|NEXT_PUBLIC_CONTENTSTACK_DELIVERY_TOKEN| D[cdn.contentstack.io]
C -->|NEXT_PUBLIC_CONTENTSTACK_PREVIEW_TOKEN| E[rest-preview.contentstack.com]# Production environment variables (.env.production) NEXT_PUBLIC_CONTENTSTACK_API_KEY=your_api_key NEXT_PUBLIC_CONTENTSTACK_DELIVERY_TOKEN=your_delivery_token NEXT_PUBLIC_CONTENTSTACK_ENVIRONMENT=production NEXT_PUBLIC_CONTENTSTACK_REGION=eu NEXT_PUBLIC_CONTENTSTACK_PREVIEW=false # Preview environment variables (.env.preview) NEXT_PUBLIC_CONTENTSTACK_API_KEY=your_api_key NEXT_PUBLIC_CONTENTSTACK_DELIVERY_TOKEN=your_delivery_token NEXT_PUBLIC_CONTENTSTACK_PREVIEW_TOKEN=your_preview_token NEXT_PUBLIC_CONTENTSTACK_ENVIRONMENT=production NEXT_PUBLIC_CONTENTSTACK_REGION=eu NEXT_PUBLIC_CONTENTSTACK_PREVIEW=true
Both deployments share the same NEXT_PUBLIC_CONTENTSTACK_REGION value. The region determines all API hosts -- you never hardcode host URLs directly. The preview deployment adds NEXT_PUBLIC_CONTENTSTACK_PREVIEW_TOKEN and sets NEXT_PUBLIC_CONTENTSTACK_PREVIEW=true.
This approach is favored because it provides complete isolation. The production deployment never touches the preview API, eliminating any chance of draft content leaking to public visitors. The preview deployment can have different caching rules, different authentication, and different infrastructure scaling without affecting production.
In Contentstack, you configure this under Settings > Live Preview. The Live Preview configuration requires a Preview URL - this is the base URL of your preview deployment (for example, https://preview.example.com). When an editor opens Live Preview in the entry editor, Contentstack loads this URL in an iframe and passes context parameters including the entry UID, content type, and locale.
Approach 2: single host with mode switching
Some teams prefer running a single deployment that switches behavior dynamically based on request context:
// Next.js middleware example: detect preview mode from query parameters
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const isPreview = request.nextUrl.searchParams.has("live_preview");
if (isPreview) {
const response = NextResponse.next();
response.headers.set("Cache-Control", "no-store, no-cache, must-revalidate");
response.headers.set("X-Content-Mode", "preview");
return response;
}
return NextResponse.next();
}This approach reduces infrastructure cost - one deployment instead of two - but introduces risk. A bug in the mode-switching logic could expose draft content to production visitors, or apply production caching to preview requests. For teams with strict content governance requirements, the separate host approach is safer.
URL routing: preview must mirror production
A subtle but critical requirement: the URL structure of your preview deployment must match your production deployment exactly. If the production URL for a blog post is www.example.com/blog/q3-earnings-report, the preview URL must be preview.example.com/blog/q3-earnings-report.
Worth noting: Editors navigate preview by content, not by URL. When an editor opens Live Preview for an entry, Contentstack constructs the preview URL by combining the preview host base URL with the entry's URL path. If your preview deployment uses a different routing scheme than production, editors will land on 404 pages or see the wrong content.
Consider a corporate website where a marketing page lives at /solutions/enterprise. The preview configuration in Contentstack must resolve to preview.example.com/solutions/enterprise. If your preview deployment is a separate build that uses different route conventions, the mapping breaks.
// Contentstack Live Preview URL resolution // Base URL configured in Settings > Live Preview: https://preview.example.com // Entry URL path from content type URL field: /solutions/enterprise // Resolved preview URL: https://preview.example.com/solutions/enterprise // Your production and preview deployments MUST both resolve this path // to the same page component rendering the same entry
The practical guideline is straightforward: deploy the same application code to both production and preview. The only difference should be environment variables controlling which token and API host to use.
Caching: the fundamental divergence
Caching is where preview and production requirements directly conflict, and where most preview implementations break down.
Production caching strategy
For production, aggressive caching is desirable and expected:
- CDN caching: Cache rendered pages at the edge for minutes or hours. Contentstack publishes trigger webhook-based invalidation.
- Browser caching: Set Cache-Control: public, max-age=3600 or longer for static pages.
- API response caching: Cache Delivery API responses in-memory or in Redis to reduce API calls.
- ISR/SSG caching: Use framework Incremental Static Regeneration or full static generation to avoid runtime API calls entirely.
This is standard practice. The Delivery API responses are immutable between publishes, so caching them aggressively is safe and efficient.
Preview caching strategy
For preview, caching is the enemy. An editor saves a draft, opens Live Preview, and expects to see the change immediately. If any layer - CDN, browser, application, or API response cache - serves a stale version, the editor loses trust in the preview system.
function setCacheHeaders(res: Response, mode: "production" | "preview") {
if (mode === "preview") {
// Prevent ALL caching for preview responses
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "0");
res.setHeader("Surrogate-Control", "no-store");
// CDN-specific headers (Vercel, Cloudflare, Fastly)
res.setHeader("CDN-Cache-Control", "no-store");
} else {
// Aggressive caching for production
res.setHeader("Cache-Control", "public, s-maxage=3600, stale-while-revalidate=86400");
}
}The Surrogate-Control and CDN-Cache-Control headers are important because some CDN platforms (Fastly, Cloudflare) respect these headers separately from Cache-Control. If you only set Cache-Control: no-store, the CDN might still cache the response based on its own rules.
Caching pitfalls in practice
Pitfall 1: CDN caching preview responses. If your preview deployment sits behind Cloudflare, Vercel's Edge Network, or another CDN, ensure the CDN configuration skips caching for the preview hostname entirely. On Vercel, this means setting cache headers in your application code - Vercel respects Cache-Control headers from your application. On Cloudflare, create a page rule or cache rule for preview.example.com/* with cache level set to "Bypass."
Pitfall 2: Browser caching from a previous production visit. If a user visits the production site and their browser caches a page, then visits the preview site with the same URL path, the browser might serve the cached production version. The no-store directive prevents this, but only if it is set on the preview response. The initial preview request must already include the correct cache headers.
Pitfall 3: Application-level caching in SSR. Frameworks like Next.js have their own data caching layer. In Next.js App Router, fetch() calls are cached by default. For preview, you'll want to explicitly opt out:
import { getContentstackEndpoints } from "@timbenniks/contentstack-endpoints";
const endpoints = getContentstackEndpoints(process.env.NEXT_PUBLIC_CONTENTSTACK_REGION || "na");
async function getPageContent(slug: string, isPreview: boolean) {
const host = isPreview ? endpoints.preview : endpoints.contentDelivery;
const token = isPreview
? process.env.NEXT_PUBLIC_CONTENTSTACK_PREVIEW_TOKEN
: process.env.NEXT_PUBLIC_CONTENTSTACK_DELIVERY_TOKEN;
const tokenHeader = isPreview ? "preview_token" : "access_token";
const response = await fetch(
`${host}/v3/content_types/page/entries?query={"url":"/${slug}"}`,
{
headers: {
api_key: process.env.NEXT_PUBLIC_CONTENTSTACK_API_KEY!,
[tokenHeader]: token!,
environment: "production",
},
cache: isPreview ? "no-store" : "force-cache",
next: isPreview ? { revalidate: 0 } : { revalidate: 3600 },
}
);
return response.json();
}How the Live Preview SDK detects preview mode
The Contentstack Live Preview SDK (@contentstack/live-preview-utils) automates much of the preview detection and token switching. When initialized, the SDK checks whether the application is loaded inside the Contentstack entry editor iframe. If it detects the iframe context - by reading query parameters and listening for postMessage events from the Contentstack app - it activates preview mode automatically.
import Contentstack from "@contentstack/delivery-sdk";
import ContentstackLivePreview from "@contentstack/live-preview-utils";
import { getContentstackEndpoints } from "@timbenniks/contentstack-endpoints";
const endpoints = getContentstackEndpoints(
process.env.NEXT_PUBLIC_CONTENTSTACK_REGION || "na",
true
);
const stack = Contentstack.stack({
apiKey: process.env.NEXT_PUBLIC_CONTENTSTACK_API_KEY!,
deliveryToken: process.env.NEXT_PUBLIC_CONTENTSTACK_DELIVERY_TOKEN!,
environment: process.env.NEXT_PUBLIC_CONTENTSTACK_ENVIRONMENT!,
region: process.env.NEXT_PUBLIC_CONTENTSTACK_REGION,
live_preview: {
enable: process.env.NEXT_PUBLIC_CONTENTSTACK_PREVIEW === "true",
preview_token: process.env.NEXT_PUBLIC_CONTENTSTACK_PREVIEW_TOKEN,
host: endpoints.preview,
},
});
ContentstackLivePreview.init({
ssr: false,
enable: process.env.NEXT_PUBLIC_CONTENTSTACK_PREVIEW === "true",
mode: "builder",
stackSdk: stack.config,
stackDetails: {
apiKey: process.env.NEXT_PUBLIC_CONTENTSTACK_API_KEY!,
environment: process.env.NEXT_PUBLIC_CONTENTSTACK_ENVIRONMENT!,
},
clientUrlParams: {
host: endpoints.application,
},
editButton: {
enable: true,
exclude: ["outsideLivePreviewPortal"],
},
});The clientUrlParams.host must match the Contentstack web app for your region (for example, eu-app.contentstack.com for AWS EU). The live_preview.host must match the REST Preview host for your region. Both are resolved automatically from the region string by getContentstackEndpoints() -- pass true as the second argument to strip the https:// prefix, which is what the SDK expects for host values.
The editButton configuration adds a floating edit button that opens the Visual Builder when an editor clicks it. The exclude: ["outsideLivePreviewPortal"] setting ensures it only appears inside the Contentstack preview iframe, not when visitors browse the preview URL directly.
When Live Preview is active, the SDK intercepts calls made through the Contentstack Delivery SDK and redirects them to the preview API host with the Preview Token. This means your application code can use the same SDK calls for both production and preview - the Live Preview SDK handles the routing transparently.
The SDK also manages the live_preview hash, a session-specific identifier that Contentstack uses to serve the exact in-memory draft state the editor is currently working on. Without this hash, the preview API returns the last saved draft. With the hash, it returns the real-time editing state, including changes the editor has typed but not yet saved.
Putting it together: corporate website example
Consider a corporate website with the following setup:
- Production: www.acmecorp.com, deployed on Vercel, using Delivery Token
- Preview: preview.acmecorp.com, deployed on a second Vercel project, using Preview Token
- Content types: page, blog_post, press_release
The Contentstack configuration under Settings > Live Preview sets the preview URL to https://preview.acmecorp.com. Both deployments share the same Git repository and build pipeline. The only difference is the environment variables injected at build time.
When a marketing editor creates a new press release and wants to preview it before publishing:
- The editor opens the press release entry in Contentstack and clicks the Live Preview icon.
- Contentstack loads https://preview.acmecorp.com/press-releases/q4-results in the preview panel iframe.
- The preview deployment receives the request, detects preview mode (the Live Preview SDK reads the iframe context), and fetches the draft press release using the Preview Token from rest-preview.contentstack.com.
- The page renders with the unpublished content. No caching occurs - the response headers include Cache-Control: no-store.
- The editor modifies the headline. The Live Preview SDK receives a postMessage event with the updated content and re-renders the page in real time.
Meanwhile, public visitors to www.acmecorp.com/press-releases see only published press releases. The production deployment uses the Delivery Token, which cannot access the draft. The CDN caches the production response aggressively, serving it in milliseconds.
Common mistakes
Mistake 1: using the Delivery Token in the preview deployment
If the preview environment uses a Delivery Token instead of a Preview Token, editors only see already-published content. New entries and draft changes are invisible. This often goes unnoticed during initial setup because the developer tests with already-published entries. The bug surfaces when an editor creates a brand-new entry and preview shows a 404.
Mistake 2: applying production cache rules to the preview host
Teams that use a single CDN configuration for both production and preview domains inadvertently cache draft content. The editor sees a stale version, refreshes, sees the same stale version, and concludes that preview is broken. The fix is explicit: configure no-cache rules for the preview domain at the CDN level and in the application response headers.
Mistake 3: mismatched URL paths between preview and production
If the preview deployment uses a different routing structure (for example, /preview/blog/slug instead of /blog/slug), Contentstack cannot construct the correct preview URL. Editors see 404 pages or the wrong content. Both deployments must resolve the same URL paths to the same content.