Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/trigger_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ on:
workflow_dispatch:
inputs:
releaseType:
description: stable, canary, or release candidate?
description: stable, canary, beta, or release candidate?
required: true
type: choice
options:
- canary
- stable
- release-candidate
- beta

semverType:
description: semver type?
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/trigger_release_new.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ on:
- canary
- stable
- release-candidate
- beta

force:
description: Forced Release
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default function Home() {
href="https://vercel.com/templates?framework=next.js"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Template
Templates
</a>{' '}
or the{' '}
<a
Expand Down
2 changes: 2 additions & 0 deletions crates/next-build-test/nextConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"disableStaticImages": false,
"minimumCacheTTL": 60,
"formats": ["image/avif", "image/webp"],
"maximumRedirects": 3,
"dangerouslyAllowLocalIP": false,
"dangerouslyAllowSVG": false,
"contentSecurityPolicy": "script-src 'none'; frame-src 'none'; sandbox;",
"contentDispositionType": "inline",
Expand Down
118 changes: 117 additions & 1 deletion crates/next-core/src/app_structure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,83 @@ impl Issue for DuplicateParallelRouteIssue {
}
}

#[turbo_tasks::value]
struct MissingDefaultParallelRouteIssue {
app_dir: FileSystemPath,
app_page: AppPage,
slot_name: RcStr,
}

#[turbo_tasks::function]
fn missing_default_parallel_route_issue(
app_dir: FileSystemPath,
app_page: AppPage,
slot_name: RcStr,
) -> Vc<MissingDefaultParallelRouteIssue> {
MissingDefaultParallelRouteIssue {
app_dir,
app_page,
slot_name,
}
.cell()
}

#[turbo_tasks::value_impl]
impl Issue for MissingDefaultParallelRouteIssue {
#[turbo_tasks::function]
fn file_path(&self) -> Result<Vc<FileSystemPath>> {
Ok(self
.app_dir
.join(&self.app_page.to_string())?
.join(&format!("@{}", self.slot_name))?
.cell())
}

#[turbo_tasks::function]
fn stage(self: Vc<Self>) -> Vc<IssueStage> {
IssueStage::AppStructure.cell()
}

fn severity(&self) -> IssueSeverity {
IssueSeverity::Error
}

#[turbo_tasks::function]
async fn title(&self) -> Vc<StyledString> {
StyledString::Text(
format!(
"Missing required default.js file for parallel route at {}/@{}",
self.app_page, self.slot_name
)
.into(),
)
.cell()
}

#[turbo_tasks::function]
async fn description(&self) -> Vc<OptionStyledString> {
Vc::cell(Some(
StyledString::Text(
format!(
"The parallel route slot \"@{}\" is missing a default.js file. When using \
parallel routes, each slot must have a default.js file to serve as a \
fallback.\n\nCreate a default.js file at: {}/@{}/default.js",
self.slot_name, self.app_page, self.slot_name
)
.into(),
)
.resolved_cell(),
))
}

#[turbo_tasks::function]
fn documentation_link(&self) -> Vc<RcStr> {
Vc::cell(rcstr!(
"https://nextjs.org/docs/messages/slot-missing-default"
))
}
}

