Реестр карт сайта
Пакет создаёт XML-карты сайта — по файлу на источник плюс индекс — и отдаёт их по /sitemap.xml и /sitemap-{name}.xml. Генерация использует spatie/laravel-sitemap:
composer require spatie/laravel-sitemapРегистрация источников
Регистрируйте именованные источники в boot() сервис-провайдера:
use App\Models\Post;
use Rankbeam\Seo\Facades\SEO;
// A model class — every (indexable) record's getUrlForSEO()
SEO::sitemaps()->register('posts', Post::class);
// A closure returning URLs
SEO::sitemaps()->register('pages', fn () => ['/about', '/contact']);
// Any iterable of URLs
SEO::sitemaps()->register('legal', ['/imprint', '/privacy']);Каждый источник записывается в sitemap-{name}.xml; sitemap.xml становится индексом со списком всех карт.
API реестра также предоставляет has($name), names(), forget($name) и flush().
Источники из конфигурации
Источники можно задать в конфигурации: config/seo.php принимает модели и статические URL:
'sitemap' => [
'models' => [
\App\Models\Post::class => ['priority' => 0.8, 'changefreq' => 'weekly'],
],
'static_urls' => [
['url' => '/', 'priority' => 1.0, 'changefreq' => 'daily'],
],
],Реестр имеет приоритет перед автоматическим обнаружением
Если модель уже охвачена именованным зарегистрированным источником, автоматическое обнаружение её пропускает. Регистрация 'posts' не создаст дополнительно sitemap-post.xml.
Генерация
php artisan seo:sitemapФайлы записываются на диск, заданный в seo.sitemap.disk (по умолчанию public). Запускайте команду по расписанию, чтобы карты сайта оставались актуальными:
// routes/console.php or bootstrap/app.php scheduling
Schedule::command('seo:sitemap')->daily();Карты, превышающие seo.sitemap.max_urls_per_sitemap (по умолчанию 50 000 — лимит спецификации XML), автоматически разбиваются на части.
Выдача
Маршруты пакета отдают файлы, созданные командой, с заголовками XML, кеширования и X-Robots-Tag: noindex:
/sitemap.xml— индекс или единственная карта сайта/sitemap-posts.xml— именованный источник
Если вы отдаёте собственную статически сгенерированную карту сайта, отключите маршруты:
// config/seo.php
'routes' => ['enabled' => false],Оформленная карта сайта в браузере
Движок Spatie выводит необработанный XML. Карта сайта Rankbeam в браузере выглядит как читаемая страница с фирменным оформлением: каждый URL показан в таблице вместе с lastmod, частотой изменений, приоритетом, количеством изображений и языковых альтернатив, а также замечаниями проверки:

