By Wallace Ferreira2 min read
Static Blog Routes with generateStaticParams
How dated post URLs are prerendered from one use case, why every other URL is a 404, and how the sitemap and canonical URLs stay in sync.
On this page
Every post on this blog lives at a dated URL, /{locale}/blog/{yyyy}/{MM}/{slug}, and every one
of those pages is HTML generated at build time. Three small pieces make that work.
One function lists every route
generateStaticParams returns the params of every page to prerender. The post page and its
OpenGraph image need exactly the same list, so it lives in one route-glue file next to them:
export async function blogPostStaticParams(): Promise<BlogPostRouteParams[]> {
const result = await new ListBlogPosts(
getServerContainer().blogPostRepository,
).execute({ locale: DEFAULT_LOCALE });
if (result.isLeft()) return [];
return LOCALES.flatMap((locale) =>
result.value.map(({ publishedAt, slug }) => ({
locale,
...publicationSegments(publishedAt),
slug,
})),
);
}It calls the same use case as the listing page, so a draft can never get a route: ListBlogPosts
only returns published posts. The year and month come from publicationSegments, which reads the
date in UTC so the build machine's timezone can't move a post to another month.
Everything else is a 404
export const dynamicParams = false;
export const generateStaticParams = blogPostStaticParams;With dynamicParams = false, any URL that isn't in the list is a 404 in production: an unknown
slug, an unpublished post, or a real slug under the wrong month. Nothing renders on demand.
next dev ignores that and renders any params, so the page checks again before rendering. It
loads the post by slug and compares the URL's year and month with its publication date; a mismatch
calls notFound(). Both environments give the same answer.
SEO falls out of the same data
The URL is built in one place, blogPostPath(publishedAt, slug), and everything that needs it
reuses it:
- Metadata: the canonical URL and the
hreflangalternates for every locale come frombuildAlternates(path, locale). - Sitemap:
sitemap.tscallsListBlogPostsand maps each post throughblogPostPathfor every locale. - Links: cards, breadcrumbs and prev/next all use the same helper.
Because the page list, the sitemap and the links read the same use case and the same path helper, they can't disagree. A post that is in the sitemap has a page, and a page always has its canonical URL.
About the author