Skip to content

Commit 7610ba4

Browse files
Desel72matthewp
andauthored
Fix periods in URLs with trailing slashes causing 404s in dev server (#16154)
* Fix periods in URLs with trailing slashes causing 404s in dev server (#16140) Pages with dots in their filenames (e.g. `hello.world.astro`) were incorrectly treated as file-extension paths, forcing `trailingSlash: 'never'` regardless of user config. Only endpoints with file extensions should force this behavior. * ci: retry flaky e2e tests * ci: retry flaky e2e tests --------- Co-authored-by: Matthew Phillips <matthewphillips@cloudflare.com>
1 parent b5b8093 commit 7610ba4

3 files changed

Lines changed: 81 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Fixes pages with dots in their filenames (e.g. `hello.world.astro`) returning 404 when accessed with a trailing slash in the dev server. The `trailingSlashForPath` function now only forces `trailingSlash: 'never'` for endpoints with file extensions, allowing pages to correctly respect the user's `trailingSlash` config.

packages/astro/src/core/routing/create-manifest.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,11 @@ function createFileBasedRoutes(
241241
const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
242242
? `/${segments.map((segment) => segment[0].content).join('/')}`
243243
: null;
244-
const trailingSlash = trailingSlashForPath(pathname, settings.config);
244+
const trailingSlash = trailingSlashForPath(
245+
pathname,
246+
settings.config,
247+
item.isPage ? 'page' : 'endpoint',
248+
);
245249
const pattern = getPattern(segments, settings.config.base, trailingSlash);
246250
const route = joinSegments(segments);
247251
routes.push({
@@ -382,7 +386,11 @@ function createRoutesFromEntriesByDir(
382386
const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic)
383387
? `/${segments.map((segment) => segment[0].content).join('/')}`
384388
: null;
385-
const trailingSlash = trailingSlashForPath(pathname, settings.config);
389+
const trailingSlash = trailingSlashForPath(
390+
pathname,
391+
settings.config,
392+
item.isPage ? 'page' : 'endpoint',
393+
);
386394
const pattern = getPattern(segments, settings.config.base, trailingSlash);
387395
const route = joinSegments(segments);
388396
routes.push({
@@ -428,11 +436,14 @@ function groupEntriesByDir(entries: RouteEntry[]): Map<string, RouteEntry[]> {
428436
}
429437

430438
// Get trailing slash rule for a path, based on the config and whether the path has an extension.
439+
// Only endpoints with file extensions (like /feed.xml) should force 'never' for trailing slashes.
440+
// Pages with dots in their names (like /hello.world) should respect the user's trailingSlash config.
431441
const trailingSlashForPath = (
432442
pathname: string | null,
433443
config: AstroConfig,
444+
type: 'page' | 'endpoint',
434445
): AstroConfig['trailingSlash'] =>
435-
pathname && hasFileExtension(pathname) ? 'never' : config.trailingSlash;
446+
type === 'endpoint' && pathname && hasFileExtension(pathname) ? 'never' : config.trailingSlash;
436447

437448
function createInjectedRoutes({ settings, cwd }: CreateRouteManifestParams): RouteData[] {
438449
const { config } = settings;
@@ -457,7 +468,7 @@ function createInjectedRoutes({ settings, cwd }: CreateRouteManifestParams): Rou
457468
? `/${segments.map((segment) => segment[0].content).join('/')}`
458469
: null;
459470

460-
const trailingSlash = trailingSlashForPath(pathname, config);
471+
const trailingSlash = trailingSlashForPath(pathname, config, type);
461472
const pattern = getPattern(segments, settings.config.base, trailingSlash);
462473
const params = segments
463474
.flat()

packages/astro/test/units/routing/manifest.test.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,67 @@ describe('routing - createRoutesList', () => {
418418
]);
419419
});
420420

421+
it('pages with dots in filenames respect trailingSlash config. issues#16140', async () => {
422+
const fixture = await createFixture({
423+
'/src/pages/hello.world.astro': `<h1>test</h1>`,
424+
'/src/pages/feed.xml.ts': `export const GET = () => new Response('<xml />')`,
425+
});
426+
427+
// With trailingSlash: 'ignore', page with dot should match both with and without trailing slash
428+
const ignoreSettings = await createBasicSettings({
429+
root: fixture.path,
430+
trailingSlash: 'ignore',
431+
});
432+
const ignoreManifest = await createRoutesList({
433+
cwd: fixture.path,
434+
settings: ignoreSettings,
435+
});
436+
const pageRoute = ignoreManifest.routes.find((r) => r.route === '/hello.world');
437+
assert.ok(pageRoute, 'page route should exist');
438+
assert.equal(
439+
pageRoute.pattern.test('/hello.world'),
440+
true,
441+
'should match without trailing slash',
442+
);
443+
assert.equal(pageRoute.pattern.test('/hello.world/'), true, 'should match with trailing slash');
444+
445+
// Endpoint with file extension should still force 'never'
446+
const endpointRoute = ignoreManifest.routes.find((r) => r.route === '/feed.xml');
447+
assert.ok(endpointRoute, 'endpoint route should exist');
448+
assert.equal(
449+
endpointRoute.pattern.test('/feed.xml'),
450+
true,
451+
'endpoint should match without trailing slash',
452+
);
453+
assert.equal(
454+
endpointRoute.pattern.test('/feed.xml/'),
455+
false,
456+
'endpoint should not match with trailing slash',
457+
);
458+
459+
// With trailingSlash: 'always', page with dot should only match with trailing slash
460+
const alwaysSettings = await createBasicSettings({
461+
root: fixture.path,
462+
trailingSlash: 'always',
463+
});
464+
const alwaysManifest = await createRoutesList({
465+
cwd: fixture.path,
466+
settings: alwaysSettings,
467+
});
468+
const alwaysPageRoute = alwaysManifest.routes.find((r) => r.route === '/hello.world');
469+
assert.ok(alwaysPageRoute, 'page route should exist with trailingSlash always');
470+
assert.equal(
471+
alwaysPageRoute.pattern.test('/hello.world/'),
472+
true,
473+
'should match with trailing slash',
474+
);
475+
assert.equal(
476+
alwaysPageRoute.pattern.test('/hello.world'),
477+
false,
478+
'should not match without trailing slash',
479+
);
480+
});
481+
421482
it('should concatenate each part of the segment. issues#10122', async () => {
422483
const fixture = await createFixture({
423484
'/src/pages/a-[b].astro': `<h1>test</h1>`,

0 commit comments

Comments
 (0)