К основному контенту

Паттерн · TypeScript

Динамический sitemap.xml в Next.js (app/sitemap.ts)

Единый sitemap со статическими и контентными URL; согласование с collectIndexableUrls и каноникалом.

Уровень: среднийОценка времени: ~40 мин

Генератор sitemap собирает стабильные пути и slug’и из CMS/файлов; lastmod берёт из даты контента, если она есть.

  • Каждый URL в sitemap должен совпадать с каноническим
  • не включайте noindex-страницы
  • при большом каталоге используйте разбиение или sitemap-индекс

app/sitemap.ts в App Router возвращает массив URL. Соберите в один проход статические маршруты и динамические slug’и (блог, библиотека, кейсы).

Код

Ниже — фактическая логика из app/sitemap.ts этого проекта (статические пути, кейсы, посты, паттерны библиотеки):

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
}

Проверка

Что именно проверить:

  1. Файл открывается. В браузере откройте https://ваш-домен/sitemap.xml — должна загрузиться XML-страница без HTML-ошибки 404. Если видите приложение Next вместо XML, проверьте, что в app/ есть sitemap.ts (или sitemap.js) и что он экспортирует функцию по умолчанию.

  2. Search Console. В Google Search Console раздел «Файлы Sitemap» → добавьте URL вида https://ваш-домен/sitemap.xml. Статус «Не получено» или ошибки парсинга значит, что файл недоступен роботу или XML некорректен.

  3. Совпадение с canonical. Выберите несколько URL из файла, откройте эти страницы, в исходном коде найдите <link rel="canonical" ...>. Адрес должен совпадать с URL в sitemap (включая завершающий слэш /, если именно так задан каноникал на сайте).

  4. Согласованность с noindex. Страницы с robots: { index: false } или <meta name="robots" content="noindex"> обычно не включают в sitemap — иначе вы даёте поиску противоречивые сигналы.

Первоисточники

Нужно внедрить под ваш домен и стек?

Короткая форма: имя, телефон и сайт. После отправки — ответ с порядком работ и ориентиром по этапам; задачу уточняют при контакте.