795 words
4 minutes

Astro Pagination URLs with .html: Use paginate({ format })

2026-08-12
Frontend
Astro
/
Frontend
/
JavaScript
/
Migration

If an Astro static build writes pages such as dist/blog/2.html but the Next link points to /blog/2, the problem is the pagination URL contract, not the collection data. Astro 7.1 added a format callback to paginate() so generated current, next, prev, first, and last URLs can match the files you deploy.

The direct fix is:

return paginate(posts, {
pageSize: 10,
format: (url) => url + '.html',
});

Use that callback only when your host expects extension-bearing files. If your server rewrites clean URLs or you use directory output, keep the clean pagination URLs instead.

Do not confuse two different format settings#

Astro has two related but separate controls:

SettingControlsTypical value
build.formatThe shape of generated page files'file'
paginate(...).formatThe URLs placed in pagination metadata and linksurl => url + '.html'

For example, this configuration asks Astro to produce file-shaped output:

import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'static',
build: {
format: 'file',
},
});

That setting does not automatically guarantee that every URL returned by a paginated route includes .html. The route generator still needs to produce links that the deployed host can resolve.

Conversely, adding .html to a pagination URL does not turn directory output into file output. Change the two settings as a pair only when the deployment contract requires it.

Add the callback to the paginated route#

Suppose the route is src/pages/blog/[page].astro and the site uses static file output:

---
import type { GetStaticPathsOptions } from 'astro';
const posts = [
{ slug: 'first', title: 'First post' },
{ slug: 'second', title: 'Second post' },
{ slug: 'third', title: 'Third post' },
];
export function getStaticPaths({ paginate }: GetStaticPathsOptions) {
return paginate(posts, {
pageSize: 2,
format: (url) => url + '.html',
});
}
const { page } = Astro.props;
---
<h1>Page {page.currentPage}</h1>
{page.data.map((post) => <h2>{post.title}</h2>)}
{page.url.prev && <a href={page.url.prev}>Previous</a>}
{page.url.next && <a href={page.url.next}>Next</a>}

The callback receives the URL Astro computed for that pagination page. Transform that value instead of rebuilding the page number yourself. That preserves Astro’s handling of the first page, the configured base, and any route parameters.

The callback is also useful for a deployment that needs another stable convention:

const formatPaginationUrl = (url) => url + '.html';
return paginate(posts, {
pageSize: 20,
format: formatPaginationUrl,
});

Keep the function deterministic. Do not add a query string, locale prefix, or random value unless the deployed route and canonical URL are designed for it.

The most useful check is to inspect the built output before debugging the browser:

Terminal window
pnpm build
find dist/blog -maxdepth 2 -type f | sort
rg -n 'blog/[0-9]+(\.html)?' dist/blog

With build.format: 'file', expect a file-shaped result such as:

dist/blog.html
dist/blog/2.html
dist/blog/3.html

The exact first-page path depends on the route shape. What matters is that page.url.next and page.url.prev match files that actually exist in dist/.

Then request the same URLs through the host:

Terminal window
curl -I https://example.com/blog.html
curl -I https://example.com/blog/2.html
curl -I https://example.com/blog/2

If /blog/2.html is 200 and /blog/2 is 404, the callback is required for that host. If both variants work because the server rewrites extensionless paths, you can keep clean URLs, but document the rewrite and test it separately. Do not depend on a local preview that silently supplies routing behavior your static host does not have.

For a Caddy-served static site, compare the generated files, the document root, and the slash or extension policy together. Astro Static Route 404 Behind Caddy covers that host-level boundary.

When not to append .html#

Use the callback only when the public URL contract requires it:

  • Keep clean URLs with build.format: 'directory' and a host that serves index.html from each directory.
  • Keep clean URLs when a reverse proxy explicitly rewrites /blog/2 to the correct file.
  • Append .html when the host exposes the generated files directly and has no extensionless rewrite.
  • Do not add .html to an API endpoint or an on-demand route just because a static page uses file output.

If the project is upgrading from an older Astro release, check the installed version before copying the callback. The format option was added to paginate() in Astro 7.1; upgrading the dependency and changing route URLs should be reviewed as one deployment change.

FAQ#

A: Not by itself. build.format controls generated file shape, while paginate({ format }) lets you transform the URLs used by the pagination metadata. Align both with the host that serves the files.

Q: Can I use paginate({ format }) with directory output?#

A: Yes, the callback can transform the URL for any pagination route, but adding .html to a directory-shaped deployment usually creates a mismatch. Use it only when the resulting URL is the one your host serves.

A: The first page may be loaded through a route your host handles specially, while the generated next URL points to an extensionless file that does not exist. Compare page.url.next with the corresponding path in dist/.

References:

Astro 7.1: Full control over pagination URLs

Astro routing reference: paginate()

Astro configuration reference: build.format

Astro Pagination URLs with .html: Use paginate({ format })
https://laplusda.com/en/posts/astro-pagination-format-html/
Author
Zero
Published at
2026-08-12
License
CC BY-NC-SA 4.0
Was this article useful?

Report a typo or broken link, or suggest a related topic.