Skip to content

Commit 047e641

Browse files
authored
[segment cache]: fix trailingSlash handling with output: export (#84465)
When `output: export`, `experimental.clientSegmentCache`, and `trailingSlash: true` were all enabled, clicking links would update the URL but fail to render the page content. The issue was in how segment cache URLs were being constructed for static export mode. When a URL had a trailing slash (like /another/), the code tried to strip it using `substring(0, -1)`. However, substring() treats negative indices as 0, so this became `substring(0, 0)` and returned an empty string. This meant URLs like /another/ would generate segment requests to `/__next._tree.txt` instead of `/another/__next._tree.txt`. I also updated the brittle snapshot-like tests that were in here as well since we output more files with segment cache. I don't think this type of test adds that much value but didn't want to change any test behavior.
1 parent 90e580b commit 047e641

File tree

3 files changed

+207
-89
lines changed

3 files changed

+207
-89
lines changed

packages/next/src/client/components/segment-cache-impl/cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2203,7 +2203,7 @@ function addSegmentPathToUrlInOutputExportMode(
22032203
// path. Instead, we append it to the end of the pathname.
22042204
const staticUrl = new URL(url)
22052205
const routeDir = staticUrl.pathname.endsWith('/')
2206-
? staticUrl.pathname.substring(0, -1)
2206+
? staticUrl.pathname.slice(0, -1)
22072207
: staticUrl.pathname
22082208
const staticExportFilename =
22092209
convertSegmentPathToStaticExportFilename(segmentPath)

test/client-segment-cache-tests-manifest.json

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -87,30 +87,6 @@
8787
"app dir - form prefetching should prefetch a loading state for the form's target"
8888
]
8989
},
90-
"test/integration/app-dir-export/test/config.test.ts": {
91-
"failed": [
92-
"app dir - with output export (next dev / next build) production mode should correctly emit exported assets to config.distDir"
93-
]
94-
},
95-
"test/integration/app-dir-export/test/dynamicapiroute-prod.test.ts": {
96-
"failed": [
97-
"app dir - with output export - dynamic api route prod production mode should work in prod with dynamicApiRoute 'error'",
98-
"app dir - with output export - dynamic api route prod production mode should work in prod with dynamicApiRoute 'force-static'"
99-
]
100-
},
101-
"test/integration/app-dir-export/test/dynamicpage-prod.test.ts": {
102-
"failed": [
103-
"app dir - with output export - dynamic api route prod production mode should work in prod with dynamicPage 'error'",
104-
"app dir - with output export - dynamic api route prod production mode should work in prod with dynamicPage 'force-static'",
105-
"app dir - with output export - dynamic api route prod production mode should work in prod with dynamicPage undefined"
106-
]
107-
},
108-
"test/integration/app-dir-export/test/trailing-slash-start.test.ts": {
109-
"failed": [
110-
"app dir - with output export - trailing slash prod production mode should work in prod with trailingSlash 'false'",
111-
"app dir - with output export - trailing slash prod production mode should work in prod with trailingSlash 'true'"
112-
]
113-
},
11490
"test/production/app-dir/build-output-prerender/build-output-prerender.test.ts": {
11591
"failed": [
11692
"build-output-prerender with a next config file with --debug-prerender prints a warning and the customized experimental flags",

test/integration/app-dir-export/test/utils.ts

Lines changed: 206 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -28,71 +28,213 @@ export const nextConfig = new File(join(appDir, 'next.config.js'))
2828
const slugPage = new File(join(appDir, 'app/another/[slug]/page.js'))
2929
const apiJson = new File(join(appDir, 'app/api/json/route.js'))
3030

31-
export const expectedWhenTrailingSlashTrue = [
32-
'404.html',
33-
'404/index.html',
34-
// Turbopack and plain next.js have different hash output for the file name
35-
// Turbopack will output favicon in the _next/static/media folder
36-
...(process.env.IS_TURBOPACK_TEST
37-
? [expect.stringMatching(/_next\/static\/media\/favicon\.[0-9a-f]+\.ico/)]
38-
: []),
39-
expect.stringMatching(/_next\/static\/media\/test\.[0-9a-f]+\.png/),
40-
'_next/static/test-build-id/_buildManifest.js',
41-
...(process.env.IS_TURBOPACK_TEST
42-
? ['_next/static/test-build-id/_clientMiddlewareManifest.json']
43-
: []),
44-
'_next/static/test-build-id/_ssgManifest.js',
45-
'_not-found/index.html',
46-
'_not-found/index.txt',
47-
'another/first/index.html',
48-
'another/first/index.txt',
49-
'another/index.html',
50-
'another/index.txt',
51-
'another/second/index.html',
52-
'another/second/index.txt',
53-
'api/json',
54-
'api/txt',
55-
'client/index.html',
56-
'client/index.txt',
57-
'favicon.ico',
58-
'image-import/index.html',
59-
'image-import/index.txt',
60-
'index.html',
61-
'index.txt',
62-
'robots.txt',
63-
]
31+
export const expectedWhenTrailingSlashTrue = process.env
32+
.__NEXT_EXPERIMENTAL_CLIENT_SEGMENT_CACHE
33+
? [
34+
'404.html',
35+
'404/index.html',
36+
'__next.__PAGE__.txt',
37+
'__next._index.txt',
38+
'__next._tree.txt',
39+
// Turbopack and plain next.js have different hash output for the file name
40+
// Turbopack will output favicon in the _next/static/media folder
41+
...(process.env.IS_TURBOPACK_TEST
42+
? [
43+
expect.stringMatching(
44+
/_next\/static\/media\/favicon\.[0-9a-f]+\.ico/
45+
),
46+
]
47+
: []),
48+
expect.stringMatching(/_next\/static\/media\/test\.[0-9a-f]+\.png/),
49+
'_next/static/test-build-id/_buildManifest.js',
50+
...(process.env.IS_TURBOPACK_TEST
51+
? ['_next/static/test-build-id/_clientMiddlewareManifest.json']
52+
: []),
53+
'_next/static/test-build-id/_ssgManifest.js',
54+
'_not-found/__next._index.txt',
55+
'_not-found/__next._not-found.__PAGE__.txt',
56+
'_not-found/__next._not-found.txt',
57+
'_not-found/__next._tree.txt',
58+
'_not-found/index.html',
59+
'_not-found/index.txt',
60+
'another/__next._index.txt',
61+
'another/__next._tree.txt',
62+
'another/__next.another.__PAGE__.txt',
63+
'another/__next.another.txt',
64+
'another/first/__next._index.txt',
65+
'another/first/__next._tree.txt',
66+
'another/first/__next.another.$d$slug.__PAGE__.txt',
67+
'another/first/__next.another.$d$slug.txt',
68+
'another/first/__next.another.txt',
69+
'another/first/index.html',
70+
'another/first/index.txt',
71+
'another/index.html',
72+
'another/index.txt',
73+
'another/second/__next._index.txt',
74+
'another/second/__next._tree.txt',
75+
'another/second/__next.another.$d$slug.__PAGE__.txt',
76+
'another/second/__next.another.$d$slug.txt',
77+
'another/second/__next.another.txt',
78+
'another/second/index.html',
79+
'another/second/index.txt',
80+
'api/json',
81+
'api/txt',
82+
'client/__next._index.txt',
83+
'client/__next._tree.txt',
84+
'client/__next.client.__PAGE__.txt',
85+
'client/__next.client.txt',
86+
'client/index.html',
87+
'client/index.txt',
88+
'favicon.ico',
89+
'image-import/__next._index.txt',
90+
'image-import/__next._tree.txt',
91+
'image-import/__next.image-import.__PAGE__.txt',
92+
'image-import/__next.image-import.txt',
93+
'image-import/index.html',
94+
'image-import/index.txt',
95+
'index.html',
96+
'index.txt',
97+
'robots.txt',
98+
]
99+
: [
100+
'404.html',
101+
'404/index.html',
102+
// Turbopack and plain next.js have different hash output for the file name
103+
// Turbopack will output favicon in the _next/static/media folder
104+
...(process.env.IS_TURBOPACK_TEST
105+
? [
106+
expect.stringMatching(
107+
/_next\/static\/media\/favicon\.[0-9a-f]+\.ico/
108+
),
109+
]
110+
: []),
111+
expect.stringMatching(/_next\/static\/media\/test\.[0-9a-f]+\.png/),
112+
'_next/static/test-build-id/_buildManifest.js',
113+
...(process.env.IS_TURBOPACK_TEST
114+
? ['_next/static/test-build-id/_clientMiddlewareManifest.json']
115+
: []),
116+
'_next/static/test-build-id/_ssgManifest.js',
117+
'_not-found/index.html',
118+
'_not-found/index.txt',
119+
'another/first/index.html',
120+
'another/first/index.txt',
121+
'another/index.html',
122+
'another/index.txt',
123+
'another/second/index.html',
124+
'another/second/index.txt',
125+
'api/json',
126+
'api/txt',
127+
'client/index.html',
128+
'client/index.txt',
129+
'favicon.ico',
130+
'image-import/index.html',
131+
'image-import/index.txt',
132+
'index.html',
133+
'index.txt',
134+
'robots.txt',
135+
]
64136

65-
const expectedWhenTrailingSlashFalse = [
66-
'404.html',
67-
// Turbopack will output favicon in the _next/static/media folder
68-
...(process.env.IS_TURBOPACK_TEST
69-
? [expect.stringMatching(/_next\/static\/media\/favicon\.[0-9a-f]+\.ico/)]
70-
: []),
71-
expect.stringMatching(/_next\/static\/media\/test\.[0-9a-f]+\.png/),
72-
'_next/static/test-build-id/_buildManifest.js',
73-
...(process.env.IS_TURBOPACK_TEST
74-
? ['_next/static/test-build-id/_clientMiddlewareManifest.json']
75-
: []),
76-
'_next/static/test-build-id/_ssgManifest.js',
77-
'_not-found.html',
78-
'_not-found.txt',
79-
'another.html',
80-
'another.txt',
81-
'another/first.html',
82-
'another/first.txt',
83-
'another/second.html',
84-
'another/second.txt',
85-
'api/json',
86-
'api/txt',
87-
'client.html',
88-
'client.txt',
89-
'favicon.ico',
90-
'image-import.html',
91-
'image-import.txt',
92-
'index.html',
93-
'index.txt',
94-
'robots.txt',
95-
]
137+
const expectedWhenTrailingSlashFalse = process.env
138+
.__NEXT_EXPERIMENTAL_CLIENT_SEGMENT_CACHE
139+
? [
140+
'404.html',
141+
'__next.__PAGE__.txt',
142+
'__next._index.txt',
143+
'__next._tree.txt',
144+
// Turbopack will output favicon in the _next/static/media folder
145+
...(process.env.IS_TURBOPACK_TEST
146+
? [
147+
expect.stringMatching(
148+
/_next\/static\/media\/favicon\.[0-9a-f]+\.ico/
149+
),
150+
]
151+
: []),
152+
expect.stringMatching(/_next\/static\/media\/test\.[0-9a-f]+\.png/),
153+
'_next/static/test-build-id/_buildManifest.js',
154+
...(process.env.IS_TURBOPACK_TEST
155+
? ['_next/static/test-build-id/_clientMiddlewareManifest.json']
156+
: []),
157+
'_next/static/test-build-id/_ssgManifest.js',
158+
'_not-found.html',
159+
'_not-found.txt',
160+
'_not-found/__next._index.txt',
161+
'_not-found/__next._not-found.__PAGE__.txt',
162+
'_not-found/__next._not-found.txt',
163+
'_not-found/__next._tree.txt',
164+
'another.html',
165+
'another.txt',
166+
'another/__next._index.txt',
167+
'another/__next._tree.txt',
168+
'another/__next.another.__PAGE__.txt',
169+
'another/__next.another.txt',
170+
'another/first.html',
171+
'another/first.txt',
172+
'another/first/__next._index.txt',
173+
'another/first/__next._tree.txt',
174+
'another/first/__next.another.$d$slug.__PAGE__.txt',
175+
'another/first/__next.another.$d$slug.txt',
176+
'another/first/__next.another.txt',
177+
'another/second.html',
178+
'another/second.txt',
179+
'another/second/__next._index.txt',
180+
'another/second/__next._tree.txt',
181+
'another/second/__next.another.$d$slug.__PAGE__.txt',
182+
'another/second/__next.another.$d$slug.txt',
183+
'another/second/__next.another.txt',
184+
'api/json',
185+
'api/txt',
186+
'client.html',
187+
'client.txt',
188+
'client/__next._index.txt',
189+
'client/__next._tree.txt',
190+
'client/__next.client.__PAGE__.txt',
191+
'client/__next.client.txt',
192+
'favicon.ico',
193+
'image-import.html',
194+
'image-import.txt',
195+
'image-import/__next._index.txt',
196+
'image-import/__next._tree.txt',
197+
'image-import/__next.image-import.__PAGE__.txt',
198+
'image-import/__next.image-import.txt',
199+
'index.html',
200+
'index.txt',
201+
'robots.txt',
202+
]
203+
: [
204+
'404.html',
205+
// Turbopack will output favicon in the _next/static/media folder
206+
...(process.env.IS_TURBOPACK_TEST
207+
? [
208+
expect.stringMatching(
209+
/_next\/static\/media\/favicon\.[0-9a-f]+\.ico/
210+
),
211+
]
212+
: []),
213+
expect.stringMatching(/_next\/static\/media\/test\.[0-9a-f]+\.png/),
214+
'_next/static/test-build-id/_buildManifest.js',
215+
...(process.env.IS_TURBOPACK_TEST
216+
? ['_next/static/test-build-id/_clientMiddlewareManifest.json']
217+
: []),
218+
'_next/static/test-build-id/_ssgManifest.js',
219+
'_not-found.html',
220+
'_not-found.txt',
221+
'another.html',
222+
'another.txt',
223+
'another/first.html',
224+
'another/first.txt',
225+
'another/second.html',
226+
'another/second.txt',
227+
'api/json',
228+
'api/txt',
229+
'client.html',
230+
'client.txt',
231+
'favicon.ico',
232+
'image-import.html',
233+
'image-import.txt',
234+
'index.html',
235+
'index.txt',
236+
'robots.txt',
237+
]
96238

97239
export async function getFiles(cwd = exportDir) {
98240
const opts = { cwd, nodir: true }

0 commit comments

Comments
 (0)