# Live preview - Frontend implementation

Configure your React, Next.js, Remix, Vue, Nuxt, Astro, or vanilla JavaScript frontend to serve DRAFT content for live preview.

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](https://hygraph.com/docs/developer-guides/schema/live-preview#create-a-draft-preview-token).
- 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](https://hygraph.com/docs/developer-guides/schema/live-preview#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}`.

**Warning:**
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.

```ts
// 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.

```ts
// 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

```bash
# .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](https://hygraph.com/marketplace/starters/skncre-starter-next).

## 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`.

**Warning:**
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.

```js
// 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.

```jsx
// 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.

```bash
# 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](https://hygraph.com/docs/developer-guides/schema/click-to-edit).

## 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

```ts
// 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.

```ts
// 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

```ts
// 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

```bash
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](https://hygraph.com/docs/developer-guides/schema/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`.

**Warning:**
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.

```js
// 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.

```vue
<!-- 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.

```bash
# 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](#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](https://hygraph.com/docs/developer-guides/schema/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`.

**Warning:**
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.

```ts
// 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](https://nuxt.com/docs/api/composables/use-preview-mode).

### 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.

```ts
// 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.

```ts
// 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.

```vue
<!-- 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

```bash
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](https://hygraph.com/marketplace/starters/skncre-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}`.

**Warning:**
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

```js
// 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

```astro
---
// 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.

```bash
# 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
```

```bash
# 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](https://hygraph.com/marketplace/starters/skncre-starter-astro). 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`.

**Warning:**
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.

```js
// 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

```bash
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](https://hygraph.com/docs/developer-guides/schema/click-to-edit-vanilla-js).

## What's next

- [Live preview](https://hygraph.com/docs/developer-guides/schema/live-preview): Configure live preview so editors can see draft content rendered in your frontend before publishing.
- [Get started with Click to Edit](https://hygraph.com/docs/developer-guides/schema/click-to-edit): Add SDK-powered edit buttons on top of a live preview.
