Как создать sitemap для Laravel сайта

Правильная карта сайта (sitemap.xml) для Laravel сайтов, как создать и обновлять для быстрой индексации.

Карта сайта для Laravel-проекта — это не просто дань поисковым системам. Это способ управлять краулинговым бюджетом и своевременно сообщать поисковикам о новых страницах. По состоянию на 2026 год XML sitemap остаётся основным инструментом для передачи списка URL в Google Search Console и Яндекс.Вебмастер. В этой статье разберём, как создать sitemap Laravel-приложения с помощью пакета spatie/laravel-sitemap, настроить динамическую генерацию через маршруты, организовать кэширование и интегрировать отправку уведомлений по протоколу IndexNow.

Пакет spatie/laravel-sitemap: установка и первые шаги

Пакет spatie/laravel-sitemap закрывает все базовые потребности: генерация статического файла, обход страниц по ссылкам, ручное добавление URL с метаданными. На момент написания статьи актуальна версия 7.x, совместимая с Laravel 12 и PHP 8.3. Пакет активно поддерживается и не требует сложной настройки.

Для установки выполните команду composer. После этого можно сразу создавать карту сайта. Пакет не требует публикации конфигов или провайдеров — всё работает из коробки.

composer require spatie/laravel-sitemap

Пакет предлагает два основных режима работы: автоматический обход страниц (crawl) и ручное построение карты. Выбор зависит от структуры проекта. Для простых лендингов и небольших сайтов подойдёт crawl. Для крупных проектов с тысячами сущностей и сложной логикой — ручное добавление URL из моделей.

Автоматический crawl

Метод create() с указанием стартового URL запускает обход внутренних ссылок. Пакет проходит по страницам, собирает теги <a> и добавляет найденные пути в карту. Такой подход удобен, когда все страницы доступны по ссылкам с главной или из навигации.

use Spatie\Sitemap\Sitemap;

Sitemap::create('https://example.com')
    ->writeToFile(public_path('sitemap.xml'));

В проекте с 50 страницами услуг, связанных перекрёстными ссылками, этот код генерирует файл sitemap.xml за 3–5 секунд. Для сайтов с авторизацией или JavaScript-рендерингом автоматический обход не подходит — он не видит страницы, требующие сессии.

Можно задать максимальное количество ссылок и фильтрацию по шаблону URL:

Sitemap::create('https://example.com')
    ->configureCrawler(function (Crawler $crawler) {
        $crawler->setMaximumDepth(3);
        $crawler->ignoreRobots(false);
    })
    ->hasCrawled(function (Url $url) {
        return !str_contains($url->segment(1), 'admin');
    })
    ->writeToFile(public_path('sitemap.xml'));

Настройка глубины важна для иерархических структур. При глубине больше 3 краулер может собрать служебные страницы пагинации или фильтров, которые не нужно индексировать. Фильтр hasCrawled исключает административные разделы и другие закрытые области.

Автоматический обход не учитывает приоритеты и частоту обновления. Поисковые системы получают все URL с одинаковыми значениями changeFrequency и priority. Для небольших информационных сайтов это некритично. Но для интернет-магазинов и каталогов лучше использовать ручной режим.

Ручное добавление URL

Когда страницы генерируются из базы данных, проще перебрать модели и добавить каждую запись как отдельный URL с точными метаданными. Пакет предоставляет fluent-интерфейс для добавления атрибутов: lastModificationDate, changeFrequency, priority.

use Spatie\Sitemap\Sitemap;
use Spatie\Sitemap\Tags\Url;
use App\Models\Product;

$sitemap = Sitemap::create();

Product::where('is_active', true)->chunk(500, function ($products) use ($sitemap) {
    foreach ($products as $product) {
        $sitemap->add(
            Url::create(route('products.show', $product))
                ->setLastModificationDate($product->updated_at)
                ->setChangeFrequency(Url::CHANGE_FREQUENCY_WEEKLY)
                ->setPriority(0.8)
        );
    }
});

$sitemap->writeToFile(public_path('sitemap.xml'));

В примере товары разбиваются на чанки по 500 записей, чтобы не загружать память при 100 000 позиций. Метод chunk обрабатывает модели порциями и освобождает ресурсы.

Для мультиязычных сайтов можно генерировать отдельные URL для каждой локали:

use Spatie\Sitemap\Sitemap;
use Spatie\Sitemap\Tags\Url;

