From 7dd6b24c2a5300a0ead6b2cd5938bebd41863954 Mon Sep 17 00:00:00 2001 From: Uni Date: Tue, 31 Mar 2026 05:10:46 +0200 Subject: [PATCH 1/3] 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. --- .changeset/fix-dotted-page-trailing-slash.md | 5 ++ .../astro/src/core/routing/create-manifest.ts | 19 ++++-- .../astro/test/units/routing/manifest.test.js | 61 +++++++++++++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 .changeset/fix-dotted-page-trailing-slash.md diff --git a/.changeset/fix-dotted-page-trailing-slash.md b/.changeset/fix-dotted-page-trailing-slash.md new file mode 100644 index 000000000000..830813477a3d --- /dev/null +++ b/.changeset/fix-dotted-page-trailing-slash.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +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. diff --git a/packages/astro/src/core/routing/create-manifest.ts b/packages/astro/src/core/routing/create-manifest.ts index a271dbb49a5c..729ad6200b73 100644 --- a/packages/astro/src/core/routing/create-manifest.ts +++ b/packages/astro/src/core/routing/create-manifest.ts @@ -241,7 +241,11 @@ function createFileBasedRoutes( const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) ? `/${segments.map((segment) => segment[0].content).join('/')}` : null; - const trailingSlash = trailingSlashForPath(pathname, settings.config); + const trailingSlash = trailingSlashForPath( + pathname, + settings.config, + item.isPage ? 'page' : 'endpoint', + ); const pattern = getPattern(segments, settings.config.base, trailingSlash); const route = joinSegments(segments); routes.push({ @@ -382,7 +386,11 @@ function createRoutesFromEntriesByDir( const pathname = segments.every((segment) => segment.length === 1 && !segment[0].dynamic) ? `/${segments.map((segment) => segment[0].content).join('/')}` : null; - const trailingSlash = trailingSlashForPath(pathname, settings.config); + const trailingSlash = trailingSlashForPath( + pathname, + settings.config, + item.isPage ? 'page' : 'endpoint', + ); const pattern = getPattern(segments, settings.config.base, trailingSlash); const route = joinSegments(segments); routes.push({ @@ -428,11 +436,14 @@ function groupEntriesByDir(entries: RouteEntry[]): Map { } // Get trailing slash rule for a path, based on the config and whether the path has an extension. +// Only endpoints with file extensions (like /feed.xml) should force 'never' for trailing slashes. +// Pages with dots in their names (like /hello.world) should respect the user's trailingSlash config. const trailingSlashForPath = ( pathname: string | null, config: AstroConfig, + type: 'page' | 'endpoint', ): AstroConfig['trailingSlash'] => - pathname && hasFileExtension(pathname) ? 'never' : config.trailingSlash; + type === 'endpoint' && pathname && hasFileExtension(pathname) ? 'never' : config.trailingSlash; function createInjectedRoutes({ settings, cwd }: CreateRouteManifestParams): RouteData[] { const { config } = settings; @@ -457,7 +468,7 @@ function createInjectedRoutes({ settings, cwd }: CreateRouteManifestParams): Rou ? `/${segments.map((segment) => segment[0].content).join('/')}` : null; - const trailingSlash = trailingSlashForPath(pathname, config); + const trailingSlash = trailingSlashForPath(pathname, config, type); const pattern = getPattern(segments, settings.config.base, trailingSlash); const params = segments .flat() diff --git a/packages/astro/test/units/routing/manifest.test.js b/packages/astro/test/units/routing/manifest.test.js index d26a6dec3f12..e1a7514a9a0b 100644 --- a/packages/astro/test/units/routing/manifest.test.js +++ b/packages/astro/test/units/routing/manifest.test.js @@ -418,6 +418,67 @@ describe('routing - createRoutesList', () => { ]); }); + it('pages with dots in filenames respect trailingSlash config. issues#16140', async () => { + const fixture = await createFixture({ + '/src/pages/hello.world.astro': `

test

`, + '/src/pages/feed.xml.ts': `export const GET = () => new Response('')`, + }); + + // With trailingSlash: 'ignore', page with dot should match both with and without trailing slash + const ignoreSettings = await createBasicSettings({ + root: fixture.path, + trailingSlash: 'ignore', + }); + const ignoreManifest = await createRoutesList({ + cwd: fixture.path, + settings: ignoreSettings, + }); + const pageRoute = ignoreManifest.routes.find((r) => r.route === '/hello.world'); + assert.ok(pageRoute, 'page route should exist'); + assert.equal( + pageRoute.pattern.test('/hello.world'), + true, + 'should match without trailing slash', + ); + assert.equal(pageRoute.pattern.test('/hello.world/'), true, 'should match with trailing slash'); + + // Endpoint with file extension should still force 'never' + const endpointRoute = ignoreManifest.routes.find((r) => r.route === '/feed.xml'); + assert.ok(endpointRoute, 'endpoint route should exist'); + assert.equal( + endpointRoute.pattern.test('/feed.xml'), + true, + 'endpoint should match without trailing slash', + ); + assert.equal( + endpointRoute.pattern.test('/feed.xml/'), + false, + 'endpoint should not match with trailing slash', + ); + + // With trailingSlash: 'always', page with dot should only match with trailing slash + const alwaysSettings = await createBasicSettings({ + root: fixture.path, + trailingSlash: 'always', + }); + const alwaysManifest = await createRoutesList({ + cwd: fixture.path, + settings: alwaysSettings, + }); + const alwaysPageRoute = alwaysManifest.routes.find((r) => r.route === '/hello.world'); + assert.ok(alwaysPageRoute, 'page route should exist with trailingSlash always'); + assert.equal( + alwaysPageRoute.pattern.test('/hello.world/'), + true, + 'should match with trailing slash', + ); + assert.equal( + alwaysPageRoute.pattern.test('/hello.world'), + false, + 'should not match without trailing slash', + ); + }); + it('should concatenate each part of the segment. issues#10122', async () => { const fixture = await createFixture({ '/src/pages/a-[b].astro': `

test

`, From ad01c5ad9f0160fc3412e7d6324b2459b5e4893b Mon Sep 17 00:00:00 2001 From: Uni Date: Tue, 31 Mar 2026 14:05:02 +0200 Subject: [PATCH 2/3] ci: retry flaky e2e tests From fede27f14c7c17925529fcf3c072083f7037d52d Mon Sep 17 00:00:00 2001 From: Uni Date: Tue, 31 Mar 2026 14:05:12 +0200 Subject: [PATCH 3/3] ci: retry flaky e2e tests