A preview route intentionally renders content that the public reader must reject. It needs a separate authenticated or narrowly scoped access path, a specific revision, and a no-store response policy. A random-looking URL alone is not sufficient if it is long-lived, reusable, or copied into logs and referral headers. The preview service should confirm the editor's current permission for the record, then fetch the requested revision from a privileged CMS connection kept on the server. The public page must not load draft data through the same cache key used for published content. Preview markup can reuse the public renderer, but data admission, authorization, cache headers, and search visibility must remain separate.
CMS Draft Preview Authorization and Cache Isolation
Working case
Editor 47 previews revision 9 of article 447 while revision 8 remains public. A CDN cache ignores the query string and stores the preview response under the normal article path. The next anonymous visitor receives unfinished text. A second implementation sets noindex in the HTML yet still allows the shared cache to store the body, so the leak remains. A third sends a preview token to browser analytics through the page URL. The repaired flow checks an editor session, record permission, and revision, obtains content server-side, sets no-store on every preview response, omits third-party scripts where possible, and keeps published pages on separate keys. Revoking access immediately blocks another preview request, even if the editor saved the URL earlier.
Implementation boundary
function previewDecision({ editorId, articleId, revision, grants }) {
const permitted = grants.some(grant =>
grant.editorId === editorId && grant.articleId === articleId
);
return {
allowed: permitted && Number.isInteger(revision) && revision > 0,
cacheControl: "private, no-store",
robots: "noindex, nofollow"
};
}
console.log(previewDecision({
editorId: 47, articleId: 447, revision: 9,
grants: [{ editorId: 47, articleId: 447 }]
}));Use a server-controlled preview endpoint whose input names record ID and revision but does not itself grant access. Authenticate the caller and check edit permission on that record for each request. If a signed short-lived capability is used for a cross-domain preview, bind its audience, record ID, revision, expiry, and nonce, and verify it server-side; do not treat a generic signed slug as adequate. Exclude preview responses from shared caches and service-worker offline stores. Send Cache-Control: private, no-store and prevent indexing, but regard these headers as defense in depth rather than substitutes for authorization. Keep draft HTML out of page metadata, feed generation, and prefetch targets. Preview must not mutate publication state or purge the public cache.
Cost and boundaries
Permission checks and privileged revision fetches cost at least one CMS or database read per preview unless the server has a correctly scoped private cache. That is acceptable for a low-volume editor path; sharing a public cache to save this read is the wrong trade. A signed capability adds verification work and a replay window until expiry unless it is one-time or revocable. A session-based preview avoids a shareable secret but requires editor login on the preview domain. Limit response size and rendering time for malformed drafts, and time out CMS calls. Measure forbidden attempts, preview failures, and accidental cache hits without logging raw capability tokens. The public page's steady-state latency should be independent of preview traffic.
Failure trace
Request revision 9 anonymously, as an editor lacking article 447 permission, as editor 47, and after editor 47 loses permission. Only the authorized current session may see it. Inspect the response headers at the origin and CDN; no shared cache should store the draft. Prime the public revision 8 first, preview revision 9, then request the public path anonymously and assert revision 8 still appears. Inspect rendered HTML for third-party requests, canonical URLs, JSON metadata, and link previews that could carry the preview credential. Try an expired capability and a capability for another article or revision. Simulate a CMS timeout and return a private error response without falling back to another editor's draft.
Verification
- Every preview request rechecks permission for the exact record.
- A preview response never populates a shared public cache key.
- The public route keeps serving its last published revision.
Practice drill
Create a preview service for article 447 with two revisions. Keep the public reader unprivileged. Pass a reviewer identity into the preview handler, check a case-specific edit grant, and retrieve the requested draft through a server-only CMS client. Emit no-store and noindex, disable shared caching, and render a visible preview label so editors can distinguish it from the public page. Add a test proxy that deliberately ignores query strings and show that the published path never handles draft requests. Exercise revoked permission, a missing revision, a replayed or expired capability, and a failure in the privileged CMS connection.
Decision note
Preview is a private content read with revision-specific permission; it is not a temporary form of publication.
Common Mistakes
- Relying on noindex to protect confidential draft content.
- Using a long-lived preview URL as the sole authorization proof.
- Reusing the public cache key for preview data.
Related lessons
CMS Content, Publication, and Preview Boundaries; CMS Public Projections and Route Identity; CMS Media Variants, Alt Text, and Asset Ownership; CMS Publish Events, Cache, Search, and Rollback; Authorization and Tenant Boundaries; HTTP Delivery and Cache Ownership; Browser Security and Data Stewardship.
Apply and check
Build Project: CMS Publication Reconciliation and review Web Development: CMS publication contracts.