Sitemap::create()
    ->add(Url::create('https://example.com/en/about')
        ->setAlternatives([
            ['locale' => 'de', 'url' => 'https://example.com/de/about'],
        ]))
    ->writeToFile(public_path('sitemap.xml'));

Тег <xhtml:link rel="alternate"> помогает поисковикам связать языковые версии и избежать дублирования контента.

Динамический sitemap через Route

Генерация статического файла на диске подходит для редких обновлений. Когда контент меняется несколько раз в день, удобнее отдавать карту сайта по запросу через контроллер. В Laravel для этого регистрируют маршрут, который динамически собирает sitemap и возвращает XML-ответ.

Route::get('/sitemap.xml', [SitemapController::class, 'index']);

Такой подход гарантирует, что поисковый робот всегда получит актуальный список страниц. Минус — нагрузка на сервер при каждой проверке. Для смягчения применяют кэширование.

Кэширование

Динамическую карту сайта помещают в кэш на несколько часов. Это снижает нагрузку на базу данных и ускоряет ответ. При обновлении контента кэш сбрасывается.

use Illuminate\Support\Facades\Cache;
use Spatie\Sitemap\Sitemap;

class SitemapController
{
    public function index()
    {
        return Cache::remember('sitemap', 3600, function () {
            $sitemap = Sitemap::create();
            
            Post::where('published', true)
                ->each(function (Post $post) use ($sitemap) {
                    $sitemap->add(Url::create(route('posts.show', $post))
                        ->setLastModificationDate($post->updated_at)
                        ->setChangeFrequency(Url::CHANGE_FREQUENCY_DAILY)
                        ->setPriority(0.7));
                });

            return $sitemap->render();
        });
    }
}

Время жизни кэша подбирают под частоту обновлений. Для новостного портала с 50 публикациями в день кэш на 6 часов приведёт к задержке индексации. В таких случаях кэш инвалидируют при создании или изменении модели через Observer.

class PostObserver
{
    public function saved(Post $post)
    {
        Cache::forget('sitemap');
    }
    
    public function deleted(Post $post)
    {
        Cache::forget('sitemap');
    }
}

Для обработки мягкого удаления (soft deletes) в sitemap не должны попадать удалённые записи. Модель с трейтом SoftDeletes исключается автоматически, если в запросе использовать whereNull('deleted_at'). Но проще через глобальный scope или метод модели.

Post::query()
    ->where('published', true)
    ->whereNull('deleted_at')
    ->each(/*...*/);

Или использовать стандартный scope для работы с soft deletes:

Post::withoutTrashed()->where('published', true)->each(/*...*/);

Таким образом, удалённые страницы не попадут в sitemap, и поисковики не получат 404 ошибки.

Пагинация: Sitemap Index

По стандарту sitemap.org один файл может содержать не более 50 000 URL и весить не больше 50 МБ. Для крупных проектов разбивают карту на несколько файлов и объединяют их индексным sitemap. Пакет spatie/laravel-sitemap поддерживает создание Sitemap Index.

use Spatie\Sitemap\SitemapIndex;
use Spatie\Sitemap\Tags\Sitemap as SitemapTag;

$sitemapIndex = SitemapIndex::create();

$sitemapIndex->add(
    SitemapTag::create(route('sitemap.pages'))
        ->setLastModificationDate(Carbon::now())
);
$sitemapIndex->add(
    SitemapTag::create(route('sitemap.products'))
        ->setLastModificationDate(Carbon::now())
);

return $sitemapIndex->render();

Каждый дочерний sitemap генерируется отдельно и отдаётся по своему маршруту:

Route::get('/sitemaps/products.xml', [SitemapController::class, 'products']);

Внутри контроллера products применяют тот же подход с кэшированием и пакетной обработкой. Пагинация для 200 000 товаров может выглядеть так:

public function products($page = 1)
{
    $itemsPerPage = 40000;
    $offset = ($page - 1) * $itemsPerPage;

    return Cache::remember("sitemap_products_{$page}", 86400, function () use ($offset, $itemsPerPage) {
        $sitemap = Sitemap::create();
        Product::offset($offset)->limit($itemsPerPage)->each(function ($product) use ($sitemap) {
            $sitemap->add(Url::create(route('products.show', $product))
                ->setLastModificationDate($product->updated_at)
                ->setChangeFrequency(Url::CHANGE_FREQUENCY_WEEKLY)
                ->setPriority(0.7));
        });
        return $sitemap->render();
    });
}

