Skip to main content
Last Reviewed: 2026-09-18

Drupal draft preview for Next.js

Preview unpublished Drupal content in a Next.js site on Pantheon using draft mode and Simple OAuth


This tutorial shows you how to let Drupal editors preview unpublished content in a decoupled Next.js site on Pantheon. An editor clicks Preview in Drupal, lands on the Next.js site, and sees the draft revision rendered by the real front end.

Learning objectives

This tutorial walks you through:

  • Creating a Simple OAuth consumer so Next.js can read unpublished content
  • Adding the /api/draft and /api/disable-draft routes
  • Fetching the working-copy revision with resourceVersion
  • Configuring the Drupal next_site preview URL and entity types

Requirements

  • A Next.js 16 site and a Drupal 11 backend on Pantheon — see the Drupal + Next.js Quick Start
  • drupal/next 2.x and drupal/simple_oauth 6.x on the Drupal site
  • next-drupal 2.x in the Next.js project
  • Terminus, logged in
Warning:
next-drupal 2.x is required

The next-drupal/draft entry point — enableDraftMode, disableDraftMode, and getDraftData — only exists in next-drupal 2.x. Version 1.6 predates the App Router and does not export it. Check your version before you start:

How it works

  1. An editor clicks Preview on an unpublished node. Drupal builds a signed preview URL pointing at your site's /api/draft route, carrying path, timestamp, secret, plugin, resourceVersion, and locale parameters.

  2. Your /api/draft route hands the request to enableDraftMode(), which POSTs those parameters back to Drupal's /next/draft-url endpoint for validation. Drupal recomputes the signature and confirms it. The signature uses Drupal's own private key and hash salt, so there is no preview secret to share with Next.js.

  3. If validation passes, Next.js enables draft mode by setting cookies, stores the draft data, and redirects to the content's real path.

  4. Your page reads the draft data, sees a resourceVersion, and fetches that revision from JSON:API with an OAuth bearer token instead of the published one.

The secret is time-limited and computed from the timestamp, path, and resource version, so a preview link cannot be replayed indefinitely or edited to expose a different node.

Information:
Using the demo upstream?

Sites installed from demo-nextjs-drupal-backend with version 1.1.0 or later of the pantheon_nextjs_demo recipe already have the role, scope, consumer, and per-content-type preview configured below. Skip to Add the draft routes.

Create the OAuth consumer

Next.js needs authenticated access to read unpublished content. Simple OAuth uses roles as scopes.

Create a role

  1. Go to /admin/people/roles and click + Add role.

  2. Name it Next.js preview, with the machine name nextjs_preview, and save.

  3. Assign it these permissions at /admin/people/permissions:

    • View published content
    • Bypass content access control
Information:
Why bypass content access control

This permission lets Next.js read unpublished nodes and revisions. It applies only to authenticated requests made with this consumer's token, not to anonymous traffic. Keep the client secret in Pantheon Secrets and never expose it to the browser.

Create a user and generate keys

  1. Add a user at /admin/people/create and assign it the Next.js preview role.

  2. Go to /admin/config/people/simple_oauth and click Generate keys. Store them outside the docroot — on Pantheon, use the private files path.

Create the scope and consumer

  1. At /admin/config/people/simple_oauth/oauth2_scope/dynamic/add, create a scope:

    • Machine-readable Name: nextjs_preview
    • Grant Types: Client Credentials
    • Granularity: Role
    • Role: Next.js preview
  2. At /admin/config/services/consumer/add, create a consumer:

    • Label: Next.js site
    • Client ID: default_consumer
    • Secret: a strong random value
    • Grant Types: Client Credentials
    • Scopes: nextjs_preview
    • User: the user you created
  3. Note the client ID and secret — the secret is hashed on save and cannot be read back.

Configure the Drupal site

Set the preview URL

  1. Go to /admin/config/services/next and click Edit next to your site.
  2. Set Preview URL to https://dev-my-nextjs-site.pantheonsite.io/api/draft.
  3. Click Save.

The Preview secret field on this form is not used for draft mode with next-drupal 2.x; you can leave it as it is.

Enable preview for entity types

  1. Go to /admin/config/services/next/entity-types.
  2. Click Configure entity type and select the content type, for example Article.
  3. On the Draft Mode tab, select Site selector as the Plugin.
  4. Select your site under Next.js sites.
  5. Click Save.

Repeat for every content type editors preview.

Add the draft routes

Create the two routes next-drupal provides. Both are GET.

enableDraftMode() validates the request against Drupal, sets the draft cookies, and redirects. You do not need to check the secret yourself — Drupal does it.

Configure the Drupal client

The client needs OAuth credentials to read unpublished content:

Leave withAuth off by default and turn it on per request for draft reads, as the page below does. The consumer's role can read unpublished content, so authenticating every request would let unpublished content into responses that get cached and served to visitors.

Render the draft revision

In draft mode, fetch the working copy rather than the published default. getDraftData() returns the path and resourceVersion Drupal sent:

Warning:
Never cache draft responses

Draft content must use cache: 'no-store' and carry no cache tags. Caching a draft would store unpublished content in shared storage and risk serving it from the edge CDN to anonymous visitors.

Give editors a visible way out of draft mode. The banner above links to /api/disable-draft, which clears the cookies and returns the site to published content.

Set the secrets

Set the OAuth credentials and the preview secret on the Next.js site, replacing my-nextjs-site:

Push a commit afterward so a new build picks up the values.

Test draft preview

  1. In Drupal, create an Article and save it as Unpublished.

  2. Click Preview. Drupal redirects you to the Next.js site.

  3. Confirm the draft renders and the preview banner appears.

  4. Click Exit preview and confirm the unpublished article is no longer reachable.

If preview fails, check the following:

  • Redirected to the live URL instead of the draft. Drupal falls back to the live URL for anonymous users and for users with no role beyond Authenticated user. Confirm the editor has a role such as Content editor.
  • 422 "The provided secret is invalid." The preview link was altered, or Drupal's hash salt or private key changed after the link was made. Click Preview in Drupal again for a fresh link.
  • Every draft read fails with 401, and token requests return "Check the scope parameter". The consumer has no scope. Attach the nextjs_preview scope to it.
  • "The provided secret has expired." Preview links are short-lived by design. Raise Secret expiration at /admin/config/services/next/settings if your editors need longer, but keep it short.
  • Draft renders the published version. resourceVersion is not reaching the JSON:API request, or withAuth is not set for the draft request.
  • 403 from JSON:API. The consumer's user is missing the Next.js preview role, or the role lacks Bypass content access control.

Check the runtime logs for detail:

Conclusion

Drupal editors can now preview unpublished content in the real Next.js front end, with access controlled by short-lived signed URLs and an OAuth consumer.

To continue: