Files
1dc5c066cb docs: Rewrite Rendering Section and React Essentials Page (#51579)
We initially wrote the [React
page](https://nextjs.org/docs/getting-started/react-essentials) to
introduce Server Components in the App Router, but over time, some
implementation details have changed, and the information has become
stale. The React team is working on adding new docs, so I'd like to
change the narrative on the Next.js docs from "client vs. server
components" to "writing code for the server and for the client" - and
link back to the React documentation when it becomes available.

As React developers, we're very familiar with writing code for the
client, it's nice and simple. But doing so comes at the expense of not
being familiar with the server. The aim of these docs is to help
developers become proficient in both the client and server environments.

I'd like to take it back to the foundations, and not use abstractions
like SSG and CSR, MPAs or SPAs, as those terms come with their own set
of assumptions that make it harder to understand how RSC works. Instead,
we'll focus on the request lifecycle, show how application code flows
from the server to the client, and discuss the trade-offs of doing
operations in each environment.

- [x] Page: Rendering Fundamentals
   - [x] Environments: Client and Server
   - [x] Request-response lifecycle
   - [x] Network Boundary
- [x] Page: Server Components
    - [x] Benefits and use cases of server rendering
    - [x] How to use Server Components in Next.js
    - [x] How Server Components are rendered
    - [x] Static Rendering
    - [x] Dynamic Rendering
    - [x] Streaming
- [x] Page: Client Components
    - [x] Benefits and use cases of client rendering
    - [x] How to use Client Components in Next.js
    - [x] How Client Components are rendered
        - [x] Initial vs. Subsquent navigation
- [x] Page: Composition Patterns
    - [x] When to use client and server components
    - [x] Server Component Patterns
    - [x] Client Component Patterns
    - [x] Interleaving Client and Server Components
- [ ] ~Diagrams~ will follow up with new PR.
- [x] Set up redirects: https://github.com/vercel/front/pull/24917

---------

Co-authored-by: Térence Hollander <[email protected]>
Co-authored-by: shawnthwei <[email protected]>
Co-authored-by: Michael Novotny <[email protected]>
2023-08-24 08:48:44 -05:00

245 lines
8.9 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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.
---
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.
Next.js has a feature called **Draft Mode** which solves this problem. Here are instructions on how to use it.
## Step 1: Create and access the 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`
Then, import `draftMode` from `next/headers` 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) {
draftMode().enable()
return new Response('Draft mode is enabled')
}
```
```js filename="app/api/draft/route.js" switcher
// route handler enabling draft mode
import { draftMode } from 'next/headers'
export async function GET(request) {
draftMode().enable()
return new Response('Draft mode is enabled')
}
```
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).
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.
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.
**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.
**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`
```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 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`.
```ts filename="app/api/draft/route.ts" switcher
// route handler with secret and slug
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
export async function GET(request: Request) {
// Parse query string parameters
const { searchParams } = new URL(request.url)
const secret = searchParams.get('secret')
const slug = searchParams.get('slug')
// Check the secret and next parameters
// This secret should only be known to this route handler and the CMS
if (secret !== 'MY_SECRET_TOKEN' || !slug) {
return new Response('Invalid token', { status: 401 })
}
// 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(slug)
// If the slug doesn't exist prevent draft mode from being enabled
if (!post) {
return new Response('Invalid slug', { status: 401 })
}
// Enable Draft Mode by setting the cookie
draftMode().enable()
// Redirect to the path from the fetched post
// We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities
redirect(post.slug)
}
```
```js filename="app/api/draft/route.js" switcher
// route handler with secret and slug
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
export async function GET(request) {
// Parse query string parameters
const { searchParams } = new URL(request.url)
const secret = searchParams.get('secret')
const slug = searchParams.get('slug')
// Check the secret and next parameters
// This secret should only be known to this route handler and the CMS
if (secret !== 'MY_SECRET_TOKEN' || !slug) {
return new Response('Invalid token', { status: 401 })
}
// 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(slug)
// If the slug doesn't exist prevent draft mode from being enabled
if (!post) {
return new Response('Invalid slug', { status: 401 })
}
// Enable Draft Mode by setting the cookie
draftMode().enable()
// Redirect to the path from the fetched post
// We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities
redirect(post.slug)
}
```
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
The next step is to update your page to check the value of `draftMode().isEnabled`.
If you request a page which has the cookie set, then data will be fetched at **request time** (instead of at build time).
Furthermore, the value of `isEnabled` will be `true`.
```tsx filename="app/page.tsx" switcher
// page that fetches data
import { draftMode } from 'next/headers'
async function getData() {
const { isEnabled } = draftMode()
const url = isEnabled
? 'https://draft.example.com'
: 'https://production.example.com'
const res = await fetch(url)
return res.json()
}
export default async function Page() {
const { title, desc } = await getData()
return (
<main>
<h1>{title}</h1>
<p>{desc}</p>
</main>
)
}
```
```jsx filename="app/page.js" switcher
// page that fetches data
import { draftMode } from 'next/headers'
async function getData() {
const { isEnabled } = draftMode()
const url = isEnabled
? 'https://draft.example.com'
: 'https://production.example.com'
const res = await fetch(url)
return res.json()
}
export default async function Page() {
const { title, desc } = await getData()
return (
<main>
<h1>{title}</h1>
<p>{desc}</p>
</main>
)
}
```
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.