Sitemap Generator
Self-updating sitemap module—dynamically creates XML sitemaps for all your public collections and a master /sitemap-index.xml.
What it does
- Scans each collection, paginating after 1 000 documents, and serves /sitemap/[id].xml routes.
- Builds a master /sitemap-index.xml that links every generated sitemap.
- Calculates <changefreq> from updatedAt, sets <priority> to 0.8, and uses `NEXT_PUBLIC_SERVER_URL` for absolute URLs.
- Runs as dynamic Next.js route handlers, so the XML always reflects the latest database state.
Install
pnpm dlx [email protected] add https://p.livog.com/r/sitemap.json
Code
import { getPayload } from '@/payload/utils/get-payload'
import { UNIQUE_PATH_COLLECTIONS } from '@/payload/fields/path/config'
import type { MetadataRoute } from 'next'
import fs from 'fs/promises'
import path from 'path'
type CollectionSlug = (typeof UNIQUE_PATH_COLLECTIONS)[number]
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL!
export const dynamic = 'force-dynamic'
export const fetchCache = 'default-no-store'
const limitPerPage = 1000
const prerenderManifestPath = path.join(process.cwd(), '.next', 'prerender-manifest.json')
async function getStaticRoutesPaths(): Promise<string[]> {
try {
const manifestRaw = await fs.readFile(prerenderManifestPath, 'utf8')
const { routes = {} } = JSON.parse(manifestRaw)
return Object.keys(routes)
.filter((route) => !path.extname(route)) // Filter out files
} catch {
return []
}
}
export async function generateSitemaps() {
const sitemapIds: { id: string }[] = []
const payload = await getPayload()
for (const collection of UNIQUE_PATH_COLLECTIONS) {
const { totalDocs } = await payload.count({
collection
})
if (!totalDocs) continue
const totalPages = Math.ceil(totalDocs / limitPerPage)
if (totalPages <= 1) {
sitemapIds.push({ id: collection })
continue
}
for (let page = 1; page <= totalPages; page++) {
sitemapIds.push({ id: `${collection}-${page}` })
}
}
const staticPaths = await getStaticRoutesPaths()
if (staticPaths.length) sitemapIds.push({ id: 'static' })
return sitemapIds
}
async function fetchDocumentsFromCollection(collection: CollectionSlug, pageNumber: number) {
const payload = await getPayload()
try {
const response = await payload.find({
collection,
limit: limitPerPage,
page: pageNumber,
where: { 'meta.noindex': { not_equals: true } },
select: {
id: true,
path: true,
updatedAt: true
}
})
const docs = response?.docs || []
return docs
} catch (error) {
console.error('Error fetching documents:', error)
return []
}
}
function getChangeFrequency(updatedAt: string): 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never' {
const updatedDate = new Date(updatedAt)
const now = new Date()
const diffTime = now.getTime() - updatedDate.getTime()
const diffHours = diffTime / (1000 * 60 * 60)
const diffDays = diffTime / (1000 * 60 * 60 * 24)
const diffWeeks = diffTime / (1000 * 60 * 60 * 24 * 7)
const diffMonths = diffTime / (1000 * 60 * 60 * 24 * 30)
const diffYears = diffTime / (1000 * 60 * 60 * 24 * 365)
if (diffHours < 1) return 'hourly'
if (diffDays < 1) return 'daily'
if (diffWeeks < 1) return 'weekly'
if (diffMonths < 1) return 'monthly'
if (diffYears < 1) return 'yearly'
return 'never'
}
export default async function sitemap({ id }: { id: string }): Promise<MetadataRoute.Sitemap> {
const [collection, pageStr] = id.split('-')
if (id === 'static') {
const staticPaths = await getStaticRoutesPaths()
return staticPaths.map((route) => ({
url: `${serverUrl}${route}`,
lastModified: new Date().toISOString(),
changeFrequency: 'monthly',
priority: 0.7
}))
}
if (!UNIQUE_PATH_COLLECTIONS.includes(collection as CollectionSlug)) return []
const pageNumber = pageStr ? parseInt(pageStr, 10) : 1
const documents = await fetchDocumentsFromCollection(collection as CollectionSlug, pageNumber)
return documents.map((document) => ({
url: `${serverUrl}${document.path}`,
lastModified: document.updatedAt,
changeFrequency: getChangeFrequency(document.updatedAt),
priority: 0.8
}))
}
Usage
- Set
NEXT_PUBLIC_SERVER_URL
in your .env
Good To Know
Installing this item also pulls in the Path Field, guaranteeing every document has a unique, hierarchical path.
Recommended robots route
Add app/robots.ts
so search engines can discover the sitemap index automatically:
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/'
},
sitemap: `${process.env.NEXT_PUBLIC_SERVER_URL}/sitemap-index.xml`
}
}