Hygraph
Docs

#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 DRAFT stage 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 DRAFT stage for the preview environment.
  • Change from PUBLISHED to DRAFT in 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 DRAFT stage 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.

#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.ts
import { 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 token
if (!previewToken || !slug) {
return new Response('Invalid token', { status: 401 });
}
// Fetch the slug from Hygraph to ensure we don't run into redirect loops
const 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 data
const { data } = await res.json();
// If the data returns undefined or in the wrong shape, return an error
if (!data?.page) {
return new Response('Invalid slug', { status: 401 });
}
// Workaround for https://github.com/vercel/next.js/issues/49927
// Enable draft mode as usual
draftMode().enable();
// Update the cookie and set SameSite=None so it works inside the Hygraph iframe
const 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.tsx
import { 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-mode
const { 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) {
title
description
slug
}
}
`;
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 served
const 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.local
HYGRAPH_ENDPOINT=https://your-region.cdn.hygraph.com/content/your-project-id/master
HYGRAPH_PREVIEW_TOKEN=your-draft-stage-token
HYGRAPH_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.ts
export default defineNuxtPlugin((nuxtApp) => {
const route = useRoute();
// Check for ?preview=true in the URL — set by your Studio preview URL template
const preview = route.query.preview === 'true';
if (preview) {
// Refresh page data whenever a new page finishes loading in preview mode
nuxtApp.hook('page:finish', () => {
refreshNuxtData();
});
}
// Expose $preview globally so page components can switch the query stage
return { 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 ran
const { $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) {
title
description
}
}
`;
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.mjs
import { 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 it
output: '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].astro
import { 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) {
title
description
}
}
`;
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 only
ASTRO_USE_PREVIEW=true
HYGRAPH_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