diff --git a/.changeset/happy-ways-sin.md b/.changeset/happy-ways-sin.md new file mode 100644 index 0000000000..e100a31355 --- /dev/null +++ b/.changeset/happy-ways-sin.md @@ -0,0 +1,7 @@ +--- +'astro': patch +--- + +Fix getStaticPaths regression + +This reverts a previous change meant to remove a dependency, to fix a regression with multiple nested spread routes. diff --git a/packages/astro/package.json b/packages/astro/package.json index f3a2b7d0bb..bd9bcc7177 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -168,6 +168,7 @@ "ora": "^8.1.0", "p-limit": "^6.1.0", "p-queue": "^8.0.1", + "path-to-regexp": "6.2.2", "preferred-pm": "^4.0.0", "prompts": "^2.4.2", "rehype": "^13.0.1", diff --git a/packages/astro/src/core/routing/manifest/generator.ts b/packages/astro/src/core/routing/manifest/generator.ts index 30758bfd1c..4ab635ec66 100644 --- a/packages/astro/src/core/routing/manifest/generator.ts +++ b/packages/astro/src/core/routing/manifest/generator.ts @@ -1,5 +1,7 @@ import type { AstroConfig, RoutePart } from '../../../@types/astro.js'; +import { compile } from 'path-to-regexp'; + /** * Sanitizes the parameters object by normalizing string values and replacing certain characters with their URL-encoded equivalents. * @param {Record} params - The parameters object to be sanitized. @@ -22,40 +24,45 @@ export function getRouteGenerator( segments: RoutePart[][], addTrailingSlash: AstroConfig['trailingSlash'], ) { + const template = segments + .map((segment) => { + return ( + '/' + + segment + .map((part) => { + if (part.spread) { + return `:${part.content.slice(3)}(.*)?`; + } else if (part.dynamic) { + return `:${part.content}`; + } else { + return part.content + .normalize() + .replace(/\?/g, '%3F') + .replace(/#/g, '%23') + .replace(/%5B/g, '[') + .replace(/%5D/g, ']') + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + }) + .join('') + ); + }) + .join(''); + + // Unless trailingSlash config is set to 'always', don't automatically append it. + let trailing: '/' | '' = ''; + if (addTrailingSlash === 'always' && segments.length) { + trailing = '/'; + } + const toPath = compile(template + trailing); return (params: Record): string => { const sanitizedParams = sanitizeParams(params); + const path = toPath(sanitizedParams); - // Unless trailingSlash config is set to 'always', don't automatically append it. - let trailing: '/' | '' = ''; - if (addTrailingSlash === 'always' && segments.length) { - trailing = '/'; - } - - const path = - segments - .map((segment) => { - return ( - '/' + - segment - .map((part) => { - if (part.spread) { - return `${sanitizedParams[part.content.slice(3)] || ''}`; - } else if (part.dynamic) { - return `${sanitizedParams[part.content] || ''}`; - } else { - return part.content - .normalize() - .replace(/\?/g, '%3F') - .replace(/#/g, '%23') - .replace(/%5B/g, '[') - .replace(/%5D/g, ']'); - } - }) - .join('') - ); - }) - .join('') + trailing; - + // When generating an index from a rest parameter route, `path-to-regexp` will return an + // empty string instead "/". This causes an inconsistency with static indexes that may result + // in the incorrect routes being rendered. + // To fix this, we return "/" when the path is empty. return path || '/'; }; } diff --git a/packages/astro/test/fixtures/get-static-paths-pages/src/pages/archive/[...slug]/[...page].astro b/packages/astro/test/fixtures/get-static-paths-pages/src/pages/archive/[...slug]/[...page].astro new file mode 100644 index 0000000000..cf5a886400 --- /dev/null +++ b/packages/astro/test/fixtures/get-static-paths-pages/src/pages/archive/[...slug]/[...page].astro @@ -0,0 +1,37 @@ +--- +export async function getStaticPaths({ paginate }) { + + const paths = [ + { + slug: 'news/july-2024', + items: ['item 1', 'item 2', 'item 3', 'item 4', 'item 5', 'item 6'], + contentType: 'news', + monthYear: 'july-2024', + } + ]; + + return paths.flatMap((path) => { + return paginate(path.items, { + params: { slug: path.slug }, + props: { + contentType: path.contentType, + monthYear: path.monthYear, + }, + pageSize: 2, + }); + }); +} + +const { slug, page } = Astro.params; +--- + + + + Testing + + +

Testing

+

{slug}

+

{page}

+ + diff --git a/packages/astro/test/get-static-paths-pages.test.js b/packages/astro/test/get-static-paths-pages.test.js index 170e1d485a..b62b1baa1a 100644 --- a/packages/astro/test/get-static-paths-pages.test.js +++ b/packages/astro/test/get-static-paths-pages.test.js @@ -25,4 +25,12 @@ describe('getStaticPaths with trailingSlash: ignore', () => { let $ = cheerio.load(html); assert.equal($('h1').text(), 'Page 2'); }); + + // for regression: https://github.com/withastro/astro/issues/11990 + it('nested static paths generate', async () => { + let html = await fixture.readFile('/archive/news/july-2024/2/index.html'); + let $ = cheerio.load(html); + assert.equal($('#slug').text(), 'news'); + assert.equal($('#page').text(), 'july-2024/2'); + }) }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f7eebdf61..a054e7f2b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -693,6 +693,9 @@ importers: p-queue: specifier: ^8.0.1 version: 8.0.1 + path-to-regexp: + specifier: 6.2.2 + version: 6.2.2 preferred-pm: specifier: ^4.0.0 version: 4.0.0 @@ -9543,6 +9546,9 @@ packages: resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==} engines: {node: '>=16 || 14 >=14.17'} + path-to-regexp@6.2.2: + resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -15576,6 +15582,8 @@ snapshots: lru-cache: 10.2.0 minipass: 7.1.2 + path-to-regexp@6.2.2: {} + path-type@4.0.0: {} path-type@5.0.0: {}