Для этого каждая сгенерированная карта ссылается на таблицу стилей XSL:
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="https://your-app.test/sitemap.xsl"?>
<urlset ...>Поисковые системы игнорируют эту инструкцию, поэтому карта остаётся обычным машиночитаемым XML-документом. Меняется только то, что видит человек. Индекс и все дочерние карты оформлены одинаково.
Включено по умолчанию. В отличие от расширений изображений/hreflang, таблица стилей не добавляет данные и не выполняет работу для каждой записи: это одна строка инструкции, которую роботы пропускают. Поэтому оформление включено сразу. Для обычного XML его можно отключить:
Требуется spatie/laravel-sitemap ≥ 8.1
Инструкция записывается через метод Spatie setStylesheet(), добавленный в spatie/laravel-sitemap 8.1. Если приложение устанавливает более старую версию — так бывает при некоторых сочетаниях PHP/Laravel, — карты создаются как обычный XML без оформления. Ничего не ломается. Выполните composer update spatie/laravel-sitemap, чтобы получить оформленное представление.
// config/seo.php
'sitemap' => [
'stylesheet' => ['enabled' => false],
],Замечания проверки
Отрисованная страница отмечает две вещи, которые можно проверить, не выходя из браузера:
- URL без
lastmod— отсутствие показывается, а значение не выдумывается. Google меньше доверяет карте сайта, которая сообщает неверную актуальность, поэтому таблица стилей отмечает пробел, а не заполняет его. - Неабсолютные URL —
<loc>, который не является абсолютным URLhttp(s).
Размещение таблицы стилей у себя
По умолчанию пакет отдаёт таблицу стилей через собственный маршрут /sitemap.xsl и ссылается на него из каждой карты. Браузеры применяют XSLT только с того же origin, что и карта сайта. Поэтому если карты размещены на другом origin, например в CDN, опубликуйте файл и укажите свою копию в конфигурации:
php artisan vendor:publish --tag=seo-assets// config/seo.php
'sitemap' => [
'stylesheet' => [
'url' => 'https://cdn.example.com/vendor/seo/sitemap.xsl',
],
],Безопасность заложена в реализацию
Каждое значение, выводимое таблицей стилей, включая URL, проходит экранирование вывода XSLT. Значение <loc> становится кликабельной ссылкой только для URL http(s). Поэтому вредоносное содержимое URL не может внедрить разметку или ссылку javascript: на страницу. При изменении опубликованного .xsl сохраните это поведение: не добавляйте disable-output-escaping.
Что попадает в карту
Источники моделей включают записи, для которых итоговые значения разрешают индексацию. Модель с итоговой директивой robots noindex в карту не попадёт. URL берутся из getUrlForSEO() — того же метода, который задаёт канонические URL, поэтому карта сайта и канонический URL не расходятся.
Расширения изображений и hreflang
Два необязательных расширения дополняют каждый URL модели данными, которые пакет уже определяет для этой записи. Оба выключены по умолчанию; включите нужные в config/seo.php:
'sitemap' => [
'images' => true, // <image:image> per URL
'alternates' => true, // <xhtml:link rel="alternate"> per URL
],Они применяются к моделям с трейтом HasSEO; значения берутся из полностью определённого seoData() модели:
imagesдобавляет запись карты изображений Google на основе итогового OG-изображения или изображения контента. Это то же значение, которое выводится какog:image, поэтому карта не расходится со страницей. Если собственного изображения у записи нет, используется общееdefault_og_imageсайта. Включайте расширение, только если изображение для каждого URL имеет смысл для вашего контента.alternatesдобавляет записи<xhtml:link rel="alternate" hreflang="…">изgetSEOAlternates()модели — те же ссылки hreflang, которые выводятся в<head>страницы. Возвращайте абсолютные URL:
public function getSEOAlternates(): ?array
{
return [
['hreflang' => 'en', 'href' => route('posts.show', [$this, 'locale' => 'en'])],
['hreflang' => 'fr', 'href' => route('posts.show', [$this, 'locale' => 'fr'])],
['hreflang' => 'x-default', 'href' => route('posts.show', $this)],
];
}hreflang требует взаимных ссылок и ссылки на себя
Google учитывает аннотацию, только когда каждая языковая версия перечисляет себя и все остальные, а ссылки взаимны: каждая страница ссылается обратно. Поэтому getSEOAlternates() должен возвращать полный набор, и каждая локализованная версия должна возвращать тот же полный набор. Используйте корректные коды language[-Script][-REGION] или x-default и абсолютные URL http(s). Записи без непустого hreflang или href пропускаются.
Перед записью к списку применяются политики seo.hreflang: коды нормализуются в BCP 47 (it_IT → it-IT), а include_self / x_default могут добавить ссылку на себя и x-default. Карта сайта всегда содержит тот же список, что и <head> страницы. Бесплатный аудит сообщает о hreflang_invalid_code, hreflang_duplicate_code и hreflang_missing_self; для проверки взаимности нужен обход (Pro).
Затраты при большом объёме
Начиная с ядра 3.20.1, проверка включения модели и расширения изображений/hreflang повторно используют один и тот же итоговый seoData() при формировании каждого URL модели. Повторное использование заканчивается после этого URL, в том числе при ошибке; следующая генерация или другая локаль определяет данные заново. В 3.20.0 при выключенном кешировании резолвера — это значение по умолчанию — проверка включения вместе с расширениями могла дважды проходить цепочку приоритетов. Каждое определение итоговых значений всё ещё может обращаться к кешу или базе данных, а собственные геттеры getSEO*() — добавлять запросы. Запускайте seo:sitemap по расписанию, а не в веб-запросе. Измеряйте производительность при приближении к лимиту в 50 000 URL и оставляйте расширения выключенными, если такие записи не нужны.
Конфигурация уже опубликована?
config/seo.php объединяется поверхностно, поэтому приложение, опубликовавшее файл конфигурации до этого выпуска, не получит ключи sitemap.images / sitemap.alternates автоматически. Одних переменных окружения SEO_SITEMAP_IMAGES / SEO_SITEMAP_ALTERNATES недостаточно, чтобы их включить. Добавьте два ключа в опубликованный массив sitemap (см. блок выше) или опубликуйте конфигурацию заново.
Полный контроль: теги Spatie, созданные вручную
Для данных, которых нет среди итоговых значений, — подписей изображений, записей видео или новостей, собственных наборов hreflang — верните из зарегистрированного источника полностью созданный вручную Spatie\Sitemap\Tags\Url. Построитель передаёт теги Url без изменений и никогда не добавляет к ним собственные расширения, поэтому вы сохраняете полный контроль:
use Spatie\Sitemap\Tags\Url;
SEO::sitemaps()->register('videos', fn () => Video::query()
->get()
->map(fn (Video $video) => Url::create($video->url)
->addImage($video->thumbnail_url, caption: $video->title)
->addVideo(
thumbnailLoc: $video->thumbnail_url,
title: $video->title,
description: $video->description,
contentLoc: $video->file_url,
)
->addAlternate($video->frenchUrl, 'fr')
));Тот же механизм доступен для отдельной записи: если модель реализует Sitemapable, а её toSitemapTag() возвращает Url, результат выводится в точности в возвращённом виде.