Такой подход позволяет уложиться в лимиты и не уронить память. Индексный файл ссылается на все страницы пагинации.

Artisan-команда для генерации

Когда генерация sitemap по HTTP-запросу нежелательна (например, на проектах с высоким трафиком или на мультитенантных системах), создают кастомную Artisan-команду. Она пишет файлы на диск, а веб-сервер отдаёт их статически. Это снимает нагрузку с PHP-процессов.

php artisan make:command GenerateSitemap

Логику помещают в метод handle. Пример для простого новостного сайта:

class GenerateSitemap extends Command
{
    protected $signature = 'sitemap:generate';
    protected $description = 'Генерация sitemap.xml';

    public function handle()
    {
        $sitemap = Sitemap::create();

        Post::where('published', true)->each(function (Post $post) use ($sitemap) {
            $sitemap->add(Url::create(route('posts.show', $post))
                ->setLastModificationDate($post->updated_at)
                ->setChangeFrequency(Url::CHANGE_FREQUENCY_DAILY));
        });

        $sitemap->writeToFile(public_path('sitemap.xml'));
        $this->info('Sitemap сгенерирован: ' . url('sitemap.xml'));
    }
}

Для автоматического запуска по расписанию команду регистрируют в app/Console/Kernel.php (Laravel 11 использует routes/console.php для расписаний). Запуск раз в сутки покрывает большинство кейсов.

// В routes/console.php (Laravel 11+)
use Illuminate\Support\Facades\Schedule;

Schedule::command('sitemap:generate')->dailyAt('03:00');

Для проектов с частыми обновлениями можно запускать генерацию каждый час или даже каждые 15 минут. Но тогда важно оценить время выполнения. Если генерация занимает больше периода запуска, возникают накладки. На одном проекте с 80 000 товаров команда занимала 3 минуты. Расписание настроили раз в 30 минут, добавив флаг блокировки ->withoutOverlapping().

Интеграция с IndexNow

Протокол IndexNow позволяет отправлять уведомления об изменении страниц напрямую в поисковые системы. Яндекс и Bing поддерживают его с 2022 года, Google с 2024 года проводит эксперименты, а в 2026 году протокол стал стандартом де-факто для быстрой индексации. Для Laravel-приложений автоматический пинг настраивают через Observer или систему событий.

При создании или обновлении модели отправляется POST-запрос с URL страницы. Используем HTTP-клиент Laravel:

use Illuminate\Support\Facades\Http;

class PostObserver
{
    public function saved(Post $post)
    {
        $url = route('posts.show', $post);
        
        Http::post('https://api.indexnow.org/indexnow', [
            'host' => parse_url(config('app.url'), PHP_URL_HOST),
            'key' => config('services.indexnow.key'),
            'urlList' => [$url],
        ]);
    }
}

Ключ генерируется один раз и размещается в корне сайта как текстовый файл. В конфиге services.php хранится его значение. Observer регистрируется в AppServiceProvider:

Post::observe(PostObserver::class);

Для массовых обновлений (например, импорт 1000 товаров) отправлять запрос на каждый URL избыточно. Пакет IndexNow API поддерживает до 10 000 URL в одном запросе. В Observer можно собирать URL в очередь и отправлять пачкой через событие Terminating или с использованием очередей Laravel.

На практике удобнее использовать сервисы-агрегаторы вроде Index-Now.ru. Они предоставляют единый API для отправки уведомлений в Яндекс, Bing и Google через IndexNow. Это избавляет от необходимости следить за статусами отдельных поисковиков и управлять ключами. После генерации sitemap достаточно отправить список URL в Index-Now.ru, а сервис распределит их по поисковым системам.

Технические детали для production-окружения

Обработка мягкого удаления (soft deletes)

Модели с SoftDeletes по умолчанию фильтруются глобальным scope, и all() или each() не возвращают удалённые записи. Но если в запросе используется ручное условие или join, нужно явно исключить удалённые:

Post::withoutTrashed()
    ->where('published', true)
    ->chunk(1000, function ($posts) use ($sitemap) { /*...*/ });

Для безопасности всегда проверяйте, что отдаваемые URL возвращают 200. Иногда при восстановлении из резервной копии или ручных манипуляциях в базе появляются висячие ссылки. Периодическая валидация sitemap через поисковые консоли выявляет такие ошибки.

