#Live preview - Frontend implementation
Live preview requires your frontend to serve content from the Hygraph DRAFT stage when loaded inside Studio's preview iframe. This page covers how to implement that for Next.js, Nuxt, and Astro.
Ways to query the DRAFT stage
- Create a URL for your web app that queries the
DRAFTstage of your endpoint. This could look like:https://preview.yourwebsite.com. You could also use a query parameter on the URL, like this:https://yourwebsite.com?preview=true. - Create a Permanent Auth Token that returns the
DRAFTstage for the preview environment. - Change from
PUBLISHEDtoDRAFTin your app based on a preview context inside your frontend.
#Prerequisites
- Added the Preview widget and defined a URL template in Studio.
- A Permanent Auth Token that returns the
DRAFTstage for the preview environment. - Your High Performance Content API endpoint.
- Find it under Project Settings > Access > Endpoints > High Performance Content API in your Hygraph project.
#Next.js
Next.js uses the draftMode API from next/headers to toggle between DRAFT and PUBLISHED content. A preview route sets the draft mode cookie; your page components read isEnabled to decide which stage to query.
In Next.js 13 and later, the draftMode cookie is set with SameSite=Lax by default, which prevents it from working inside an iframe. The route handler below applies a workaround that sets SameSite=None after enabling draft mode.
#Step 1: Create the preview route handler
Create app/api/preview/route.ts. This route validates the secret token and slug, enables draft mode, and redirects to the correct page.
// app/api/preview/route.tsimport { draftMode, cookies } from 'next/headers';import { redirect } from 'next/navigation';export async function GET(request: Request) {const { searchParams } = new URL(request.url);const previewToken = searchParams.get('previewToken');const slug = searchParams.get('slug');// Check for a slug and for a preview tokenif (!previewToken || !slug) {return new Response('Invalid token', { status: 401 });}// Fetch the slug from Hygraph to ensure we don't run into redirect loopsconst res = await fetch(process.env.HYGRAPH_ENDPOINT!, {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({query: `query SinglePage($slug: String!) {page(where: { slug: $slug }, stage: DRAFT) {slug}}`,variables: { slug },}),});// Return the dataconst { data } = await res.json();// If the data returns undefined or in the wrong shape, return an errorif (!data?.page) {return new Response('Invalid slug', { status: 401 });}// Workaround for https://github.com/vercel/next.js/issues/49927// Enable draft mode as usualdraftMode().enable();// Update the cookie and set SameSite=None so it works inside the Hygraph iframeconst cookieStore = cookies();const cookie = cookieStore.get('__prerender_bypass');cookies().set({name: '__prerender_bypass',value: cookie?.value,httpOnly: true,path: '/',secure: true,sameSite: 'none',});redirect(`/${data.page.slug}`);}
#Step 2: Query DRAFT or PUBLISHED based on draft mode
In your page component, read isEnabled from draftMode() to switch between stages.
// app/[slug]/page.tsximport { request } from 'graphql-request';import { draftMode } from 'next/headers';export default async function Page({ params }: { params: { slug: string } }) {// isEnabled is true if the draft mode cookie has been set by the preview route// See: https://nextjs.org/docs/app/building-your-application/configuring/draft-modeconst { isEnabled } = draftMode();// Default stage is PUBLISHED. Switch to DRAFT when draft mode is active.const query = `query Page($slug: String!, $stage: Stage! = PUBLISHED) {page(where: { slug: $slug }, stage: $stage) {titledescriptionslug}}`;const variables = {stage: isEnabled ? 'DRAFT' : 'PUBLISHED',slug: params.slug,};// Use the preview token in draft mode so the DRAFT stage is accessible,// and the production token otherwise so only PUBLISHED content is servedconst endpoint = process.env.HYGRAPH_ENDPOINT!;const token = isEnabled? process.env.HYGRAPH_PREVIEW_TOKEN!: process.env.HYGRAPH_PRODUCTION_TOKEN!;const { page } = await request(endpoint, query, variables, {Authorization: `Bearer ${token}`,});return (<main><h1>{page.title}</h1><p>{page.description}</p></main>);}
#Step 3: Configure environment variables
# .env.localHYGRAPH_ENDPOINT=https://your-region.cdn.hygraph.com/content/your-project-id/masterHYGRAPH_PREVIEW_TOKEN=your-draft-stage-tokenHYGRAPH_PRODUCTION_TOKEN=your-published-stage-token
#Preview URL template
Set this as the URL template in your Studio Preview widget:
https://your-domain.com/api/preview?secret=MY_SECRET_TOKEN&slug={slug}
The /api/preview route validates the token, sets draft mode, and redirects to the content page. The content page then queries the DRAFT stage.
For a full implementation including more complex content models, see the SKNCRE Cosmetics Shop Starter.
#Nuxt
Nuxt uses a plugin to detect a ?preview=true query parameter and expose a global $preview variable. Page components use this variable to switch the query stage.
#Step 1: Create the preview plugin
Create /plugins/preview.ts. Nuxt reads this automatically on startup.
// plugins/preview.tsexport default defineNuxtPlugin((nuxtApp) => {const route = useRoute();// Check for ?preview=true in the URL — set by your Studio preview URL templateconst preview = route.query.preview === 'true';if (preview) {// Refresh page data whenever a new page finishes loading in preview modenuxtApp.hook('page:finish', () => {refreshNuxtData();});}// Expose $preview globally so page components can switch the query stagereturn { provide: { preview } };});
The plugin checks for ?preview=true and refreshes page data when the preview URL is loaded. The $preview variable is now available in all components.
For more ways to configure Nuxt preview mode, see the Nuxt usePreviewMode documentation.
#Step 2: Query DRAFT or PUBLISHED based on preview state
<!-- pages/[slug].vue --><script setup>import { request } from 'graphql-request';// $preview is true if ?preview=true was present in the URL when the plugin ranconst { $preview } = useNuxtApp();const route = useRoute();// Default stage is PUBLISHED. Switch to DRAFT when $preview is true.const query = `query Page($slug: String!, $stage: Stage! = PUBLISHED) {page(where: { slug: $slug }, stage: $stage) {titledescription}}`;const variables = {stage: $preview ? 'DRAFT' : 'PUBLISHED',slug: route.params.slug,};const { data: page } = await request(process.env.HYGRAPH_ENDPOINT,query,variables);</script><template><main><h1>{{ page.title }}</h1><p>{{ page.description }}</p></main></template>
#Preview URL template
https://your-domain.com/{slug}?preview=true
For a full Nuxt implementation, see the SKNCRE Cosmetics Shop Starter.
#Astro
Astro is focused on Static Site Generation (SSG) and due to its incredible flexibility in terms of using frontend frameworks, Astro does not have a built-in preview mode like Next.js or Nuxt. Static generation caches responses, which means editors will always see stale content in preview. You can configure the Astro site to run in SSR (server-side rendered) mode and use an environment variable to switch the query stage.
#Step 1: Enable SSR in astro.config.mjs
// astro.config.mjsimport { defineConfig } from 'astro/config';import vercel from '@astrojs/vercel/serverless';export default defineConfig({// SSR is required — static generation caches responses and editors// will always see stale content in preview without itoutput: 'server',adapter: vercel(), // or node, netlify, cloudflare});
Other adapters (node, netlify, cloudflare) are supported. The key requirement is output: 'server'.
#Step 2: Read the preview environment variable in pages
---// src/pages/[slug].astroimport { request } from 'graphql-request';// ASTRO_USE_PREVIEW=true is set in your preview deployment environment only.// Do not set this in production — it would serve DRAFT content to all visitors.const isPreview = import.meta.env.ASTRO_USE_PREVIEW === 'true';// Default stage is PUBLISHED. Switch to DRAFT in the preview deployment.const query = `query Page($slug: String!, $stage: Stage! = PUBLISHED) {page(where: { slug: $slug }, stage: $stage) {titledescription}}`;const variables = {stage: isPreview ? 'DRAFT' : 'PUBLISHED',slug: Astro.params.slug,};const { page } = await request(import.meta.env.HYGRAPH_ENDPOINT,query,variables);---<html><head><title>{page.title}</title></head><body><main><h1>{page.title}</h1><p>{page.description}</p></main></body></html>
#Step 3: Set the environment variable in your preview deployment
Set ASTRO_USE_PREVIEW=true in your preview environment, such as Vercel or Netlify. Do not set it in your production environment.
# Preview environment onlyASTRO_USE_PREVIEW=trueHYGRAPH_ENDPOINT=https://your-region.cdn.hygraph.com/content/your-project-id/master
#Preview URL template
Because Astro does not use a redirect-based preview route, your preview URL points directly to the content page. The preview deployment reads DRAFT content by default because ASTRO_USE_PREVIEW is set.
https://preview.your-domain.com/{slug}
For a full Astro implementation, see the SKNCRE Cosmetics Shop Starter.
#What's next
- Live preview: Configure live preview so editors can see draft content rendered in your frontend before publishing.
- Get started with Click to Edit: Add SDK-powered edit buttons on top of a live preview.