Hygraph
Docs
View as Markdown

#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 React, Next.js, Remix, Vue, Nuxt, Astro, and vanilla JavaScript.

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

  • A Permanent Auth Token that returns the DRAFT stage for the preview environment. Create one in Set up live preview.
  • Your High Performance Content API endpoint.
    • Find it under Project Settings > Access > Endpoints > High Performance Content API in your Hygraph project.

URL templates to paste into the Studio Preview widget are listed in Choose a preview URL. Each framework section below includes the matching template.

#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. Use this URL template in Studio: https://your-domain.com/api/preview?secret=MY_SECRET_TOKEN&slug={slug}.

#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 secret = searchParams.get('secret');
const slug = searchParams.get('slug');
// Check the secret against the value configured in your environment
if (secret !== process.env.HYGRAPH_PREVIEW_SECRET || !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',
Authorization: `Bearer ${process.env.HYGRAPH_PREVIEW_TOKEN}`,
},
body: JSON.stringify({
query: `
query SinglePage($slug: String!) {
page(where: { slug: $slug }, stage: DRAFT) {
slug
}
}
`,
variables: { slug },
}),
});
const { data } = await res.json();
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 (await required in Next.js 15+)
const draft = await draftMode();
draft.enable();
// Update the cookie and set SameSite=None so it works inside the Hygraph iframe
const cookieStore = await cookies();
const cookie = cookieStore.get('__prerender_bypass');
cookieStore.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: Promise<{ 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 { slug } = await params;
const { isEnabled } = await 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,
};
// 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 Next.js 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
HYGRAPH_PREVIEW_SECRET=MY_SECRET_TOKEN

For a full implementation including more complex content models, see the SKNCRE Cosmetics Shop Starter.

#React

For a client-side React app, such as Vite, Create React App, with no server of its own, route the preview request through a serverless function instead of calling Hygraph directly from the browser. This keeps your Permanent Auth Token off the client. Use this URL template in Studio: https://your-domain.com/{slug}?preview=true.

#Step 1: Create a React serverless preview function

Deploy a serverless function (for example, a Vercel or Netlify function) that picks the stage and token based on a preview flag, then queries Hygraph.

Do not prefix HYGRAPH_PREVIEW_TOKEN or HYGRAPH_PRODUCTION_TOKEN with VITE_, REACT_APP_, or any build-tool prefix that exposes variables to the browser bundle. Only the serverless function needs them.

// api/preview.js
export default async function handler(req, res) {
const { slug, preview } = req.query;
const isPreview = preview === 'true';
const stage = isPreview ? 'DRAFT' : 'PUBLISHED';
const token = isPreview
? process.env.HYGRAPH_PREVIEW_TOKEN
: process.env.HYGRAPH_PRODUCTION_TOKEN;
const query = `
query Page($slug: String!, $stage: Stage!) {
page(where: { slug: $slug }, stage: $stage) {
title
description
slug
}
}
`;
const response = await fetch(process.env.HYGRAPH_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ query, variables: { slug, stage } }),
});
const { data } = await response.json();
res.status(200).json(data.page);
}

#Step 2: Call the proxy from your React page

Read the preview query parameter and pass it through to your function. The component never touches the Hygraph endpoint or token directly.

// src/Page.jsx
import { useEffect, useState } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
export default function Page() {
const { slug } = useParams();
const [searchParams] = useSearchParams();
const isPreview = searchParams.get('preview') === 'true';
const [page, setPage] = useState(null);
useEffect(() => {
fetch(`/api/preview?slug=${slug}&preview=${isPreview}`)
.then((res) => res.json())
.then(setPage);
}, [slug, isPreview]);
if (!page) return null;
return (
<main>
<h1>{page.title}</h1>
<p>{page.description}</p>
</main>
);
}

#Step 3: Configure React environment variables

Set these on the serverless function's host, not in a client-exposed .env file.

# Serverless function environment
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

