Карта сайта для 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 позволяет отправлять уведомления в несколько поисковых систем сразу, без настройки каждой отдельно. Подобные решения особенно полезны при запуске новых разделов, когда каждая минута до появления страницы в выдаче влияет на трафик.