Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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/ready-times-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': major
---

allow dynamic parameters in .html.astro routes
26 changes: 21 additions & 5 deletions packages/astro/src/core/app/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,11 +470,27 @@ export abstract class BaseApp<P extends Pipeline = AppPipeline> {
});
}
let pathname = this.getPathnameFromRequest(request);
// In dev, the route may have matched a normalized pathname (after .html stripping).
// Apply the same normalization for correct param extraction.
if (this.isDev()) {
pathname = pathname.replace(/\/index\.html$/, '/').replace(/\.html$/, '');
}
// In dev, normalized paths (like stripping .html) are often used for matching.
// However, if the route explicitly matches the .html path (e.g., [slug].html.astro),
// we skip normalization to ensure correct param extraction.
if (this.isDev()) {
if (pathname.endsWith('.html')) {
const isIndexHtml = /\/index\.html$/.test(pathname);
const isExactMatch = routeData && routeData.pattern.test(pathname);

// Normalize only if it's index.html or if the current route doesn't
// explicitly support the .html extension.
if (isIndexHtml || !isExactMatch) {
pathname = pathname.replace(/\/index\.html$/, '/').replace(/\.html$/, '');

// Re-match to ensure routeData aligns with the newly normalized pathname.
const result = await this.devMatch(pathname);
if (result) {
routeData = result.routeData;
}
}
}
}
const defaultStatus = this.getDefaultStatusCode(routeData, pathname);

let response;
Expand Down
20 changes: 12 additions & 8 deletions packages/astro/src/core/render/params-and-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,19 @@ export function getParams(route: RouteData, pathname: string): Params {
// The RegExp pattern expects a decoded string, but the pathname is encoded
// when the URL contains non-English characters.
let path = pathname;
// The path could contain `.html` at the end. We remove it so we can correctly the parameters
// with the generated keyed parameters.
if (pathname.endsWith('.html')) {
path = path.slice(0, -5);
}
// The path could contain `.html` at the end. We remove it for standard routes
// to match parameters correctly, but we keep it if the route explicitly
// targets `.html` files (e.g. [slug].html.astro) to ensure correct extraction.
let paramsMatch =
route.pattern.exec(path) ||
route.fallbackRoutes.map((fallbackRoute) => fallbackRoute.pattern.exec(path)).find((x) => x);

const paramsMatch =
route.pattern.exec(path) ||
route.fallbackRoutes.map((fallbackRoute) => fallbackRoute.pattern.exec(path)).find((x) => x);
if (!paramsMatch && pathname.endsWith('.html')) {
path = pathname.slice(0, -5);
paramsMatch =
route.pattern.exec(path) ||
route.fallbackRoutes.map((fallbackRoute) => fallbackRoute.pattern.exec(path)).find((x) => x);
}
if (!paramsMatch) return {};
const params: Params = {};
route.params.forEach((key, i) => {
Expand Down
7 changes: 7 additions & 0 deletions packages/astro/test/fixtures/ssr-html-route/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
});
9 changes: 9 additions & 0 deletions packages/astro/test/fixtures/ssr-html-route/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "ssr-html-route",
"type": "module",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
const { slug } = Astro.params;
---
<html>
<head><title>Dynamic HTML Route</title></head>
<body>
<div>Slug: {slug}</div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
---
<div>Home</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
// URL: /standard.html
---
<html>
<head><title>Standard Route</title></head>
<body>
<div>standard</div>
</body>
</html>
40 changes: 40 additions & 0 deletions packages/astro/test/ssr-html-route.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import assert from 'node:assert/strict';
import { before, after, describe, it } from 'node:test';
import { loadFixture } from './test-utils.js';

describe('SSR: [slug].html.astro routing', () => {
Comment thread
Princesseuh marked this conversation as resolved.
Outdated
let fixture;
let devServer;

before(async () => {
fixture = await loadFixture({
root: './fixtures/ssr-html-route/',
});
devServer = await fixture.startDevServer();
});

after(async () => {
if (devServer) await devServer.stop();
});

it('extracts params correctly from [slug].html.astro', async () => {
const response = await fixture.fetch('/dummy.html');
assert.equal(response.status, 200);
const html = await response.text();
assert.match(html, /Slug: dummy/);
});

it('should still work for standard [slug].astro by stripping .html from URL', async () => {
const response = await fixture.fetch('/standard.html');
assert.equal(response.status, 200);
const html = await response.text();
assert.match(html, /standard/);
});

it('should normalize index.html to /', async () => {
const response = await fixture.fetch('/index.html');
assert.equal(response.status, 200);
const html = await response.text();
assert.match(html, /Home/);
});
});
Loading