diff --git a/docs/02-app/01-building-your-application/07-configuring/05-mdx.mdx b/docs/02-app/01-building-your-application/07-configuring/05-mdx.mdx index a42ac30975786..4cbbda31cf8b3 100644 --- a/docs/02-app/01-building-your-application/07-configuring/05-mdx.mdx +++ b/docs/02-app/01-building-your-application/07-configuring/05-mdx.mdx @@ -24,25 +24,45 @@ Output: Next.js can support both local MDX content inside your application, as well as remote MDX files fetched dynamically on the server. The Next.js plugin handles transforming markdown and React components into HTML, including support for usage in Server Components (the default in App Router). -## `@next/mdx` +> **Good to know**: View the [Portfolio Starter Kit](https://vercel.com/templates/next.js/portfolio-starter-kit) template for a complete working example. -The `@next/mdx` package is used to configure Next.js so it can process markdown and MDX. **It sources data from local files**, allowing you to create pages with a `.mdx` extension, directly in your `/pages` or `/app` directory. +## Install dependencies -Let's walk through how to configure and use MDX with Next.js. +The `@next/mdx` package, and related packages, are used to configure Next.js so it can process markdown and MDX. **It sources data from local files**, allowing you to create pages with a `.md` or `.mdx` extension, directly in your `/pages` or `/app` directory. -## Getting Started - -Install packages needed to render MDX: +Install these packages to render MDX with Next.js: ```bash filename="Terminal" npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx ``` - +## Configure `next.config.mjs` + +Update the `next.config.mjs` file at your project's root to configure it to use MDX: + +```js filename="next.config.mjs" switcher +import createMDX from '@next/mdx' + +/** @type {import('next').NextConfig} */ +const nextConfig = { + // Configure `pageExtensions` to include markdown and MDX files + pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], + // Optionally, add any other Next.js config below +} + +const withMDX = createMDX({ + // Add markdown plugins here, as desired +}) + +// Merge MDX config with Next.js config +export default withMDX(nextConfig) +``` -Create a `mdx-components.tsx` file at the root of your application (`src/` or the parent folder of `app/`): +This allows `.md` and `.mdx` files to act as pages, routes, or imports in your application. -> **Good to know**: `mdx-components.tsx` is required to use MDX with App Router and will not work without it. +## Add a `mdx-components.tsx` file + +Create a `mdx-components.tsx` (or `.js`) file in the root of your project to define global MDX Components. For example, at the same level as `pages` or `app`, or inside `src` if applicable. ```tsx filename="mdx-components.tsx" switcher import type { MDXComponents } from 'mdx/types' @@ -62,33 +82,87 @@ export function useMDXComponents(components) { } ``` +> **Good to know**: +> +> - `mdx-components.tsx` is **required** to use `@next/mdx` with App Router and will not work without it. +> - Learn more about the [`mdx-components.tsx` file convention](/docs/app/api-reference/file-conventions/mdx-components). +> - Learn how to [use custom styles and components](#using-custom-styles-and-components). + +## Rendering MDX + +You can render MDX using Next.js's file based routing or by importing MDX files into other pages. + +### Using file based routing + +When using file based routing, you can use MDX pages like any other page. + + + +In App Router apps, that includes being able to use [metadata](/docs/app/building-your-application/optimizing/metadata). + +Create a new MDX page within the `/app` directory: + +```txt + my-project + ├── app + │ └── mdx-page + │ └── page.(mdx/md) + |── mdx-components.(tsx/js) + └── package.json +``` + -Update the `next.config.js` file at your project's root to configure it to use MDX: + -```js filename="next.config.js" -const withMDX = require('@next/mdx')() +Create a new MDX page within the `/pages` directory: -/** @type {import('next').NextConfig} */ -const nextConfig = { - // Configure `pageExtensions` to include MDX files - pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'], - // Optionally, add any other Next.js config below -} +```txt + my-project + |── mdx-components.(tsx/js) + ├── pages + │ └── mdx-page.(mdx/md) + └── package.json +``` -module.exports = withMDX(nextConfig) + + +You can use MDX in these files, and even import React components, directly inside your MDX page: + +```mdx +import { MyComponent } from 'my-component' + +# Welcome to my MDX page! + +This is some **bold** and _italics_ text. + +This is a list in markdown: + +- One +- Two +- Three + +Checkout my React component: + + ``` +Navigating to the `/mdx-page` route should display your rendered MDX page. + +### Using imports + -Then, create a new MDX page within the `/app` directory: +Create a new page within the `/app` directory and an MDX file wherever you'd like: ```txt - your-project + my-project ├── app - │ └── my-mdx-page - │ └── page.mdx - |── mdx-components.(tsx/jsx) + │ └── mdx-page + │ └── page.(tsx/js) + ├── markdown + │ └── welcome.(mdx/md) + |── mdx-components.(tsx/js) └── package.json ``` @@ -96,21 +170,24 @@ Then, create a new MDX page within the `/app` directory: -Then, create a new MDX page within the `/pages` directory: +Create a new page within the `/pages` directory and an MDX file wherever you'd like: ```txt - your-project + my-project ├── pages - │ └── my-mdx-page.mdx + │ └── mdx-page.(tsx/js) + ├── markdown + │ └── welcome.(mdx/md) + |── mdx-components.(tsx/js) └── package.json ``` -Now you can use markdown and import React components directly inside your MDX page: +You can use MDX in these files, and even import React components, directly inside your MDX page: -```mdx -import { MyComponent } from 'my-components' +```mdx filename="markdown/welcome.mdx" switcher +import { MyComponent } from 'my-component' # Welcome to my MDX page! @@ -127,37 +204,172 @@ Checkout my React component: ``` -Navigating to the `/my-mdx-page` route should display your rendered MDX. +Import the MDX file inside the page to display the content: -## Remote MDX + -If your markdown or MDX files or content lives _somewhere else_, you can fetch it dynamically on the server. This is useful for content stored in a separate local folder, CMS, database, or anywhere else. A popular community package for this use is [`next-mdx-remote`](https://github.com/hashicorp/next-mdx-remote#react-server-components-rsc--nextjs-app-directory-support). +```tsx filename="app/mdx-page/page.tsx" switcher +import Welcome from '@/markdown/welcome.mdx' -> **Good to know**: Please proceed with caution. MDX compiles to JavaScript and is executed on the server. You should only fetch MDX content from a trusted source, otherwise this can lead to remote code execution (RCE). +export default function Page() { + return +} +``` -The following example uses `next-mdx-remote`: +```jsx filename="app/mdx-page/page.js" switcher +import Welcome from '@/markdown/welcome.mdx' + +export default function Page() { + return +} +``` + + + + + +```tsx filename="pages/mdx-page.tsx" switcher +import Welcome from '@/markdown/welcome.mdx' + +export default function Page() { + return +} +``` + +```jsx filename="pages/mdx-page.js" switcher +import Welcome from '@/markdown/welcome.mdx' + +export default function Page() { + return +} +``` + + + +Navigating to the `/mdx-page` route should display your rendered MDX page. + +## Using custom styles and components + +Markdown, when rendered, maps to native HTML elements. For example, writing the following markdown: + +```md +## This is a heading + +This is a list in markdown: + +- One +- Two +- Three +``` + +Generates the following HTML: + +```html +

