---
title: "Preview Mode"
description: "Preview draft and unpublished WordPress content in your Nuxt app"
canonical_url: "https://wpnuxt.com/guide/preview-mode"
---
# Preview Mode

> Preview draft and unpublished WordPress content in your Nuxt app

WPNuxt supports WordPress preview mode, allowing editors to preview draft and unsaved content directly in your Nuxt frontend.

<callout type="warning">

Preview mode requires **WPGraphQL 2.21 or later**. Older versions only support the `asPreview` query argument, which WPGraphQL 2.21 deprecated and WPNuxt no longer sends.

</callout>

## How It Works

WPGraphQL 2.21 replaced the `asPreview` argument with a request-level preview context, sent as the `X-GraphQL-Preview` header. Instead of swapping the whole node for its revision, WordPress now overlays the newest autosave **per field**, so the node keeps its published identity: `databaseId`, `id` and `uri` stay those of the published post.

1. An editor clicks **Preview** in the WordPress editor
2. WordPress redirects to your Nuxt site with its preview query params (`preview=true`, `preview_id` or `p`/`page_id`, `preview_nonce`, and `_thumbnail_id` when the featured image changed)
3. WPNuxt's client options detect these parameters and pass them to the server
4. The server turns them into an `X-GraphQL-Preview` header and forwards the editor's authentication to WordPress
5. WordPress returns the previewed content via GraphQL

```text
WordPress Editor → Click "Preview"
    → https://your-nuxt-site.com/my-post?preview=true&preview_id=123&preview_nonce=45d5b05f1b
        → WPNuxt client collects the preview params
            → Server sends X-GraphQL-Preview: database_id=123, nonce="45d5b05f1b"
              plus the editor's authentication
                → Previewed content returned and rendered
```

