Repository: vercel/next.js
Pull request: Fix: Do not force BLOCKING_STATIC_RENDER for DOM bots

The issue was that DOM bots with PPR-enabled routes were being forced to use BLOCKING_STATIC_RENDER mode, which prevented them from using the fallback cache lookup mechanism. This caused them to generate new content with isRevalidating: true, which blocks postponed data and triggers the Math.random() static generation bailout error.

For HTML bots, they keep using BLOCKING_STATIC_RENDER because SSG was turned off for them and won't see the SSG bailout error. For DOM bots, they should behave like a normal user.

Fixes NAR-273

Changed files:
filename: packages/next/src/build/templates/app-page.ts
patch: @@ -653,11 +653,13 @@ export async function handler(
         fallbackMode = parseFallbackField(prerenderInfo.fallback)
       }
 
-      // When serving a bot request, we want to serve a blocking render and not
-      // the prerendered page. This ensures that the correct content is served
+      // When serving a HTML bot request, we want to serve a blocking render and
+      // not the prerendered page. This ensures that the correct content is served
       // to the bot in the head.
       if (fallbackMode === FallbackMode.PRERENDER && isBot(userAgent)) {
-        fallbackMode = FallbackMode.BLOCKING_STATIC_RENDER
+        if (!isRoutePPREnabled || isHtmlBot) {
+          fallbackMode = FallbackMode.BLOCKING_STATIC_RENDER
+        }
       }
 
       if (previousCacheEntry?.isStale === -1) {

filename: test/e2e/app-dir/cache-components-bot-ua/app/[slug]/page.tsx
patch: @@ -0,0 +1,29 @@
+import React from 'react'
+
+import type { Metadata } from 'next'
+
+export async function generateMetadata(): Promise<Metadata> {
+  await new Promise((resolve) => setTimeout(resolve, 1000)) // Simulate a delay
+  return {
+    title: 'Home',
+    description: 'Welcome to the home page',
+  }
+}
+
+export default async function Home({
+  params,
+}: {
+  params: Promise<{ slug: string }>
+}) {
+  const { slug } = await params
+  // Math.random() will cause SSG_BAILOUT if static generation runs
+  // Bots should bypass static generation in PPR to avoid this error
+  const randomValue = Math.random()
+
+  return (
+    <>
+      <h1>{slug}</h1>
+      <p>{randomValue}</p>
+    </>
+  )
+}

filename: test/e2e/app-dir/cache-components-bot-ua/app/layout.tsx
patch: @@ -0,0 +1,22 @@
+import React, { Suspense } from 'react'
+import type { Metadata } from 'next'
+
+export const metadata: Metadata = {
+  title: 'PPR Bot SSG Bypass Test',
+  description:
+    'Test bot static generation bypass with PPR and cache components',
+}
+
+export default function RootLayout({
+  children,
+}: {
+  children: React.ReactNode
+}) {
+  return (
+    <html lang="en">
+      <body>
+        <Suspense>{children}</Suspense>
+      </body>
+    </html>
+  )
+}

filename: test/e2e/app-dir/cache-components-bot-ua/cache-components-bot-ua.test.ts
patch: @@ -0,0 +1,32 @@
+import { nextTestSetup } from 'e2e-utils'
+
+describe('cache-components PPR bot static generation bypass', () => {
+  const { next } = nextTestSetup({
+    files: __dirname,
+  })
+
+  it('should bypass static generation for DOM bot requests to avoid SSG_BAILOUT', async () => {
+    const res = await next.fetch('/foo', {
+      headers: {
+        'user-agent': 'Googlebot',
+      },
+    })
+    // With cache components + PPR enabled, DOM bots should behave like regular users
+    // and use the fallback cache mechanism. This allows them to handle dynamic content
+    // like Math.random() without triggering SSG_BAILOUT errors.
+    expect(res.status).toBe(200)
+
+    // Verify that the response contains the page content
+    const html = await res.text()
+
+    // Check that the page rendered successfully
+    // With PPR, content is streamed via script tags
+    expect(html).toContain('\\"children\\":\\"foo\\"')
+
+    // Verify Math.random() was executed (check for a decimal number in the streamed content)
+    expect(html).toMatch(/\\"children\\":0\.\d+/)
+
+    // With PPR, content is streamed, but the important thing is that
+    // the page rendered without a 500 error
+  })
+})

filename: test/e2e/app-dir/cache-components-bot-ua/next.config.js
patch: @@ -0,0 +1,8 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+  experimental: {
+    cacheComponents: true,
+  },
+}
+
+module.exports = nextConfig