This is a heading

+ +

This is a list in markdown:

+ +
    +
  • One
  • +
  • Two
  • +
  • Three
  • +
+``` + +To style your markdown, you can provide custom components that map to the generated HTML elements. Styles and components can be implemented globally, locally, and with shared layouts. + +### Global styles and components + +Adding styles and components in `mdx-components.tsx` will affect _all_ MDX files in your application. + +```tsx filename="mdx-components.tsx" switcher +import type { MDXComponents } from 'mdx/types' +import Image, { ImageProps } from 'next/image' + +// This file allows you to provide custom React components +// to be used in MDX files. You can import and use any +// React component you want, including inline styles, +// components from other libraries, and more. + +export function useMDXComponents(components: MDXComponents): MDXComponents { + return { + // Allows customizing built-in components, e.g. to add styling. + h1: ({ children }) => ( +

{children}

+ ), + img: (props) => ( + + ), + ...components, + } +} +``` + +```js filename="mdx-components.js" switcher +import Image from 'next/image' + +// This file allows you to provide custom React components +// to be used in MDX files. You can import and use any +// React component you want, including inline styles, +// components from other libraries, and more. + +export function useMDXComponents(components) { + return { + // Allows customizing built-in components, e.g. to add styling. + h1: ({ children }) => ( +

{children}

+ ), + img: (props) => ( + + ), + ...components, + } +} +``` + +### Local styles and components + +You can apply local styles and components to specific pages by passing them into imported MDX components. These will merge with and override [global styles and components](#global-styles-and-components). -```tsx filename="app/my-mdx-page-remote/page.tsx" switcher -import { MDXRemote } from 'next-mdx-remote/rsc' +```tsx filename="app/mdx-page/page.tsx" switcher +import Welcome from '@/markdown/welcome.mdx' -export default async function RemoteMdxPage() { - // MDX text - can be from a local file, database, CMS, fetch, anywhere... - const res = await fetch('https://...') - const markdown = await res.text() - return +function CustomH1({ children }) { + return

{children}

+} + +const overrideComponents = { + h1: CustomH1, +} + +export default function Page() { + return } ``` -```jsx filename="app/my-mdx-page-remote/page.js" switcher -import { MDXRemote } from 'next-mdx-remote/rsc' +```jsx filename="app/mdx-page/page.js" switcher +import Welcome from '@/markdown/welcome.mdx' -export default async function RemoteMdxPage() { - // MDX text - can be from a local file, database, CMS, fetch, anywhere... - const res = await fetch('https://...') - const markdown = await res.text() - return +function CustomH1({ children }) { + return

{children}

+} + +const overrideComponents = { + h1: CustomH1, +} + +export default function Page() { + return } ``` @@ -165,62 +377,54 @@ export default async function RemoteMdxPage() { -```tsx filename="pages/my-mdx-page-remote.tsx" switcher -import { serialize } from 'next-mdx-remote/serialize' -import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote' +```tsx filename="pages/mdx-page.tsx" switcher +import Welcome from '@/markdown/welcome.mdx' -interface Props { - mdxSource: MDXRemoteSerializeResult +function CustomH1({ children }) { + return

