Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend draftmode to support arbitrary context data #75330

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 68 additions & 5 deletions docs/01-app/03-api-reference/04-functions/draft-mode.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ export default async function Page() {

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. |
| Method | Description |
| ------------- | --------------------------------------------------------------------------------- |
| `isEnabled` | A boolean value that indicates if Draft Mode is enabled. |
| `previewData` | A string containing ad-hoc data associated with the draft mode session |
| `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

Expand Down Expand Up @@ -69,6 +70,34 @@ export async function GET(request) {
}
```

### Enabling Draft Mode with Contextual Data

To enable Draft Mode with contextual data, pass a string to the `enable()` method:

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

export async function GET(request: Request) {
const context = {
customerType: 'premium',
}

const draft = await draftMode()
draft().enable(JSON.stringify(context))
return new Response('Draft mode is enabled')
}
```

```js filename="app/draft/route.js" switcher
const context = {
customerType: 'premium',
}

const draft = await draftMode()
draft().enable(JSON.stringify(context))
return new Response('Draft mode is enabled')
```

### Disabling Draft Mode

By default, the Draft Mode session ends when the browser is closed.
Expand Down Expand Up @@ -129,6 +158,40 @@ export default async function Page() {
}
```

### Getting Draft Mode Contextual Data

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

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

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

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

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

## Version History

| Version | Changes |
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/api-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export function checkIsOnDemandRevalidate(
}

export const COOKIE_NAME_PRERENDER_BYPASS = `__prerender_bypass`
export const COOKIE_NAME_PRERENDER_BYPASS_PREVIEW = `__prerender_bypass_preview`
export const COOKIE_NAME_PRERENDER_DATA = `__next_preview_data`

export const RESPONSE_LIMIT_DEFAULT = 4 * 1024 * 1024
Expand Down
30 changes: 29 additions & 1 deletion packages/next/src/server/async-storage/draft-mode-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import type { NextRequest } from '../web/spec-extension/request'

import {
COOKIE_NAME_PRERENDER_BYPASS,
COOKIE_NAME_PRERENDER_BYPASS_PREVIEW,
checkIsOnDemandRevalidate,
} from '../api-utils'
import type { __ApiPreviewProps } from '../api-utils'

export class DraftModeProvider {
public readonly isEnabled: boolean
public readonly previewData: string | undefined

/**
* @internal - this declaration is stripped via `tsc --stripInternal`
Expand Down Expand Up @@ -47,11 +49,17 @@ export class DraftModeProvider {
previewProps.previewModeId === 'development-id'))
)

if (this.isEnabled) {
this.previewData = cookies.get(
COOKIE_NAME_PRERENDER_BYPASS_PREVIEW
)?.value
}

this._previewModeId = previewProps?.previewModeId
this._mutableCookies = mutableCookies
}

enable() {
enable(previewData?: string) {
if (!this._previewModeId) {
throw new Error(
'Invariant: previewProps missing previewModeId this should never happen'
Expand All @@ -66,6 +74,17 @@ export class DraftModeProvider {
secure: process.env.NODE_ENV !== 'development',
path: '/',
})

if (previewData) {
this._mutableCookies.set({
name: COOKIE_NAME_PRERENDER_BYPASS_PREVIEW,
value: previewData,
httpOnly: true,
sameSite: process.env.NODE_ENV !== 'development' ? 'none' : 'lax',
secure: process.env.NODE_ENV !== 'development',
path: '/',
})
}
}

disable() {
Expand All @@ -81,5 +100,14 @@ export class DraftModeProvider {
path: '/',
expires: new Date(0),
})

this._mutableCookies.set({
name: COOKIE_NAME_PRERENDER_BYPASS_PREVIEW,
value: '',
httpOnly: true,
sameSite: process.env.NODE_ENV !== 'development' ? 'none' : 'lax',
secure: process.env.NODE_ENV !== 'development',
expires: new Date(0),
})
}
}
47 changes: 44 additions & 3 deletions packages/next/src/server/request/draft-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,21 @@ function createExoticDraftMode(
enumerable: true,
configurable: true,
})

Object.defineProperty(promise, 'previewData', {
get() {
return instance.previewData
},
set(newValue) {
Object.defineProperty(promise, 'previewData', {
value: newValue,
writable: true,
enumerable: true,
})
},
enumerable: true,
configurable: true,
})
;(promise as any).enable = instance.enable.bind(instance)
;(promise as any).disable = instance.disable.bind(instance)

Expand Down Expand Up @@ -136,6 +151,23 @@ function createExoticDraftModeWithDevWarnings(
configurable: true,
})

Object.defineProperty(promise, 'previewData', {
get() {
const expression = '`draftMode().previewData`'
syncIODev(route, expression)
return instance.previewData
},
set(newValue) {
Object.defineProperty(promise, 'previewData', {
value: newValue,
writable: true,
enumerable: true,
})
},
enumerable: true,
configurable: true,
})

Object.defineProperty(promise, 'enable', {
value: function get() {
const expression = '`draftMode().enable()`'
Expand Down Expand Up @@ -164,18 +196,27 @@ class DraftMode {
constructor(provider: null | DraftModeProvider) {
this._provider = provider
}

get isEnabled() {
if (this._provider !== null) {
return this._provider.isEnabled
}
return false
}
public enable() {
// We we have a store we want to track dynamic data access to ensure we

get previewData() {
if (this._provider !== null) {
return this._provider.previewData
}
return undefined
}

public enable(previewData?: string) {
// We have a store we want to track dynamic data access to ensure we
// don't statically generate routes that manipulate draft mode.
trackDynamicDraftMode('draftMode().enable()')
if (this._provider !== null) {
this._provider.enable()
this._provider.enable(previewData)
}
}
public disable() {
Expand Down
8 changes: 8 additions & 0 deletions test/e2e/app-dir/draft-mode/app/enable-with-context/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { draftMode } from 'next/headers'

export async function GET() {
;(await draftMode()).enable('test-context')
return new Response(
"Enabled in Route Handler using `(await draftMode()).enable('test-context')`, check cookies"
)
}
5 changes: 4 additions & 1 deletion test/e2e/app-dir/draft-mode/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import { draftMode } from 'next/headers'

export default async function Page() {
const { isEnabled } = await draftMode()
const { isEnabled, previewData } = await draftMode()

return (
<>
Expand All @@ -13,6 +13,9 @@ export default async function Page() {
<p>
State: <strong id="mode">{isEnabled ? 'ENABLED' : 'DISABLED'}</strong>
</p>
<p>
Context: <strong id="context">{isEnabled ? previewData : 'N/A'}</strong>
</p>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { draftMode } from 'next/headers'

export async function GET() {
;(await draftMode()).enable('test-context')
return new Response(
"Enabled in Route Handler using `(await draftMode()).enable('test-context')`, check cookies"
)
}
5 changes: 4 additions & 1 deletion test/e2e/app-dir/draft-mode/app/with-edge/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import { draftMode } from 'next/headers'

export default async function Page() {
const { isEnabled } = await draftMode()
const { isEnabled, previewData } = await draftMode()

return (
<>
Expand All @@ -13,6 +13,9 @@ export default async function Page() {
<p>
State: <strong id="mode">{isEnabled ? 'ENABLED' : 'DISABLED'}</strong>
</p>
<p>
Context: <strong id="context">{isEnabled ? previewData : 'N/A'}</strong>
</p>
</>
)
}
18 changes: 18 additions & 0 deletions test/e2e/app-dir/draft-mode/draft-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,24 @@ describe('app dir - draft mode', () => {
expect(Cookie).toBeDefined()
})

it('should have set-cookie header with context data', async () => {
const res = await next.fetch(`${basePath}enable-with-context`)
const h = res.headers.get('set-cookie') || ''
const cookies = h
.split(',')
.filter((c) => c.trim().startsWith('__prerender_bypass'))
Cookie = cookies.map((c) => c.split(';')[0]).join('; ')
expect(Cookie).toBeDefined()
expect(cookies).toHaveLength(2)
})

it('should have accessible context data returned from draftmode', async () => {
const opts = { headers: { Cookie } }
const $ = await next.render$(basePath, {}, opts)
expect($('#mode').text()).toBe('ENABLED')
expect($('#context').text()).toBe('test-context')
})

it('should have set-cookie header with redirect location', async () => {
const res = await next.fetch(`${basePath}enable-and-redirect`, {
redirect: 'manual',
Expand Down
Loading