Перейти к содержимому

Реестр карт сайта

Пакет создаёт XML-карты сайта — по файлу на источник плюс индекс — и отдаёт их по /sitemap.xml и /sitemap-{name}.xml. Генерация использует spatie/laravel-sitemap:

bash
composer require spatie/laravel-sitemap

Регистрация источников

Регистрируйте именованные источники в boot() сервис-провайдера:

php
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:

php
'sitemap' => [
    'models' => [
        \App\Models\Post::class => ['priority' => 0.8, 'changefreq' => 'weekly'],
    ],
    'static_urls' => [
        ['url' => '/', 'priority' => 1.0, 'changefreq' => 'daily'],
    ],
],

Реестр имеет приоритет перед автоматическим обнаружением

Если модель уже охвачена именованным зарегистрированным источником, автоматическое обнаружение её пропускает. Регистрация 'posts' не создаст дополнительно sitemap-post.xml.

Генерация

bash
php artisan seo:sitemap

Файлы записываются на диск, заданный в seo.sitemap.disk (по умолчанию public). Запускайте команду по расписанию, чтобы карты сайта оставались актуальными:

php
// 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 — именованный источник

Если вы отдаёте собственную статически сгенерированную карту сайта, отключите маршруты:

php
// config/seo.php
'routes' => ['enabled' => false],

Оформленная карта сайта в браузере

Движок Spatie выводит необработанный XML. Карта сайта Rankbeam в браузере выглядит как читаемая страница с фирменным оформлением: каждый URL показан в таблице вместе с lastmod, частотой изменений, приоритетом, количеством изображений и языковых альтернатив, а также замечаниями проверки:

Карта сайта Rankbeam в браузере: читаемая таблица с фирменным оформлением

Для этого каждая сгенерированная карта ссылается на таблицу стилей XSL:

xml
<?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, чтобы получить оформленное представление.

php
// config/seo.php
'sitemap' => [
    'stylesheet' => ['enabled' => false],
],

Замечания проверки

Отрисованная страница отмечает две вещи, которые можно проверить, не выходя из браузера:

  • URL без lastmod — отсутствие показывается, а значение не выдумывается. Google меньше доверяет карте сайта, которая сообщает неверную актуальность, поэтому таблица стилей отмечает пробел, а не заполняет его.
  • Неабсолютные URL<loc>, который не является абсолютным URL http(s).

Размещение таблицы стилей у себя

По умолчанию пакет отдаёт таблицу стилей через собственный маршрут /sitemap.xsl и ссылается на него из каждой карты. Браузеры применяют XSLT только с того же origin, что и карта сайта. Поэтому если карты размещены на другом origin, например в CDN, опубликуйте файл и укажите свою копию в конфигурации:

bash
php artisan vendor:publish --tag=seo-assets
php
// 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:

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:
php
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_ITit-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 без изменений и никогда не добавляет к ним собственные расширения, поэтому вы сохраняете полный контроль:

php
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, результат выводится в точности в возвращённом виде.

rankbeam/laravel-seo распространяется под лицензией MIT.