Кэширование на уровне HTTP

Когда sitemap отдаётся через контроллер не кэшированным ответом Laravel, а сгенерированным заранее статическим файлом, веб-сервер может добавить заголовки кэширования. Для Nginx настройка выглядит так:

location = /sitemap.xml {
    add_header Cache-Control "public, max-age=3600";
    add_header ETag $upstream_http_etag;
    try_files $uri /index.php?$query_string;
}

Заголовок Cache-Control указывает поисковикам, что файл можно кэшировать на час. ETag позволяет проверять актуальность без повторной загрузки. Это снижает нагрузку при частых запросах от поисковых роботов.

Генерация sitemap для мультитенантных приложений

В системах, где один код обслуживает несколько доменов (например, saas-платформы), sitemap нужно генерировать для каждого тенанта отдельно. Файлы именуются с идентификатором тенанта: sitemap-{tenant_id}.xml.

foreach (Tenant::all() as $tenant) {
    tenancy()->initialize($tenant);
    
    Sitemap::create()
        ->add(/* URL товаров тенанта */)
        ->writeToFile(public_path("sitemaps/sitemap-{$tenant->id}.xml"));
}

Важно следить, чтобы в каждом файле были URL только одного домена. Поисковые системы игнорируют URL с других хостов. В корне каждого домена должен быть свой индексный файл.

Artisan schedule для периодической генерации

Когда контент обновляется непредсказуемо, генерацию sitemap ставят на расписание. В routes/console.php для Laravel 12:

use Illuminate\Support\Facades\Schedule;

Schedule::command('sitemap:generate')->hourlyAt(15)
    ->withoutOverlapping()
    ->appendOutputTo(storage_path('logs/sitemap.log'));

withoutOverlapping() предотвращает одновременный запуск двух процессов. appendOutputTo() сохраняет логи для отладки. В продакшене логи ротируются системой.

Сравнение подходов к созданию sitemap

МетодПодходит дляНагрузкаАктуальность данных
Автоматический crawlНебольшие сайты (до 500 страниц)НизкаяПо факту генерации
Ручное добавление через моделиКаталоги, блогиСредняяПри каждой генерации
Динамический контроллер с кэшемСайты с частыми обновлениямиНизкая (при кэше)Задержка до сброса кэша
Artisan-команда + статический файлВысоконагруженные проектыНизкая (статический файл)По расписанию

Выбор метода зависит от объёма контента и требований по скорости индексации. Для большинства проектов комбинация Artisan-команды с расписанием и отправка уведомлений через IndexNow даёт лучший баланс.

Часто задаваемые вопросы

Нужно ли добавлять все страницы в sitemap?

Добавляйте только страницы, которые должны индексироваться и приносят трафик. Служебные страницы, страницы тегов с дублированным контентом, личные кабинеты в карту не включают. Это экономит краулинговый бюджет.

Как часто нужно обновлять sitemap?

Частота зависит от типа контента. Для новостных сайтов — каждый час, для блогов — раз в сутки, для лендингов — при изменениях. Поисковые роботы проверяют sitemap не моментально, поэтому слишком частая генерация не даст эффекта.

Что делать с мультиязычными версиями страниц?

Используйте атрибут xhtml:link rel="alternate" для указания языковых вариантов. Это помогает поисковикам показывать правильную версию пользователю и избегать дублей. Пакет spatie/laravel-sitemap поддерживает этот механизм через метод setAlternatives.

Как проверить правильность sitemap?

После генерации откройте файл в браузере и прогоните через валидатор sitemap.org. В Google Search Console в разделе Sitemaps отображаются ошибки. Чаще всего они связаны с неправильными URL или превышением лимитов.

Влияет ли sitemap на позиции в поиске?

Наличие sitemap не гарантирует рост позиций. Он ускоряет обнаружение новых страниц и помогает поисковикам понять структуру сайта. Но ключевое влияние на ранжирование оказывают качество контента и внешние ссылки.

На проектах, где скорость индексации критична, органичным дополнением к sitemap становится отправка URL через IndexNow API. В связке с собственной Artisan-командой пакет spatie/laravel-sitemap закрывает все потребности Laravel-разработчика. А сервис Index-Now.ru позволяет отправлять уведомления в несколько поисковых систем сразу, без настройки каждой отдельно. Подобные решения особенно полезны при запуске новых разделов, когда каждая минута до появления страницы в выдаче влияет на трафик.