🚧 Under development — suggestions for new items are always welcome! ✨

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

  1. 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`
  }
}