The header is an [RFC 8941](https://www.rfc-editor.org/rfc/rfc8941) structured-field dictionary:

```http
X-GraphQL-Preview: database_id=123, featured_image_database_id=456, nonce="45d5b05f1b"
```

<callout type="warning">

**Authentication is required.** The `nonce` is accepted but never verified by WPGraphQL and grants nothing on its own — without a valid WordPress cookie, application password or JWT, the preview request silently returns the published content.

</callout>

<callout type="info">

WordPress core marks only `title`, `content` and `excerpt` as previewable. Custom fields need their plugin to opt in (ACF fields require WPGraphQL for ACF 2.8 or later); until then they show published values, even though they appeared previewed under the old `asPreview` behaviour.

</callout>

## Upgrading from `asPreview`

The default `PageById` and `PostById` queries no longer take an `$asPreview` variable. If you overrode either query in your `extend/queries/` folder, drop the argument there too. Queries that still pass it keep validating — WPNuxt downloads the schema with deprecated arguments included — but the argument only does anything for requests that carry no preview context, and then it performs the old whole-node swap, replacing `databaseId`, `id` and `uri` with the revision's:

```graphql [extend/queries/Posts.gql]
query PostById($id: ID!) {
  post(id: $id, idType: DATABASE_ID) {
    ...Post
  }
}
```

## Client Options

WPNuxt automatically passes the WordPress preview parameters from the URL to the GraphQL middleware. This is handled by the default client options:

```ts [app/graphqlMiddleware.clientOptions.ts]
import { defineGraphqlClientOptions } from 'nuxt-graphql-middleware/client-options'
import { useRoute } from 'vue-router'

export default defineGraphqlClientOptions<{
  preview?: string
  previewToken?: string
  previewId?: string
  previewThumbnailId?: string
  previewNonce?: string
}>({
  buildClientContext() {
    const route = useRoute()
    const query = route?.query ?? {}
    const isPreview = query.preview === 'true'
    const asString = (value: unknown) => typeof value === 'string' ? value : undefined
    const previewId = asString(query.preview_id) || asString(query.p) || asString(query.page_id)

    return {
      preview: isPreview ? 'true' : undefined,
      previewToken: asString(query.token),
      previewId: isPreview ? previewId : undefined,
      previewThumbnailId: isPreview ? asString(query._thumbnail_id) : undefined,
      previewNonce: isPreview ? asString(query.preview_nonce) : undefined
    }
  }
})
```

<callout type="info">

`useRoute` must be imported from `vue-router`, not from `#imports` — this file is also loaded in the Nitro typecheck context, where `#imports` does not export `useRoute`.

</callout>

<callout type="info">

WPNuxt provides this as a default via its Nuxt layer — you don't need to create the file unless you want to customize. If you do create `app/graphqlMiddleware.clientOptions.ts` in your project, it takes priority over the WPNuxt default.

</callout>

## Server Options

The server-side options build the `X-GraphQL-Preview` header from that context and forward the editor's authentication — the incoming `Authorization` header, an [@wpnuxt/auth](https://wpnuxt.com/auth) cookie, or the JWT from the `token` query param, in that order:

```ts [server/graphqlMiddleware.serverOptions.ts]
import { defineGraphqlServerOptions } from 'nuxt-graphql-middleware/server-options'
import { getHeader } from 'h3'

export default defineGraphqlServerOptions({
  async serverFetchOptions(event, _operation, _operationName, context) {
    const client = context?.client
    const previewHeader = client?.preview === 'true' && client.previewId
      ? `database_id=${client.previewId}`
      : undefined

    return {
      headers: {
        Cookie: getHeader(event, 'cookie') || '',
        Authorization: getHeader(event, 'authorization') || '',
        ...(previewHeader ? { 'X-GraphQL-Preview': previewHeader } : {})
      }
    }
  }
})
```

WPNuxt's own default does a bit more than this simplified example: it also adds `featured_image_database_id` and `nonce` to the header, and only builds it when `preview=true` and the preview id is a positive integer. Invalid input is dropped silently, the same way WPGraphQL treats unauthorised or malformed preview input.

<callout type="info">

WPNuxt provides this as a default via its Nuxt layer — you don't need to create the file unless you want to customize. If you do create `server/graphqlMiddleware.serverOptions.ts` in your project, it takes priority over the WPNuxt default.

</callout>

## WordPress Configuration

### Set the Preview URL

Configure WordPress to redirect previews to your Nuxt frontend. Add this to your theme's `functions.php` or a custom plugin. Keep the query params WordPress put on the original link — they carry the preview id and nonce that WPNuxt turns into the preview header:

```php
add_filter('preview_post_link', function ($link, $post) {
    $nuxt_url = 'https://your-nuxt-site.com';
    $slug = $post->post_name ?: $post->ID;

    // Preserve WordPress's own preview params (preview_id, preview_nonce, _thumbnail_id, p, ...)
    $query = parse_url($link, PHP_URL_QUERY);
    parse_str($query ?: '', $params);
    $params['preview'] = 'true';

    return $nuxt_url . '/' . $slug . '?' . http_build_query($params);
}, 10, 2);
```

Or use the [Headless WordPress plugin](https://wordpress.org/plugins/developer-oriented-starter-content/) to configure the preview URL in the WordPress admin.

### Authentication for Previews

Preview mode requires the editor to be authenticated. WPNuxt supports two approaches:

1. **Cookie forwarding** (default) — The server options forward WordPress auth cookies automatically. This works when your Nuxt site and WordPress share a domain or you have proper CORS configuration.
2. **JWT tokens** — If using the [Headless Login](https://wpnuxt.com/auth/headless-login) plugin, pass a JWT token via the `token` query parameter. The client options extract it and the server options send it as the `Authorization` header.

## Displaying Preview Content

Your existing pages work with preview mode automatically. The `useNodeByUri()` composable returns the previewed content when valid preview credentials are present. Because the node keeps its published identity, preview state is queryable through `isPreview` and `previewRevisionDatabaseId`, which the default `ContentNode` fragment already selects:

```vue
<script setup lang="ts">
const route = useRoute()

const { data: post } = await useNodeByUri(
  { uri: route.path },
  { watch: [() => route.path] }
)

// Show a preview banner when in preview mode
const isPreview = computed(() => route.query.preview === 'true')
</script>

<template>
  <div>
    <div
      v-if="isPreview"
      class="bg-yellow-100 text-yellow-800 p-3 text-center text-sm"
    >
      Preview mode — this content is not published yet.
    </div>

    <article v-if="post">
      <h1>{{ post.title }}</h1>
      <div v-sanitize-html="post.content" />
    </article>
  </div>
</template>
```

## Disabling Cache for Previews

Preview content must not be cached. WPNuxt's server-side cache on `/api/wpnuxt/query/**` is keyed on the request URL alone: it neither varies on authentication nor bypasses caching for authenticated requests, and on a self-hosted (Nitro) setup it strips the `Authorization` and `Cookie` headers before the server options can read them. This is tracked in [#300](https://github.com/wpnuxt/wpnuxt/issues/300).

Until that is fixed, disable the server cache when you rely on preview mode:

```ts [nuxt.config.ts]
wpNuxt: {
  cache: {
    enabled: false
  }
}
```

Or keep caching for the rest of the site and override the route rule for the GraphQL endpoint:

```ts [nuxt.config.ts]
nitro: {
  routeRules: {
    '/api/wpnuxt/query/**': { cache: false }
  }
}
```

On Vercel, caching happens at the CDN edge instead, so set the same route rule if preview responses must not be shared.

In addition, disable client-side caching per query in preview mode:

```ts
const route = useRoute()
const isPreview = route.query.preview === 'true'

const { data: post } = await useNodeByUri(
  { uri: route.path },
  {
    clientCache: !isPreview,
    watch: [() => route.path]
  }
)
```

## Testing Previews Locally

1. Start your Nuxt dev server
2. Log in to WordPress admin
3. Open a post, change the title without saving, and click **Preview** — WordPress will redirect to your Nuxt site
4. The unsaved title should render while `databaseId` stays the published post's
5. If WordPress and Nuxt are on different ports, ensure cookies are forwarded correctly

<callout type="warning">

For local development, WordPress and Nuxt typically run on different ports (e.g., `:8080` and `:3000`). Cookie forwarding may not work across different origins. Use JWT tokens via the `token` query parameter as an alternative.

</callout>

## Related Pages

- **WordPress Setup** — Configure WordPress for headless use
- **Caching** — How WPNuxt caches GraphQL responses


## Sitemap

See the full [sitemap](https://wpnuxt.com/sitemap.md) for all pages.
