diff --git a/packages/astro/src/core/render/paginate.ts b/packages/astro/src/core/render/paginate.ts index 11f5ec2c2a06..f1ba5a29db35 100644 --- a/packages/astro/src/core/render/paginate.ts +++ b/packages/astro/src/core/render/paginate.ts @@ -21,11 +21,12 @@ export function generatePaginateFunction( args: PaginateOptions = {}, ): ReturnType { const generate = getRouteGenerator(routeMatch.segments, trailingSlash); - let { pageSize: _pageSize, params: _params, props: _props } = args; + let { pageSize: _pageSize, params: _params, props: _props, format: _format } = args; const pageSize = _pageSize || 10; const paramName = 'page'; const additionalParams = _params || {}; const additionalProps = _props || {}; + const formatUrl = _format || ((url: string) => url); let includesFirstPageNumber: boolean; if (routeMatch.params.includes(`...${paramName}`)) { includesFirstPageNumber = false; @@ -47,36 +48,40 @@ export function generatePaginateFunction( ...additionalParams, [paramName]: includesFirstPageNumber || pageNum > 1 ? String(pageNum) : undefined, }; - const current = addRouteBase(generate({ ...params }), base); + const current = formatUrl(addRouteBase(generate({ ...params }), base)); const next = pageNum === lastPage ? undefined - : addRouteBase(generate({ ...params, page: String(pageNum + 1) }), base); + : formatUrl(addRouteBase(generate({ ...params, page: String(pageNum + 1) }), base)); const prev = pageNum === 1 ? undefined - : addRouteBase( - generate({ - ...params, - page: - !includesFirstPageNumber && pageNum - 1 === 1 ? undefined : String(pageNum - 1), - }), - base, + : formatUrl( + addRouteBase( + generate({ + ...params, + page: + !includesFirstPageNumber && pageNum - 1 === 1 ? undefined : String(pageNum - 1), + }), + base, + ), ); const first = pageNum === 1 ? undefined - : addRouteBase( - generate({ - ...params, - page: includesFirstPageNumber ? '1' : undefined, - }), - base, + : formatUrl( + addRouteBase( + generate({ + ...params, + page: includesFirstPageNumber ? '1' : undefined, + }), + base, + ), ); const last = pageNum === lastPage ? undefined - : addRouteBase(generate({ ...params, page: String(lastPage) }), base); + : formatUrl(addRouteBase(generate({ ...params, page: String(lastPage) }), base)); return { params, props: { diff --git a/packages/astro/src/types/public/common.ts b/packages/astro/src/types/public/common.ts index 9f70d6644449..b4188a36d73c 100644 --- a/packages/astro/src/types/public/common.ts +++ b/packages/astro/src/types/public/common.ts @@ -42,6 +42,19 @@ export interface PaginateOptions `${url}.html`, + * }) + * ``` + */ + format?: (url: string) => string; } /** diff --git a/packages/astro/test/units/render/paginate.test.ts b/packages/astro/test/units/render/paginate.test.ts index d8b73eb49d1e..b129351d692e 100644 --- a/packages/astro/test/units/render/paginate.test.ts +++ b/packages/astro/test/units/render/paginate.test.ts @@ -133,6 +133,90 @@ describe('Pagination — multiple params (color + page)', () => { }); }); +describe('Pagination — format option transforms URLs', () => { + const route = createRouteData({ + route: '/blog/[...page]', + segments: [ + [{ content: 'blog', dynamic: false, spread: false }], + [{ content: '...page', dynamic: true, spread: true }], + ], + }); + const paginate = generatePaginateFunction(route, '/', 'ignore'); + const pages = paginate(items, { + pageSize: 10, + format: (url) => `${url}.html`, + }); + + it('applies format to current URL', () => { + assert.equal(pages[0].props.page.url.current, '/blog.html'); + assert.equal(pages[1].props.page.url.current, '/blog/2.html'); + }); + + it('applies format to next URL', () => { + assert.equal(pages[0].props.page.url.next, '/blog/2.html'); + }); + + it('applies format to prev URL', () => { + assert.equal(pages[1].props.page.url.prev, '/blog.html'); + }); + + it('applies format to first URL', () => { + assert.equal(pages[2].props.page.url.first, '/blog.html'); + }); + + it('applies format to last URL', () => { + assert.equal(pages[0].props.page.url.last, '/blog/3.html'); + }); + + it('does not apply format to undefined URLs', () => { + assert.equal(pages[0].props.page.url.prev, undefined); + assert.equal(pages[2].props.page.url.next, undefined); + assert.equal(pages[0].props.page.url.first, undefined); + assert.equal(pages[2].props.page.url.last, undefined); + }); +}); + +describe('Pagination — format option with base path', () => { + const route = createRouteData({ + route: '/posts/[...page]', + segments: [ + [{ content: 'posts', dynamic: false, spread: false }], + [{ content: '...page', dynamic: true, spread: true }], + ], + }); + const paginate = generatePaginateFunction(route, '/site', 'ignore'); + const pages = paginate(items, { + pageSize: 10, + format: (url) => `${url}.html`, + }); + + it('applies format after base is prepended', () => { + assert.equal(pages[0].props.page.url.current, '/site/posts.html'); + assert.equal(pages[1].props.page.url.current, '/site/posts/2.html'); + assert.equal(pages[0].props.page.url.next, '/site/posts/2.html'); + assert.equal(pages[1].props.page.url.prev, '/site/posts.html'); + }); +}); + +describe('Pagination — without format option (default behavior unchanged)', () => { + const route = createRouteData({ + route: '/blog/[...page]', + segments: [ + [{ content: 'blog', dynamic: false, spread: false }], + [{ content: '...page', dynamic: true, spread: true }], + ], + }); + const paginate = generatePaginateFunction(route, '/', 'ignore'); + const pages = paginate(items, { pageSize: 10 }); + + it('URLs have no file extension by default', () => { + assert.equal(pages[0].props.page.url.current, '/blog'); + assert.equal(pages[1].props.page.url.current, '/blog/2'); + assert.equal(pages[0].props.page.url.next, '/blog/2'); + assert.equal(pages[1].props.page.url.prev, '/blog'); + }); +}); + describe('Pagination — root spread, correct prev URL — Migrated from astro-pagination-root-spread.test.js', () => { // 4 items, pageSize 1 → 4 pages; root spread means page 1 has no number in URL. const route = createRouteData({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37286f3661ed..f9393cc0ab02 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6736,7 +6736,7 @@ importers: specifier: ^4.22.0 version: 4.22.3 - triage/17120: + triage/13604: dependencies: astro: specifier: workspace:*