Previewing Hygraph Drafts with Content Stages
This guide, part of Hygraph GraphQL Content Federation, builds draft previews for a Next.js site backed by Hygraph. Editors click a preview button in the Hygraph app and see their unpublished changes rendered by the real frontend, while readers keep seeing published content. It covers how content stages work, passing the stage as a query variable, a token scoped to draft reads, preview URLs configured per model, draft mode, related entries in drafts and previews of scheduled releases.
In Hygraph, every entry exists in content stages. Saving an entry updates its DRAFT version; publishing copies it to PUBLISHED. Queries select a stage with the stage argument, and a token can also have a default stage. Previewing is therefore a matter of asking for DRAFT instead of PUBLISHED, with a token allowed to read drafts, in a mode that only editors can enter. No second API, dataset or deployment is needed.
The Problem
A software company’s marketing team previewed pages by publishing them to a hidden path and sharing the link. Because the entries were published, they appeared in the sitemap, in the site search index and in RSS feeds within minutes, and twice an unannounced feature page was picked up by a news aggregator before launch. The team wanted real previews, but its first attempt used the single read token for everything with stage: DRAFT whenever a preview=1 parameter was present, so anyone could see drafts by adding the parameter.
How Stage-Based Preview Works
Stage as a variable. Every query declares $stage: Stage! and passes it to the root field. Readers’ requests pass PUBLISHED; draft mode passes DRAFT. Queries stay identical, so previews show exactly the fields and layout that production will show.
Separate tokens. A permanent auth token with read permissions on the PUBLISHED stage serves the site. A second token, allowed to read DRAFT, serves previews. Neither needs mutation permissions, and both stay on the server.
Preview URLs per model. In the model’s settings, configure preview URLs with placeholders for the entry’s fields, such as its slug, pointing at the frontend’s draft route. The Hygraph app then shows preview buttons on entries of that model.
Draft mode. The draft route validates a shared secret, enables Next.js draft mode and redirects to the page. The fetch helper reads draft mode to choose the stage, the token and caching.
Related entries. In the DRAFT stage, relations resolve to the draft versions of related entries, so a new article with a new author previews correctly.
Implementation
Configure a preview URL on each model with a page, for example for the article model:
https://www.example.com/api/draft?secret=<PREVIEW_SECRET>&slug=/blog/{slug}
Keep the secret out of Hygraph’s UI if your plan allows environment-specific values; otherwise treat it as a shared secret and rotate it when team members leave.
The draft route validates the secret and the target path:
// app/api/draft/route.ts
import { draftMode } from "next/headers";
import { redirect } from "next/navigation";
import { timingSafeEqual } from "node:crypto";
export async function GET(req: Request) {
const p = new URL(req.url).searchParams;
const secret = Buffer.from(p.get("secret") ?? "");
const expected = Buffer.from(process.env.PREVIEW_SECRET!);
if (secret.length !== expected.length || !timingSafeEqual(secret, expected)) return new Response("invalid secret", { status: 401 });
const slug = p.get("slug") ?? "/";
if (!slug.startsWith("/") || slug.startsWith("//")) return new Response("invalid slug", { status: 400 });
(await draftMode()).enable();
redirect(slug);
}
Queries declare the stage and locales as variables:
// queries/page.ts
export const PAGE = /* GraphQL */ `
query Page($slug: String!, $stage: Stage!, $locales: [Locale!]!) {
page(where: { slug: $slug }, stage: $stage, locales: $locales) {
id
title
seo { title description noIndex }
sections {
__typename
... on Hero { heading subheading image { url width height altText } }
... on FeatureGrid { features { title text } }
... on CallToAction { label href }
}
}
}
`;
The fetch helper from the Hygraph topic chooses stage, token and caching from draft mode, so pages call it without knowing whether they are previewed. Add a banner in the root layout in draft mode with a link to a route that disables draft mode.
Keeping drafts out of feeds and sitemaps
Sitemaps, RSS feeds, search indexing jobs and Open Graph image generation must always query PUBLISHED with the read token, regardless of draft mode. Give them their own fetch function that hard-codes the stage, rather than relying on the page helper, so a job that happens to run in a request with the draft cookie can never publish drafts to the outside world.
Previewing releases
Scheduled releases in Hygraph publish a set of entries at a given time. Before the release, those entries’ changes live in DRAFT, so a normal preview shows them. To review a release as a whole, open the preview for its main page; related entries that are part of the release appear in their draft versions too. Remember that DRAFT includes all unpublished changes, not only the release’s, so reviewers may see work that belongs to a later release. Teams with many parallel releases sometimes add a custom content stage for release review, where the plan allows it.
Configuration Reference
| Item | Recommendation | Why |
|---|---|---|
| Stage | $stage variable on every query |
One query for preview and production. |
| Read token | PUBLISHED only, no mutations | Readers never see drafts. |
| Preview token | DRAFT read, server only | Revocable independently. |
| Preview URLs | per model, pointing at the draft route | One click from entry to preview. |
| Secret check | constant time, then relative redirect | No open redirect, no guessable preview. |
| Feeds, sitemaps | hard-coded PUBLISHED | Drafts never leak. |
| Robots | noindex in draft mode |
Previews never indexed. |
Gotchas & Edge Cases
- Default stage on tokens. A token’s default stage applies when a query omits the argument; always pass it explicitly so behaviour does not depend on token settings.
- Unpublished relations in production. An entry published while its related author is still a draft shows no author in PUBLISHED; publish related entries together or handle missing relations.
- Localized drafts. Pass the editor’s locale in the preview URL and use it in the query, or editors previewing a translation see the default locale.
- Static generation. Pages generated at build time render dynamically in draft mode; make sure their data functions work at request time.
- Caching layers. Bypass your CDN for requests carrying the draft mode cookie.
Worked Example
The software company replaced its hidden-path publishing with stage-based previews: preview URLs on its page, article and feature models, a draft route validating a secret, separate tokens for DRAFT and PUBLISHED, and hard-coded PUBLISHED queries for sitemaps, feeds and search indexing. Unannounced pages stopped appearing in feeds and aggregators, because nothing was published before launch. Editors found the preview button faster than their old workflow, and the number of entries published and unpublished again within a day, a sign of preview-by-publishing, fell to almost zero.
Previews for Reviewers Outside the Team
Stakeholders without Hygraph access, such as legal reviewers or partners, often need to see drafts. Forwarding the preview URL shares the secret, which then works for every draft. A better approach is a share route that creates a signed, expiring token for one entry and locale, stored server-side with its expiry, and a review route that validates it and renders only that entry in the DRAFT stage. Log each use and revoke tokens when the entry is published. This keeps the global preview secret inside the editorial team while giving reviewers exactly what they need, for as long as they need it.
Rollout Checklist
- Declare
$stageon every query and pass it from the fetch helper. - Create separate server-side tokens for PUBLISHED and DRAFT reads.
- Configure preview URLs on every model with a page.
- Validate the secret and redirect only to relative paths.
- Hard-code PUBLISHED for sitemaps, feeds and indexing jobs.
- Show a draft banner with an exit link and send
noindex.
Frequently Asked Questions
Can I use one token for both stages?
It works, but a leak then exposes drafts. Separate tokens limit the damage and allow rotating the preview token independently.
Does preview need a separate deployment?
No. Draft mode on the production deployment is enough; some teams prefer a staging deployment to keep preview traffic separate.
Do remote fields work in draft mode?
Yes, they resolve the same way; they read from the remote API, which has no notion of Hygraph stages.
How do I preview content that has no page, such as an author?
Configure its preview URL to a page where the author appears, such as the author profile or a recent article.
Why does my preview show published content?
Usually a fetch path that ignores draft mode, such as a layout or metadata function, or a CDN that cached the page for readers. Check which query ran with which stage in the logs.