{children}

} -export default function RemoteMdxPage({ mdxSource }: Props) { - return +const overrideComponents = { + h1: CustomH1, } -export async function getStaticProps() { - // MDX text - can be from a local file, database, CMS, fetch, anywhere... - const res = await fetch('https:...') - const mdxText = await res.text() - const mdxSource = await serialize(mdxText) - return { props: { mdxSource } } +export default function Page() { + return } ``` -```jsx filename="pages/my-mdx-page-remote.js" switcher -import { serialize } from 'next-mdx-remote/serialize' -import { MDXRemote } from 'next-mdx-remote' +```jsx filename="pages/mdx-page.js" switcher +import Welcome from '@/markdown/welcome.mdx' -export default function RemoteMdxPage({ mdxSource }) { - return +function CustomH1({ children }) { + return

{children}

} -export async function getStaticProps() { - // MDX text - can be from a local file, database, CMS, fetch, anywhere... - const res = await fetch('https:...') - const mdxText = await res.text() - const mdxSource = await serialize(mdxText) - return { props: { mdxSource } } +const overrideComponents = { + h1: CustomH1, +} + +export default function Page() { + return } ```
-Navigating to the `/my-mdx-page-remote` route should display your rendered MDX. - -## Layouts +### Shared layouts To share a layout across MDX pages, you can use the [built-in layouts support](/docs/app/building-your-application/routing/pages-and-layouts#layouts) with the App Router. -```tsx filename="app/my-mdx-page/layout.tsx" switcher +```tsx filename="app/mdx-page/layout.tsx" switcher export default function MdxLayout({ children }: { children: React.ReactNode }) { // Create any shared layout or styles here return
{children}
} ``` -```jsx filename="app/my-mdx-page/layout.js" switcher +```jsx filename="app/mdx-page/layout.js" switcher export default function MdxLayout({ children }) { // Create any shared layout or styles here return
{children}
@@ -262,6 +466,154 @@ export default function MDXPage({ children }) { +### Using Tailwind typography plugin + +If you are using [Tailwind](https://tailwindcss.com) to style your application, using the [`@tailwindcss/typography` plugin](https://tailwindcss.com/docs/plugins#typography) will allow you to reuse your Tailwind configuration and styles in your markdown files. + +The plugin adds a set of `prose` classes that can be used to add typographic styles to content blocks that come from sources, like markdown. + +[Install Tailwind typography](https://github.com/tailwindlabs/tailwindcss-typography?tab=readme-ov-file#installation) and use with [shared layouts](#shared-layouts) to add the `prose` you want. + + + +```tsx filename="app/mdx-page/layout.tsx" switcher +export default function MdxLayout({ children }: { children: React.ReactNode }) { + // Create any shared layout or styles here + return ( +
+ {children} +
+ ) +} +``` + +```jsx filename="app/mdx-page/layout.js" switcher +export default function MdxLayout({ children }) { + // Create any shared layout or styles here + return ( +
+ {children} +
+ ) +} +``` + +
+ + + +To share a layout around MDX pages, create a layout component: + +```tsx filename="components/mdx-layout.tsx" switcher +export default function MdxLayout({ children }: { children: React.ReactNode }) { + // Create any shared layout or styles here + return ( +
+ {children} +
+ ) +} +``` + +```jsx filename="components/mdx-layout.js" switcher +export default function MdxLayout({ children }) { + // Create any shared layout or styles here + return ( +
+ {children} +
+ ) +} +``` + +Then, import the layout component into the MDX page, wrap the MDX content in the layout, and export it: + +```mdx +import MdxLayout from '../components/mdx-layout' + +# Welcome to my MDX page! + +export default function MDXPage({ children }) { + return {children} + +} +``` + +## Frontmatter + +Frontmatter is a YAML like key/value pairing that can be used to store data about a page. `@next/mdx` does **not** support frontmatter by default, though there are many solutions for adding frontmatter to your MDX content, such as: + +- [remark-frontmatter](https://github.com/remarkjs/remark-frontmatter) +- [remark-mdx-frontmatter](https://github.com/remcohaszing/remark-mdx-frontmatter) +- [gray-matter](https://github.com/jonschlinkert/gray-matter) + +`@next/mdx` **does** allow you to use exports like any other JavaScript component: + +```mdx filename="content/blog-post.mdx" switcher +export const metadata = { + author: 'John Doe', +} + +# Blog post +``` + +Metadata can now be referenced outside of the MDX file: + + + +```tsx filename="app/blog/page.tsx" switcher +import BlogPost, { metadata } from '@/content/blog-post.mdx' + +export default function Page() { + console.log('metadata': metadata) + //=> { author: 'John Doe' } + return +} +``` + +```jsx filename="app/blog/page.js" switcher +import BlogPost, { metadata } from '@/content/blog-post.mdx' + +export default function Page() { + console.log('metadata': metadata) + //=> { author: 'John Doe' } + return +} +``` + + + + + +```tsx filename="pages/blog.tsx" switcher +import BlogPost, { metadata } from '@/content/blog-post.mdx' + +export default function Page() { + console.log('metadata': metadata) + //=> { author: 'John Doe' } + return +} +``` + +```jsx filename="pages/blog.js" switcher +import BlogPost, { metadata } from '@/content/blog-post.mdx' + +export default function Page() { + console.log('metadata': metadata) + //=> { author: 'John Doe' } + return +} +``` + + + +A common use case for this is when you want to iterate over a collection of MDX and extract data. For example, creating a blog index page from all blog posts. You can use packages like [Node's `fs` module](https://nodejs.org/api/fs.html) or [globby](https://www.npmjs.com/package/globby) to read a directory of posts and extract the metadata. + +> **Good to know**: +> +> - Using `fs`, `globby`, etc. can only be used server-side. +> - View the [Portfolio Starter Kit](https://vercel.com/templates/next.js/portfolio-starter-kit) template for a complete working example. + ## Remark and Rehype Plugins You can optionally provide `remark` and `rehype` plugins to transform the MDX content. @@ -277,7 +629,7 @@ import createMDX from '@next/mdx' /** @type {import('next').NextConfig} */ const nextConfig = { // Configure `pageExtensions`` to include MDX files - pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'], + pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], // Optionally, add any other Next.js config below } @@ -293,111 +645,84 @@ const withMDX = createMDX({ export default withMDX(nextConfig) ``` -## Frontmatter - -Frontmatter is a YAML like key/value pairing that can be used to store data about a page. `@next/mdx` does **not** support frontmatter by default, though there are many solutions for adding frontmatter to your MDX content, such as: - -- [remark-frontmatter](https://github.com/remarkjs/remark-frontmatter) -- [remark-mdx-frontmatter](https://github.com/remcohaszing/remark-mdx-frontmatter) -- [gray-matter](https://github.com/jonschlinkert/gray-matter). - -To access page metadata with `@next/mdx`, you can export a metadata object from within the `.mdx` file: +## Remote MDX -```mdx -export const metadata = { - author: 'John Doe', -} +If your MDX files or content lives _somewhere else_, you can fetch it dynamically on the server. This is useful for content stored in a separate local folder, CMS, database, or anywhere else. A popular community package for this use is [`next-mdx-remote`](https://github.com/hashicorp/next-mdx-remote#react-server-components-rsc--nextjs-app-directory-support). -# My MDX page -``` +> **Good to know**: Please proceed with caution. MDX compiles to JavaScript and is executed on the server. You should only fetch MDX content from a trusted source, otherwise this can lead to remote code execution (RCE). -## Custom Elements +The following example uses `next-mdx-remote`: -One of the pleasant aspects of using markdown, is that it maps to native `HTML` elements, making writing fast, and intuitive: + -```md -This is a list in markdown: +```tsx filename="app/mdx-page-remote/page.tsx" switcher +import { MDXRemote } from 'next-mdx-remote/rsc' -- One -- Two -- Three +export default async function RemoteMdxPage() { + // MDX text - can be from a local file, database, CMS, fetch, anywhere... + const res = await fetch('https://...') + const markdown = await res.text() + return +} ``` -The above generates the following `HTML`: - -```html -

