Repository: vercel/next.js
Pull request: fix(build): improve static path generation performance and fix parameter collisions

## Summary

• **Performance**: Replace O(n²) parameter deduplication with O(n) Map-based approach
• **Architecture**: Rewrite `assignErrorIfEmpty` using [Trie data structure](https://en.wikipedia.org/wiki/Trie)
• **Testing**: Add comprehensive test coverage for edge cases including parameter collisions and complex route hierarchies

## Technical Details

### Performance Optimization
- **Before**: O(n²) nested loops comparing each parameter combination against all previous ones
- **After**: O(n) Map-based deduplication using unique string keys for each parameter combination
- **Impact**: Significant performance improvement for routes with many parameter combinations

### Trie-Based Route Analysis
Replaces linear route comparison with efficient Trie data structure:

- **Nodes**: Represent unique parameter combinations 
- **Edges**: Represent parameter values with collision-safe keys
- **Algorithm**: [DFS traversal](https://en.wikipedia.org/wiki/Depth-first_search) determines `throwOnEmptyStaticShell` based on child relationships

**Example**: For routes `/blog/[slug]` and `/blog/first-post`:
- Trie identifies `/blog/[slug]` as parent to concrete route `/blog/first-post`
- Sets `throwOnEmptyStaticShell = false` for parent, `true` for concrete route

Changed files:
filename: packages/next/src/build/static-paths/app.test.ts
patch: @@ -7,7 +7,7 @@ import {
 import type { PrerenderedRoute } from './types'
 
 describe('assignErrorIfEmpty', () => {
-  it('should assign throwOnEmptyStaticShell false for a static route', () => {
+  it('should assign throwOnEmptyStaticShell true for a static route with no children', () => {
     const prerenderedRoutes: PrerenderedRoute[] = [
       {
         params: {},
@@ -25,7 +25,7 @@ describe('assignErrorIfEmpty', () => {
     expect(prerenderedRoutes[0].throwOnEmptyStaticShell).toBe(true)
   })
 
-  it('should assign throwOnEmptyStaticShell to the prerendered routes', () => {
+  it('should assign throwOnEmptyStaticShell based on route hierarchy', () => {
     const prerenderedRoutes: PrerenderedRoute[] = [
       {
         params: {},
@@ -110,6 +110,216 @@ describe('assignErrorIfEmpty', () => {
     expect(prerenderedRoutes[3].throwOnEmptyStaticShell).toBe(true)
     expect(prerenderedRoutes[4].throwOnEmptyStaticShell).toBe(false)
   })
+
+  it('should handle parameter value collisions', () => {
+    const params = [{ slug: ['foo', 'bar'] }, { slug: 'foo,bar' }]
+
+    const unique = filterUniqueRootParamsCombinations(['slug'], params)
+
+    expect(unique).toEqual([{ slug: ['foo', 'bar'] }, { slug: 'foo,bar' }])
+  })
+
+  it('should handle multiple routes at the same trie node', () => {
+    const prerenderedRoutes: PrerenderedRoute[] = [
+      {
+        params: { id: '1' },
+        pathname: '/1/[name]',
+        encodedPathname: '/1/[name]',
+        fallbackRouteParams: ['name'],
+        fallbackMode: FallbackMode.NOT_FOUND,
+        fallbackRootParams: [],
+        throwOnEmptyStaticShell: true,
+      },
+      {
+        params: { id: '1' },
+        pathname: '/1/[name]/[extra]',
+        encodedPathname: '/1/[name]/[extra]',
+        fallbackRouteParams: ['name', 'extra'],
+        fallbackMode: FallbackMode.NOT_FOUND,
+        fallbackRootParams: [],
+        throwOnEmptyStaticShell: true,
+      },
+      {
+        params: { id: '1', name: 'test' },
+        pathname: '/1/test',
+        encodedPathname: '/1/test',
+        fallbackRouteParams: [],
+        fallbackMode: FallbackMode.NOT_FOUND,
+        fallbackRootParams: [],
+        throwOnEmptyStaticShell: true,
+      },
+    ]
+
+    assignErrorIfEmpty(prerenderedRoutes, ['id', 'name', 'extra'])
+
+    expect(prerenderedRoutes[0].throwOnEmptyStaticShell).toBe(false)
+    expect(prerenderedRoutes[1].throwOnEmptyStaticShell).toBe(false)
+    expect(prerenderedRoutes[2].throwOnEmptyStaticShell).toBe(true)
+  })
+
+  it('should handle empty input', () => {
+    const prerenderedRoutes: PrerenderedRoute[] = []
+    assignErrorIfEmpty(prerenderedRoutes, [])
+    expect(prerenderedRoutes).toEqual([])
+  })
+
+  it('should handle blog/[slug] not throwing when concrete routes exist (from docs example)', () => {
+    const prerenderedRoutes: PrerenderedRoute[] = [
+      {
+        params: {},
+        pathname: '/blog/[slug]',
+        encodedPathname: '/blog/[slug]',
+        fallbackRouteParams: ['slug'],
+        fallbackMode: FallbackMode.NOT_FOUND,
+        fallbackRootParams: [],
+        throwOnEmptyStaticShell: true,
+      },
+      {
+        params: { slug: 'first-post' },
+        pathname: '/blog/first-post',
+        encodedPathname: '/blog/first-post',
+        fallbackRouteParams: [],
+        fallbackMode: FallbackMode.NOT_FOUND,
+        fallbackRootParams: [],
+        throwOnEmptyStaticShell: true,
+      },
+      {
+        params: { slug: 'second-post' },
+        pathname: '/blog/second-post',
+        encodedPathname: '/blog/second-post',
+        fallbackRouteParams: [],
+        fallbackMode: FallbackMode.NOT_FOUND,
+        fallbackRootParams: [],
+        throwOnEmptyStaticShell: true,
+      },
+    ]
+
+    assignErrorIfEmpty(prerenderedRoutes, ['slug'])
+
+    expect(prerenderedRoutes[0].throwOnEmptyStaticShell).toBe(false) // Should not throw - has concrete children
+    expect(prerenderedRoutes[1].throwOnEmptyStaticShell).toBe(true) // Should throw - concrete route
+    expect(prerenderedRoutes[2].throwOnEmptyStaticShell).toBe(true) // Should throw - concrete route
+  })
+
+  it('should handle catch-all routes with different fallback parameter counts (from docs example)', () => {
+    const prerenderedRoutes: PrerenderedRoute[] = [
+      {
+        params: {},
+        pathname: '/[id]/[...slug]',
+        encodedPathname: '/[id]/[...slug]',
+        fallbackRouteParams: ['id', 'slug'],
+        fallbackMode: FallbackMode.NOT_FOUND,
+        fallbackRootParams: [],
+        throwOnEmptyStaticShell: true,
+      },
+      {
+        params: { id: '1234' },
+        pathname: '/1234/[...slug]',
+        encodedPathname: '/1234/[...slug]',
+        fallbackRouteParams: ['slug'],
+        fallbackMode: FallbackMode.NOT_FOUND,
+        fallbackRootParams: [],
+        throwOnEmptyStaticShell: true,
+      },
+      {
+        params: { id: '1234', slug: ['about', 'us'] },
+        pathname: '/1234/about/us',
+        encodedPathname: '/1234/about/us',
+        fallbackRouteParams: [],
+        fallbackMode: FallbackMode.NOT_FOUND,
+        fallbackRootParams: [],
+        throwOnEmptyStaticShell: true,
+      },
+    ]
+
+    assignErrorIfEmpty(prerenderedRoutes, ['id', 'slug'])
+
+    expect(prerenderedRoutes[0].throwOnEmptyStaticShell).toBe(false) // Should not throw - has children
+    expect(prerenderedRoutes[1].throwOnEmptyStaticShell).toBe(false) // Should not throw - has children
+    expect(prerenderedRoutes[2].throwOnEmptyStaticShell).toBe(true) // Should throw - concrete route
+  })
+
+  it('should handle nested routes with multiple parameter depths', () => {
+    const prerenderedRoutes: PrerenderedRoute[] = [
+      {
+        params: {},
+        pathname: '/[category]/[subcategory]/[item]',
+        encodedPathname: '/[category]/[subcategory]/[item]',
+        fallbackRouteParams: ['category', 'subcategory', 'item'],
+        fallbackMode: FallbackMode.NOT_FOUND,
+        fallbackRootParams: [],
+        throwOnEmptyStaticShell: true,
+      },
+      {
+        params: { category: 'electronics' },
+        pathname: '/electronics/[subcategory]/[item]',
+        encodedPathname: '/electronics/[subcategory]/[item]',
+        fallbackRouteParams: ['subcategory', 'item'],
+        fallbackMode: FallbackMode.NOT_FOUND,
+        fallbackRootParams: [],
+        throwOnEmptyStaticShell: true,
+      },
+      {
+        params: { category: 'electronics', subcategory: 'phones' },
+        pathname: '/electronics/phones/[item]',
+        encodedPathname: '/electronics/phones/[item]',
+        fallbackRouteParams: ['item'],
+        fallbackMode: FallbackMode.NOT_FOUND,
+        fallbackRootParams: [],
+        throwOnEmptyStaticShell: true,
+      },
+      {
+        params: {
+          category: 'electronics',
+          subcategory: 'phones',
+          item: 'iphone',
+        },
+        pathname: '/electronics/phones/iphone',
+        encodedPathname: '/electronics/phones/iphone',
+        fallbackRouteParams: [],
+        fallbackMode: FallbackMode.NOT_FOUND,
+        fallbackRootParams: [],
+        throwOnEmptyStaticShell: true,
+      },
+    ]
+
+    assignErrorIfEmpty(prerenderedRoutes, ['category', 'subcategory', 'item'])
+
+    // All except the last one should not throw on empty static shell
+    expect(prerenderedRoutes[0].throwOnEmptyStaticShell).toBe(false)
+    expect(prerenderedRoutes[1].throwOnEmptyStaticShell).toBe(false)
+    expect(prerenderedRoutes[2].throwOnEmptyStaticShell).toBe(false)
+    expect(prerenderedRoutes[3].throwOnEmptyStaticShell).toBe(true)
+  })
+
+  it('should handle routes at same trie node with different fallback parameter lengths', () => {
+    const prerenderedRoutes: PrerenderedRoute[] = [
+      {
+        params: { locale: 'en' },
+        pathname: '/en/[...segments]',
+        encodedPathname: '/en/[...segments]',
+        fallbackRouteParams: ['segments'],
+        fallbackMode: FallbackMode.NOT_FOUND,
+        fallbackRootParams: [],
+        throwOnEmptyStaticShell: true,
+      },
+      {
+        params: { locale: 'en' },
+        pathname: '/en',
+        encodedPathname: '/en',
+        fallbackRouteParams: [],
+        fallbackMode: FallbackMode.NOT_FOUND,
+        fallbackRootParams: [],
+        throwOnEmptyStaticShell: true,
+      },
+    ]
+
+    assignErrorIfEmpty(prerenderedRoutes, ['locale', 'segments'])
+
+    // The route with more fallback params should not throw on empty static shell
+    expect(prerenderedRoutes[0].throwOnEmptyStaticShell).toBe(false)
+    expect(prerenderedRoutes[1].throwOnEmptyStaticShell).toBe(true)
+  })
 })
 
 describe('filterUniqueParams', () => {
@@ -154,4 +364,23 @@ describe('filterUniqueRootParamsCombinations', () => {
 
     expect(unique).toEqual([{ id: '1' }, { id: '2' }])
   })
+
+  it('should handle multiple root parameters', () => {
+    const params = [
+      { lang: 'en', region: 'US', page: 'home' },
+      { lang: 'en', region: 'US', page: 'about' },
+      { lang: 'fr', region: 'CA', page: 'home' },
+      { lang: 'fr', region: 'CA', page: 'about' },
+    ]
+
+    const unique = filterUniqueRootParamsCombinations(
+      ['lang', 'region'],
+      params
+    )
+
+    expect(unique).toEqual([
+      { lang: 'en', region: 'US' },
+      { lang: 'fr', region: 'CA' },
+    ])
+  })
 })

filename: packages/next/src/build/static-paths/app.ts
patch: @@ -1,4 +1,4 @@
-import type { ParamValue, Params } from '../../server/request/params'
+import type { Params } from '../../server/request/params'
 import type { AppPageModule } from '../../server/route-modules/app-page/module'
 import type { AppSegment } from '../segment-config/app/app-segments'
 import type { PrerenderedRoute, StaticPathsResult } from './types'
@@ -18,68 +18,66 @@ import escapePathDelimiters from '../../shared/lib/router/utils/escape-path-deli
 import { createIncrementalCache } from '../../export/helpers/create-incremental-cache'
 import type { NextConfigComplete } from '../../server/config-shared'
 
-/**
- * Compares two parameters to see if they're equal.
- *
- * @param a - The first parameter.
- * @param b - The second parameter.
- * @returns Whether the parameters are equal.
- */
-function areParamValuesEqual(a: ParamValue, b: ParamValue) {
-  // If they're equal, then we can return true.
-  if (a === b) {
-    return true
-  }
-
-  // If they're both arrays, then we can return true if they have the same
-  // length and all the items are the same.
-  if (Array.isArray(a) && Array.isArray(b)) {
-    if (a.length !== b.length) {
-      return false
-    }
-
-    return a.every((item, index) => item === b[index])
-  }
-
-  // Otherwise, they're not equal.
-  return false
-}
-
 /**
  * Filters out duplicate parameters from a list of parameters.
+ * This function uses a Map to efficiently store and retrieve unique parameter combinations.
  *
- * @param routeParamKeys - The keys of the parameters.
- * @param routeParams - The list of parameters to filter.
- * @returns The list of unique parameters.
+ * @param routeParamKeys - The keys of the parameters. These should be sorted to ensure consistent key generation.
+ * @param routeParams - The list of parameter objects to filter.
+ * @returns A new array containing only the unique parameter combinations.
  */
 export function filterUniqueParams(
   routeParamKeys: readonly string[],
   routeParams: readonly Params[]
 ): Params[] {
-  const unique: Params[] = []
+  // A Map is used to store unique parameter combinations. The key of the Map
+  // is a string representation of the parameter combination, and the value
+  // is the actual `Params` object.
+  const unique = new Map<string, Params>()
 
-  paramsLoop: for (const params of routeParams) {
-    uniqueLoop: for (const item of unique) {
-      for (const key of routeParamKeys) {
-        // If the param is not the same, then we need to check the next item
-        if (!areParamValuesEqual(item[key], params[key])) {
-          continue uniqueLoop
-        }
+  // Iterate over each parameter object in the input array.
+  for (const params of routeParams) {
+    let key = '' // Initialize an empty string to build the unique key for the current `params` object.
+
+    // Iterate through the `routeParamKeys` (which are assumed to be sorted).
+    // This consistent order is crucial for generating a stable and unique key
+    // for each parameter combination.
+    for (const paramKey of routeParamKeys) {
+      const value = params[paramKey]
+
+      // Construct a part of the key using the parameter key and its value.
+      // A type prefix (`A:` for Array, `S:` for String, `U:` for undefined) is added to the value
+      // to prevent collisions. For example, `['a', 'b']` and `'a,b'` would
+      // otherwise generate the same string representation, leading to incorrect
+      // deduplication. This ensures that different types with the same string
+      // representation are treated as distinct.
+      let valuePart: string
+      if (Array.isArray(value)) {
+        valuePart = `A:${value.join(',')}`
+      } else if (value === undefined) {
+        valuePart = `U:undefined`
+      } else {
+        valuePart = `S:${value}`
       }
-
-      // If we got here, then all params matched and we found a duplicate
-      continue paramsLoop
+      key += `${paramKey}:${valuePart}|`
     }
 
-    // If we got here, then we checked all items and found no duplicates
-    unique.push(params)
+    // If the generated key is not already in the `unique` Map, it means this
+    // parameter combination is unique so far. Add it to the Map.
+    if (!unique.has(key)) {
+      unique.set(key, params)
+    }
   }
 
-  return unique
+  // Convert the Map's values (the unique `Params` objects) back into an array
+  // and return it.
+  return Array.from(unique.values())
 }
 
 /**
  * Filters out all combinations of root params from a list of parameters.
+ * This function extracts only the root parameters from each parameter object
+ * and then filters out duplicate combinations using a Map for efficiency.
  *
  * Given the following root param ('lang'), and the following routeParams:
  *
@@ -100,33 +98,58 @@ export function filterUniqueParams(
  * ]
  * ```
  *
- * @param rootParamKeys - The keys of the root params.
- * @param routeParams - The list of parameters to filter.
- * @returns The list of combinations of root params.
+ * @param rootParamKeys - The keys of the root params. These should be sorted
+ *   to ensure consistent key generation for the internal Map.
+ * @param routeParams - The list of parameter objects to filter.
+ * @returns A new array containing only the unique combinations of root params.
  */
 export function filterUniqueRootParamsCombinations(
   rootParamKeys: readonly string[],
   routeParams: readonly Params[]
 ): Params[] {
-  const combinations: Params[] = []
-
-  paramsLoop: for (const params of routeParams) {
-    const combination: Params = {}
+  // A Map is used to store unique combinations of root parameters.
+  // The key of the Map is a string representation of the root parameter
+  // combination, and the value is the `Params` object containing only
+  // the root parameters.
+  const combinations = new Map<string, Params>()
 
-    // Collect all root params. As soon as we don't find a root param, break.
-    for (const key of rootParamKeys) {
-      if (params[key]) {
-        combination[key] = params[key]
+  // Iterate over each parameter object in the input array.
+  for (const params of routeParams) {
+    const combination: Params = {} // Initialize an object to hold only the root parameters.
+    let key = '' // Initialize an empty string to build the unique key for the current root parameter combination.
+
+    // Iterate through the `rootParamKeys` (which are assumed to be sorted).
+    // This consistent order is crucial for generating a stable and unique key
+    // for each root parameter combination.
+    for (const rootKey of rootParamKeys) {
+      const value = params[rootKey]
+      combination[rootKey] = value // Add the root parameter and its value to the combination object.
+
+      // Construct a part of the key using the root parameter key and its value.
+      // A type prefix (`A:` for Array, `S:` for String, `U:` for undefined) is added to the value
+      // to prevent collisions. This ensures that different types with the same
+      // string representation are treated as distinct.
+      let valuePart: string
+      if (Array.isArray(value)) {
+        valuePart = `A:${value.join(',')}`
+      } else if (value === undefined) {
+        valuePart = `U:undefined`
       } else {
-        // Skip this combination if we don't have all root params
-        continue paramsLoop
+        valuePart = `S:${value}`
       }
+      key += `${rootKey}:${valuePart}|`
     }
 
-    combinations.push(combination)
+    // If the generated key is not already in the `combinations` Map, it means
+    // this root parameter combination is unique so far. Add it to the Map.
+    if (!combinations.has(key)) {
+      combinations.set(key, combination)
+    }
   }
 
-  return filterUniqueParams(rootParamKeys, combinations)
+  // Convert the Map's values (the unique root parameter `Params` objects)
+  // back into an array and return it.
+  return Array.from(combinations.values())
 }
 
 /**
@@ -223,47 +246,158 @@ function validateParams(
   return valid
 }
 
+interface TrieNode {
+  /**
+   * The children of the node. Each key is a unique string representation of a parameter value,
+   * and the value is the next TrieNode in the path.
+   */
+  children: Map<string, TrieNode>
+
+  /**
+   * The routes that are associated with this specific parameter combination (node).
+   * These are the routes whose concrete parameters lead to this node in the Trie.
+   */
+  routes: PrerenderedRoute[]
+}
+
 /**
  * Assigns the throwOnEmptyStaticShell property to each of the prerendered routes.
+ * This function uses a Trie data structure to efficiently determine whether each route
+ * should throw an error when its static shell is empty.
+ *
+ * A route should not throw on empty static shell if it has child routes in the Trie. For example,
+ * if we have two routes, `/blog/first-post` and `/blog/[slug]`, the route for
+ * `/blog/[slug]` should not throw because `/blog/first-post` is a more specific concrete route.
  *
  * @param prerenderedRoutes - The prerendered routes.
  * @param routeParamKeys - The keys of the route parameters.
  */
 export function assignErrorIfEmpty(
   prerenderedRoutes: readonly PrerenderedRoute[],
   routeParamKeys: readonly string[]
-) {
-  // If we're rendering a more specific route, then we don't need to error
-  // if the route is empty.
-  for (const prerenderedRoute of prerenderedRoutes) {
-    let throwOnEmptyStaticShell: boolean = true
-
-    // If the route has fallbackRouteParams, then we need to check if the
-    // route is a more specific route.
-    const { fallbackRouteParams, params } = prerenderedRoute
-    if (fallbackRouteParams && fallbackRouteParams.length > 0) {
-      siblingLoop: for (const other of prerenderedRoutes) {
-        // Skip the current route.
-        if (other === prerenderedRoute) continue
+): void {
+  // If there are no routes to process, exit early.
+  if (prerenderedRoutes.length === 0) {
+    return
+  }
 
-        for (const key of routeParamKeys) {
-          // If the key is a fallback route param, then we can skip it, because
-          // it always matches.
-          if (fallbackRouteParams.includes(key)) {
-            throwOnEmptyStaticShell = false
-            break siblingLoop
-          }
+  // Initialize the root of the Trie. This node represents the starting point
+  // before any parameters have been considered.
+  const root: TrieNode = { children: new Map(), routes: [] }
 
-          // If the param value is not equal, then we can break out of the loop
-          // because the route is not a more specific route.
-          if (!areParamValuesEqual(params[key], other.params[key])) {
-            continue siblingLoop
-          }
+  // Phase 1: Build the Trie.
+  // Iterate over each prerendered route and insert it into the Trie.
+  // Each route's concrete parameter values form a path in the Trie.
+  for (const route of prerenderedRoutes) {
+    let currentNode = root // Start building the path from the root for each route.
+
+    // Iterate through the sorted parameter keys. The order of keys is crucial
+    // for ensuring that routes with the same concrete parameters follow the
+    // same path in the Trie, regardless of the original order of properties
+    // in the `params` object.
+    for (const key of routeParamKeys) {
+      // Check if the current route actually has a concrete value for this parameter.
+      // If a dynamic segment is not filled (i.e., it's a fallback), it won't have
+      // this property, and we stop building the path for this route at this point.
+      if (route.params.hasOwnProperty(key)) {
+        const value = route.params[key]
+
+        // Generate a unique key for the parameter's value. This is critical
+        // to prevent collisions between different data types that might have
+        // the same string representation (e.g., `['a', 'b']` vs `'a,b'`).
+        // A type prefix (`A:` for Array, `S:` for String, `U:` for undefined)
+        // is added to the value to prevent collisions. This ensures that
+        // different types with the same string representation are treated as
+        // distinct.
+        let valueKey: string
+        if (Array.isArray(value)) {
+          valueKey = `A:${value.join(',')}`
+        } else if (value === undefined) {
+          valueKey = `U:undefined`
+        } else {
+          valueKey = `S:${value}`
+        }
+
+        // Look for a child node corresponding to this `valueKey` from the `currentNode`.
+        let childNode = currentNode.children.get(valueKey)
+        if (!childNode) {
+          // If the child node doesn't exist, create a new one and add it to
+          // the current node's children.
+          childNode = { children: new Map(), routes: [] }
+          currentNode.children.set(valueKey, childNode)
         }
+        // Move deeper into the Trie to the `childNode` for the next parameter.
+        currentNode = childNode
       }
     }
+    // After processing all concrete parameters for the route, add the full
+    // `PrerenderedRoute` object to the `routes` array of the `currentNode`.
+    // This node represents the unique concrete parameter combination for this route.
+    currentNode.routes.push(route)
+  }
+
+  // Phase 2: Traverse the Trie to assign the `throwOnEmptyStaticShell` property.
+  // This is done using an iterative Depth-First Search (DFS) approach with an
+  // explicit stack to avoid JavaScript's recursion depth limits (stack overflow)
+  // for very deep routing structures.
+  const stack: TrieNode[] = [root] // Initialize the stack with the root node.
+
+  while (stack.length > 0) {
+    const node = stack.pop()! // Pop the next node to process from the stack.
+
+    // `hasChildren` indicates if this node has any more specific concrete
+    // parameter combinations branching off from it. If true, it means this
+    // node represents a prefix for other, more specific routes.
+    const hasChildren = node.children.size > 0
+
+    // If the current node has routes associated with it (meaning, routes whose
+    // concrete parameters lead to this node's path in the Trie).
+    if (node.routes.length > 0) {
+      // Determine the minimum number of fallback parameters among all routes
+      // that are associated with this current Trie node. This is used to
+      // identify if a route should not throw on empty static shell relative to another route *at the same level*
+      // of concrete parameters, but with fewer fallback parameters.
+      let minFallbacks = Infinity
+      for (const r of node.routes) {
+        // `fallbackRouteParams?.length ?? 0` handles cases where `fallbackRouteParams`
+        // might be `undefined` or `null`, treating them as 0 length.
+        minFallbacks = Math.min(
+          minFallbacks,
+          r.fallbackRouteParams?.length ?? 0
+        )
+      }
 
-    prerenderedRoute.throwOnEmptyStaticShell = throwOnEmptyStaticShell
+      // Now, for each `PrerenderedRoute` associated with this node:
+      for (const route of node.routes) {
+        // A route is ok not to throw on an empty static shell (and thus
+        // `throwOnEmptyStaticShell` should be `false`) if either of the
+        // following conditions is met:
+        // 1. `hasChildren` is true: This node has further concrete parameter children.
+        //    This means the current route is a parent to more specific routes (e.g.,
+        //    `/blog/[slug]` should not throw when concrete routes like `/blog/first-post` exist).
+        // OR
+        // 2. `route.fallbackRouteParams.length > minFallbacks`: This route has
+        //    more fallback parameters than another route at the same Trie node.
+        //    This implies the current route is a more general version that should not throw
+        //    compared to a more specific route that has fewer fallback parameters
+        //    (e.g., `/1234/[...slug]` should not throw relative to `/[id]/[...slug]`).
+        if (
+          hasChildren ||
+          (route.fallbackRouteParams &&
+            route.fallbackRouteParams.length > minFallbacks)
+        ) {
+          route.throwOnEmptyStaticShell = false // Should not throw on empty static shell.
+        } else {
+          route.throwOnEmptyStaticShell = true // Should throw on empty static shell.
+        }
+      }
+    }
+
+    // Add all children of the current node to the stack. This ensures that
+    // the traversal continues to explore deeper paths in the Trie.
+    for (const child of node.children.values()) {
+      stack.push(child)
+    }
   }
 }
 
@@ -489,7 +623,7 @@ export async function buildAppStaticPaths({
   if (hadAllParamsGenerated || isRoutePPREnabled) {
     if (isRoutePPREnabled) {
       // Discover all unique combinations of the rootParams so we can generate
-      // shells for each of them if they're available.
+      // routes that won't throw on empty static shell for each of them if they're available.
       routeParams.unshift(
         ...filterUniqueRootParamsCombinations(rootParamKeys, routeParams)
       )