#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
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
- A Permanent Auth Token that returns the
DRAFTstage 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}.
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. The cookie is also marked secure, so local preview over http://localhost may not persist the cookie inside the Studio iframe. Prefer an HTTPS preview URL for iframe testing.
#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 secret = searchParams.get('secret');const slug = searchParams.get('slug');// Check the secret against the value configured in your environmentif (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 loopsconst 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 iframeconst 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.tsximport { 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-modeconst { 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) {titledescriptionslug}}`;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 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 Next.js 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-tokenHYGRAPH_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.
Anyone who discovers this URL pattern can view your draft content. For sensitive content, add a secret check like the Next.js or Remix examples do, or restrict access to your preview deployment.
#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.jsexport 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) {titledescriptionslug}}`;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.jsximport { 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 environmentHYGRAPH_ENDPOINT=https://your-region.cdn.hygraph.com/content/your-project-id/masterHYGRAPH_PREVIEW_TOKEN=your-draft-stage-tokenHYGRAPH_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.tsimport { 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 iframepath: '/',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.tsximport { 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.tsximport { 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) {titledescriptionslug}}`;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/masterHYGRAPH_PREVIEW_TOKEN=your-draft-stage-tokenHYGRAPH_PRODUCTION_TOKEN=your-published-stage-tokenHYGRAPH_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.
Anyone who discovers this URL pattern can view your draft content. For sensitive content, add a secret check like the Next.js or Remix examples do, or restrict access to your preview deployment.
#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.jsexport 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) {titledescriptionslug}}`;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 environmentHYGRAPH_ENDPOINT=https://your-region.cdn.hygraph.com/content/your-project-id/masterHYGRAPH_PREVIEW_TOKEN=your-draft-stage-tokenHYGRAPH_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.
Anyone who discovers this URL pattern can view your draft content. For sensitive content, add a secret check like the Next.js or Remix examples do, or restrict access to your preview deployment.
#Step 1: Create the Nuxt preview plugin
Create /plugins/preview.ts. Nuxt reads this automatically on startup.
// plugins/preview.tsexport default defineNuxtPlugin((nuxtApp) => {const route = useRoute();// Refresh page data when navigating while ?preview=true is presentnuxtApp.hook('page:finish', () => {if (route.query.preview === 'true') {refreshNuxtData();}});// Expose a reactive $preview flag for templates and page logicreturn {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.tsexport 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.tsexport 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) {titledescription}}`;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/masterHYGRAPH_PREVIEW_TOKEN=your-draft-stage-tokenHYGRAPH_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}.
This setup always serves draft content on the preview host. Keep the preview deployment access-restricted (for example behind authentication or your hosting provider's protection).
#Step 1: Enable SSR in the Astro config
// 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 Astro preview environment variable in pages
---// src/pages/[slug].astroimport { 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) {titledescription}}`;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 onlyHYGRAPH_PREVIEW=trueHYGRAPH_ENDPOINT=https://your-region.cdn.hygraph.com/content/your-project-id/masterHYGRAPH_PREVIEW_TOKEN=your-draft-stage-token
# Production environmentHYGRAPH_PREVIEW=falseHYGRAPH_ENDPOINT=https://your-region.cdn.hygraph.com/content/your-project-id/masterHYGRAPH_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.
Anyone who discovers this URL pattern can view your draft content. For sensitive content, add a secret check like the Next.js or Remix examples do, or restrict access to your preview deployment.
#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.jsimport 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) {titledescriptionslug}}`;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/masterHYGRAPH_PREVIEW_TOKEN=your-draft-stage-tokenHYGRAPH_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
- 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.