diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 8a598b6cc3737..39a2ae918b9d0 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -109,6 +109,7 @@ import { } from '../client-component-renderer-logger' import { createServerModuleMap } from './action-utils' import { isNodeNextRequest } from '../base-http/helpers' +import { parseParameter } from '../../shared/lib/router/utils/route-regex' export type GetDynamicParamFromSegment = ( // [slug] / [[slug]] / [...slug] @@ -207,6 +208,7 @@ export type CreateSegmentPath = (child: FlightSegmentPath) => FlightSegmentPath */ function makeGetDynamicParamFromSegment( params: { [key: string]: any }, + pagePath: string, flightRouterState: FlightRouterState | undefined ): GetDynamicParamFromSegment { return function getDynamicParamFromSegment( @@ -234,26 +236,46 @@ function makeGetDynamicParamFromSegment( } if (!value) { - // Handle case where optional catchall does not have a value, e.g. `/dashboard/[[...slug]]` when requesting `/dashboard` - if ( - segmentParam.type === 'optional-catchall' || - segmentParam.type === 'catchall' - ) { - // If we weren't able to match the segment to a URL param, and we have a catch-all route, - // provide all of the known params (in array format) to the route - // It should be safe to assume the order of these params is consistent with the order of the segments. - // However, if not, we could re-parse the `pagePath` with `getRouteRegex` and iterate over the positional order. - value = Object.values(params).map((i) => encodeURIComponent(i)) - const hasValues = value.length > 0 - const type = dynamicParamTypes[segmentParam.type] + const isCatchall = segmentParam.type === 'catchall' + const isOptionalCatchall = segmentParam.type === 'optional-catchall' + + if (isCatchall || isOptionalCatchall) { + const dynamicParamType = dynamicParamTypes[segmentParam.type] + // handle the case where an optional catchall does not have a value, + // e.g. `/dashboard/[[...slug]]` when requesting `/dashboard` + if (isOptionalCatchall) { + return { + param: key, + value: null, + type: dynamicParamType, + treeSegment: [key, '', dynamicParamType], + } + } + + // handle the case where a catchall or optional catchall does not have a value, + // e.g. `/foo/bar/hello` and `@slot/[...catchall]` or `@slot/[[...catchall]]` is matched + value = pagePath + .split('/') + // remove the first empty string + .slice(1) + // replace any dynamic params with the actual values + .map((pathSegment) => { + const param = parseParameter(pathSegment) + + // if the segment matches a param, return the param value + // otherwise, it's a static segment, so just return that + return params[param.key] ?? param.key + }) + return { param: key, - value: hasValues ? value : null, - type: type, + value, + type: dynamicParamType, // This value always has to be a string. - treeSegment: [key, hasValues ? value.join('/') : '', type], + treeSegment: [key, value.join('/'), dynamicParamType], } } + return findDynamicParamFromRouterState(flightRouterState, segment) } @@ -795,6 +817,7 @@ async function renderToHTMLOrFlightImpl( const getDynamicParamFromSegment = makeGetDynamicParamFromSegment( params, + pagePath, // `FlightRouterState` is unconditionally provided here because this method uses it // to extract dynamic params as a fallback if they're not present in the path. parsedFlightRouterState diff --git a/packages/next/src/shared/lib/router/utils/route-regex.ts b/packages/next/src/shared/lib/router/utils/route-regex.ts index f6bff51cbfd03..f46c94e112dd0 100644 --- a/packages/next/src/shared/lib/router/utils/route-regex.ts +++ b/packages/next/src/shared/lib/router/utils/route-regex.ts @@ -25,7 +25,7 @@ export interface RouteRegex { * - `[foo]` -> `{ key: 'foo', repeat: false, optional: true }` * - `bar` -> `{ key: 'bar', repeat: false, optional: false }` */ -function parseParameter(param: string) { +export function parseParameter(param: string) { const optional = param.startsWith('[') && param.endsWith(']') if (optional) { param = param.slice(1, -1) diff --git a/test/e2e/app-dir/parallel-routes-breadcrumbs/app/@slot/[...catchAll]/page.tsx b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/@slot/[...catchAll]/page.tsx index f9c359f5c0712..bb230623f6c6a 100644 --- a/test/e2e/app-dir/parallel-routes-breadcrumbs/app/@slot/[...catchAll]/page.tsx +++ b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/@slot/[...catchAll]/page.tsx @@ -1,4 +1,4 @@ -export default function Page({ params: { catchAll } }) { +export default function Page({ params: { catchAll = [] } }) { return (

Parallel Route!

diff --git a/test/e2e/app-dir/parallel-routes-breadcrumbs/app/foo/[lang]/bar/page.tsx b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/foo/[lang]/bar/page.tsx new file mode 100644 index 0000000000000..63c35624b21cd --- /dev/null +++ b/test/e2e/app-dir/parallel-routes-breadcrumbs/app/foo/[lang]/bar/page.tsx @@ -0,0 +1,7 @@ +export default function StaticPage() { + return ( +
+

/foo/[lang]/bar Page!

+
+ ) +} diff --git a/test/e2e/app-dir/parallel-routes-breadcrumbs/parallel-routes-breadcrumbs.test.ts b/test/e2e/app-dir/parallel-routes-breadcrumbs/parallel-routes-breadcrumbs.test.ts index 63722ed214f11..e1e09ab7f1825 100644 --- a/test/e2e/app-dir/parallel-routes-breadcrumbs/parallel-routes-breadcrumbs.test.ts +++ b/test/e2e/app-dir/parallel-routes-breadcrumbs/parallel-routes-breadcrumbs.test.ts @@ -44,4 +44,19 @@ describe('parallel-routes-breadcrumbs', () => { expect(await slot.text()).toContain('Album: album2') expect(await slot.text()).toContain('Track: track3') }) + + it('should render the breadcrumbs correctly with the non-dynamic route segments', async () => { + const browser = await next.browser('/foo/en/bar') + const slot = await browser.waitForElementByCss('#slot') + + expect(await browser.elementByCss('h1').text()).toBe('Parallel Route!') + expect(await browser.elementByCss('h2').text()).toBe( + '/foo/[lang]/bar Page!' + ) + + // verify slot is rendering the params + expect(await slot.text()).toContain('Artist: foo') + expect(await slot.text()).toContain('Album: en') + expect(await slot.text()).toContain('Track: bar') + }) })