diff --git a/.changeset/ready-times-wave.md b/.changeset/ready-times-wave.md new file mode 100644 index 000000000000..677c39589d0b --- /dev/null +++ b/.changeset/ready-times-wave.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes SSR dynamic routes with `.html` extension (e.g. `[slug].html.astro`) not working diff --git a/packages/astro/src/core/app/base.ts b/packages/astro/src/core/app/base.ts index b66acc71b728..0edd9e67df44 100644 --- a/packages/astro/src/core/app/base.ts +++ b/packages/astro/src/core/app/base.ts @@ -35,6 +35,7 @@ import { AstroIntegrationLogger, Logger } from '../logger/core.js'; import { type CreateRenderContext, RenderContext } from '../render-context.js'; import { redirectTemplate } from '../routing/3xx.js'; import { ensure404Route } from '../routing/astro-designed-error-pages.js'; +import { routeHasHtmlExtension } from '../routing/helpers.js'; import { matchRoute } from '../routing/match.js'; import { type CacheLike, applyCacheHeaders } from '../cache/runtime/cache.js'; import { Router } from '../routing/router.js'; @@ -471,8 +472,8 @@ export abstract class BaseApp
{
}
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()) {
+ // Skip normalization if the route already has an .html extension in its definition.
+ if (this.isDev() && !routeHasHtmlExtension(routeData)) {
pathname = pathname.replace(/\/index\.html$/, '/').replace(/\.html$/, '');
}
const defaultStatus = this.getDefaultStatusCode(routeData, pathname);
diff --git a/packages/astro/src/core/render/params-and-props.ts b/packages/astro/src/core/render/params-and-props.ts
index 4affaf38e874..1c43a15d0162 100644
--- a/packages/astro/src/core/render/params-and-props.ts
+++ b/packages/astro/src/core/render/params-and-props.ts
@@ -5,7 +5,7 @@ import type { RouteData } from '../../types/public/internal.js';
import { DEFAULT_404_COMPONENT } from '../constants.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import type { Logger } from '../logger/core.js';
-import { routeIsFallback, routeIsRedirect } from '../routing/helpers.js';
+import { routeHasHtmlExtension, routeIsFallback, routeIsRedirect } from '../routing/helpers.js';
import type { RouteCache } from './route-cache.js';
import { callGetStaticPaths, findPathItemByKey } from './route-cache.js';
@@ -87,16 +87,15 @@ export function getParams(route: RouteData, pathname: string): Params {
if (!route.params.length) return {};
// 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);
- }
+ // Strip `.html` from the pathname unless `.html` is a static part of the route definition
+ // itself (e.g. `[slug].html.astro`). Dynamic params like `[id]` would otherwise greedily
+ // capture the `.html` suffix (e.g. `id = '42.html'` instead of `id = '42'`).
+ const path =
+ pathname.endsWith('.html') && !routeHasHtmlExtension(route) ? pathname.slice(0, -5) : pathname;
+
+ const allPatterns = [route, ...route.fallbackRoutes].map((r) => r.pattern);
+ const paramsMatch = allPatterns.map((pattern) => 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) return {};
const params: Params = {};
route.params.forEach((key, i) => {
diff --git a/packages/astro/src/core/routing/helpers.ts b/packages/astro/src/core/routing/helpers.ts
index 64e9975ac691..291daad45fa5 100644
--- a/packages/astro/src/core/routing/helpers.ts
+++ b/packages/astro/src/core/routing/helpers.ts
@@ -64,6 +64,17 @@ export function getCustom500Route(manifestData: RoutesList): RouteData | undefin
return manifestData.routes.find((r) => isRoute500(r.route));
}
+/**
+ * Returns true if the route definition contains `.html` as a static segment part,
+ * as is the case for routes like `[slug].html.astro`. Used to avoid stripping the
+ * `.html` suffix from pathnames that intentionally include it.
+ */
+export function routeHasHtmlExtension(route: RouteData): boolean {
+ return route.segments.some((segment) =>
+ segment.some((part) => !part.dynamic && part.content.includes('.html')),
+ );
+}
+
export function hasNonPrerenderedProjectRoute(
routes: Array