In a headless CMS, “preview” often sounds like a simple feature: click a button, see your draft page. In practice it becomes a reliability problem. Editors need to trust what they see, developers need to control what is exposed, and both need the system to behave consistently across environments.
A good preview system is boring. It loads quickly, respects permissions, and matches production rendering as closely as possible. It also fails safely: if something goes wrong, it does not leak private content or create confusing, half-cached pages.
This post lays out a practical design that works for many CMS and frontend stacks. It is intentionally conceptual and implementation-agnostic, so you can adapt it whether you use server rendering, static generation, or a hybrid approach.
What “preview” means in a headless CMS
Preview is not a single capability. It is a bundle of expectations that differ by team. Before building anything, define which of these you need:
- Draft visibility: editors can view unpublished changes.
- Accurate rendering: the preview uses the same templates and components as production.
- Context: the editor can preview a piece of content in the page it appears on (for example, a hero banner inside a landing page).
- Isolation: one editor’s preview session does not affect another editor or the public website.
- Shareability: optionally, editors can share a preview link with stakeholders, usually time-limited.
Most preview failures happen when teams treat previews like a “staging site with extra data.” That approach mixes concerns: staging is an environment for testing code, while preview is a way to render draft content using trustworthy code.
Key Takeaways
- Make preview sessions user-scoped and time-limited, not globally cached.
- Use a dedicated “preview resolver” to map CMS entries to site routes.
- Define your preview mode as a set of guarantees: draft access, isolation, and rendering parity.
- Fail closed: if auth or routing is unclear, do not render private content.
A simple preview architecture that stays sane
A reliable preview setup typically has three moving parts: a way to authenticate preview sessions, a way to resolve “what URL should this content appear at,” and a way to render using draft data instead of published data.
The core flow (preview handshake)
- An editor clicks “Preview” in the CMS.
- The CMS opens your site’s
/api/previewendpoint with enough information to identify the content (entry ID, model type, locale) plus a signed token or secret. - Your site verifies the signature, then creates a short-lived preview session (often a cookie scoped to the domain and path).
- Your site resolves the content to a route (for example,
/products/atlas), then redirects the browser there. - The page renders in “preview mode,” fetching draft content using editor permissions or a server-side credential designed for preview.
Conceptually, your endpoints and responsibilities can look like this:
CMS "Preview" button
-> /api/preview (verify token, set preview session)
-> /api/resolve (entryId -> canonical route)
-> /some/page (render with preview data, no public caching)
Keeping a separate resolver step is useful because “entry ID” is not a URL. Pages often derive their route from a slug, a parent relationship, or a routing rule like “/blog/{category}/{slug}.” A resolver makes those rules explicit and testable.
Draft data without breaking caching
Preview mode and caching do not naturally mix. If you use a CDN or server cache, you must ensure preview responses are not stored as public artifacts. The simplest rule is: when preview mode is on, send headers that prevent shared caching and avoid putting preview pages into any static build output.
If your stack supports it, fetch draft content only on the server. That keeps tokens out of the browser and reduces accidental leakage through logs, devtools, or misconfigured analytics.
Content lifecycle and permissions
Preview is also a workflow feature. Editors are not only changing content, they are moving it through states such as draft, review, scheduled, and published. A preview system should mirror those states rather than invent new ones.
Decide early which permissions apply in preview mode:
- Who can preview drafts? CMS role-based access is a good source of truth.
- Can someone preview content they cannot read in the CMS? usually no.
- Can a preview link be shared? if yes, define an expiry and whether it should bypass the recipient’s CMS login.
A common, practical policy is “preview requires CMS authentication.” It is less convenient for external stakeholders, but it is much safer. If you need shareable links, treat them like expiring invitations and track who created them.
Implementation checklist you can copy
Use this as a build checklist and a review checklist. The goal is not a fancy preview experience, it is a predictable one.
Security and session handling
- Verify the CMS request with a signed secret or token, not just a query parameter.
- Set preview session cookies as
HttpOnly,Secure, and with an appropriateSameSitepolicy for your flow. - Set an expiry (for example, 30 to 120 minutes) and provide a
/api/preview/exitendpoint to clear it. - Fail closed: if token validation fails, show an error page that does not reveal content.
Routing and rendering parity
- Create a resolver that maps (content type, entry ID) to a canonical route.
- Use the same components/templates for preview and production. Avoid “preview-only” templates unless absolutely necessary.
- Handle missing routes gracefully (for example, content not yet placed on any page).
- Log resolver misses as structured events so you can fix mapping gaps.
Caching and performance
- Disable shared caching for preview responses (CDN and server side).
- If you use incremental regeneration, ensure preview requests do not trigger permanent rebuilds.
- Debounce or batch CMS refetches where possible, but keep it per-session.
- Provide a visible indicator in preview mode so editors do not confuse it with production.
Real-world example: product pages with shared components
Imagine a small B2B company with a headless CMS and a marketing site. Product pages live at /products/{slug}. Each product page is a composition: product details, a pricing table, testimonials, and a “request a demo” block that is shared across many pages.
An editor updates the pricing table and wants to see how it looks on three different products before publishing. Without a good preview system, they might:
- Publish the draft to staging and hope staging matches production.
- Ask engineering to deploy a special branch.
- Copy content into a doc and approximate layout manually.
With the architecture described earlier:
- The CMS “Preview” button for the pricing table entry opens your site’s preview endpoint.
- Your resolver knows where that pricing table is used. It offers a sensible default route (for example, the first associated product page) and optionally supports a query like
?context=/products/atlasfor choosing a specific page. - The page renders with draft pricing data while everything else remains published, so the editor sees the real-world composition.
- Because preview mode is session-scoped and non-cached, another editor can simultaneously preview a different draft without conflict.
This pattern reduces “preview anxiety”: the editor can iterate quickly and confidently, while engineering can keep production caching intact.
Common mistakes (and how to avoid them)
- Using a single global “preview environment.” This often turns into shared state where one person’s draft appears for another. Prefer user-scoped preview sessions.
- Letting preview pages be cached publicly. A CDN misconfiguration can expose draft content. In preview mode, disable shared caching and avoid indexing.
- Skipping the resolver and guessing URLs. If your CMS stores slugs, it is tempting to build URLs directly. That breaks when routing depends on relationships, locales, or section rules. Centralize mapping in one place.
- Embedding privileged tokens in the browser. Preview tokens can leak via logs, screenshots, or analytics. Fetch drafts server-side when you can.
- Preview parity drift. Over time, teams add “temporary preview hacks” that diverge from production rendering. Add a periodic check: preview uses the same components and styling bundle as production.
If you only fix one thing, fix caching. A safe preview that is occasionally imperfect is better than a perfect preview that leaks drafts.
When NOT to build a custom preview system
A custom preview system is worth it when content changes frequently and quality matters. It may be overkill when:
- Your site is mostly static and edits are rare, so manual review in a staging environment is acceptable.
- Your CMS and frontend already provide a robust built-in preview integration that meets your needs without custom tokens and routing logic.
- You cannot commit to maintaining the resolver rules as content models evolve (this maintenance is small but real).
- Your compliance or security requirements mandate strict separation where drafts must never leave the CMS, even into a preview renderer.
In those cases, a simpler workflow like “draft in CMS, review in CMS, publish, then validate quickly” can be more sustainable than a complex preview layer.
Conclusion
A dependable preview system is a trust-building feature: editors trust what they see, developers trust what is protected, and stakeholders trust the process. The calm approach is to scope preview to a short-lived session, resolve routes explicitly, render with production templates, and keep caching rules unambiguous.
If you treat preview as a product surface, not an afterthought, you reduce rework and prevent the subtle failures that make publishing feel risky.
FAQ
Should previews show only the draft entry, or the entire page as draft?
Start with “draft entry inside an otherwise published page.” It is easier to reason about, reduces unintended changes, and helps editors validate content in real context. Full-page draft rendering can be added later when you have clear rules for page composition.
How long should a preview session last?
Short enough to reduce risk, long enough to avoid annoyance. Many teams choose 30 to 120 minutes with an explicit “Exit preview” action. If you support shareable links, make those even shorter and auditable.
Can we allow stakeholders without CMS access to view previews?
You can, but treat it as a separate feature: expiring, unguessable links with clear scope and logging. If you do not need it often, requiring CMS login is simpler and safer.
What is the minimum a resolver must do?
Given a content identifier (type and ID), it must return the canonical route where that content should be previewed, or return “no route” with a helpful message. Anything beyond that, like choosing context pages, is optional.
How do we test previews without relying on manual clicking?
Write a small set of automated checks around resolver rules and preview session validation. For example: “entry ID X resolves to route Y,” “invalid token fails,” and “preview responses are not cached.” These tests catch regressions when content models change.