SEO-оптимизация Laravel сайта

Практическое руководство по SEO-оптимизации сайта на Laravel. Настройка мета-тегов, скорости загрузки и краулингового бюджета.

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 и Яндекс.Вебмастер.