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
5 changes: 5 additions & 0 deletions .changeset/fix-dotted-page-trailing-slash.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 15 additions & 4 deletions packages/astro/src/core/routing/create-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -428,11 +436,14 @@ function groupEntriesByDir(entries: RouteEntry[]): Map<string, RouteEntry[]> {
}

// 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;
Expand All @@ -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()
Expand Down
61 changes: 61 additions & 0 deletions packages/astro/test/units/routing/manifest.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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': `<h1>test</h1>`,
'/src/pages/feed.xml.ts': `export const GET = () => new Response('<xml />')`,
});

// 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': `<h1>test</h1>`,
Expand Down
Loading