From 95c9664a575ddf07af5eab7358e9cca8eb71dea7 Mon Sep 17 00:00:00 2001 From: Shu Uesugi Date: Thu, 5 Mar 2020 10:18:54 -0800 Subject: [PATCH 1/7] Add preview mode file --- docs/advanced-features/preview-mode.md | 5 +++++ docs/manifest.json | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 docs/advanced-features/preview-mode.md diff --git a/docs/advanced-features/preview-mode.md b/docs/advanced-features/preview-mode.md new file mode 100644 index 000000000000000..f45a6446c94e3d5 --- /dev/null +++ b/docs/advanced-features/preview-mode.md @@ -0,0 +1,5 @@ +--- +description: Next.js has the preview mode for statically generated pages. You can learn how it works here. +--- + +# Preview Mode diff --git a/docs/manifest.json b/docs/manifest.json index fb6dd32df41a138..1f934f9a4029acd 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -80,6 +80,10 @@ { "title": "Advanced Features", "routes": [ + { + "title": "Preview Mode", + "path": "/docs/advanced-features/preview-mode.md" + }, { "title": "Dynamic Import", "path": "/docs/advanced-features/dynamic-import.md" From df3c065557f4f8e25aeb948443db222ec0107862 Mon Sep 17 00:00:00 2001 From: Shu Uesugi Date: Thu, 5 Mar 2020 10:11:08 -0800 Subject: [PATCH 2/7] Update descriptions for data fetching --- docs/basic-features/data-fetching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-features/data-fetching.md b/docs/basic-features/data-fetching.md index 0b1cf4f2133801f..7d1fbae3ca78cb9 100644 --- a/docs/basic-features/data-fetching.md +++ b/docs/basic-features/data-fetching.md @@ -1,5 +1,5 @@ --- -description: Next.js can handle data fetching in multiple ways for server-rendered and static pages. Learn how it works here. +description: Next.js has 2 pre-rendering modes: Static Generation and Server-side rendering. Learn how they work here. --- # Data fetching From 90ab5982266f401ed2b8036b2ddc842d00acb3cc Mon Sep 17 00:00:00 2001 From: Shu Uesugi Date: Thu, 5 Mar 2020 21:16:16 -0800 Subject: [PATCH 3/7] Add preview mode documentation --- docs/advanced-features/preview-mode.md | 195 +++++++++++++++++++++++++ docs/basic-features/data-fetching.md | 19 ++- docs/basic-features/pages.md | 7 + 3 files changed, 220 insertions(+), 1 deletion(-) diff --git a/docs/advanced-features/preview-mode.md b/docs/advanced-features/preview-mode.md index f45a6446c94e3d5..da5d7fc59cb0064 100644 --- a/docs/advanced-features/preview-mode.md +++ b/docs/advanced-features/preview-mode.md @@ -3,3 +3,198 @@ description: Next.js has the preview mode for statically generated pages. You ca --- # Preview Mode + +> This document is for Next.js versions 9.3 and up. If you're using older versions of Next.js, refer to our [previous documentation](https://nextjs.org/docs/tag/v9.2.2/basic-features/pages). + +In the [Pages documentation](/docs/basic-features/pages.md) and the [Data Fetching documentation](/docs/basic-features/data-fetching.md), we talked about how to pre-render a page at build time (**Static Generation**) using `getStaticProps` and `getStaticPaths`. + +Static Generation is useful when your pages fetch data from a headless CMS. However, it's not ideal when you're writing a your draft on your headless CMS and want to **preview** the draft immediately on your page. You'd want to 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 bypass Static Generation only for this specific case. + +Next.js has the feature called **Preview Mode** which solves this problem. Here's an instruction on how to use it. + +## Step 1. Create and access a preview API route + +> Take a look at the [API Routes documentation](/docs/api-routes/introduction.md) first if you're not familiar with Next.js API Routes. + +First, create a **preview API route**. It can have any name - e.g. `pages/api/preview.js` (or `.ts` if using TypeScript). + +In this API route, you need to call `setPreviewData` on the response object. The argument for `setPreviewData` can be an object or string, and this will be used by `getStaticProps` (more on this later). For now, we'll just use `{}`. + +```js +export default (req, res) => { + // ... + res.setPreviewData({}) + // ... +} +``` + +`res.setPreviewData` sets some **cookies** on the browser which turns on the preview mode. Any requests to Next.js containing these cookies will be considered as the **preview mode**, and the behavior for statically generated pages will change (more on this later). + +You can test this manually by creating an API route like below and accessing it from your browser manually: + +```js +// A simple example for testing it manually from your browser. +// If this is located at pages/api/preview.js, then +// open /api/preview from your browser. +export default (req, res) => { + res.setPreviewData({}) + res.end() +} +``` + +If you use your browser's developer tools, you'll notice that some cookies will be set on this request. + +### Securely accessing it from your Headless CMS + +In practice, you'd want to call this API route _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 would take. + +These steps assume that the headless CMS you're using supports setting **custom preview URLs**. If it doesn't, you can still use this method to secure your preview URLs, but you'll need to construct and access the preview URL manually. + +**First**, you should create a **secret token string** using a token generator of your choice. You can store it in an environment variable such as `PREVIEW_TOKEN` ([environment variables documentation](/docs/api-reference/next.config.js/environment-variables.md)). 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 preview URLs. + +**Second**, if your headless CMS supports setting custom preview URLs, specify the following as the preview URL. (This assumes that your preview API route is located at `pages/api/preview.js`.) + +```bash +https:///api/preview?secret=&next= +``` + +- `` should be your deployment domain. +- `` should be replaced with the secret token you generated. +- `` should be the path for the page that you want to preview. If you want to preview `/posts/foo`, then you should use `&next=/posts/foo`. + +Your headless CMS might allow you to include a variable in the preview URL so that `` can be set dynamically based on the CMS's data like so: `&next=/posts/{entry.fields.slug}` + +**Finally**, in the preview API route: + +- Check that the secret matches and that the `next` parameter exists (if not, the request should fail). +- Call `res.setPreviewData`. +- Then redirect the browser to the path specified by `next`. (The following example uses the [307 redirect](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307)). + +```js +export default (req, res) => { + // Check the secret and next parameters + if (req.query.secret === process.env.PREVIEW_TOKEN && res.query.next) { + // Set the Preview Mode cookies + res.setPreviewData({}) + + // Redirect to the path specified by the next parameter + res.writeHead(307, { Location: res.query.next }) + res.end() + } else { + res.status(401).json({ message: 'Invalid token' }) + } +} +``` + +If it succeeds, then the browser will be redirected to the path you want to preview with the preview mode cookies being set. + +## Step 2. Update `getStaticProps` + +The next step is to update `getStaticProps` to support the preview mode. + +If you request a page which has `getStaticProps` with the preview mode cookies set (via `res.setPreviewData`), then `getStaticProps` will be called at **request time** (instead of at build time). + +Furthermore, it will be called with a `context` object where: + +- `context.preview` will be `true`. +- `context.previewData` will be the same as the argument used for `setPreviewData`. + +```js +export async function getStaticProps(context) { + // If you request this page with the preview mode cookies set: + // + // - context.preview will be true + // - context.previewData will be the same as + // the argument used for `setPreviewData`. +} +``` + +We used `res.setPreviewData({})` in the preview API route, so `context.previewData` will be `{}`. You can use this to pass data from the preview API route to `getStaticProps` if necessary. + +If you're also using `getStaticPaths`, then `context.params` will also be available. + +### Fetch preview data + +You can update `getStaticProps` to fetch different data based on `context.preview` and/or `context.previewData`. + +For example, your headless CMS might have a different API endpoint for draft posts. If so, you can use `context.preview` to modify the API endpoint URL like below: + +```js +export async function getStaticProps(context) { + // If context.preview is true, append "/preview" to the API endpoint + // to request draft data instead of published data. This will vary + // based on which headless CMS you're using. + const res = await fetch(`https://.../${context.preview ? 'preview' : ''}`) + // ... +} +``` + +That's it! If you access the preview API route (with `secret` and `next`) from your headless CMS or manually, you should now be able to see the preview content. And if you update your draft without publishing, you should be able to preview the draft. + +```bash +# Set this as the preview URL on your headless CMS or access manually, +# and you should be able to see the preview. +https:///api/preview?secret=&next= +``` + +## More Details + +### Clear the preview mode cookies + +By default, no expiration date is set for the preview mode cookies, so the preview mode ends when the browser is closed. + +To clear the preview cookies manually, you can create an API route which calls `clearPreviewData` and then access this API route. + +```js +export default (req, res) => { + // Clears the preview mode cookies. + // This function accepts no arguments. + res.clearPreviewData() + // ... +} +``` + +### Specify the preview mode duration + +`setPreviewData` takes an optional second parameter which should be an options object. It accepts the following keys: + +- `maxAge`: Specifies the number (in seconds) for the preview session to last for. + +```js +setPreviewData(data, { + maxAge: 60 * 60, // The preview mode cookies expire in 1 hour +}) +``` + +### `previewData` size limits + +You can pass an object or a string to `setPreviewData` and have it be available in `getStaticProps`. However, because the data will be stored in a cookie, there's a size limitation. Currently, preview data is limited to 2KB. + +### Works with `getServerSideProps` + +The preview mode works on `getServerSideProps` as well. It will also be called with a `context` object containing `preview` and `previewData`. + +## Learn more + +The following pages might also be useful. + + + + + + diff --git a/docs/basic-features/data-fetching.md b/docs/basic-features/data-fetching.md index 7d1fbae3ca78cb9..348a52195ca199d 100644 --- a/docs/basic-features/data-fetching.md +++ b/docs/basic-features/data-fetching.md @@ -30,7 +30,9 @@ export async function getStaticProps(context) { The `context` parameter is an object containing the following keys: -- `params`: `params` contains the route parameters for pages using dynamic routes. For example, if the page name is `[id].js` , then `params` will look like `{ id: ... }`. To learn more, take a look at the [Dynamic Routing documentation](/docs/routing/dynamic-routes.md). You should use this together with `getStaticPaths`, which we’ll explain later. +- `params` contains the route parameters for pages using dynamic routes. For example, if the page name is `[id].js` , then `params` will look like `{ id: ... }`. To learn more, take a look at the [Dynamic Routing documentation](/docs/routing/dynamic-routes.md). You should use this together with `getStaticPaths`, which we’ll explain later. +- `preview` is `true` if the page is in the preview mode and `false` otherwise. See the [Preview Mode documentation](/docs/advanced-features/preview-mode.md). +- `previewData` contains the preview data set by `setPreviewData`. See the [Preview Mode documentation](/docs/advanced-features/preview-mode.md). ### Simple Example @@ -108,6 +110,12 @@ Also, you must use `export async function getStaticProps() {}` — it will **not In development (`next dev`), `getStaticProps` will be called on every request. +#### Preview Mode + +In some cases, you might want to temporarily bypass Static Generation and render the page at **request time** instead of build time. For example, you might be using a headless CMS and want to preview drafts before they're published. + +This use case is supported by Next.js by the feature called **Preview Mode**. Learn more on the [Preview Mode documentation](/docs/advanced-features/preview-mode.md). + ## `getStaticPaths` (Static Generation) If a page has dynamic routes ([documentation](/docs/routing/dynamic-routes.md)) and uses `getStaticProps` it needs to define a list of paths that have to be rendered to HTML at build time. @@ -305,6 +313,8 @@ The `context` parameter is an object containing the following keys: - `req`: [The HTTP request object](https://nodejs.org/api/http.html#http_class_http_incomingmessage). - `res`: [The HTTP response object](https://nodejs.org/api/http.html#http_class_http_serverresponse). - `query`: The query string. +- `preview`: `preview` is `true` if the page is in the preview mode and `false` otherwise. See the [Preview Mode documentation](/docs/advanced-features/preview-mode.md). +- `previewData`: The preview data set by `setPreviewData`. See the [Preview Mode documentation](/docs/advanced-features/preview-mode.md). ### Simple example @@ -380,6 +390,13 @@ function Profile() { We recommend you to read the following sections next: + + + +
Routing: From 6a8520cab517b3d06f4064cae3e03ea9b3831363 Mon Sep 17 00:00:00 2001 From: Shu Uesugi Date: Thu, 5 Mar 2020 21:42:42 -0800 Subject: [PATCH 4/7] Use quotes for front matter --- docs/basic-features/data-fetching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-features/data-fetching.md b/docs/basic-features/data-fetching.md index 348a52195ca199d..b6e71809895f664 100644 --- a/docs/basic-features/data-fetching.md +++ b/docs/basic-features/data-fetching.md @@ -1,5 +1,5 @@ --- -description: Next.js has 2 pre-rendering modes: Static Generation and Server-side rendering. Learn how they work here. +description: 'Next.js has 2 pre-rendering modes: Static Generation and Server-side rendering. Learn how they work here.' --- # Data fetching From 48db5fb28ee2af8499c090cfe091c89a05e86c91 Mon Sep 17 00:00:00 2001 From: Shu Uesugi Date: Thu, 5 Mar 2020 21:43:33 -0800 Subject: [PATCH 5/7] Wordsmith --- docs/advanced-features/preview-mode.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-features/preview-mode.md b/docs/advanced-features/preview-mode.md index da5d7fc59cb0064..8d2950d1acd9167 100644 --- a/docs/advanced-features/preview-mode.md +++ b/docs/advanced-features/preview-mode.md @@ -68,7 +68,7 @@ Your headless CMS might allow you to include a variable in the preview URL so th - Check that the secret matches and that the `next` parameter exists (if not, the request should fail). - Call `res.setPreviewData`. -- Then redirect the browser to the path specified by `next`. (The following example uses the [307 redirect](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307)). +- Then redirect the browser to the path specified by `next`. (The following example uses a [307 redirect](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307)). ```js export default (req, res) => { From efec3505666a3484bd46eb8f881372b161e26e8a Mon Sep 17 00:00:00 2001 From: Shu Uesugi Date: Thu, 5 Mar 2020 21:44:15 -0800 Subject: [PATCH 6/7] Fix URL --- docs/advanced-features/preview-mode.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-features/preview-mode.md b/docs/advanced-features/preview-mode.md index 8d2950d1acd9167..820c76229c38fa9 100644 --- a/docs/advanced-features/preview-mode.md +++ b/docs/advanced-features/preview-mode.md @@ -193,7 +193,7 @@ The following pages might also be useful.
- + Environment Variables: Learn more about environment variables in Next.js. From b6af00e3ba1e514b2e1b21d5a12c6326b6b40c6d Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Fri, 6 Mar 2020 12:06:13 +0100 Subject: [PATCH 7/7] Minor updates --- docs/advanced-features/preview-mode.md | 78 ++++++++++++++++---------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/docs/advanced-features/preview-mode.md b/docs/advanced-features/preview-mode.md index 820c76229c38fa9..64c7c0ff8947746 100644 --- a/docs/advanced-features/preview-mode.md +++ b/docs/advanced-features/preview-mode.md @@ -4,21 +4,21 @@ description: Next.js has the preview mode for statically generated pages. You ca # Preview Mode -> This document is for Next.js versions 9.3 and up. If you're using older versions of Next.js, refer to our [previous documentation](https://nextjs.org/docs/tag/v9.2.2/basic-features/pages). +> This document is for Next.js versions 9.3 and up. If you’re using older versions of Next.js, refer to our [previous documentation](https://nextjs.org/docs/tag/v9.2.2/basic-features/pages). In the [Pages documentation](/docs/basic-features/pages.md) and the [Data Fetching documentation](/docs/basic-features/data-fetching.md), we talked about how to pre-render a page at build time (**Static Generation**) using `getStaticProps` and `getStaticPaths`. -Static Generation is useful when your pages fetch data from a headless CMS. However, it's not ideal when you're writing a your draft on your headless CMS and want to **preview** the draft immediately on your page. You'd want to 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 bypass Static Generation only for this specific case. +Static Generation 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 **preview** the draft immediately on your page. You’d want to 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 bypass Static Generation only for this specific case. -Next.js has the feature called **Preview Mode** which solves this problem. Here's an instruction on how to use it. +Next.js has the feature called **Preview Mode** which solves this problem. Here’s an instruction on how to use it. ## Step 1. Create and access a preview API route -> Take a look at the [API Routes documentation](/docs/api-routes/introduction.md) first if you're not familiar with Next.js API Routes. +> Take a look at the [API Routes documentation](/docs/api-routes/introduction.md) first if you’re not familiar with Next.js API Routes. First, create a **preview API route**. It can have any name - e.g. `pages/api/preview.js` (or `.ts` if using TypeScript). -In this API route, you need to call `setPreviewData` on the response object. The argument for `setPreviewData` can be an object or string, and this will be used by `getStaticProps` (more on this later). For now, we'll just use `{}`. +In this API route, you need to call `setPreviewData` on the response object. The argument for `setPreviewData` should be an object, and this can be used by `getStaticProps` (more on this later). For now, we’ll just use `{}`. ```js export default (req, res) => { @@ -38,51 +38,63 @@ You can test this manually by creating an API route like below and accessing it // open /api/preview from your browser. export default (req, res) => { res.setPreviewData({}) - res.end() + res.end('Preview mode enabled') } ``` -If you use your browser's developer tools, you'll notice that some cookies will be set on this request. +If you use your browser’s developer tools, you’ll notice that the `__prerender_bypass` and `__next_preview_data` cookies will be set on this request. ### Securely accessing it from your Headless CMS -In practice, you'd want to call this API route _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 would take. +In practice, you’d want to call this API route _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. -These steps assume that the headless CMS you're using supports setting **custom preview URLs**. If it doesn't, you can still use this method to secure your preview URLs, but you'll need to construct and access the preview URL manually. +These steps assume that the headless CMS you’re using supports setting **custom preview URLs**. If it doesn’t, you can still use this method to secure your preview URLs, but you’ll need to construct and access the preview URL manually. -**First**, you should create a **secret token string** using a token generator of your choice. You can store it in an environment variable such as `PREVIEW_TOKEN` ([environment variables documentation](/docs/api-reference/next.config.js/environment-variables.md)). 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 preview URLs. +**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 preview URLs. **Second**, if your headless CMS supports setting custom preview URLs, specify the following as the preview URL. (This assumes that your preview API route is located at `pages/api/preview.js`.) ```bash -https:///api/preview?secret=&next= +https:///api/preview?secret=&slug= ``` - `` should be your deployment domain. - `` should be replaced with the secret token you generated. -- `` should be the path for the page that you want to preview. If you want to preview `/posts/foo`, then you should use `&next=/posts/foo`. +- `` should be the path for the page that you want to preview. If you want to preview `/posts/foo`, then you should use `&slug=/posts/foo`. -Your headless CMS might allow you to include a variable in the preview URL so that `` can be set dynamically based on the CMS's data like so: `&next=/posts/{entry.fields.slug}` +Your headless CMS might allow you to include a variable in the preview URL so that `` can be set dynamically based on the CMS’s data like so: `&slug=/posts/{entry.fields.slug}` **Finally**, in the preview API route: -- Check that the secret matches and that the `next` parameter exists (if not, the request should fail). +- Check that the secret matches and that the `slug` parameter exists (if not, the request should fail). +- - Call `res.setPreviewData`. -- Then redirect the browser to the path specified by `next`. (The following example uses a [307 redirect](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307)). +- Then redirect the browser to the path specified by `slug`. (The following example uses a [307 redirect](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307)). ```js -export default (req, res) => { +export default async (req, res) => { // Check the secret and next parameters - if (req.query.secret === process.env.PREVIEW_TOKEN && res.query.next) { - // Set the Preview Mode cookies - res.setPreviewData({}) - - // Redirect to the path specified by the next parameter - res.writeHead(307, { Location: res.query.next }) - res.end() - } else { - res.status(401).json({ message: 'Invalid token' }) + // This secret should only be know to this API route and the CMS + if (req.query.secret !== 'MY_SECRET_TOKEN' || !res.query.slug) { + return res.status(401).json({ message: 'Invalid token' }) + } + + // Fetch the headless CMS to check if the provided `slug` exists + // getPostBySlug would implement the required fetching logic to the headless CMS + const post = await getPostBySlug(req.query.slug) + + // If the slug doesn't exist prevent preview mode from being enabled + if (!post) { + return res.status(401).json({ message: 'Invalid slug' }) } + + // Enable Preview Mode by setting the cookies + res.setPreviewData({}) + + // Redirect to the path from the fetched post + // We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities + res.writeHead(307, { Location: post.slug }) + res.end() } ``` @@ -109,9 +121,9 @@ export async function getStaticProps(context) { } ``` -We used `res.setPreviewData({})` in the preview API route, so `context.previewData` will be `{}`. You can use this to pass data from the preview API route to `getStaticProps` if necessary. +We used `res.setPreviewData({})` in the preview API route, so `context.previewData` will be `{}`. You can use this to pass session information from the preview API route to `getStaticProps` if necessary. -If you're also using `getStaticPaths`, then `context.params` will also be available. +If you’re also using `getStaticPaths`, then `context.params` will also be available. ### Fetch preview data @@ -129,12 +141,12 @@ export async function getStaticProps(context) { } ``` -That's it! If you access the preview API route (with `secret` and `next`) from your headless CMS or manually, you should now be able to see the preview content. And if you update your draft without publishing, you should be able to preview the draft. +That’s it! If you access the preview API route (with `secret` and `slug`) from your headless CMS or manually, you should now be able to see the preview content. And if you update your draft without publishing, you should be able to preview the draft. ```bash # Set this as the preview URL on your headless CMS or access manually, # and you should be able to see the preview. -https:///api/preview?secret=&next= +https:///api/preview?secret=&slug= ``` ## More Details @@ -168,11 +180,15 @@ setPreviewData(data, { ### `previewData` size limits -You can pass an object or a string to `setPreviewData` and have it be available in `getStaticProps`. However, because the data will be stored in a cookie, there's a size limitation. Currently, preview data is limited to 2KB. +You can pass an object to `setPreviewData` and have it be available in `getStaticProps`. However, because the data will be stored in a cookie, there’s a size limitation. Currently, preview data is limited to 2KB. ### Works with `getServerSideProps` -The preview mode works on `getServerSideProps` as well. It will also be called with a `context` object containing `preview` and `previewData`. +The preview mode works on `getServerSideProps` as well. It will also be available on the `context` object containing `preview` and `previewData`. + +### Unique per `next build` + +The bypass cookie value and private key for encrypting the `previewData` changes when a `next build` is ran, this ensures that the bypass cookie can’t be guessed. ## Learn more