Pattern · TypeScript
Dynamic sitemap.xml in Next.js (app/sitemap.ts)
One sitemap with static and content URLs; aligned with collectIndexableUrls and canonical URLs.
Level: intermediateTime estimate: ~40 min
The sitemap generator collects stable paths and slugs from CMS/files; lastmod comes from content dates when available.
- Every sitemap URL must match the canonical
- do not include noindex pages
- for large catalogs use splitting or a sitemap index
app/sitemap.tsin the App Router returns an array of URLs. In one pass collect static routes and dynamic slugs (blog, library, case studies).
Code
Below is the actual logic from this project’s app/sitemap.ts (static paths, cases, posts, library patterns):
import type { MetadataRoute } from 'next'
import { caseSlugOrder } from '@/data/cases'
import { SITE_STATIC_PATHS } from '@/data/site-static-paths'
import { getAllPosts, getPostModifiedDate } from '@/lib/blog'
import { getAllLibraryEntries, getLibraryModifiedDate } from '@/lib/library'
import { getStaticPagesLastModified } from '@/lib/site-static-lastmod'
import { getSiteUrl } from '@/lib/site'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const site = getSiteUrl()
const base = site.replace(/\/$/, '')
const staticLastMod = getStaticPagesLastModified()
const posts = await getAllPosts()
const library = await getAllLibraryEntries()
const entries: MetadataRoute.Sitemap = [
...SITE_STATIC_PATHS.map((path) => ({
url: `${base}${path}`,
lastModified: staticLastMod,
changeFrequency: 'weekly' as const,
priority: path === '/' ? 1 : 0.7,
})),
...caseSlugOrder.map((slug) => ({
url: `${base}/kejsy/${slug}/`,
lastModified: staticLastMod,
changeFrequency: 'monthly' as const,
priority: 0.65,
})),
...posts.map((p) => ({
url: `${base}/blog/${p.slug}/`,
lastModified: new Date(getPostModifiedDate(p)),
changeFrequency: 'monthly' as const,
priority: 0.6,
})),
...library.map((e) => ({
url: `${base}/biblioteka/${e.slug}/`,
lastModified: new Date(getLibraryModifiedDate(e)),
changeFrequency: 'monthly' as const,
priority: 0.62,
})),
]
return entries
}
Verification
What to check:
-
The file loads. Open
https://your-domain/sitemap.xml— you should get XML, not an HTML 404. If you see the Next app shell, ensureapp/sitemap.ts(or.js) exists and exports a default function. -
Search Console. In Google Search Console → Sitemaps, submit
https://your-domain/sitemap.xml. “Couldn’t fetch” or parse errors mean the bot cannot reach valid XML. -
Canonical match. Pick several URLs from the file, open the pages, find
<link rel="canonical" ...>. It must match the sitemap URL (including a trailing/if that is your canonical policy). -
Noindex consistency. Pages with
robots: { index: false }or<meta name="robots" content="noindex">should usually not be listed — otherwise you send conflicting signals.
Sources
More patterns
Blog posts
- SEO
B2B multilingual sites: why a translation widget is not the same as searchable locales
On-site translation and indexable language versions are different jobs. We unpack URLs, meta, trust, and a maturity ladder for exports and supply chains—without magical thinking.
Read article - SEO
SEO architecture: why a site does not sell without structure
How the right site structure affects sales and rankings. Why design comes second and semantics first.
Read article
Need this implemented for your domain and stack?
Short form: name, phone, and site. After you submit, we reply with next steps and a phase outline; details are refined on a call.