fn page_path_except_parallel(loader_tree: &AppPageLoaderTree) -> Option<AppPage> {
if loader_tree.page.iter().any(|v| {
matches!(
Expand Down Expand Up @@ -1114,6 +1191,29 @@ async fn directory_tree_to_loader_tree_internal(

if let Some(subtree) = subtree {
if let Some(key) = parallel_route_key {
let is_inside_catchall = app_page.is_catchall();

// Validate that parallel routes (except "children") have a default.js file.
// Skip this validation if the slot is UNDER a catch-all route (i.e., the
// parallel route is a child of a catch-all segment).
// For example:
// /[...catchAll]/@slot - is_inside_catchall = true (skip validation) ✓
// /@slot/[...catchAll] - is_inside_catchall = false (require default) ✓
// The catch-all provides fallback behavior, so default.js is not required.
if key != "children"
&& subdirectory.modules.default.is_none()
&& !is_inside_catchall
{
missing_default_parallel_route_issue(
app_dir.clone(),
app_page.clone(),
key.into(),
)
.to_resolved()
.await?
.emit();
}

tree.parallel_routes.insert(key.into(), subtree);
continue;
}
Expand Down Expand Up @@ -1173,8 +1273,24 @@ async fn directory_tree_to_loader_tree_internal(
None
};

let is_inside_catchall = app_page.is_catchall();

// Only emit the issue if this is not the children slot and there's no default
// component. The children slot is implicit and doesn't require a default.js
// file. Also skip validation if the slot is UNDER a catch-all route.
if default.is_none() && key != "children" && !is_inside_catchall {
missing_default_parallel_route_issue(
app_dir.clone(),
app_page.clone(),
key.clone(),
)
.to_resolved()
.await?
.emit();
}

tree.parallel_routes.insert(
key,
key.clone(),
default_route_tree(app_dir.clone(), global_metadata, app_page.clone(), default)
.await?,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -654,8 +654,6 @@ impl ReactServerComponentValidator {
// "unstable_cache", // useless in client, but doesn't technically error
"unstable_cacheLife",
"unstable_cacheTag",
"unstable_expirePath",
"unstable_expireTag",
// "unstable_noStore" // no-op in client, but allowed for legacy reasons
],
),
Expand Down
70 changes: 65 additions & 5 deletions docs/01-app/01-getting-started/09-caching-and-revalidating.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ related:
- app/api-reference/functions/unstable_cache
- app/api-reference/functions/revalidatePath
- app/api-reference/functions/revalidateTag
- app/api-reference/functions/updateTag
---

Caching is a technique for storing the result of data fetching and other computations so that future requests for the same data can be served faster, without doing the work again. While revalidation allows you to update cache entries without having to rebuild your entire application.
Expand All @@ -19,6 +20,7 @@ Next.js provides a few APIs to handle caching and revalidation. This guide will
- [`unstable_cache`](#unstable_cache)
- [`revalidatePath`](#revalidatepath)
- [`revalidateTag`](#revalidatetag)
- [`updateTag`](#updatetag)

## `fetch`

Expand Down Expand Up @@ -154,7 +156,12 @@ See the [`unstable_cache` API reference](/docs/app/api-reference/functions/unsta

## `revalidateTag`

`revalidateTag` is used to revalidate cache entries based on a tag and following an event. To use it with `fetch`, start by tagging the function with the `next.tags` option:
`revalidateTag` is used to revalidate cache entries based on a tag and following an event. The function now supports two behaviors:

- **With `profile="max"`**: Uses stale-while-revalidate semantics, serving stale content while fetching fresh content in the background
- **Without the second argument**: Legacy behavior that immediately expires the cache (deprecated)

To use it with `fetch`, start by tagging the function with the `next.tags` option:

```tsx filename="app/lib/data.ts" highlight={3-5} switcher
export async function getUserById(id: string) {
Expand Down Expand Up @@ -204,21 +211,21 @@ export const getUserById = unstable_cache(

Then, call `revalidateTag` in a [Route Handler](/docs/app/api-reference/file-conventions/route) or Server Action:

```tsx filename="app/lib/actions.ts" highlight={1} switcher
```tsx filename="app/lib/actions.ts" highlight={1,5} switcher
import { revalidateTag } from 'next/cache'

export async function updateUser(id: string) {
// Mutate data
revalidateTag('user')
revalidateTag('user', 'max') // Recommended: Uses stale-while-revalidate
}
```

```jsx filename="app/lib/actions.js" highlight={1} switcher
```jsx filename="app/lib/actions.js" highlight={1,5} switcher
import { revalidateTag } from 'next/cache'

export async function updateUser(id) {
// Mutate data
revalidateTag('user')
revalidateTag('user', 'max') // Recommended: Uses stale-while-revalidate
}
```

Expand Down Expand Up @@ -247,3 +254,56 @@ export async function updateUser(id) {
```

See the [`revalidatePath` API reference](/docs/app/api-reference/functions/revalidatePath) to learn more.

## `updateTag`

`updateTag` is specifically designed for Server Actions to immediately expire cached data for read-your-own-writes scenarios. Unlike `revalidateTag`, it can only be used within Server Actions and immediately expires the cache entry.

```tsx filename="app/lib/actions.ts" highlight={1,6} switcher
import { updateTag } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
// Create post in database
const post = await db.post.create({
data: {
title: formData.get('title'),
content: formData.get('content'),
},
})

// Immediately expire cache so the new post is visible
updateTag('posts')
updateTag(`post-${post.id}`)

redirect(`/posts/${post.id}`)
}
```

```jsx filename="app/lib/actions.js" highlight={1,6} switcher
import { updateTag } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(formData) {
// Create post in database
const post = await db.post.create({
data: {
title: formData.get('title'),
content: formData.get('content'),
},
})

// Immediately expire cache so the new post is visible
updateTag('posts')
updateTag(`post-${post.id}`)

redirect(`/posts/${post.id}`)
}
```

The key differences between `revalidateTag` and `updateTag`:

- **`updateTag`**: Only in Server Actions, immediately expires cache, for read-your-own-writes
- **`revalidateTag`**: In Server Actions and Route Handlers, supports stale-while-revalidate with `profile="max"`

See the [`updateTag` API reference](/docs/app/api-reference/functions/updateTag) to learn more.
48 changes: 47 additions & 1 deletion docs/01-app/03-api-reference/02-components/image.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,52 @@ module.exports = {
}
```

#### `maximumRedirects`

The default image optimization loader will follow HTTP redirects when fetching remote images up to 3 times.

```js filename="next.config.js"
module.exports = {
images: {
maximumRedirects: 3,
},
}
```

You can configure the number of redirects to follow when fetching remote images. Setting the value to `0` will disable following redirects.

```js filename="next.config.js"
module.exports = {
images: {
maximumRedirects: 0,
},
}
```

#### `dangerouslyAllowLocalIP`

In rare cases when self-hosting Next.js on a private network, you may want to allow optimizing images from local IP addresses on the same network. This is not recommended for most users because it could allow malicious users to access content on your internal network.

By default, the value is false.

```js filename="next.config.js"
module.exports = {
images: {
dangerouslyAllowLocalIP: false,
},
}
```

If you need to optimize remote images hosted elsewhere in your local network, you can set the value to true.

```js filename="next.config.js"
module.exports = {
images: {
dangerouslyAllowLocalIP: true,
},
}
```

#### `dangerouslyAllowSVG`

`dangerouslyAllowSVG` allows you to serve SVG images.
Expand Down Expand Up @@ -1284,7 +1330,7 @@ export default function Home() {

| Version | Changes |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `v16.0.0` | `qualities` default configuration changed to `[75]`, `preload` prop added, `priority` prop deprecated. |
| `v16.0.0` | `qualities` default configuration changed to `[75]`, `preload` prop added, `priority` prop deprecated, `dangerouslyAllowLocalIP` config added, `maximumRedirects` config added. |
| `v15.3.0` | `remotePatterns` added support for array of `URL` objects. |
| `v15.0.0` | `contentDispositionType` configuration default changed to `attachment`. |
| `v14.2.23` | `qualities` configuration added. |
Expand Down
12 changes: 10 additions & 2 deletions docs/01-app/03-api-reference/03-file-conventions/default.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,17 @@ Consider the following folder structure. The `@team` slot has a `settings` page,

When navigating to `/settings`, the `@team` slot will render the `settings` page while maintaining the currently active page for the `@analytics` slot.

On refresh, Next.js will render a `default.js` for `@analytics`. If `default.js` doesn't exist, a `404` is rendered instead.
On refresh, Next.js will render a `default.js` for `@analytics`. If `default.js` doesn't exist, an error is returned for named slots (`@team`, `@analytics`, etc) and requires you to define a `default.js` in order to continue. If you want to preserve the old behavior of returning a 404 in these situations, you can create a `default.js` that contains:

Additionally, since `children` is an implicit slot, you also need to create a `default.js` file to render a fallback for `children` when Next.js cannot recover the active state of the parent page.
```tsx filename="app/@team/default.js"
import { notFound } from 'next/navigation'

export default function Default() {
notFound()
}
```

Additionally, since `children` is an implicit slot, you also need to create a `default.js` file to render a fallback for `children` when Next.js cannot recover the active state of the parent page. If you don't create a `default.js` for the `children` slot, it will return a 404 page for the route.

## Reference

Expand Down
Loading
Loading