This is a list in markdown:

+```jsx filename="app/mdx-page-remote/page.js" switcher +import { MDXRemote } from 'next-mdx-remote/rsc' -
    -
  • One
  • -
  • Two
  • -
  • Three
  • -
+export default async function RemoteMdxPage() { + // MDX text - can be from a local file, database, CMS, fetch, anywhere... + const res = await fetch('https://...') + const markdown = await res.text() + return +} ``` -When you want to style your own elements for a custom feel to your website or application, you can pass in shortcodes. These are your own custom components that map to `HTML` elements. - - - -To do this, open the `mdx-components.tsx` file at the root of your application and add custom elements: - -To do this, create a `mdx-components.tsx` file at the root of your application (the parent folder of `pages/` or `src/`) and add custom elements: - - +```tsx filename="pages/mdx-page-remote.tsx" switcher +import { serialize } from 'next-mdx-remote/serialize' +import { MDXRemote, MDXRemoteSerializeResult } from 'next-mdx-remote' -```tsx filename="mdx-components.tsx" switcher -import type { MDXComponents } from 'mdx/types' -import Image, { ImageProps } from 'next/image' +interface Props { + mdxSource: MDXRemoteSerializeResult +} -// This file allows you to provide custom React components -// to be used in MDX files. You can import and use any -// React component you want, including inline styles, -// components from other libraries, and more. +export default function RemoteMdxPage({ mdxSource }: Props) { + return +} -export function useMDXComponents(components: MDXComponents): MDXComponents { - return { - // Allows customizing built-in components, e.g. to add styling. - h1: ({ children }) =>

