Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
39 changes: 22 additions & 17 deletions packages/astro/src/core/render/paginate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ export function generatePaginateFunction(
args: PaginateOptions<Props, Params> = {},
): ReturnType<PaginateFunction> {
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;
Expand All @@ -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: {
Expand Down
13 changes: 13 additions & 0 deletions packages/astro/src/types/public/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ export interface PaginateOptions<PaginateProps extends Props, PaginateParams ext
params?: PaginateParams;
/** object of props to forward to `page` result */
props?: PaginateProps;
/**
* Transform each pagination URL before it is set on the page result.
* Useful for adding a file extension (e.g. `.html`) when deploying to a
* static file server that does not support clean URLs.
*
* @example
* ```ts
* paginate(items, {
* format: (url) => `${url}.html`,
* })
* ```
*/
format?: (url: string) => string;
}

/**
Expand Down
84 changes: 84 additions & 0 deletions packages/astro/test/units/render/paginate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading