diff --git a/.changeset/witty-wasps-tie.md b/.changeset/witty-wasps-tie.md new file mode 100644 index 000000000000..ddb7c987f715 --- /dev/null +++ b/.changeset/witty-wasps-tie.md @@ -0,0 +1,13 @@ +--- +'astro': patch +--- + +Improved the error message when a redirect can't be mapped to its destination. For example, the following redirect +will throw a new error because `/category/[categories]/` contains one dynamic segment, while `/category/[categories]/[page]` has two dynamic segments, +and Astro doesn't know how map the parameters: + +```js +export default defineConfig({ + redirects: { "/category/[categories]": "/category/[categories]/[page]" } +}) +``` diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index e8348355da46..eaf4937da454 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -981,6 +981,25 @@ export const UnsupportedExternalRedirect = { hint: 'An external redirect must start with http or https, and must be a valid URL.', } satisfies ErrorData; +/** + * @docs + * @see + * - [Astro.redirect](https://docs.astro.build/en/reference/api-reference/#redirect) + * @description + * A redirect can't be mapped when origin and destination have different segments. + */ +export const RedirectMappingMismatch = { + name: 'RedirectMappingMismatch', + title: "A redirect can't be mapped when origin and destination have different segments", + message: ( + fromRoute: string, + toRoute: string, + fromDynamicSegments: number, + toDynamicSegments: number, + ) => + `The number of dynamic segments don't match. The route ${fromRoute} has ${fromDynamicSegments} segments, while ${toRoute} has ${toDynamicSegments} segments.`, +} satisfies ErrorData; + /** * @docs * @see diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index ad093f04b73c..8ed6120df1b9 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -14,6 +14,7 @@ import type { RouteData, RoutePart } from '../../../types/public/internal.js'; import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from '../../constants.js'; import { MissingIndexForInternationalization, + RedirectMappingMismatch, UnsupportedExternalRedirect, } from '../../errors/errors-data.js'; import { AstroError } from '../../errors/index.js'; @@ -364,6 +365,12 @@ function createRedirectRoutes( }); } + const redirectRoute = routeMap.get(destination); + + if (redirectRoute) { + validateRouteMapping(segments, from, redirectRoute.segments ?? [], redirectRoute.route ?? ''); + } + routes.push({ type: 'redirect', // For backwards compatibility, a redirect is never considered an index route. @@ -377,7 +384,7 @@ function createRedirectRoutes( pathname: pathname || void 0, prerender: getPrerenderDefault(config), redirect: to, - redirectRoute: routeMap.get(destination), + redirectRoute, fallbackRoutes: [], distURL: [], origin: 'project', @@ -387,6 +394,54 @@ function createRedirectRoutes( return routes; } +/** + * Checks whether `a` can be mapped to `b`. If the requirements don't match, + * an error is thrown. + * @param fromSegments + * @param fromRoute + * @param toSegments + * @param toRoute + */ +function validateRouteMapping( + fromSegments: RoutePart[][], + fromRoute: string, + toSegments: RoutePart[][], + toRoute: string, +) { + const fromHasSpread = fromSegments.some((routePart) => routePart.some((part) => part.spread)); + const toHasSpread = toSegments.some((routePart) => routePart.some((part) => part.spread)); + + if (fromHasSpread || toHasSpread) { + return; + } + + const fromDynamicSegments = fromSegments.reduce((dynamicSegments, routePart) => { + if (routePart.some((part) => part.dynamic || part.spread)) { + dynamicSegments += 1; + } + return dynamicSegments; + }, 0); + + const toDynamicSegments = toSegments.reduce((dynamicSegments, routePart) => { + if (routePart.some((part) => part.dynamic || part.spread)) { + dynamicSegments += 1; + } + return dynamicSegments; + }, 0); + + if (fromDynamicSegments != toDynamicSegments) { + throw new AstroError({ + ...RedirectMappingMismatch, + message: RedirectMappingMismatch.message( + fromRoute, + toRoute, + fromDynamicSegments, + toDynamicSegments, + ), + }); + } +} + /** * Checks whether a route segment is static. */ diff --git a/packages/astro/test/fixtures/redirects/src/pages/old/[category]/[slug].astro b/packages/astro/test/fixtures/redirects/src/pages/old/[category]/[slug].astro new file mode 100644 index 000000000000..f9162ed675b4 --- /dev/null +++ b/packages/astro/test/fixtures/redirects/src/pages/old/[category]/[slug].astro @@ -0,0 +1,12 @@ +--- + +export function getStaticPaths() { + return [{ params: { category: 'test', slug: 'test' } }] +} + +const { slug, category } = Astro.params +--- + +
+ {slug} and {category} +
diff --git a/packages/astro/test/fixtures/redirects/src/pages/old/[category]/index.astro b/packages/astro/test/fixtures/redirects/src/pages/old/[category]/index.astro new file mode 100644 index 000000000000..70d870acd8df --- /dev/null +++ b/packages/astro/test/fixtures/redirects/src/pages/old/[category]/index.astro @@ -0,0 +1,8 @@ +--- + +export function getStaticPaths() { + return [{ params: { category: 'test' } }] +} +--- + +