Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
type CacheControl,
} from '../../lib/cache-control'
import { normalizeRepeatedSlashes } from '../../../shared/lib/utils'
import { isDynamicRoute } from '../../../shared/lib/router/utils'
import { getRedirectStatus } from '../../../lib/redirect-status'
import {
CACHE_ONE_YEAR_SECONDS,
Expand Down Expand Up @@ -179,7 +180,11 @@ export const getHandler = ({
}

// ensure /index and / is normalized to one key
cacheKey = cacheKey === '/index' ? '/' : cacheKey
// Only applies for non-dynamic routes — dynamic routes with slug=index
// must keep /index to avoid cache-key collision with the root page.
if (cacheKey === '/index' && !isDynamicRoute(srcPage)) {
cacheKey = '/'
}
}

if (hasStaticPaths && !isDraftMode) {
Expand Down
24 changes: 21 additions & 3 deletions packages/next/src/server/route-modules/route-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1033,9 +1033,10 @@ export abstract class RouteModule<
)
}

if (resolvedPathname === '/index') {
resolvedPathname = '/'
}
resolvedPathname = normalizeResolvedPathname(
resolvedPathname,
normalizedSrcPage
)

if (
res &&
Expand Down Expand Up @@ -1171,3 +1172,20 @@ export abstract class RouteModule<
return cacheEntry
}
}

/**
* Normalizes resolvedPathname for ISR cache key usage.
*
* Only normalizes /index to / when the source page is not a dynamic route.
* For dynamic routes (e.g., [slug] with slug="index"), resolvedPathname
* must remain /index to avoid colliding with the / page's ISR cache key.
*/
export function normalizeResolvedPathname(
resolvedPathname: string,
normalizedSrcPage: string
): string {
if (resolvedPathname === '/index' && !isDynamicRoute(normalizedSrcPage)) {
resolvedPathname = '/'
}
return resolvedPathname
}
28 changes: 28 additions & 0 deletions test/unit/normalize-resolved-pathname.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-env jest */
import { normalizeResolvedPathname } from 'next/dist/server/route-modules/route-module'

describe('normalizeResolvedPathname', () => {
it('should normalize /index to / for static root page', () => {
expect(normalizeResolvedPathname('/index', '/')).toBe('/')
})

it('should NOT normalize /index to / for [slug] dynamic route', () => {
expect(normalizeResolvedPathname('/index', '/[slug]')).toBe('/index')
})

it('should NOT normalize /index to / for catch-all [...slug] route', () => {
expect(normalizeResolvedPathname('/index', '/[...slug]')).toBe('/index')
})

it('should NOT normalize /index to / for optional catch-all [[...slug]] route', () => {
expect(normalizeResolvedPathname('/index', '/[[...slug]]')).toBe('/index')
})

it('should pass through other pathnames unchanged', () => {
expect(normalizeResolvedPathname('/about', '/about')).toBe('/about')
expect(normalizeResolvedPathname('/posts/123', '/posts/[id]')).toBe(
'/posts/123'
)
expect(normalizeResolvedPathname('/', '/')).toBe('/')
})
})