Skip to main content

Show Product Preview Page Using Next.js Draft Mode

· 5 min read

In a headless Shopify storefront, you often need to preview a product page before publishing content changes. Next.js Draft Mode gives a clean way to do this in development.

This guide explains a practical flow using:

  1. A preview secret configuration with SHOPIFY_PREVIEW_SECRET
  2. A preview UI page at /preview
  3. An API route that validates secret and enables Draft Mode
  4. Admin API fetch for unpublished products in preview mode
  5. A disable endpoint to exit preview

Why Draft Mode for Preview​

Draft Mode lets Next.js bypass static caching rules and render fresh content for selected requests. This is ideal when you want to review a product page before public users can see changes.

Step 1: Add Preview Secret in Environment​

Add a shared secret in your environment file:

SHOPIFY_PREVIEW_SECRET=your_preview_secret_here

Use a strong random value and keep it server-side only.

Step 2: Create a Preview Entry Page​

Create a page(eg: /preview) where you can submit secret + product handle.

// src/app/preview/page.tsx
<form method="GET" action="/api/draft">
<input name="secret" type="password" required />
<input name="handle" type="text" placeholder="your-product-handle" />
<input name="path" type="text" placeholder="/products/your-product-handle" />
<button type="submit">Enable Preview</button>
</form>

With this form:

  1. secret is used for verification
  2. handle is converted to /products/your-product-handle
  3. path can be used directly for any route preview

Step 3: Enable Draft Mode in API Route​

Create an API endpoint that validates the secret, enables Draft Mode, then redirects.

// src/app/api/draft/route.ts
import { draftMode } from "next/headers";
import { NextResponse } from "next/server";

export async function GET(request: Request) {
const url = new URL(request.url);
const secret = url.searchParams.get("secret");
const configuredSecret = process.env.SHOPIFY_PREVIEW_SECRET;

if (!configuredSecret) {
return NextResponse.json(
{ error: "Missing SHOPIFY_PREVIEW_SECRET." },
{ status: 500 },
);
}

if (!secret || secret !== configuredSecret) {
return NextResponse.json({ error: "Invalid preview secret." }, { status: 401 });
}

const handle = url.searchParams.get("handle");
const path = url.searchParams.get("path");

const redirectPath = handle
? `/products/${encodeURIComponent(handle)}`
: path && path.startsWith("/")
? path
: null;

if (!redirectPath) {
return NextResponse.json(
{ error: "Provide either a valid handle or path query param." },
{ status: 400 },
);
}

const draft = await draftMode();
draft.enable();

return NextResponse.redirect(new URL(redirectPath, url.origin));
}

Step 4: Open a Product in Preview​

In development, open:

http://localhost:3000/preview

Then:

  1. Enter the preview secret
  2. Enter product handle like my-test-product
  3. Submit

You will be redirected to /products/my-test-product with Draft Mode enabled.

Step 4.1: Configure Shopify Admin API for Unpublished Product Preview​

Storefront API usually returns only published products. To preview an unpublished product, fetch product details from Shopify Admin API when Draft Mode is enabled.

Add Admin API environment variables:

SHOPIFY_ADMIN_GRAPHQL_ENDPOINT=https://your-store.myshopify.com/admin/api/2026-01/graphql.json
SHOPIFY_ADMIN_ACCESS_TOKEN=your_admin_api_access_token

Create a server-only Admin GraphQL client:

// src/integrations/shopify-admin/shopify-admin-client.ts
import "server-only";
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";

const createShopifyAdminClient = () => {
const graphqlEndpoint = process.env.SHOPIFY_ADMIN_GRAPHQL_ENDPOINT;
const accessToken = process.env.SHOPIFY_ADMIN_ACCESS_TOKEN;

if (!graphqlEndpoint || !accessToken) {
throw new Error(
"Missing SHOPIFY_ADMIN_GRAPHQL_ENDPOINT or SHOPIFY_ADMIN_ACCESS_TOKEN.",
);
}

return new ApolloClient({
link: new HttpLink({
uri: graphqlEndpoint,
headers: {
"X-Shopify-Access-Token": accessToken,
"Content-Type": "application/json",
},
fetchOptions: {
cache: "no-store",
},
}),
cache: new InMemoryCache(),
});
};

Step 4.2: Admin GraphQL Query for Product by Handle​

Use an Admin GraphQL query that fetches product details by handle:

# src/integrations/shopify-admin/get-product-by-handle/get-product-by-handle.admin.shopify.graphql
query AdminProductByHandle($handle: String!) {
productByIdentifier(identifier: { handle: $handle }) {
title
description
tags
options {
name
values
}
images(first: 20) {
edges {
node {
url
altText
}
}
}
variants(first: 100) {
edges {
node {
id
title
price
inventoryQuantity
inventoryPolicy
selectedOptions {
name
value
}
}
}
}
}
}

This query works well for preview because it returns details needed by the product page UI, including options, images, variants, and tags.

Step 4.3: Switch Between Storefront API and Admin API​

In product page code, check Draft Mode and switch the data source:

  1. Draft Mode ON: fetch via Admin API (supports unpublished product preview)
  2. Draft Mode OFF: fetch via Storefront API (public storefront behavior)
import { draftMode } from "next/headers";
import { getProduct } from "@/integrations/shopify/product";
import { getAdminProductByHandle } from "@/integrations/shopify-admin/get-product-by-handle";

const isDraftMode = (await draftMode()).isEnabled;

if (isDraftMode) {
const adminProduct = await getAdminProductByHandle(handle);
// render preview from adminProduct
} else {
const storefrontProduct = await getProduct({ handle });
// render public product from storefrontProduct
}

This switch is the key part of previewing unpublished products safely in dev mode.

Step 5: Disable Preview Mode​

Add a route to disable Draft Mode and redirect back.

// src/app/api/draft/disable/route.ts
import { draftMode } from "next/headers";
import { NextResponse } from "next/server";

export async function GET(request: Request) {
const url = new URL(request.url);
const path = url.searchParams.get("path") || "/";

if (!path.startsWith("/")) {
return NextResponse.json({ error: "Invalid path." }, { status: 400 });
}

const draft = await draftMode();
draft.disable();

return NextResponse.redirect(new URL(path, url.origin));
}

Example usage:

http://localhost:3000/api/draft/disable?path=/products/my-test-product

The logic for preview mode is added to the headless Shopify project.