For click-to-edit overlays on top of this preview flow, see Click to Edit setup.

#Remix

Remix runs loaders on the server, so you can use a cookie-backed session to persist preview state across requests, the same way the Next.js route handler above uses a cookie. Use this URL template in Studio: https://your-domain.com/preview?secret=MY_SECRET_TOKEN&slug={slug}.

The session cookie uses sameSite: 'none' and secure: true so it works inside the Studio iframe. Local preview over http://localhost may not persist that cookie; prefer an HTTPS preview URL for iframe testing.

#Step 1: Create a session for preview state

// app/sessions.server.ts
import { createCookieSessionStorage } from '@remix-run/node';
export const { getSession, commitSession } = createCookieSessionStorage({
cookie: {
name: '__preview_mode',
secrets: [process.env.HYGRAPH_PREVIEW_SECRET!],
secure: true,
sameSite: 'none', // required so the cookie works inside the Studio preview iframe
path: '/',
httpOnly: true,
},
});

#Step 2: Create the preview resource route

This route validates a secret token, sets the session, and redirects to the page.

// app/routes/preview.tsx
import { redirect, type LoaderFunctionArgs } from '@remix-run/node';
import { getSession, commitSession } from '~/sessions.server';
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const slug = url.searchParams.get('slug');
const secret = url.searchParams.get('secret');
if (!slug || secret !== process.env.HYGRAPH_PREVIEW_SECRET) {
throw new Response('Invalid token', { status: 401 });
}
const session = await getSession(request.headers.get('Cookie'));
session.set('isPreview', true);
return redirect(`/${slug}`, {
headers: { 'Set-Cookie': await commitSession(session) },
});
}

#Step 3: Query DRAFT or PUBLISHED in the page loader

// app/routes/$slug.tsx
import { json, type LoaderFunctionArgs } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { getSession } from '~/sessions.server';
export async function loader({ request, params }: LoaderFunctionArgs) {
const session = await getSession(request.headers.get('Cookie'));
const isPreview = session.get('isPreview') === true;
const stage = isPreview ? 'DRAFT' : 'PUBLISHED';
const token = isPreview
? process.env.HYGRAPH_PREVIEW_TOKEN
: process.env.HYGRAPH_PRODUCTION_TOKEN;
const query = `
query Page($slug: String!, $stage: Stage!) {
page(where: { slug: $slug }, stage: $stage) {
title
description
slug
}
}
`;
const res = await fetch(process.env.HYGRAPH_ENDPOINT!, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ query, variables: { slug: params.slug, stage } }),
});
const { data } = await res.json();
return json({ page: data.page });
}
export default function Slug() {
const { page } = useLoaderData<typeof loader>();
return (
<main>
<h1>{page.title}</h1>
<p>{page.description}</p>
</main>
);
}

#Step 4: Configure Remix environment variables

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
HYGRAPH_PREVIEW_SECRET=your-secret-token

For click-to-edit overlays on top of this preview flow, see Click to Edit - Remix.

#Vue

For a client-side Vue 3 app with Vue Router (no Nuxt), route the preview request through a serverless function instead of calling Hygraph directly from the browser. This keeps your Permanent Auth Token off the client. Use this URL template in Studio: https://your-domain.com/{slug}?preview=true.

#Step 1: Create a Vue serverless preview function

Deploy a serverless function (for example, a Vercel or Netlify function) that picks the stage and token based on a preview flag, then queries Hygraph. Do not prefix HYGRAPH_PREVIEW_TOKEN or HYGRAPH_PRODUCTION_TOKEN with VITE_ or any build-tool prefix that exposes variables to the browser bundle. Only the serverless function needs them.

