Skip to content

Commit

Permalink
Add support for draft mode (#48669)
Browse files Browse the repository at this point in the history
Draft Mode is very similar to Preview Mode but doesn't include any
additional data.

This PR implements support for Draft Mode in `pages` and a future PR
will implement support in `app`.

fix NEXT-992
  • Loading branch information
styfle committed Apr 23, 2023
1 parent 2e99645 commit 743a59d
Show file tree
Hide file tree
Showing 12 changed files with 430 additions and 7 deletions.
65 changes: 60 additions & 5 deletions packages/next/src/server/api-utils/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,23 @@ export function tryGetPreviewData(
const previewModeId = cookies.get(COOKIE_NAME_PRERENDER_BYPASS)?.value
const tokenPreviewData = cookies.get(COOKIE_NAME_PRERENDER_DATA)?.value

// Case: preview mode cookie set but data cookie is not set
if (
previewModeId &&
!tokenPreviewData &&
previewModeId === options.previewModeId
) {
// This is "Draft Mode" which doesn't use
// previewData, so we return an empty object
// for backwards compat with "Preview Mode".
const data = {}
Object.defineProperty(req, SYMBOL_PREVIEW_DATA, {
value: data,
enumerable: false,
})
return data
}

// Case: neither cookie is set.
if (!previewModeId && !tokenPreviewData) {
return false
Expand Down Expand Up @@ -262,8 +279,42 @@ function sendJson(res: NextApiResponse, jsonBody: any): void {
res.send(JSON.stringify(jsonBody))
}

function isNotValidData(str: string): boolean {
return typeof str !== 'string' || str.length < 16
function isValidData(str: any): str is string {
return typeof str === 'string' && str.length >= 16
}

function setDraftMode<T>(
res: NextApiResponse<T>,
options: {
enable: boolean
previewModeId?: string
}
): NextApiResponse<T> {
if (!isValidData(options.previewModeId)) {
throw new Error('invariant: invalid previewModeId')
}
const expires = options.enable ? undefined : new Date(0)
// To delete a cookie, set `expires` to a date in the past:
// https://tools.ietf.org/html/rfc6265#section-4.1.1
// `Max-Age: 0` is not valid, thus ignored, and the cookie is persisted.
const { serialize } =
require('next/dist/compiled/cookie') as typeof import('cookie')
const previous = res.getHeader('Set-Cookie')
res.setHeader(`Set-Cookie`, [
...(typeof previous === 'string'
? [previous]
: Array.isArray(previous)
? previous
: []),
serialize(COOKIE_NAME_PRERENDER_BYPASS, options.previewModeId, {
httpOnly: true,
sameSite: process.env.NODE_ENV !== 'development' ? 'none' : 'lax',
secure: process.env.NODE_ENV !== 'development',
path: '/',
expires,
}),
])
return res
}

function setPreviewData<T>(
Expand All @@ -274,13 +325,13 @@ function setPreviewData<T>(
path?: string
} & __ApiPreviewProps
): NextApiResponse<T> {
if (isNotValidData(options.previewModeId)) {
if (!isValidData(options.previewModeId)) {
throw new Error('invariant: invalid previewModeId')
}
if (isNotValidData(options.previewModeEncryptionKey)) {
if (!isValidData(options.previewModeEncryptionKey)) {
throw new Error('invariant: invalid previewModeEncryptionKey')
}
if (isNotValidData(options.previewModeSigningKey)) {
if (!isValidData(options.previewModeSigningKey)) {
throw new Error('invariant: invalid previewModeSigningKey')
}

Expand Down Expand Up @@ -464,6 +515,8 @@ export async function apiResolver(
setLazyProp({ req: apiReq }, 'preview', () =>
apiReq.previewData !== false ? true : undefined
)
// Set draftMode to the same value as preview
setLazyProp({ req: apiReq }, 'draftMode', () => apiReq.preview)

// Parsing of body
if (bodyParser && !apiReq.body) {
Expand Down Expand Up @@ -503,6 +556,8 @@ export async function apiResolver(
apiRes.json = (data) => sendJson(apiRes, data)
apiRes.redirect = (statusOrUrl: number | string, url?: string) =>
redirect(apiRes, statusOrUrl, url)
apiRes.setDraftMode = (options = { enable: true }) =>
setDraftMode(apiRes, Object.assign({}, apiContext, options))
apiRes.setPreviewData = (data, options = {}) =>
setPreviewData(apiRes, data, Object.assign({}, apiContext, options))
apiRes.clearPreviewData = (options = {}) =>
Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,7 @@ export async function renderToHTML(
? { params: query as ParsedUrlQuery }
: undefined),
...(isPreview
? { preview: true, previewData: previewData }
? { draftMode: true, preview: true, previewData: previewData }
: undefined),
locales: renderOpts.locales,
locale: renderOpts.locale,
Expand Down Expand Up @@ -1023,7 +1023,7 @@ export async function renderToHTML(
? { params: params as ParsedUrlQuery }
: undefined),
...(previewData !== false
? { preview: true, previewData: previewData }
? { draftMode: true, preview: true, previewData: previewData }
: undefined),
locales: renderOpts.locales,
locale: renderOpts.locale,
Expand Down
7 changes: 7 additions & 0 deletions packages/next/src/shared/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ export interface NextApiRequest extends IncomingMessage {

env: Env

draftMode?: boolean

preview?: boolean
/**
* Preview data set on the request, if any
Expand Down Expand Up @@ -243,6 +245,11 @@ export type NextApiResponse<Data = any> = ServerResponse & {
redirect(url: string): NextApiResponse<Data>
redirect(status: number, url: string): NextApiResponse<Data>

/**
* Set draft mode
*/
setDraftMode: (options: { enable: boolean }) => NextApiResponse<Data>

/**
* Set preview data for Next.js' prerender mode
*/
Expand Down
1 change: 1 addition & 0 deletions packages/next/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export type GetStaticPropsContext<
params?: Params
preview?: boolean
previewData?: Preview
draftMode?: boolean
locale?: string
locales?: string[]
defaultLocale?: string
Expand Down
26 changes: 26 additions & 0 deletions test/integration/draft-mode/pages/another.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Link from 'next/link'

export function getStaticProps({ draftMode }) {
return {
props: {
random: Math.random(),
draftMode: Boolean(draftMode).toString(),
},
revalidate: 100000,
}
}

export default function Another(props) {
return (
<>
<h1>Another</h1>
<p>
Draft Mode: <em id="draft">{props.draftMode}</em>
</p>
<p>
Random: <em id="rand">{props.random}</em>
</p>
<Link href="/">Go home</Link>
</>
)
}
4 changes: 4 additions & 0 deletions test/integration/draft-mode/pages/api/disable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default function handler(_req, res) {
res.setDraftMode({ enable: false })
res.end('Check your cookies...')
}
4 changes: 4 additions & 0 deletions test/integration/draft-mode/pages/api/enable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default function handler(_req, res) {
res.setDraftMode({ enable: true })
res.end('Check your cookies...')
}
4 changes: 4 additions & 0 deletions test/integration/draft-mode/pages/api/read.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default (req, res) => {
const { draftMode } = req
res.json({ draftMode })
}
34 changes: 34 additions & 0 deletions test/integration/draft-mode/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useState } from 'react'
import Link from 'next/link'

export function getStaticProps({ draftMode }) {
return {
props: {
random: Math.random(),
draftMode: Boolean(draftMode).toString(),
},
revalidate: 100000,
}
}

export default function Home(props) {
const [count, setCount] = useState(0)
return (
<>
<h1>Home</h1>
<p>
Draft Mode: <em id="draft">{props.draftMode}</em>
</p>
<button id="inc" onClick={() => setCount(count + 1)}>
Increment
</button>
<p>
Count: <span id="count">{count}</span>
</p>
<p>
Random: <em id="rand">{props.random}</em>
</p>
<Link href="/another">Visit another page</Link>
</>
)
}
27 changes: 27 additions & 0 deletions test/integration/draft-mode/pages/ssp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Link from 'next/link'

export function getServerSideProps({ res, draftMode }) {
// test override header
res.setHeader('Cache-Control', 'public, max-age=3600')
return {
props: {
random: Math.random(),
draftMode: Boolean(draftMode).toString(),
},
}
}

export default function SSP(props) {
return (
<>
<h1>Server Side Props</h1>
<p>
Draft Mode: <em id="draft">{props.draftMode}</em>
</p>
<p>
Random: <em id="rand">{props.random}</em>
</p>
<Link href="/">Go home</Link>
</>
)
}
15 changes: 15 additions & 0 deletions test/integration/draft-mode/pages/to-index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Link from 'next/link'

export function getStaticProps() {
return { props: {} }
}

export default function () {
return (
<main>
<Link href="/" id="to-index">
To Index
</Link>
</main>
)
}

0 comments on commit 743a59d

Please sign in to comment.