Skip to content

Commit

Permalink
Merge branch 'canary' into fix/url-scheme-in-experimental-https
Browse files Browse the repository at this point in the history
  • Loading branch information
ijjk authored Sep 9, 2024
2 parents f7073da + a68d1fa commit 95947c3
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 71 deletions.
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
---
title: Draft Mode
description: Next.js has draft mode to toggle between static and dynamic pages. You can learn how it works with App Router here.
related:
title: Next Steps
description: See the API reference for more information on how to use Draft Mode.
links:
- app/building-your-application/configuring/draft-mode
---

Static rendering is useful when your pages fetch data from a headless CMS. However, it’s not ideal when you’re writing a draft on your headless CMS and want to view the draft immediately on your page. You’d want Next.js to render these pages at **request time** instead of build time and fetch the draft content instead of the published content. You’d want Next.js to switch to [dynamic rendering](/docs/app/building-your-application/rendering/server-components#dynamic-rendering) only for this specific case.
**Draft Mode** allows you to preview draft content from your headless CMS in your Next.js application. This is useful for static pages that are generated at build time as it allows you to switch to [dynamic rendering](/docs/app/building-your-application/rendering/server-components#dynamic-rendering) and see the draft changes without having to rebuild your entire site.

Next.js has a feature called **Draft Mode** which solves this problem. Here are instructions on how to use it.
This page walks through how to enable and use Draft Mode.

## Step 1: Create and access the Route Handler
## Step 1: Create a Route Handler

First, create a [Route Handler](/docs/app/building-your-application/routing/route-handlers). It can have any name - e.g. `app/api/draft/route.ts`
Create a [Route Handler](/docs/app/building-your-application/routing/route-handlers). It can have any name, for example, `app/api/draft/route.ts`.

Then, import `draftMode` from `next/headers` and call the `enable()` method.
```ts filename="app/api/draft/route.ts" switcher
export async function GET(request: Request) {
return new Response('')
}
```

```js filename="app/api/draft/route.js" switcher
export async function GET() {
return new Response('')
}
```

Then, import the [`draftMode`](/docs/app/api-reference/functions/draft-mode) function and call the `enable()` method.

```ts filename="app/api/draft/route.ts" switcher
// route handler enabling draft mode
import { draftMode } from 'next/headers'

export async function GET(request: Request) {
Expand All @@ -24,7 +40,6 @@ export async function GET(request: Request) {
```

```js filename="app/api/draft/route.js" switcher
// route handler enabling draft mode
import { draftMode } from 'next/headers'

export async function GET(request) {
Expand All @@ -33,38 +48,32 @@ export async function GET(request) {
}
```

This will set a **cookie** to enable draft mode. Subsequent requests containing this cookie will trigger **Draft Mode** changing the behavior for statically generated pages (more on this later).
This will set a **cookie** to enable draft mode. Subsequent requests containing this cookie will trigger draft mode and change the behavior of statically generated pages.

You can test this manually by visiting `/api/draft` and looking at your browser’s developer tools. Notice the `Set-Cookie` response header with a cookie named `__prerender_bypass`.

### Securely accessing it from your Headless CMS

In practice, you’d want to call this Route Handler _securely_ from your headless CMS. The specific steps will vary depending on which headless CMS you’re using, but here are some common steps you could take.
## Step 2: Access the Route Handler from your Headless CMS

These steps assume that the headless CMS you’re using supports setting **custom draft URLs**. If it doesn’t, you can still use this method to secure your draft URLs, but you’ll need to construct and access the draft URL manually.
> These steps assume that the headless CMS you’re using supports setting **custom draft URLs**. If it doesn’t, you can still use this method to secure your draft URLs, but you’ll need to construct and access the draft URL manually. The specific steps will vary depending on which headless CMS you’re using.
**First**, you should create a **secret token string** using a token generator of your choice. This secret will only be known by your Next.js app and your headless CMS. This secret prevents people who don’t have access to your CMS from accessing draft URLs.
To securely access the Route Handler from your headless CMS:

**Second**, if your headless CMS supports setting custom draft URLs, specify the following as the draft URL. This assumes that your Route Handler is located at `app/api/draft/route.ts`
1. Create a **secret token string** using a token generator of your choice. This secret will only be known by your Next.js app and your headless CMS.
2. If your headless CMS supports setting custom draft URLs, specify a draft URL (this assumes that your Route Handler is located at `app/api/draft/route.ts`). For example:

```bash filename="Terminal"
https://<your-site>/api/draft?secret=<token>&slug=<path>
```

- `<your-site>` should be your deployment domain.
- `<token>` should be replaced with the secret token you generated.
- `<path>` should be the path for the page that you want to view. If you want to view `/posts/foo`, then you should use `&slug=/posts/foo`.
> - `<your-site>` should be your deployment domain.
> - `<token>` should be replaced with the secret token you generated.
> - `<path>` should be the path for the page that you want to view. If you want to view `/posts/one`, then you should use `&slug=/posts/one`.
>
> Your headless CMS might allow you to include a variable in the draft URL so that `<path>` can be set dynamically based on the CMS’s data like so: `&slug=/posts/{entry.fields.slug}`
Your headless CMS might allow you to include a variable in the draft URL so that `<path>` can be set dynamically based on the CMS’s data like so: `&slug=/posts/{entry.fields.slug}`

**Finally**, in the Route Handler:

- Check that the secret matches and that the `slug` parameter exists (if not, the request should fail).
- Call `draftMode.enable()` to set the cookie.
- Then redirect the browser to the path specified by `slug`.
3. In your Route Handler, check that the secret matches and that the `slug` parameter exists (if not, the request should fail), call `draftMode.enable()` to set the cookie. Then, redirect the browser to the path specified by `slug`:

```ts filename="app/api/draft/route.ts" switcher
// route handler with secret and slug
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'

Expand Down Expand Up @@ -99,7 +108,6 @@ export async function GET(request: Request) {
```

```js filename="app/api/draft/route.js" switcher
// route handler with secret and slug
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'

Expand Down Expand Up @@ -135,7 +143,7 @@ export async function GET(request) {

If it succeeds, then the browser will be redirected to the path you want to view with the draft mode cookie.

## Step 2: Update page
## Step 3: Preview the Draft Content

The next step is to update your page to check the value of `draftMode().isEnabled`.

Expand Down Expand Up @@ -199,46 +207,4 @@ export default async function Page() {
}
```

That's it! If you access the draft Route Handler (with `secret` and `slug`) from your headless CMS or manually, you should now be able to see the draft content. And if you update your draft without publishing, you should be able to view the draft.

Set this as the draft URL on your headless CMS or access manually, and you should be able to see the draft.

```bash filename="Terminal"
https://<your-site>/api/draft?secret=<token>&slug=<path>
```

## More Details

### Clear the Draft Mode cookie

By default, the Draft Mode session ends when the browser is closed.

To clear the Draft Mode cookie manually, create a Route Handler that calls `draftMode().disable()`:

```ts filename="app/api/disable-draft/route.ts" switcher
import { draftMode } from 'next/headers'

export async function GET(request: Request) {
draftMode().disable()
return new Response('Draft mode is disabled')
}
```

```js filename="app/api/disable-draft/route.js" switcher
import { draftMode } from 'next/headers'

export async function GET(request) {
draftMode().disable()
return new Response('Draft mode is disabled')
}
```

Then, send a request to `/api/disable-draft` to invoke the Route Handler. If calling this route using [`next/link`](/docs/app/api-reference/components/link), you must pass `prefetch={false}` to prevent accidentally deleting the cookie on prefetch.

### Unique per `next build`

A new bypass cookie value will be generated each time you run `next build`.

This ensures that the bypass cookie can’t be guessed.

> **Good to know**: To test Draft Mode locally over HTTP, your browser will need to allow third-party cookies and local storage access.
If you access the draft Route Handler (with `secret` and `slug`) from your headless CMS or manually using the URL, you should now be able to see the draft content. And, if you update your draft without publishing, you should be able to view the draft.
90 changes: 89 additions & 1 deletion docs/02-app/02-api-reference/04-functions/draft-mode.mdx
Original file line number Diff line number Diff line change
@@ -1,9 +1,97 @@
---
title: draftMode
description: API Reference for the draftMode function.
related:
title: Next Steps
description: Learn how to use Draft Mode with this step-by-step guide.
links:
- app/building-your-application/configuring/draft-mode
---

The `draftMode` function allows you to detect [Draft Mode](/docs/app/building-your-application/configuring/draft-mode) inside a [Server Component](/docs/app/building-your-application/rendering/server-components).
The `draftMode` function allows you to enable and disable [Draft Mode](/docs/app/building-your-application/configuring/draft-mode), as well as check if Draft Mode is enabled in a [Server Component](/docs/app/building-your-application/rendering/server-components).

## Reference

The following methods and properties are available:

| Method | Description |
| ----------- | --------------------------------------------------------------------------------- |
| `isEnabled` | A boolean value that indicates if Draft Mode is enabled. |
| `enable()` | Enables Draft Mode in a Route Handler by setting a cookie (`__prerender_bypass`). |
| `disable()` | Disables Draft Mode in a Route Handler by deleting a cookie. |

> **Good to know:**
>
> - A new bypass cookie value will be generated each time you run `next build`. This ensures that the bypass cookie can’t be guessed.
> - To test Draft Mode locally over HTTP, your browser will need to allow third-party cookies and local storage access.
## Examples

### Enabling Draft Mode

To enable Draft Mode, create a new [Route Handler](/docs/app/building-your-application/routing/route-handlers) and call the `enable()` method:

```tsx filename="app/draft/route.ts" switcher
import { draftMode } from 'next/headers'

export async function GET(request: Request) {
draftMode().enable()
return new Response('Draft mode is enabled')
}
```

```js filename="app/draft/route.js" switcher
import { draftMode } from 'next/headers'

export async function GET(request) {
draftMode().enable()
return new Response('Draft mode is enabled')
}
```

### Disabling Draft Mode

By default, the Draft Mode session ends when the browser is closed.

To disable Draft Mode manually, call the `disable()` method in your [Route Handler](/docs/app/building-your-application/routing/route-handlers):

```tsx filename="app/draft/route.ts" switcher
import { draftMode } from 'next/headers'

export async function GET(request: Request) {
draftMode().disable()
return new Response('Draft mode is disabled')
}
```

```js filename="app/draft/route.js" switcher
import { draftMode } from 'next/headers'

export async function GET(request) {
draftMode().disable()
return new Response('Draft mode is disabled')
}
```

Then, send a request to invoke the Route Handler. If calling the route using the [`<Link>` component](/docs/app/api-reference/components/link), you must pass `prefetch={false}` to prevent accidentally deleting the cookie on prefetch.

### Checking if Draft Mode is enabled

You can check if Draft Mode is enabled in a Server Component with the `isEnabled` property:

```tsx filename="app/page.ts"
import { draftMode } from 'next/headers'

export default function Page() {
const { isEnabled } = draftMode()
return (
<main>
<h1>My Blog Post</h1>
<p>Draft Mode is currently {isEnabled ? 'Enabled' : 'Disabled'}</p>
</main>
)
}
```

```jsx filename="app/page.js"
import { draftMode } from 'next/headers'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type LayoutProps = {
children: React.ReactNode
}

export default function Layout({ children }: LayoutProps) {
return <div>{children}</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Link from 'next/link'

export default function Page() {
return (
<div>
<div>Home header</div>
<Link href="/parallel-route/product" id="product-link">
Product
</Link>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type LayoutProps = {
children: React.ReactNode
}

export default function Layout({ children }: LayoutProps) {
return <>{children}</>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Link from 'next/link'

export default function Page() {
return (
<div>
<h1 id="product-title">Product header</h1>
<Link href="/parallel-route" id="home-link">
Go to Home page
</Link>
</div>
)
}
17 changes: 17 additions & 0 deletions test/e2e/app-dir/metadata-navigation/app/parallel-route/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export default function Layout({ children, header }) {
return (
<html>
<head></head>
<body>
{header}
{children}
</body>
</html>
)
}

export const metadata = {
title: 'Home Layout',
description: 'this is the layout description',
keywords: ['nextjs', 'react'],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Home() {
return (
<div>
<h3 id="home-title">Home page content</h3>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Metadata } from 'next'

export const metadata: Metadata = {
title: 'Product Layout',
}

export default function Layout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return <>{children}</>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div>Product page content</div>
}
21 changes: 21 additions & 0 deletions test/e2e/app-dir/metadata-navigation/metadata-navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,26 @@ describe('app dir - metadata navigation', () => {
})
expect(res.status).toBe(307)
})

it('should show the index title', async () => {
const browser = await next.browser('/parallel-route')
expect(await browser.elementByCss('title').text()).toBe('Home Layout')
})

it('should show target page metadata after navigation', async () => {
const browser = await next.browser('/parallel-route')
await browser.elementByCss('#product-link').click()
await browser.waitForElementByCss('#product-title')
expect(await browser.elementByCss('title').text()).toBe('Product Layout')
})

it('should show target page metadata after navigation with back', async () => {
const browser = await next.browser('/parallel-route')
await browser.elementByCss('#product-link').click()
await browser.waitForElementByCss('#product-title')
await browser.elementByCss('#home-link').click()
await browser.waitForElementByCss('#home-title')
expect(await browser.elementByCss('title').text()).toBe('Home Layout')
})
})
})

0 comments on commit 95947c3

Please sign in to comment.