// api/preview.js
export default async function handler(req, res) {
const { slug, preview } = req.query;
const isPreview = preview === 'true';
const stage = isPreview ? 'DRAFT' : 'PUBLISHED';
const token = isPreview
? process.env.HYGRAPH_PREVIEW_TOKEN
: process.env.HYGRAPH_PRODUCTION_TOKEN;
const query = `
query Page($slug: String!, $stage: Stage!) {
page(where: { slug: $slug }, stage: $stage) {
title
description
slug
}
}
`;
const response = await fetch(process.env.HYGRAPH_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ query, variables: { slug, stage } }),
});
const { data } = await response.json();
res.status(200).json(data.page);
}

#Step 2: Call the proxy from your Vue page

Read the preview query parameter and pass it through to your function. The component never touches the Hygraph endpoint or token directly.

<!-- src/views/Page.vue -->
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const isPreview = route.query.preview === 'true';
const page = ref(null);
onMounted(async () => {
const res = await fetch(
`/api/preview?slug=${route.params.slug}&preview=${isPreview}`
);
page.value = await res.json();
});
</script>
<template>
<main v-if="page">
<h1>{{ page.title }}</h1>
<p>{{ page.description }}</p>
</main>
</template>

#Step 3: Configure Vue environment variables

Set these on the serverless function's host, not in a client-exposed .env file.

# Serverless function environment
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

Running Nuxt instead of plain Vue? Use the Nuxt section below. It queries Hygraph server-side, so it doesn't need the proxy function.

For click-to-edit overlays on top of this preview flow, see Click to Edit - Vue / Nuxt.

#Nuxt

Nuxt uses a plugin to detect a ?preview=true query parameter and expose a global $preview variable. Keep Hygraph tokens in server-only runtime config, and query Hygraph from a server API route so client-side navigation still works. Use this URL template in Studio: https://your-domain.com/{slug}?preview=true.

#Step 1: Create the Nuxt preview plugin

Create /plugins/preview.ts. Nuxt reads this automatically on startup.

// plugins/preview.ts
export default defineNuxtPlugin((nuxtApp) => {
const route = useRoute();
// Refresh page data when navigating while ?preview=true is present
nuxtApp.hook('page:finish', () => {
if (route.query.preview === 'true') {
refreshNuxtData();
}
});
// Expose a reactive $preview flag for templates and page logic
return {
provide: {
preview: computed(() => route.query.preview === 'true'),
},
};
});

The plugin refreshes page data after in-app navigation in preview mode. $preview stays in sync with the current route query.

For more ways to configure Nuxt preview mode, see the Nuxt usePreviewMode documentation.

#Step 2: Configure Nuxt runtime config

Keep Hygraph tokens server-side. Add them to private runtimeConfig in nuxt.config.ts. Only server routes can read these values and they are empty in the browser.

// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
hygraphEndpoint: process.env.HYGRAPH_ENDPOINT,
hygraphPreviewToken: process.env.HYGRAPH_PREVIEW_TOKEN,
hygraphProductionToken: process.env.HYGRAPH_PRODUCTION_TOKEN,
},
});

#Step 3: Create a Nuxt server API route

Move the Hygraph query into server/api/ so tokens stay on the server during SSR and client-side navigation.

// server/api/page.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event);
const slug = String(query.slug ?? '');
const isPreview = query.preview === 'true';
const config = useRuntimeConfig();
const stage = isPreview ? 'DRAFT' : 'PUBLISHED';
const token = isPreview
? config.hygraphPreviewToken
: config.hygraphProductionToken;
const graphqlQuery = `
query Page($slug: String!, $stage: Stage! = PUBLISHED) {
page(where: { slug: $slug }, stage: $stage) {
title
description
}
}
`;
const response = await $fetch<{ data: { page: { title: string; description: string } } }>(
config.hygraphEndpoint,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: {
query: graphqlQuery,
variables: { slug, stage },
},
}
);
return response.data.page;
});

#Step 4: Call the API route from the Nuxt page

Use useFetch so the page works on first load and on in-app navigation without exposing tokens to the browser.

