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
10 changes: 6 additions & 4 deletions packages/next/src/build/segment-config/app/app-segments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,12 @@ async function collectAppPageSegments(routeModule: AppPageRouteModule) {

// If this is a page segment, we've reached a leaf node
if (name === PAGE_SEGMENT_KEY) {
// Add all segments in the current path
// Add all segments in the current path, preferring non-parallel segments
updatedSegments.forEach((seg) => {
const key = getSegmentKey(seg)
uniqueSegments.set(key, seg)
if (!uniqueSegments.has(key)) {
uniqueSegments.set(key, seg)
}
})
}

Expand All @@ -152,7 +154,7 @@ async function collectAppPageSegments(routeModule: AppPageRouteModule) {
}

function getSegmentKey(segment: AppSegment) {
return `${segment.name}-${segment.filePath ?? ''}-${segment.paramName ?? ''}`
return `${segment.name}-${segment.filePath ?? ''}-${segment.paramName ?? ''}-${segment.isParallelRouteSegment ? 'pr' : 'np'}`
}

/**
Expand Down Expand Up @@ -244,7 +246,7 @@ export function collectFallbackRouteParams(
// Handle this segment (if it's a dynamic segment param).
const segmentParam = getSegmentParam(name)
if (segmentParam) {
const key = `${name}-${segmentParam.param}`
const key = `${name}-${segmentParam.param}-${isParallelRouteSegment ? 'pr' : 'np'}`
if (!uniqueSegments.has(key)) {
uniqueSegments.set(
key,
Expand Down
17 changes: 17 additions & 0 deletions packages/next/src/build/static-paths/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,12 +793,29 @@ export async function buildAppStaticPaths({
// we're emitting the route for the base route.
const parallelFallbackRouteParams: FallbackRouteParam[] = []

// First pass: collect all non-parallel route param names.
// This allows us to filter out parallel route params that duplicate non-parallel ones.
const nonParallelParamNames = new Set<string>()
for (const segment of segments) {
if (!segment.paramName || !segment.paramType) continue
if (!segment.isParallelRouteSegment) {
nonParallelParamNames.add(segment.paramName)
}
}

// Second pass: collect segments, ensuring non-parallel route params take precedence.
for (const segment of segments) {
// If this segment doesn't have a param name then it's not param that we
// need to resolve.
if (!segment.paramName || !segment.paramType) continue

if (segment.isParallelRouteSegment) {
// Skip parallel route params that are already defined as non-parallel route params.
// Non-parallel route params take precedence because they appear in the URL pathname.
if (nonParallelParamNames.has(segment.paramName)) {
continue
}

// Collect all the parallel route segments that have dynamic params for
// second-pass resolution.
parallelRouteSegments.push({
Expand Down
6 changes: 5 additions & 1 deletion packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2015,7 +2015,11 @@ export default abstract class Server<
if (
!this.minimalMode &&
this.nextConfig.experimental.validateRSCRequestHeaders &&
isRSCRequest
isRSCRequest &&
// In the event that we're serving a NoFallbackError, the headers will
// already be stripped so this comparison will always fail, resulting in
// a redirect loop.
!is404Page
) {
const headers = req.headers

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function BreadcrumbsDefault() {
return <div id="breadcrumbs-default">Breadcrumbs: (default)</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default async function BreadcrumbsStoryPage({
params,
}: {
params: Promise<{ locale: string; slug: string }>
}) {
const { locale, slug } = await params
return (
<div id="breadcrumbs-story">
<div id="breadcrumbs-locale">Breadcrumbs Locale: {locale}</div>
<div id="breadcrumbs-slug">Breadcrumbs Story: {slug}</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { ReactNode } from 'react'

export default function RootLayout({
children,
breadcrumbs,
}: {
children: ReactNode
breadcrumbs: ReactNode
params: Promise<{ locale: string }>
}) {
return (
<>
<div id="breadcrumbs">{breadcrumbs}</div>
<main id="main">{children}</main>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export default async function StoryPage({
params,
}: {
params: Promise<{ locale: string; slug: string }>
}) {
const { locale, slug } = await params
return (
<div id="story-page">
<div id="story-locale">Locale: {locale}</div>
<div id="story-slug">Story: {slug}</div>
</div>
)
}

export async function generateStaticParams() {
return [{ slug: 'static-123' }]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
'use client'

import Link from 'next/link'
import { useState } from 'react'

type RouteInfo = {
href: string
status: '200' | '404'
}

type RouteSection = {
title: string
columns: string[]
routes: RouteInfo[]
}

const ROUTE_SECTIONS: RouteSection[] = [
{
title: 'Base Routes',
columns: ['Route', 'Expected Status'],
routes: [
{ href: '/en', status: '200' },
{ href: '/fr', status: '200' },
{ href: '/es', status: '404' },
],
},
{
title: 'Without generateStaticParams',
columns: ['Route', 'Expected Status'],
routes: [
{ href: '/en/no-gsp/stories/dynamic-123', status: '200' },
{ href: '/fr/no-gsp/stories/dynamic-123', status: '200' },
{ href: '/es/no-gsp/stories/dynamic-123', status: '200' },
],
},
{
title: 'With generateStaticParams',
columns: ['Route', 'Expected Status'],
routes: [
{
href: '/en/gsp/stories/static-123',
status: '200',
},
{
href: '/fr/gsp/stories/static-123',
status: '200',
},
{
href: '/es/gsp/stories/static-123',
status: '404',
},
{
href: '/en/gsp/stories/dynamic-123',
status: '404',
},
{
href: '/fr/gsp/stories/dynamic-123',
status: '404',
},
{
href: '/es/gsp/stories/dynamic-123',
status: '404',
},
],
},
]

function NavLink({ href, status }: { href: string; status: '200' | '404' }) {
return (
<Link
href={href}
className={`font-mono text-xs hover:underline ${status === '200' ? 'text-green-700' : 'text-red-700'}`}
>
{href}
</Link>
)
}

function StatusBadge({ status }: { status: '200' | '404' }) {
return (
<span
className={`inline-block px-1 py-0 rounded text-xs ${status === '200' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}
>
{status}
</span>
)
}

function RouteTable({ section }: { section: RouteSection }) {
return (
<div>
<h2 className="text-sm font-bold mb-1">{section.title}</h2>
<table className="w-full border-collapse border border-gray-300 text-xs">
<thead>
<tr className="bg-gray-100">
{section.columns.map((column) => (
<th
key={column}
className="border border-gray-300 px-2 py-0.5 text-left w-1/2"
>
{column}
</th>
))}
</tr>
</thead>
<tbody>
{section.routes.map((route) => (
<tr key={route.href}>
<td className="border border-gray-300 px-2 py-0.5">
<NavLink href={route.href} status={route.status} />
</td>
<td className="border border-gray-300 px-2 py-0.5">
<StatusBadge status={route.status} />
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}

export default function Navigation() {
const [revealed, setRevealed] = useState(false)

return (
<>
<input
id="reveal"
type="checkbox"
checked={revealed}
onChange={() => setRevealed(!revealed)}
/>
{revealed && (
<nav id="nav" className="space-y-2">
{ROUTE_SECTIONS.map((section) => (
<RouteTable key={section.title} section={section} />
))}
</nav>
)}
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { ReactNode } from 'react'
import Navigation from './layout.client'

export function generateStaticParams() {
return [{ locale: 'en' }, { locale: 'fr' }]
}

export const dynamicParams = false

export default function RootLayout({
children,
}: {
children: ReactNode
params: Promise<{ locale: string }>
}) {
return (
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body className="p-2 flex flex-col gap-2 text-sm">
<Navigation />
{children}
</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function BreadcrumbsDefault() {
return <div id="breadcrumbs-default">Breadcrumbs: (default)</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default async function BreadcrumbsStoryPage({
params,
}: {
params: Promise<{ locale: string; slug: string }>
}) {
const { locale, slug } = await params
return (
<div id="breadcrumbs-story">
<div id="breadcrumbs-locale">Breadcrumbs Locale: {locale}</div>
<div id="breadcrumbs-slug">Breadcrumbs Story: {slug}</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { ReactNode } from 'react'

export default function RootLayout({
children,
breadcrumbs,
}: {
children: ReactNode
breadcrumbs: ReactNode
params: Promise<{ locale: string }>
}) {
return (
<>
<div id="breadcrumbs">{breadcrumbs}</div>
<main id="main">{children}</main>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default async function StoryPage({
params,
}: {
params: Promise<{ locale: string; slug: string }>
}) {
const { locale, slug } = await params
return (
<div id="story-page">
<div id="story-locale">Locale: {locale}</div>
<div id="story-slug">Story: {slug}</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default async function LocalePage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
return <div id="locale-page">Locale: {locale}</div>
}
Loading
Loading