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

Add support for draft mode #48669

Merged
merged 5 commits into from Apr 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
65 changes: 60 additions & 5 deletions packages/next/src/server/api-utils/node.ts
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
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
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
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
)
}