<!-- pages/[slug].vue -->
<script setup>
const route = useRoute();
const { data: page } = await useFetch('/api/page', {
query: computed(() => ({
slug: route.params.slug,
preview: route.query.preview === 'true' ? 'true' : 'false',
})),
});
</script>
<template>
<main v-if="page">
<h1>{{ page.title }}</h1>
<p>{{ page.description }}</p>
</main>
</template>

#Step 5: Configure Nuxt environment variables

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

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. Use this URL template in Studio: https://preview.your-domain.com/{slug}.

#Step 1: Enable SSR in the Astro config

// 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 Astro preview environment variable in pages

---
// src/pages/[slug].astro
import { request } from 'graphql-request';
// HYGRAPH_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.HYGRAPH_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 token = isPreview
? import.meta.env.HYGRAPH_PREVIEW_TOKEN
: import.meta.env.HYGRAPH_PRODUCTION_TOKEN;
const { page } = await request(
import.meta.env.HYGRAPH_ENDPOINT,
query,
variables,
{ Authorization: `Bearer ${token}` }
);
---
<html>
<head><title>{page.title}</title></head>
<body>
<main>
<h1>{page.title}</h1>
<p>{page.description}</p>
</main>
</body>
</html>

#Step 3: Configure Astro environment variables

Set HYGRAPH_PREVIEW=true in your preview environment, such as Vercel or Netlify. Do not set it in your production environment. The true value matches the preview=true convention used by the other frameworks.

# Preview environment only
HYGRAPH_PREVIEW=true
HYGRAPH_ENDPOINT=https://your-region.cdn.hygraph.com/content/your-project-id/master
HYGRAPH_PREVIEW_TOKEN=your-draft-stage-token
# Production environment
HYGRAPH_PREVIEW=false
HYGRAPH_ENDPOINT=https://your-region.cdn.hygraph.com/content/your-project-id/master
HYGRAPH_PRODUCTION_TOKEN=your-published-stage-token

For a full Astro implementation, see the SKNCRE Cosmetics Shop Starter. That starter may still show an older HYGRAPH_IS_PREVIEW name; use HYGRAPH_PREVIEW=true as shown above for consistency with the other frameworks. For live Studio preview after save, use SSR as shown above rather than static generation.

#Vanilla JavaScript

For frontend applications without a framework, run a minimal Node.js server that reads the preview signal from the request and switches the GraphQL query stage before rendering HTML. Keep the Permanent Auth Token server-side only, since a token embedded in client-side JavaScript is visible in DevTools to anyone who opens it. Treat it like a secret and do not expose it to the client. Use this URL template in Studio: https://your-domain.com/{slug}?preview=true.

#Step 1: Read the preview query parameter and switch stage

Check for ?preview=true on the incoming request, then pick the stage and token before querying Hygraph.

// server.js
import express from 'express';
import fetch from 'node-fetch';
const app = express();
async function fetchPage(slug, isPreview) {
const stage = isPreview ? 'DRAFT' : 'PUBLISHED';
const token = isPreview
? process.env.HYGRAPH_PREVIEW_TOKEN
: process.env.HYGRAPH_PRODUCTION_TOKEN;
const query = `
query Page($slug: String!, $stage: Stage!) {
page(where: { slug: $slug }, stage: $stage) {
title
description
slug
}
}
`;
const res = await fetch(process.env.HYGRAPH_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ query, variables: { slug, stage } }),
});
const { data } = await res.json();
return data.page;
}
app.get('/:slug', async (req, res) => {
const isPreview = req.query.preview === 'true';
const page = await fetchPage(req.params.slug, isPreview);
res.send(`
<html>
<body>
<h1>${page.title}</h1>
<p>${page.description}</p>
</body>
</html>
`);
});
app.listen(3000);

#Step 2: Configure Vanilla JavaScript environment variables

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

The route reads preview on each request and queries DRAFT only for that request. Visitors without the parameter get PUBLISHED content.

For a full implementation including the Hygraph Preview SDK for click-to-edit overlays, see Click to Edit - Vanilla JavaScript.

#What's next