diff --git a/.github/workflows/build_reusable.yml b/.github/workflows/build_reusable.yml
index 9a2f4c796f661e..e873d550e3529f 100644
--- a/.github/workflows/build_reusable.yml
+++ b/.github/workflows/build_reusable.yml
@@ -200,6 +200,22 @@ jobs:
with:
fetch-depth: 25
+ # Cache pnpm store on GitHub-hosted runners (self-hosted runners have their own persistent storage)
+ - name: Get pnpm store directory
+ if: ${{ runner.environment == 'github-hosted' }}
+ id: get-store-path
+ run: echo STORE_PATH=$(pnpm store path) >> $GITHUB_OUTPUT
+
+ - name: Cache pnpm store
+ if: ${{ runner.environment == 'github-hosted' }}
+ uses: actions/cache@v4
+ timeout-minutes: 5
+ id: cache-pnpm-store
+ with:
+ path: ${{ steps.get-store-path.outputs.STORE_PATH }}
+ key: pnpm-store-v2-${{ hashFiles('pnpm-lock.yaml') }}
+ # Do not use restore-keys since it leads to indefinite growth of the cache.
+
# local action -> needs to run after checkout
- name: Install Rust
uses: ./.github/actions/setup-rust
diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml
index ed34fd9eea9e91..46d56e1336f8c8 100644
--- a/.github/workflows/trigger_release.yml
+++ b/.github/workflows/trigger_release.yml
@@ -28,10 +28,6 @@ on:
default: false
type: boolean
- secrets:
- RELEASE_BOT_GITHUB_TOKEN:
- required: true
-
name: Trigger Release
env:
@@ -62,6 +58,9 @@ jobs:
- name: Check token
run: gh auth status
+ # This sometimes fails for unknown reasons.
+ # Ignoring failures for now to check if a failure truly implies a failed publish.
+ continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }}
diff --git a/docs/01-app/01-getting-started/07-fetching-data.mdx b/docs/01-app/01-getting-started/07-fetching-data.mdx
index df4309a2454163..8dfbf61f958794 100644
--- a/docs/01-app/01-getting-started/07-fetching-data.mdx
+++ b/docs/01-app/01-getting-started/07-fetching-data.mdx
@@ -499,7 +499,7 @@ export default async function Page({ params }) {
Start multiple requests by calling `fetch`, then await them with [`Promise.all`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all). Requests begin as soon as `fetch` is called.
-```tsx filename="app/artist/[username]/page.tsx" highlight={3,8,23} switcher
+```tsx filename="app/artist/[username]/page.tsx" highlight={3,8,24} switcher
import Albums from './albums'
async function getArtist(username: string) {
@@ -534,7 +534,7 @@ export default async function Page({
}
```
-```jsx filename="app/artist/[username]/page.js" highlight={3,8,19} switcher
+```jsx filename="app/artist/[username]/page.js" highlight={3,8,20} switcher
import Albums from './albums'
async function getArtist(username) {
diff --git a/docs/01-app/02-guides/public-static-pages.mdx b/docs/01-app/02-guides/public-static-pages.mdx
index 60eaab395b80e3..428c76446d341b 100644
--- a/docs/01-app/02-guides/public-static-pages.mdx
+++ b/docs/01-app/02-guides/public-static-pages.mdx
@@ -1,7 +1,7 @@
---
title: Building public pages
description: Learn how to build public, "static" pages that share data across users, such as landing pages, list pages (products, blogs, etc.), marketing and news sites.
-nav_title: Building public pages
+nav_title: Public pages
---
Public pages show the same content to every user. Common examples include landing pages, marketing pages, and product pages.
@@ -26,7 +26,7 @@ You can find the resources used in this example here:
Let's start with a simple header.
-```tsx filename=app/products/page.tsx
+```tsx filename="app/products/page.tsx"
// Static component
function Header() {
return
Shop
@@ -45,13 +45,11 @@ export default async function Page() {
The `` component doesn't depend on any inputs that change between requests, such as: external data, request headers, route params, the current time, or random values.
-Since its output never changes and can be determined ahead of time, this kind of component is called a **static** component.
-
-With no reason to wait for a request, Next.js can safely **prerender** the page at [build time](/docs/app/glossary#build-time).
+Since its output never changes and can be determined ahead of time, this kind of component is called a **static** component. With no reason to wait for a request, Next.js can safely **prerender** the page at [build time](/docs/app/glossary#build-time).
We can confirm this by running [`next build`](/docs/app/api-reference/cli/next#next-build-options).
-```bash filename=terminal
+```bash filename="Terminal"
Route (app) Revalidate Expire
┌ ○ /products 15m 1y
└ ○ /_not-found
@@ -65,7 +63,7 @@ Notice that the product route is marked as static, even though we didn't add any
Now, let's fetch and render our product list.
-```tsx filename=page.tsx
+```tsx filename="app/products/page.tsx"
import db from '@/db'
import { List } from '@/app/products/ui'
@@ -91,15 +89,11 @@ Unlike the header, the product list depends on external data.
#### Dynamic components
-Because this data **can** change over time, the rendered output is no longer guaranteed to be stable.
-
-This makes it a **dynamic** component.
+Since this data **can** change over time, the rendered output is no longer guaranteed to be stable. This makes the product list a **dynamic** component.
Without guidance, the framework assumes you want to fetch **fresh** data on every user request. This design choice reflects standard web behavior where a new server request renders the page.
-However, if this component is rendered at request time, fetching its data will delay the **entire** route from responding.
-
-If we refresh the page, we can see this happen.
+However, if this component is rendered at request time, fetching its data will delay the **entire** route from responding. If we refresh the page, we can see this happen.
Even though the header is rendered instantly, it can't be sent to the browser until the product list has finished fetching.
@@ -116,7 +110,7 @@ In our case, the product catalog is shared across all users, so caching is the r
We can mark a function as cacheable using the [`'use cache'`](/docs/app/api-reference/directives/use-cache) directive.
-```tsx filename=page.tsx
+```tsx filename="app/products/page.tsx"
import db from '@/db'
import { List } from '@/app/products/ui'
@@ -139,15 +133,13 @@ export default async function Page() {
}
```
-This turns it into a [cache component](/docs/app/glossary#cache-components).
-
-The first time it runs, whatever we return will be cached and reused.
+This turns it into a [cache component](/docs/app/glossary#cache-components). The first time it runs, whatever we return will be cached and reused.
If a cache component's inputs are available **before** the request arrives, it can be prerendered just like a static component.
If we refresh again, we can see the page loads instantly because the cache component doesn't block the response. And, if we run `next build` again, we can confirm the page is still static:
-```bash filename=terminal
+```bash filename="Terminal"
Route (app) Revalidate Expire
┌ ○ /products 15m 1y
└ ○ /_not-found
@@ -159,11 +151,9 @@ But, pages rarely stay static forever.
### Step 3: Add a dynamic promotion banner
-Sooner or later, even simple pages need some dynamic content.
-
-To demonstrate this, let's add a promotional banner.
+Sooner or later, even simple pages need some dynamic content. To demonstrate this, let's add a promotional banner:
-```tsx filename=app/products/page.tsx
+```tsx filename="app/products/page.tsx"
import db from '@/db'
import { List, Promotion } from '@/app/products/ui'
import { getPromotion } from '@/app/products/data'
@@ -191,9 +181,7 @@ export default async function Page() {
Once again, this starts off as dynamic. And as before, introducing blocking behavior triggers a Next.js warning.
-Last time, the data was shared, so it could be cached.
-
-This time, the promotion depends on request specific inputs like the user's location and A/B tests, so we can't cache our way out of the blocking behavior.
+Last time, the data was shared, so it could be cached. This time, the promotion depends on request specific inputs like the user's location and A/B tests, so we can't cache our way out of the blocking behavior.
### Partial prerendering
@@ -201,7 +189,7 @@ Adding dynamic content doesn't mean we have to go back to a fully blocking rende
Next.js supports streaming by default. We can use a [Suspense boundary](/docs/app/glossary#suspense-boundary) to tell the framework where to slice the streamed response into _chunks_, and what fallback UI to show while content loads.
-```tsx filename=app/products/page.tsx
+```tsx filename="app/products/page.tsx"
import { Suspense } from 'react'
import db from '@/db'
import { List, Promotion, PromotionSkeleton } from '@/app/products/ui'
@@ -234,9 +222,9 @@ The fallback is prerendered alongside the rest of our static and cached content.
With this change, Next.js can separate prerenderable work from request-time work and the route becomes [partially prerendered](/docs/app/glossary#partial-prerendering-ppr).
-Again, we can confirm this by running `next build`
+Again, we can confirm this by running `next build`:
-```bash filename=terminal
+```bash filename="Terminal"
Route (app) Revalidate Expire
┌ ◐ /products 15m 1y
└ ◐ /_not-found
diff --git a/errors/deploymentid-invalid-characters.mdx b/errors/deploymentid-invalid-characters.mdx
new file mode 100644
index 00000000000000..c40a6e22af1699
--- /dev/null
+++ b/errors/deploymentid-invalid-characters.mdx
@@ -0,0 +1,71 @@
+---
+title: '`deploymentId` contains invalid characters'
+---
+
+## Why This Error Occurred
+
+The `deploymentId` in your `next.config.js` contains characters that are not allowed. Only alphanumeric characters (a-z, A-Z, 0-9), hyphens (-), and underscores (\_) are permitted.
+
+## Possible Ways to Fix It
+
+### Option 1: Remove Invalid Characters
+
+Remove or replace any characters that are not alphanumeric, hyphens, or underscores:
+
+```js
+// ✅ Correct
+module.exports = {
+ deploymentId: 'my-deployment-123', // Only alphanumeric, hyphens, underscores
+}
+
+// ❌ Incorrect
+module.exports = {
+ deploymentId: 'my deployment 123', // Contains spaces
+ deploymentId: 'my.deployment.123', // Contains dots
+ deploymentId: 'my/deployment/123', // Contains slashes
+ deploymentId: 'my@deployment#123', // Contains special characters
+}
+```
+
+### Option 2: Sanitize the Deployment ID
+
+If you're generating the ID from environment variables or other sources, sanitize it to remove invalid characters:
+
+```js
+// next.config.js
+const rawId = process.env.DEPLOYMENT_ID || 'default-id'
+// Remove all characters that are not alphanumeric, hyphens, or underscores
+const sanitizedId = rawId.replace(/[^a-zA-Z0-9_-]/g, '')
+
+module.exports = {
+ deploymentId: sanitizedId,
+}
+```
+
+### Option 3: Use a Valid Format
+
+Common valid formats include:
+
+```js
+// next.config.js
+module.exports = {
+ // Using hyphens
+ deploymentId: 'my-deployment-id',
+
+ // Using underscores
+ deploymentId: 'my_deployment_id',
+
+ // Alphanumeric only
+ deploymentId: 'mydeployment123',
+
+ // Mixed format
+ deploymentId: 'my-deployment_123',
+}
+```
+
+## Additional Information
+
+- The deployment ID is used for skew protection and asset versioning
+- Invalid characters can cause issues with URL encoding and routing
+- Keep the ID URL-friendly by using only the allowed character set
+- The validation ensures compatibility across different systems and environments
diff --git a/errors/deploymentid-not-a-string.mdx b/errors/deploymentid-not-a-string.mdx
new file mode 100644
index 00000000000000..20d3e397c14c2a
--- /dev/null
+++ b/errors/deploymentid-not-a-string.mdx
@@ -0,0 +1,33 @@
+---
+title: '`deploymentId` must be a string'
+---
+
+## Why This Error Occurred
+
+The `deploymentId` option in your `next.config.js` must be a string value.
+
+## Possible Ways to Fix It
+
+Ensure your `deploymentId` is a string:
+
+```js
+// ✅ Correct
+module.exports = {
+ deploymentId: 'my-deployment-123',
+}
+
+// ✅ Using environment variables
+module.exports = {
+ deploymentId: process.env.GIT_HASH || 'default-id',
+}
+
+// ❌ Incorrect
+module.exports = {
+ deploymentId: 12345, // Must be a string, not a number
+}
+```
+
+The `deploymentId` can be:
+
+- A string: `deploymentId: 'my-deployment-123'`
+- `undefined` (will use `NEXT_DEPLOYMENT_ID` environment variable if set)
diff --git a/errors/deploymentid-too-long.mdx b/errors/deploymentid-too-long.mdx
new file mode 100644
index 00000000000000..09aedd9f1c8170
--- /dev/null
+++ b/errors/deploymentid-too-long.mdx
@@ -0,0 +1,37 @@
+---
+title: '`deploymentId` exceeds maximum length'
+---
+
+## Why This Error Occurred
+
+The `deploymentId` in your `next.config.js` exceeds the maximum length of 32 characters.
+
+## Possible Ways to Fix It
+
+### Option 1: Shorten Your Deployment ID
+
+Reduce the length of your `deploymentId` to 32 characters or less:
+
+```js
+// next.config.js
+module.exports = {
+ deploymentId: 'my-short-id', // ✅ 12 characters
+}
+```
+
+### Option 2: Truncate Environment Variables
+
+If using environment variables, ensure the value is truncated to 32 characters:
+
+```js
+// next.config.js
+module.exports = {
+ deploymentId: process.env.DEPLOYMENT_ID?.substring(0, 32),
+}
+```
+
+## Additional Information
+
+- The deployment ID is used for skew protection and asset versioning
+- Keep it concise but meaningful for your use case
+- Consider using hashes or shortened identifiers if you need unique values
diff --git a/packages/next/errors.json b/packages/next/errors.json
index be6799ba2d8225..a84f03b122eec3 100644
--- a/packages/next/errors.json
+++ b/packages/next/errors.json
@@ -984,5 +984,8 @@
"983": "Invariant: global-error module is required but not found in loader tree",
"984": "LRUCache: calculateSize returned %s, but size must be > 0. Items with size 0 would never be evicted, causing unbounded cache growth.",
"985": "No response is returned from route handler '%s'. Expected a Response object but received '%s' (method: %s, url: %s). Ensure you return a \\`Response\\` or a \\`NextResponse\\` in all branches of your handler.",
- "986": "Server Action arguments list is too long (%s). Maximum allowed is %s."
+ "986": "Server Action arguments list is too long (%s). Maximum allowed is %s.",
+ "987": "Invalid \\`deploymentId\\` configuration: exceeds maximum length of 32 characters. See https://nextjs.org/docs/messages/deploymentid-too-long",
+ "988": "Invalid \\`deploymentId\\` configuration: must be a string. See https://nextjs.org/docs/messages/deploymentid-not-a-string",
+ "989": "Invalid \\`deploymentId\\` configuration: contains invalid characters. Only alphanumeric characters, hyphens, and underscores are allowed. See https://nextjs.org/docs/messages/deploymentid-invalid-characters"
}
diff --git a/packages/next/src/build/generate-routes-manifest.ts b/packages/next/src/build/generate-routes-manifest.ts
index 22e264f8a794df..dc7c7559bf442d 100644
--- a/packages/next/src/build/generate-routes-manifest.ts
+++ b/packages/next/src/build/generate-routes-manifest.ts
@@ -35,6 +35,7 @@ export interface GenerateRoutesManifestOptions {
restrictedRedirectPaths: string[]
isAppPPREnabled: boolean
appType: 'pages' | 'app' | 'hybrid'
+ deploymentId?: string
}
export interface GenerateRoutesManifestResult {
@@ -60,6 +61,7 @@ export function generateRoutesManifest(
restrictedRedirectPaths,
isAppPPREnabled,
appType,
+ deploymentId,
} = options
const sortedRoutes = sortPages([...pageKeys.pages, ...(pageKeys.app ?? [])])
@@ -134,6 +136,7 @@ export function generateRoutesManifest(
queryHeader: NEXT_REWRITTEN_QUERY_HEADER,
},
skipProxyUrlNormalize: config.skipProxyUrlNormalize,
+ deploymentId: deploymentId || undefined,
ppr: isAppPPREnabled
? {
chain: {
diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts
index 5346895e2687e4..8abb2844b7c38b 100644
--- a/packages/next/src/build/index.ts
+++ b/packages/next/src/build/index.ts
@@ -488,6 +488,10 @@ export type RoutesManifest = {
}
skipProxyUrlNormalize?: boolean
caseSensitive?: boolean
+ /**
+ * User-configured deployment ID for skew protection.
+ */
+ deploymentId?: string
/**
* Configuration related to Partial Prerendering.
*/
@@ -967,6 +971,25 @@ export default async function build(
)
loadedConfig = config
+ // Validate deploymentId if provided
+ if (config.deploymentId !== undefined) {
+ if (typeof config.deploymentId !== 'string') {
+ throw new Error(
+ `Invalid \`deploymentId\` configuration: must be a string. See https://nextjs.org/docs/messages/deploymentid-not-a-string`
+ )
+ }
+ if (config.deploymentId.length > 32) {
+ throw new Error(
+ `Invalid \`deploymentId\` configuration: exceeds maximum length of 32 characters. See https://nextjs.org/docs/messages/deploymentid-too-long`
+ )
+ }
+ if (!/^[a-zA-Z0-9_-]*$/.test(config.deploymentId)) {
+ throw new Error(
+ `Invalid \`deploymentId\` configuration: contains invalid characters. Only alphanumeric characters, hyphens, and underscores are allowed. See https://nextjs.org/docs/messages/deploymentid-invalid-characters`
+ )
+ }
+ }
+
// Reading the config can modify environment variables that influence the bundler selection.
bundler = finalizeBundlerFromConfig(bundler)
nextBuildSpan.setAttribute('bundler', getBundlerForTelemetry(bundler))
@@ -1637,6 +1660,7 @@ export default async function build(
rewrites,
restrictedRedirectPaths,
isAppPPREnabled,
+ deploymentId: config.deploymentId,
})
)
diff --git a/test/rspack-dev-tests-manifest.json b/test/rspack-dev-tests-manifest.json
index 4b02cdae6179c5..90c1013c4fb27b 100644
--- a/test/rspack-dev-tests-manifest.json
+++ b/test/rspack-dev-tests-manifest.json
@@ -237,6 +237,7 @@
"Error overlay - RSC build errors should error when useActionState from react is used in server component",
"Error overlay - RSC build errors should error when useDeferredValue from react is used in server component",
"Error overlay - RSC build errors should error when useEffect from react is used in server component",
+ "Error overlay - RSC build errors should error when useEffectEvent from react is used in server component",
"Error overlay - RSC build errors should error when useFormState from react-dom is used in server component",
"Error overlay - RSC build errors should error when useFormStatus from react-dom is used in server component",
"Error overlay - RSC build errors should error when useImperativeHandle from react is used in server component",
@@ -532,10 +533,10 @@
"runtimeError": false
},
"test/development/app-dir/browser-log-forwarding/fixtures/verbose-level/verbose-level.test.ts": {
- "passed": [
+ "passed": [],
+ "failed": [
"browser-log-forwarding verbose level should forward all logs to terminal"
],
- "failed": [],
"pending": [],
"flakey": [],
"runtimeError": false
@@ -1251,13 +1252,14 @@
},
"test/development/app-hmr/hmr.test.ts": {
"passed": [
- "app-dir-hmr filesystem changes can navigate cleanly to a page that requires a change in the Webpack runtime",
"app-dir-hmr filesystem changes should have no unexpected action error for hmr",
"app-dir-hmr filesystem changes should not break when renaming a folder",
"app-dir-hmr filesystem changes should not continously poll when hitting a not found page",
"app-dir-hmr filesystem changes should update server components after navigating to a page with a different runtime"
],
- "failed": [],
+ "failed": [
+ "app-dir-hmr filesystem changes can navigate cleanly to a page that requires a change in the Webpack runtime"
+ ],
"pending": [],
"flakey": [],
"runtimeError": false
@@ -5764,10 +5766,10 @@
"runtimeError": false
},
"test/e2e/app-dir/interception-routes-output-export/interception-routes-output-export.test.ts": {
- "passed": [
+ "passed": [],
+ "failed": [
"interception-routes-output-export should error when using interception routes with static export"
],
- "failed": [],
"pending": [],
"flakey": [],
"runtimeError": false
@@ -7454,6 +7456,15 @@
"flakey": [],
"runtimeError": false
},
+ "test/e2e/app-dir/node-worker-threads/node-worker-threads.test.ts": {
+ "passed": [],
+ "failed": [],
+ "pending": [
+ "node-worker-threads webpack doesnt support bundling worker-threads"
+ ],
+ "flakey": [],
+ "runtimeError": false
+ },
"test/e2e/app-dir/non-root-project-monorepo/non-root-project-monorepo.test.ts": {
"passed": [
"non-root-project-monorepo import.meta.url should work during RSC",
@@ -7887,7 +7898,6 @@
"parallel-routes-revalidation router.refresh (regular) - searchParams: true should correctly refresh data for the intercepted route and previously active page slot",
"parallel-routes-revalidation server action revalidation handles refreshing when multiple parallel slots are active",
"parallel-routes-revalidation should handle a redirect action when called in a slot",
- "parallel-routes-revalidation should handle router.refresh() when called in a slot",
"parallel-routes-revalidation should not trigger full page when calling router.refresh() on an intercepted route",
"parallel-routes-revalidation should not trigger interception when calling router.refresh() on an intercepted route (/catchall/foobar)",
"parallel-routes-revalidation should not trigger interception when calling router.refresh() on an intercepted route (/detail-page)",
@@ -7896,7 +7906,9 @@
"parallel-routes-revalidation should refresh the correct page when a server action triggers a redirect",
"parallel-routes-revalidation should submit the action and revalidate the page data"
],
- "failed": [],
+ "failed": [
+ "parallel-routes-revalidation should handle router.refresh() when called in a slot"
+ ],
"pending": [
"parallel-routes-revalidation server action revalidation should not trigger a refresh for the page that is being redirected to"
],
@@ -9847,11 +9859,10 @@
"runtimeError": false
},
"test/e2e/app-dir/use-cache-without-experimental-flag/use-cache-without-experimental-flag.test.ts": {
- "passed": [
- "use-cache-without-experimental-flag should recover from the build error if useCache flag is set",
- "use-cache-without-experimental-flag should show a build error"
+ "passed": ["use-cache-without-experimental-flag should show a build error"],
+ "failed": [
+ "use-cache-without-experimental-flag should recover from the build error if useCache flag is set"
],
- "failed": [],
"pending": [],
"flakey": [],
"runtimeError": false
@@ -22117,12 +22128,15 @@
},
"test/integration/ssg-data-404/test/index.test.ts": {
"passed": [
- "SSG data 404 development mode should hard navigate when a new deployment occurs"
+ "SSG data 404 - hard navigate when a new deployment occurs development mode gsp to gssp",
+ "SSG data 404 - hard navigate when a new deployment occurs development mode index to gsp",
+ "SSG data 404 - hard navigate when a new deployment occurs production mode with build id gsp to gssp",
+ "SSG data 404 - hard navigate when a new deployment occurs production mode with build id index to gsp",
+ "SSG data 404 - hard navigate when a new deployment occurs production mode with deployment id gsp to gssp",
+ "SSG data 404 - hard navigate when a new deployment occurs production mode with deployment id index to gsp"
],
"failed": [],
- "pending": [
- "SSG data 404 production mode should hard navigate when a new deployment occurs"
- ],
+ "pending": [],
"flakey": [],
"runtimeError": false
},