SEO-оптимизация Laravel сайта начинается на этапе проектирования архитектуры. По состоянию на 2026 год разработчики используют Laravel 12 с PHP 8.3. Поисковые системы ожидают корректные мета-теги Laravel, быструю загрузку страниц и структурированные данные. В этой статье разберём, как выстроить продвижение Laravel сайта от маршрутизации до моментальной индексации через IndexNow. Рассмотрим пакеты, middleware, кэширование и генерацию sitemap.
SEO-архитектура Laravel приложения
MVC-паттерн в Laravel позволяет отделить логику SEO от представления. Мета-теги, заголовки и канонические ссылки формируются на основе данных из модели. Контроллер передаёт эти данные в Blade-шаблон. View Composer или middleware могут автоматически добавлять базовые теги на все страницы.
На практике создают отдельный сервис-класс или трейт для управления мета-данными. Это упрощает поддержку. Например, на проекте с 20 000 товаров мы реализовали модель SeoMeta, связанную с товарами и категориями через полиморфное отношение. Каждая страница получала уникальный title и description из базы данных.
Основные компоненты архитектуры:
- Модель для хранения SEO-полей в БД.
- View Composer, который передаёт значения по умолчанию для всех страниц.
- Blade-компонент, который рендерит полный блок .
- Middleware для каноникализации и редиректов.
Такая архитектура сокращает дублирование кода. Разработчик меняет логику в одном месте, и изменения применяются на всех страницах.
Eloquent модель для хранения SEO мета-данных
Создадим миграцию для таблицы seo_meta. В ней будем хранить title, description, opengraph-данные и канонический URL. Добавим полиморфные поля, чтобы привязываться к любым моделям: товарам, статьям, категориям.
// Миграция
Schema::create('seo_meta', function (Blueprint $table) {
$table->id();
$table->morphs('seoable'); // seoable_type, seoable_id
$table->string('title')->nullable();
$table->text('description')->nullable();
$table->string('canonical_url')->nullable();
$table->string('og_title')->nullable();
$table->text('og_description')->nullable();
$table->string('og_image')->nullable();
$table->json('additional')->nullable(); // для нестандартных тегов
$table->timestamps();
});
Модель SeoMeta использует отношение morphTo. Модели, поддерживающие SEO, реализуют интерфейс HasSeoMeta. В интерфейсе определён метод seo().
// App\Models\SeoMeta.php
class SeoMeta extends Model
{
protected $fillable = [
'title', 'description', 'canonical_url',
'og_title', 'og_description', 'og_image', 'additional'
];
protected $casts = [
'additional' => 'array',
];
public function seoable()
{
return $this->morphTo();
}
}
В моделях, например Product, описываем связь:
public function seo(): MorphOne
{
return $this->morphOne(SeoMeta::class, 'seoable');
}
Этот подход удобен при массовом управлении мета-данными. Artisan-команда может заполнить пустые поля на основе шаблонов. Например, генерировать title из названия товара и категории.
View Composer для передачи SEO-данных
View Composer позволяет передавать переменные во все представления без дополнительного кода в контроллерах. Регистрируем его в AppServiceProvider:
View::composer('*', function ($view) {
$view->with('defaultSeo', [
'title' => config('app.name'),
'description' => 'Описание сайта по умолчанию',
'canonical' => url()->current(),
]);
});
В шаблоне layout.blade.php используем переданные значения как резервные. Дочерние шаблоны могут переопределять их через @section.
Мета-теги через Blade
Blade позволяет гибко управлять секциями. Базовый layout содержит плейсхолдеры для title, description и других тегов. Дочерние шаблоны заполняют эти секции. В случае отсутствия данных срабатывают значения по умолчанию.
Базовая реализация без пакетов
В файле resources/views/layouts/app.blade.php организуем вывод мета-тегов:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<title>@hasSection('title') @yield('title') @else {{ $defaultSeo['title'] }} @endif</title>
<meta name="description" content="@hasSection('description') @yield('description') @else {{ $defaultSeo['description'] }} @endif">
<link rel="canonical" href="@hasSection('canonical') @yield('canonical') @else {{ $defaultSeo['canonical'] }} @endif">
@hasSection('og_title')
<meta property="og:title" content="@yield('og_title')">
@endif
<!-- другие теги -->
</head>
Дочерняя страница определяет секции:
@extends('layouts.app')
@section('title', $product->seo->title ?? $product->name)
@section('description', $product->seo->description ?? Str::limit(strip_tags($product->description), 160))
@section('canonical', route('products.show', $product->slug))
@section('content')
...
@endsection
Этот метод работает без дополнительных зависимостей. Но для комплексного управления OpenGraph, Twitter Cards и JSON-LD удобнее применить пакет.
Пакет artesaos/seotools
Пакет artesaos/seotools даёт фасады для генерации мета-тегов. Он интегрирован с Laravel через сервис-провайдер. Установка:
composer require artesaos/seotools
Публикуем конфигурацию и задаём значения по умолчанию в config/seotools.php. Фасад SEOMeta управляет стандартными тегами, OpenGraph — разметкой для соцсетей, TwitterCards — мета-тегами Twitter.
Пример контроллера с использованием фасадов:
use Artesaos\SEOTools\Facades\SEOMeta;
use Artesaos\SEOTools\Facades\OpenGraph;
use Artesaos\SEOTools\Facades\JsonLd;
public function show(Product $product)
{
SEOMeta::setTitle($product->seo->title ?? $product->name);
SEOMeta::setDescription($product->seo->description ?? Str::limit(strip_tags($product->description), 160));
SEOMeta::setCanonical(route('products.show', $product->slug));
OpenGraph::setTitle($product->seo->og_title ?? $product->seo->title ?? $product->name);
OpenGraph::setDescription($product->seo->og_description ?? $product->seo->description);
OpenGraph::addImage($product->seo->og_image ?? asset('images/default.png'));
OpenGraph::setUrl(route('products.show', $product->slug));
OpenGraph::addProperty('type', 'product');
JsonLd::setTitle($product->seo->title ?? $product->name);
JsonLd::addValue('@context', 'https://schema.org');
JsonLd::addValue('@type', 'Product');
return view('products.show', compact('product'));
}
В шаблоне достаточно вызвать метод SEO::generate(). Он подставит все теги. Пакет также поддерживает мультиязычность через локализацию.
На практике мы использовали SEOTools на сайте интернет-магазина с 15 000 товаров. Мета-теги формировались динамически на основе SEO-модели. Это сократило время разработки и исключило ошибки дублирования title.
Пакет spatie/laravel-seo
Spatie/laravel-seo — более лёгкий пакет. Он не требует фасадов. Мета-теги задаются через класс Spatie\LaravelSeo\Seo. Объект передаётся в представление и там рендерится.
Установка:
composer require spatie/laravel-seo
Контроллер заполняет объект:
use Spatie\LaravelSeo\Seo;
public function show(Post $post)
{
$seo = new Seo();
$seo->title($post->title);
$seo->description(Str::limit(strip_tags($post->content), 160));
$seo->image(asset($post->image));
$seo->url(route('posts.show', $post->slug));
return view('posts.show', compact('post', 'seo'));
}
В шаблоне:
<head>
{!! $seo->render() !!}
</head>
Пакет автоматически генерирует OpenGraph, Twitter Cards и канонический URL. Он также поддерживает кастомные теги. Привязка к моделям происходит вручную — разработчик сам решает, откуда брать данные.
Сравнение пакетов
| Характеристика | artesaos/seotools | spatie/laravel-seo |
|---|---|---|
| Фасады | Да | Нет |
| JSON-LD | Встроенная поддержка | Не поддерживает |
| Мультиязычность | Поддерживает | Нет |
| Привязка к Eloquent | Вручную | Вручную |
| Совместимость с Laravel 12 | Да | Да |
| Количество зависимостей | Среднее | Минимальное |
Выбор зависит от требований. SEOTools хорош для сложных проектов с разными типами страниц и JSON-LD. Spatie/laravel-seo подходит для блогов и корпоративных сайтов без необходимости в структурированных данных из коробки.
Маршрутизация и ЧПУ
Дружественные URL влияют на CTR в поисковой выдаче. Laravel позволяет легко создавать читаемые адреса с параметром slug. Ключевое правило: URL должен содержать смысловые слова, а не внутренние идентификаторы.
Стандартный маршрут для страницы товара:
Route::get('/products/{slug}', [ProductController::class, 'show'])->name('products.show');
Вместо slug можно использовать модель с автоматическим разрешением по полю, отличному от id. Для этого в модели переопределяем метод getRouteKeyName:
class Product extends Model
{
public function getRouteKeyName(): string
{
return 'slug';
}
}
Теперь Route Model Binding подставляет модель, найденную по slug. Генерация slug происходит при создании записи. На проектах используем трейт HasSlug, который использует Str::slug.
Конвенции URL
Рекомендации по структуре адресов:
- Используйте дефисы для разделения слов (kebab-case). Например:
/catalog/elektronnye-komponenty. - Избегайте заглавных букв. Laravel автоматически приводит slug к нижнему регистру.
- Не добавляйте расширение .php в конце адреса.
- Не используйте идентификаторы в URL. Ссылка
/articles/laravel-seo-optimizationлучше, чем/articles/123.
Для категорий применяем вложенные маршруты с префиксом родительского slug. Это укрепляет иерархию сайта. Например:
Route::get('/catalog/{category}/{product}', [ProductController::class, 'show'])
->where('category', '[a-z0-9-]+')
->where('product', '[a-z0-9-]+');
Важно не допускать дублей slug. Валидация при создании и обновлении модели проверяет уникальность.
Кэширование и скорость
Скорость загрузки страниц — один из факторов ранжирования. С марта 2024 года Core Web Vitals включает метрику INP (Interaction to Next Paint), которая учитывает отзывчивость интерфейса. Laravel предоставляет инструменты для кэширования на разных уровнях.
Виды кэширования в Laravel
- Route caching. Команда
php artisan route:cacheсериализует все маршруты в один файл. Ускоряет регистрацию маршрутов. - Config caching.
php artisan config:cacheобъединяет конфигурационные файлы. - View caching. Blade-шаблоны компилируются в PHP-код и кэшируются в
storage/framework/views. - Data caching. Фасад
Cacheпозволяет кэшировать результаты запросов к БД.
Для продакшен-окружения обязательно выполняем:
php artisan optimize
Эта команда запускает route:cache и config:cache. Не забывайте очищать кэш при деплое через php artisan optimize:clear, если требуется сброс.
Laravel Octane
Octane удерживает приложение в памяти между запросами, увеличивая пропускную способность. Поддерживаются серверы RoadRunner и Swoole. По тестам на проекте с высоконагруженным блогом (около 50 000 посетителей в сутки) время ответа сократилось с 220 мс до 35 мс.
Установка Octane требует настройки воркера и исключения глобальных переменных, которые не должны сохраняться между запросами. Важно отказаться от статических свойств в контроллерах и сервисах.
Octane не заменяет остальные методы кэширования. Он ускоряет начальную загрузку приложения, но не сокращает время выполнения длительных запросов к базе.
Настройка Cache-Control заголовков
Статические ресурсы (изображения, CSS, JS) должны кэшироваться на стороне клиента и CDN. Для этого в Laravel можно создать middleware, который добавляет заголовки для ответов с определёнными расширениями.
// Middleware для статики
public function handle($request, Closure $next)
{
$response = $next($request);
$extensions = ['css', 'js', 'jpg', 'png', 'svg', 'woff2'];
$extension = pathinfo($request->path(), PATHINFO_EXTENSION);
if (in_array($extension, $extensions)) {
$response->header('Cache-Control', 'public, max-age=31536000');
}
return $response;
}
Регистрируем middleware в Kernel.php. Для динамических страниц задаём короткие сроки кэширования через метод cacheHeaders в контроллере:
return response()->view('page', $data)
->header('Cache-Control', 'public, max-age=3600');
С осторожностью используйте кэширование на страницах с персонализированным контентом. Поисковые боты не должны получать кэшированные версии, содержащие данные пользователя.
Использование очередей для генерации sitemap
На больших сайтах генерация sitemap может занять десятки секунд. Задачу выносят в очередь. Создаём Job, который обходит модели и формирует XML-файлы.
Пример Job для sitemap товаров:
class GenerateProductSitemap implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle()
{
$products = Product::select('slug', 'updated_at')->cursor();
$xml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>');
foreach ($products as $product) {
$url = $xml->addChild('url');
$url->addChild('loc', route('products.show', $product->slug));
$url->addChild('lastmod', $product->updated_at->toAtomString());
$url->addChild('changefreq', 'weekly');
$url->addChild('priority', '0.7');
}
Storage::disk('public')->put('sitemaps/products.xml', $xml->asXML());
}
}
Запуск по расписанию через app/Console/Kernel.php:
$schedule->job(new GenerateProductSitemap)->daily();
Такой подход позволяет обновлять sitemap без задержек для пользователей. Подробнее о создании карты сайта — в материале «Как создать sitemap для Laravel сайта».
Middleware для SEO
Middleware в Laravel позволяют перехватывать HTTP-запросы и модифицировать ответы. Для SEO критичны редиректы на канонический URL, принудительный HTTPS и обработка завершающего слэша.
Каноникализация и редиректы
Дубли страниц с разными URL (с www и без, с / в конце и без) снижают эффективность индексации. Создадим middleware, который выполняет 301 редирект на основной домен и убирает trailing slash.
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Str;
class CanonicalizeUrl
{
public function handle($request, Closure $next)
{
$url = $request->fullUrl();
$canonical = rtrim($url, '/');
if ($url !== $canonical) {
return redirect($canonical, 301);
}
return $next($request);
}
}
Для принудительного www или non-www используем другой middleware. Лучше не смешивать логику, чтобы было проще тестировать.
public function handle($request, Closure $next)
{
$host = $request->getHost();
if (!Str::startsWith($host, 'www.')) {
$redirect = 'https://www.' . $host . $request->getRequestUri();
return redirect($redirect, 301);
}
return $next($request);
}
Регистрируем middleware в Kernel.php как глобальный или для группы web. Порядок важен: сначала www-редирект, затем HTTPS, затем trailing slash.
Принудительный HTTPS
Laravel имеет встроенный middleware RedirectIfHttps. Для продакшена настраиваем в .env:
APP_ENV=production
APP_URL=https://www.mysite.com
В AppServiceProvider добавляем:
if (config('app.env') === 'production') {
URL::forceScheme('https');
}
Это гарантирует, что все URL, генерируемые хелперами route(), будут с HTTPS. На уровне веб-сервера также настраиваем редирект. Для Nginx это правило:
server {
listen 80;
server_name mysite.com www.mysite.com;
return 301 https://www.mysite.com$request_uri;
}
На проекте с высокими требованиями к безопасности мы столкнулись с тем, что Google индексировал HTTP-версии страниц, несмотря на канонический мета-тег. После настройки 301 редиректа на уровне Nginx проблема ушла за 2 недели.
Middleware для заголовков безопасности и SEO
Middleware может добавлять заголовок X-Robots-Tag для определённых страниц. Например, страницы пагинации, поиска или фильтрации с GET-параметрами должны запрещать индексацию.
public function handle($request, Closure $next)
{
$response = $next($request);
if ($request->has('page') || $request->has('sort')) {
$response->header('X-Robots-Tag', 'noindex, follow');
}
return $response;
}
Используйте этот подход вместо мета-роботов, так как заголовок обрабатывается до загрузки страницы. Подключайте middleware к конкретным маршрутам в контроллере или через группу.
Structured Data
Структурированные данные помогают поисковым системам понимать содержимое страницы. Формат JSON-LD рекомендован Google. Laravel позволяет встраивать JSON-LD через Blade-компоненты или пакеты.
JSON-LD через Blade-компоненты
Создадим компонент structured-data, который принимает массив данных и выводит их в тег script.
// App\View\Components\StructuredData.php
class StructuredData extends Component
{
public array $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function render()
{
return view('components.structured-data');
}
}
<!-- components/structured-data.blade.php -->
<script type="application/ld+json">
{!! json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) !!}
</script>
На странице товара передаём данные:
return view('products.show', [
'product' => $product,
'structuredData' => [
'@context' => 'https://schema.org',
'@type' => 'Product',
'name' => $product->name,
'description' => $product->short_description,
'sku' => $product->sku,
'offers' => [
'@type' => 'Offer',
'price' => $product->price,
'priceCurrency' => 'RUB',
'availability' => $product->in_stock ? 'InStock' : 'OutOfStock',
],
],
]);
В layout подключаем:
@isset($structuredData)
@endisset
Такой подход не требует дополнительных пакетов. Но для сложных схем с вложенными объектами удобнее использовать специализированную библиотеку.
Пакет spatie/schema-org
Spatie/schema-org предоставляет объектно-ориентированный способ создания Schema.org разметки. Установка:
composer require spatie/schema-org
Пример генерации схемы для статьи:
use Spatie\SchemaOrg\Schema;
$articleSchema = Schema::article()
->headline($post->title)
->description($post->excerpt)
->datePublished($post->created_at)
->dateModified($post->updated_at)
->author(Schema::person()->name($post->author->name))
->publisher(Schema::organization()->name(config('app.name'))->logo(asset('logo.png')))
->image(asset($post->image));
$structuredData = $articleSchema->toArray();
return view('posts.show', compact('post', 'structuredData'));
Пакет включает типы для товаров, организаций, хлебных крошек, FAQ и других. Он валидирует структуру на соответствие спецификации. На сайте каталога мы использовали его для автоматического формирования хлебных крошек через BreadcrumbList.
Преимущество пакета — строгая типизация и поддержка всех актуальных свойств. Избегайте ошибок с пропущенными обязательными полями. При тестировании через Google Rich Results Test ошибки не возникают.
Практический пример: массовое обновление мета-данных
На крупных сайтах регулярно требуется обновить SEO-теги по определённому правилу. Например, добавить название категории в title всех товаров этой категории. Ручное изменение через админ-панель неэффективно. Поможет Artisan-команда.
Artisan-команда для SEO
Создаём команду seo:update-product-title. Она принимает ID категории и обновляет title у связанных товаров.
// app/Console/Commands/UpdateProductTitle.php
class UpdateProductTitle extends Command
{
protected $signature = 'seo:update-product-title {category_id}';
public function handle()
{
$categoryId = $this->argument('category_id');
$category = Category::with('products')->findOrFail($categoryId);
foreach ($category->products as $product) {
$seo = $product->seo()->firstOrNew();
$seo->title = $product->name . ' — купить в категории ' . $category->name;
$seo->description = $seo->description ?: Str::limit(strip_tags($product->description), 140);
$seo->save();
}
$this->info('Обновлено товаров: ' . $category->products->count());
}
}
Регистрируем команду в Kernel.php. Запускаем:
php artisan seo:update-product-title 5
Для массового обновления всех товаров можно использовать чанки и асинхронную обработку через очередь. Это защитит от превышения лимита памяти.
На проекте интернет-магазина с 30 000 товаров мы создали команду, которая раз в месяц перегенерирует description на основе первых 160 символов описания товара с добавлением цены. Это позволило поддерживать актуальность мета-тегов без участия контент-менеджеров.
IndexNow и мгновенная индексация
После обновления страниц или добавления новых нужно оперативно уведомить поисковые системы. Протокол IndexNow поддерживают Яндекс, Bing и Naver с 2022 года. Google с 2024 года тестирует экспериментальную поддержку. Сервис Index-Now.ru предоставляет удобный API для отправки URL через IndexNow.
Интегрировать отправку просто. После сохранения модели товара в контроллере или через события вызываем HTTP-запрос к IndexNow API. Пример через Http-клиент Laravel:
use Illuminate\Support\Facades\Http;
$urls = [route('products.show', $product->slug)];
$key = config('services.indexnow.key');
Http::post('https://api.index-now.ru/indexnow', [
'host' => parse_url(config('app.url'), PHP_URL_HOST),
'key' => $key,
'urlList' => $urls,
]);
Для больших объёмов используем очередь. Это повысит скорость индексации по сравнению с ожиданием естественного обхода. Подробнее про индексацию читайте в статье «Laravel — SEO и индексация».
Частые вопросы
Какой пакет для SEO в Laravel лучше выбрать в 2026 году?
Выбор зависит от потребностей. Для большинства проектов достаточно artesaos/seotools. Он поддерживает JSON-LD, OpenGraph и мультиязычность. Если нужна только базовая работа с мета-тегами без структурированных данных, используйте spatie/laravel-seo.
Нужно ли кэшировать маршруты, если используется Laravel Octane?
Octane ускоряет загрузку приложения, но не кэширует маршруты автоматически. Команда route:cache по-прежнему полезна. Она уменьшает количество файлов, которые загружает композер. Выполняйте её при каждом деплое.
Как избежать дублей страниц из-за параметров сортировки?
Используйте middleware для добавления канонического URL через заголовок HTTP или тег link. При обнаружении GET-параметров типа ?sort=price автоматически подставляйте канонический URL без параметров. Также настройте noindex для страниц со случайным набором параметров через X-Robots-Tag.
Стоит ли хранить SEO-метаданные в отдельных таблицах?
Да, если сайт содержит тысячи страниц. Отдельная таблица с полиморфной связью позволяет гибко управлять мета-тегами и добавлять кастомные поля без изменения основных моделей. Для небольших сайтов можно хранить поля title и description прямо в таблице модели.
Как ускорить индексацию новых страниц на Laravel-сайте?
Используйте протокол IndexNow. Через сервис Index-Now.ru можно отправлять до 10 000 URL за один запрос. Добавьте вызов API в события модели после создания или обновления. Не забывайте обновлять sitemap и отправлять его в Google Search Console и Яндекс.Вебмастер.