{children}

, - img: (props) => ( - - ), - ...components, - } +export async function getStaticProps() { + // MDX text - can be from a local file, database, CMS, fetch, anywhere... + const res = await fetch('https:...') + const mdxText = await res.text() + const mdxSource = await serialize(mdxText) + return { props: { mdxSource } } } ``` -```js filename="mdx-components.js" switcher -import Image from 'next/image' +```jsx filename="pages/mdx-page-remote.js" switcher +import { serialize } from 'next-mdx-remote/serialize' +import { MDXRemote } from 'next-mdx-remote' -// This file allows you to provide custom React components -// to be used in MDX files. You can import and use any -// React component you want, including inline styles, -// components from other libraries, and more. +export default function RemoteMdxPage({ mdxSource }) { + return +} -export function useMDXComponents(components) { - return { - // Allows customizing built-in components, e.g. to add styling. - h1: ({ children }) =>

{children}

, - img: (props) => ( - - ), - ...components, - } +export async function getStaticProps() { + // MDX text - can be from a local file, database, CMS, fetch, anywhere... + const res = await fetch('https:...') + const mdxText = await res.text() + const mdxSource = await serialize(mdxText) + return { props: { mdxSource } } } ``` +
+ +Navigating to the `/mdx-page-remote` route should display your rendered MDX. + ## Deep Dive: How do you transform markdown into HTML? React does not natively understand markdown. The markdown plaintext needs to first be transformed into HTML. This can be accomplished with `remark` and `rehype`. diff --git a/docs/02-app/02-api-reference/02-file-conventions/mdx-components.mdx b/docs/02-app/02-api-reference/02-file-conventions/mdx-components.mdx new file mode 100644 index 0000000000000..5ebd8b817f9e5 --- /dev/null +++ b/docs/02-app/02-api-reference/02-file-conventions/mdx-components.mdx @@ -0,0 +1,71 @@ +--- +title: mdx-components.js +description: API reference for the mdx-components.js file. +related: + title: Learn more about MDX Components + links: + - app/building-your-application/configuring/mdx +--- + +The `mdx-components.js|tsx` file is **required** to use [`@next/mdx` with App Router](/docs/app/building-your-application/configuring/mdx) and will not work without it. Additionally, you can use it to [customize styles](/docs/app/building-your-application/configuring/mdx#using-custom-styles-and-components). + +Use the file `mdx-components.tsx` (or `.js`) in the root of your project to define MDX Components. For example, at the same level as `pages` or `app`, or inside `src` if applicable. + +```tsx filename="mdx-components.tsx" switcher +import type { MDXComponents } from 'mdx/types' + +export function useMDXComponents(components: MDXComponents): MDXComponents { + return { + ...components, + } +} +``` + +```js filename="mdx-components.js" switcher +export function useMDXComponents(components) { + return { + ...components, + } +} +``` + +## Exports + +### `useMDXComponents` function + +The file must export a single function, either as a default export or named `useMDXComponents`. + +```tsx filename="mdx-components.tsx" switcher +import type { MDXComponents } from 'mdx/types' + +export function useMDXComponents(components: MDXComponents): MDXComponents { + return { + ...components, + } +} +``` + +```js filename="mdx-components.js" switcher +export function useMDXComponents(components) { + return { + ...components, + } +} +``` + +## Params + +### `components` + +When defining MDX Components, the export function accepts a single parameter, `components`. This parameter is an instance of `MDXComponents`. + +- The key is the name of the HTML element to override. +- The value is the component to render instead. + +> **Good to know**: Remember to pass all other components (i.e. `...components`) that do not have overrides. + +## Version History + +| Version | Changes | +| --------- | -------------------- | +| `v13.1.2` | MDX Components added | diff --git a/docs/02-app/02-api-reference/02-file-conventions/middleware.mdx b/docs/02-app/02-api-reference/02-file-conventions/middleware.mdx index 6202f49fa0946..802fbbe01f008 100644 --- a/docs/02-app/02-api-reference/02-file-conventions/middleware.mdx +++ b/docs/02-app/02-api-reference/02-file-conventions/middleware.mdx @@ -1,6 +1,6 @@ --- title: middleware.js -description: API reference for the middleware.js file. +description: API reference for the middleware.js file. related: title: Learn more about Middleware links: diff --git a/docs/02-app/02-api-reference/02-file-conventions/route-segment-config.mdx b/docs/02-app/02-api-reference/02-file-conventions/route-segment-config.mdx index 4f6f58161e207..048ed09ec1798 100644 --- a/docs/02-app/02-api-reference/02-file-conventions/route-segment-config.mdx +++ b/docs/02-app/02-api-reference/02-file-conventions/route-segment-config.mdx @@ -62,7 +62,11 @@ export const dynamic = 'auto' > **Good to know**: The new model in the `app` directory favors granular caching control at the `fetch` request level over the binary all-or-nothing model of `getServerSideProps` and `getStaticProps` at the page-level in the `pages` directory. The `dynamic` option is a way to opt back in to the previous model as a convenience and provides a simpler migration path. - **`'auto'`** (default): The default option to cache as much as possible without preventing any components from opting into dynamic behavior. -- **`'force-dynamic'`**: Force [dynamic rendering](/docs/app/building-your-application/rendering/server-components#dynamic-rendering), which will result in routes being rendered for each user at request time. This option is equivalent to `getServerSideProps()` in the `pages` directory. +- **`'force-dynamic'`**: Force [dynamic rendering](/docs/app/building-your-application/rendering/server-components#dynamic-rendering), which will result in routes being rendered for each user at request time. This option is equivalent to: + + - `getServerSideProps()` in the `pages` directory. + - Setting the option of every `fetch()` request in a layout or page to `{ cache: 'no-store', next: { revalidate: 0 } }`. + - Setting the segment config to `export const fetchCache = 'force-no-store'` - **`'error'`**: Force static rendering and cache the data of a layout or page by causing an error if any components use [dynamic functions](/docs/app/building-your-application/rendering/server-components#dynamic-functions) or uncached data. This option is equivalent to: - `getStaticProps()` in the `pages` directory. diff --git a/lerna.json b/lerna.json index ef5f2296f45bb..32f4decdf88b3 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "14.2.1-canary.3" + "version": "14.2.1-canary.4" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 06bc00d05e8d1..55d7312bbed9c 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "14.2.1-canary.3", + "version": "14.2.1-canary.4", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index ef614518c5d67..9840640842fd9 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "14.2.1-canary.3", + "version": "14.2.1-canary.4", "description": "ESLint configuration used by Next.js.", "main": "index.js", "license": "MIT", @@ -10,7 +10,7 @@ }, "homepage": "https://nextjs.org/docs/app/building-your-application/configuring/eslint#eslint-config", "dependencies": { - "@next/eslint-plugin-next": "14.2.1-canary.3", + "@next/eslint-plugin-next": "14.2.1-canary.4", "@rushstack/eslint-patch": "^1.3.3", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0", "eslint-import-resolver-node": "^0.3.6", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 547aa1a6b76c3..2df7038ef9de0 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "14.2.1-canary.3", + "version": "14.2.1-canary.4", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "license": "MIT", diff --git a/packages/font/package.json b/packages/font/package.json index bf6bbdd6f973f..cc27318d42479 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,6 +1,6 @@ { "name": "@next/font", - "version": "14.2.1-canary.3", + "version": "14.2.1-canary.4", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index ec198757d4021..25bfd2747023d 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "14.2.1-canary.3", + "version": "14.2.1-canary.4", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 04dde15e04c14..69b544d9a3f72 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "14.2.1-canary.3", + "version": "14.2.1-canary.4", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 2ad4a878f77e2..175eef92947ef 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "14.2.1-canary.3", + "version": "14.2.1-canary.4", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index a29f4825157a6..bbe666a6941df 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "14.2.1-canary.3", + "version": "14.2.1-canary.4", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 699073b19a9d0..314d2141ee98a 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "14.2.1-canary.3", + "version": "14.2.1-canary.4", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 9d1a07ea13da4..4bd403d03315e 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "14.2.1-canary.3", + "version": "14.2.1-canary.4", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 86d86c6ed59a9..ecc2247243dff 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "14.2.1-canary.3", + "version": "14.2.1-canary.4", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 1806b9098f2fa..ee545fd2ae85b 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "14.2.1-canary.3", + "version": "14.2.1-canary.4", "private": true, "scripts": { "clean": "node ../../scripts/rm.mjs native", diff --git a/packages/next/package.json b/packages/next/package.json index c0b47091c51f4..76e07e3c40ba0 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "14.2.1-canary.3", + "version": "14.2.1-canary.4", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -92,7 +92,7 @@ ] }, "dependencies": { - "@next/env": "14.2.1-canary.3", + "@next/env": "14.2.1-canary.4", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -149,10 +149,10 @@ "@jest/types": "29.5.0", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", - "@next/polyfill-module": "14.2.1-canary.3", - "@next/polyfill-nomodule": "14.2.1-canary.3", - "@next/react-refresh-utils": "14.2.1-canary.3", - "@next/swc": "14.2.1-canary.3", + "@next/polyfill-module": "14.2.1-canary.4", + "@next/polyfill-nomodule": "14.2.1-canary.4", + "@next/react-refresh-utils": "14.2.1-canary.4", + "@next/swc": "14.2.1-canary.4", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.41.2", "@taskr/clear": "1.1.0", diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index 9cbc4e45d2e5c..e5318dc371796 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -310,7 +310,13 @@ function createPatchedFetcher( _cache === 'no-cache' || _cache === 'no-store' || fetchCacheMode === 'force-no-store' || - fetchCacheMode === 'only-no-store' + fetchCacheMode === 'only-no-store' || + // If no explicit fetch cache mode is set, but dynamic = `force-dynamic` is set, + // we shouldn't consider caching the fetch. This is because the `dynamic` cache + // is considered a "top-level" cache mode, whereas something like `fetchCache` is more + // fine-grained. Top-level modes are responsible for setting reasonable defaults for the + // other configurations. + (!fetchCacheMode && staticGenerationStore.forceDynamic) ) { curRevalidate = 0 } diff --git a/packages/next/src/shared/lib/router/action-queue.ts b/packages/next/src/shared/lib/router/action-queue.ts index 441487824ab24..07a652a03452b 100644 --- a/packages/next/src/shared/lib/router/action-queue.ts +++ b/packages/next/src/shared/lib/router/action-queue.ts @@ -155,8 +155,11 @@ function dispatchAction( action: newAction, setState, }) - } else if (payload.type === ACTION_NAVIGATE) { - // Navigations take priority over any pending actions. + } else if ( + payload.type === ACTION_NAVIGATE || + payload.type === ACTION_RESTORE + ) { + // Navigations (including back/forward) take priority over any pending actions. // Mark the pending action as discarded (so the state is never applied) and start the navigation action immediately. actionQueue.pending.discarded = true diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 6370fb199263d..e02f4116b0f10 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "14.2.1-canary.3", + "version": "14.2.1-canary.4", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index 8c41663faeb06..c95aed311067a 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "14.2.1-canary.3", + "version": "14.2.1-canary.4", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "14.2.1-canary.3", + "next": "14.2.1-canary.4", "outdent": "0.8.0", "prettier": "2.5.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 400ebb1e28319..25e704764b2ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -744,7 +744,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 14.2.1-canary.3 + specifier: 14.2.1-canary.4 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.3.3 @@ -806,7 +806,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 14.2.1-canary.3 + specifier: 14.2.1-canary.4 version: link:../next-env '@swc/helpers': specifier: 0.5.5 @@ -927,16 +927,16 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/polyfill-module': - specifier: 14.2.1-canary.3 + specifier: 14.2.1-canary.4 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 14.2.1-canary.3 + specifier: 14.2.1-canary.4 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 14.2.1-canary.3 + specifier: 14.2.1-canary.4 version: link:../react-refresh-utils '@next/swc': - specifier: 14.2.1-canary.3 + specifier: 14.2.1-canary.4 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1551,7 +1551,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 14.2.1-canary.3 + specifier: 14.2.1-canary.4 version: link:../next outdent: specifier: 0.8.0 diff --git a/test/e2e/app-dir/actions/app-action.test.ts b/test/e2e/app-dir/actions/app-action.test.ts index b1b147b93c0d0..d5125c5496565 100644 --- a/test/e2e/app-dir/actions/app-action.test.ts +++ b/test/e2e/app-dir/actions/app-action.test.ts @@ -337,6 +337,19 @@ createNextDescribe( await check(() => browser.url(), `${next.url}/client`, true, 2) }) + it('should not block router.back() while a server action is in flight', async () => { + let browser = await next.browser('/') + + // click /client link to add a history entry + await browser.elementByCss("[href='/client']").click() + await browser.elementByCss('#slow-inc').click() + + await browser.back() + + // intentionally bailing after 2 retries so we don't retry to the point where the async function resolves + await check(() => browser.url(), `${next.url}/`, true, 2) + }) + it('should trigger a refresh for a server action that gets discarded due to a navigation', async () => { let browser = await next.browser('/client') const initialRandomNumber = await browser diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index 90c735f2f0670..b19aee58b71ec 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -104,6 +104,34 @@ createNextDescribe( ) }) + it('should infer a fetchCache of force-no-store when force-dynamic is used', async () => { + const $ = await next.render$( + '/force-dynamic-fetch-cache/no-fetch-cache' + ) + const initData = $('#data').text() + await retry(async () => { + const $2 = await next.render$( + '/force-dynamic-fetch-cache/no-fetch-cache' + ) + expect($2('#data').text()).toBeTruthy() + expect($2('#data').text()).not.toBe(initData) + }) + }) + + it('fetchCache config should supercede dynamic config when force-dynamic is used', async () => { + const $ = await next.render$( + '/force-dynamic-fetch-cache/with-fetch-cache' + ) + const initData = $('#data').text() + await retry(async () => { + const $2 = await next.render$( + '/force-dynamic-fetch-cache/with-fetch-cache' + ) + expect($2('#data').text()).toBeTruthy() + expect($2('#data').text()).toBe(initData) + }) + }) + if (!process.env.CUSTOM_CACHE_HANDLER) { it('should honor force-static with fetch cache: no-store correctly', async () => { const res = await next.fetch('/force-static-fetch-no-store') @@ -667,6 +695,10 @@ createNextDescribe( "force-cache/page_client-reference-manifest.js", "force-dynamic-catch-all/[slug]/[[...id]]/page.js", "force-dynamic-catch-all/[slug]/[[...id]]/page_client-reference-manifest.js", + "force-dynamic-fetch-cache/no-fetch-cache/page.js", + "force-dynamic-fetch-cache/no-fetch-cache/page_client-reference-manifest.js", + "force-dynamic-fetch-cache/with-fetch-cache/page.js", + "force-dynamic-fetch-cache/with-fetch-cache/page_client-reference-manifest.js", "force-dynamic-no-prerender/[id]/page.js", "force-dynamic-no-prerender/[id]/page_client-reference-manifest.js", "force-dynamic-prerender/[slug]/page.js", diff --git a/test/e2e/app-dir/app-static/app/force-dynamic-fetch-cache/no-fetch-cache/page.js b/test/e2e/app-dir/app-static/app/force-dynamic-fetch-cache/no-fetch-cache/page.js new file mode 100644 index 0000000000000..4c32ba163a6dd --- /dev/null +++ b/test/e2e/app-dir/app-static/app/force-dynamic-fetch-cache/no-fetch-cache/page.js @@ -0,0 +1,14 @@ +export const dynamic = 'force-dynamic' + +export default async function Page() { + const data = await fetch( + 'https://next-data-api-endpoint.vercel.app/api/random' + ).then((res) => res.text()) + + return ( + <> +

/force-dynamic-fetch-cache/no-fetch-cache

+

{data}

+ + ) +} diff --git a/test/e2e/app-dir/app-static/app/force-dynamic-fetch-cache/with-fetch-cache/page.js b/test/e2e/app-dir/app-static/app/force-dynamic-fetch-cache/with-fetch-cache/page.js new file mode 100644 index 0000000000000..eb87e858cb633 --- /dev/null +++ b/test/e2e/app-dir/app-static/app/force-dynamic-fetch-cache/with-fetch-cache/page.js @@ -0,0 +1,15 @@ +export const dynamic = 'force-dynamic' +export const fetchCache = 'force-cache' + +export default async function Page() { + const data = await fetch( + 'https://next-data-api-endpoint.vercel.app/api/random' + ).then((res) => res.text()) + + return ( + <> +

/force-dynamic-fetch-cache/with-fetch-cache

+

{data}

+ + ) +} diff --git a/test/e2e/app-dir/app-static/app/stale-cache-serving/app-page/page.tsx b/test/e2e/app-dir/app-static/app/stale-cache-serving/app-page/page.tsx index cd4451c7034ed..44163265365b2 100644 --- a/test/e2e/app-dir/app-static/app/stale-cache-serving/app-page/page.tsx +++ b/test/e2e/app-dir/app-static/app/stale-cache-serving/app-page/page.tsx @@ -1,4 +1,4 @@ -export const dynamic = 'force-dynamic' +export const revalidate = 0 const delay = 3000 diff --git a/test/e2e/app-dir/app-static/app/stale-cache-serving/route-handler/route.ts b/test/e2e/app-dir/app-static/app/stale-cache-serving/route-handler/route.ts index 9ae5d9325be9a..ba280534db054 100644 --- a/test/e2e/app-dir/app-static/app/stale-cache-serving/route-handler/route.ts +++ b/test/e2e/app-dir/app-static/app/stale-cache-serving/route-handler/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -export const dynamic = 'force-dynamic' +export const revalidate = 0 const delay = 3000 diff --git a/test/integration/create-next-app/index.test.ts b/test/integration/create-next-app/index.test.ts index 927594aa6d365..b043787affa97 100644 --- a/test/integration/create-next-app/index.test.ts +++ b/test/integration/create-next-app/index.test.ts @@ -31,6 +31,9 @@ describe('create-next-app', () => { }) it('should not create if the target directory is not writable', async () => { + const expectedErrorMessage = + /you do not have write permissions for this folder|EPERM: operation not permitted/ + await useTempDir(async (cwd) => { const projectName = 'dir-not-writable' @@ -45,6 +48,7 @@ describe('create-next-app', () => { ) return } + const res = await run( [ projectName, @@ -61,10 +65,12 @@ describe('create-next-app', () => { } ) - expect(res.stderr).toMatch( - /you do not have write permissions for this folder/ - ) + expect(res.stderr).toMatch(expectedErrorMessage) expect(res.exitCode).toBe(1) - }, 0o500) + }, 0o500).catch((err) => { + if (!expectedErrorMessage.test(err.message)) { + throw err + } + }) }) })