Есть готовый сайт, написанный на Laravel + Vue. Руководство попросило сделать его мультиязычным. Зная, что для Laravel “есть все”, я сходу без чтения документации назвал кратчайший срок. Но когда зашел на страничку Vue I18n то понял, что ошибся. Мультиязычность Vue происходит на клиенте, файлы переводов по формату отличаются от файлов Blade. Другими словами, если брать все «из коробки», то переводы придется дублировать и держать в разных местах, js приложение раздуется от подключения плагина I18n и к тому же на клиента придется загружать словари переводов... очевидно же, что надо делать переводы на сервере, то есть все хорошо, но без Vue I18n.

ТЗ формулировалось так:

  1. Сохранить полезный контент страниц неизменным.

  2. Сохранить все url неизменными, поскольку они в индексе у поисковиков

  3. Минимум изменений кода.

  4. Файлы переводов в одном месте.

Важная деталь — изначально проект был написан на Vue 2 и весь код страницы формировался через Blade, то есть контент, заголовки, ключевые слова, списки и ссылки загружались в браузер сразу, поисковики (важен Яндекс) их видели и страницы ранжировались нормально. Vue 3 использует другой подход — весь html должен лежать в шаблоне основного приложения и отрисовываться уже на клиенте, сама страница после загрузки пустая..

Контент

В свое время при переходе с Vue2 на Vue3 возникла дилема:

  1. Перенести контент в шаблон vue‑приложения а на странице оставить пустой <div id=app“></div>”

  2. Оставить контент в шаблоне Blade но вместо компактной runtime библиотеки Vue загружать полный Vue с функционалом компиляции в браузере.

Простой эксперимент показал, что полная версия Vue добавляет 50 Кб к версии runtime, что не является критичным, и к тому же кэшируется на клиенте. Был выбран второй вариант, чтобы сохранить контент страниц неизменным и не рисковать ранжированием в Яндексе. Это обстоятельство оказалось решающим при выборе способа добавления мультиязычности: раз компиляция на клиенте уже есть, то будем на нее полагаться.

С переводом основного контента страницы проблем не было, нативные переводы в Blade требуют только создания файлов с переводами для каждого языка в своем каталоге, и замены всех фрагментов со статическим текстом на вывод через функцию перевода {{_(‘common.MainPageH1’)}}

На какой язык переводить laravel решает исходя из текущей локали запроса, то есть весь механизм под капотом. Минимум вмешательства.

В Vue‑компонентах шаблон всегда идет вместе с кодом в одном файле.vue и подзадача формулировалась так: надо найти способ отделения шаблона от кода. Тогда темплэйт (шаблон) будет формироваться, как и основной контент страницы, через Blade на сервере, а Vite будет компилировать код js из своего отдельного источника частично, оставляю привязку переменных к шаблону на клиенте. Сайт Vue ответа на вопрос, как это сделать, не дал. Но stackoverflow ответил:

// Option api, файл компонента /components/my-component.vue
<script>
    export default {
        template: '#my-component-template',
        data: {...}  
</script>

// В основном приложении app.vue подключаем компонент с динамической загрузкой

import { defineAsyncComponent   } from 'vue'
const app = createApp({
    el: "#app",
    components: {
        'my-component': defineAsyncComponent(() => import('./components/my-component.vue'))
        
    },
  ...
  })
               
// шаблон page.blade.php
<script defer type="text/x-template" id="my-component-template">
   @include('partials.myComponentTemplate')
</script>

Далее, в инклуде пишем свой код, включая v‑if, @click и прочие аттрибуты Vue. Завернул шаблон компонента в скрипт, чтобы парсер страницы не тратил на него время, defer тут излишен скорее всего, но указывает на этот факт.

Роуты и url

Чтобы не ломать и не переписывать роуты laravel и url уже отиндексированных страниц сделал так:

  1. Язык страницы задается префиксом /en/ /fr/ который ставится перед основным url, без префикса язык по умолчанию — русский.

  2. В конфиг вэбсервера добавил секцию переадресации с добавлением к запросу языкового заголовка «X‑Language: en»

В моем случае, это секция выглядет так:

upstream php_upstream_1 {
        server 127.0.0.1:8013;
}

location ~* ^/(en|ru)(/.*)?$ {
    set $lang $1;
    set $efurl $2;
    rewrite ^ @index_php last;
}

location @index_php {
    if ($efurl = '') {set $efurl '/';}
    proxy_pass http://php_upstream_1$efurl$is_args$args;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Language $lang;

        }

А в laravel встречаем заголовок установкой локали в мидлваре, которую подключил глобально ко всем запросам.

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SetLocale
{
    public function handle(Request $request, Closure $next)
    {   
        $lang = $request->header('X-Language', config('app.default_locale'));
        app()->setLocale($lang);
        
        return $next($request);
    }
}

Конечно, чтение документации и поиски оптимальной реализации ТЗ заняли больше времени, чем непосредственно кодирование, но у Вас есть преимущество — результат поисков перед вами.

Плюсы:

  • Минимум кодирования. В blade файлах — Поиск статических фраз в интерфейсе и замена их на вывод через функцию перевода. В vue файлах по сути ничего не менялось, только перенос содержимого template компонентов в другой файл.blade.php

  • Роуты не трогались вообще. Для настройки локали добавлен middleware на 3 строки кода.

  • Никаких дополнительных сервисов, плагинов и модулей на серверной стороне, все на старых санях.

Минусы:

  • Финальная компиляция «на клиенте» технически задерживает отрисовку страницы, хотя фактически этого не видно, но гугл инструменты видят этот факт.

  • Код шаблона компонента загружается вместе с контентом основной страницы, хотя используется только при условии активации компонента. Это раздувает код страницы, но в моем случае очень незначительно — 4кБ

  • Кастомный конфиг nginx — минус спорный, ибо если есть конфиг, значит конфигурирование является нормальной практикой, но я знаю, что многие не любят такого подхода.

вот что показывает гугл тест страницы

Хотя на странице ничего визуально не меняется, время отображения самого большого элемента существенное. Влияет ли это на ранжирование — непонятно.

Комментарии (3)


  1. supercat1337
    29.08.2026 11:29

    Спасибо за публикацию, очень интересно. Правильно ли я понял, что выводится не готовый html, а сырой template, и за счёт того, что в шаблонах присутствует разметка, это решает проблему seo?

    Как решается отображение иерархии компонентов и списков? Я так понимаю, что построение списков роботы не увидят.


    1. Anrol Автор
      29.08.2026 11:29

      для страницы выводим готовый самый настоящий html, который тоже является шаблоном для основного приложения, но "с резолвингом" в браузере, это достигается загрузкой полного vue в app.vue:

      // вместо import { createApp } from 'vue';
      import { createApp } from 'vue/dist/vue.esm-bundler';

      То есть страница несет в себе весь сео-контент, что нам и надо.

      для компонентов, которые загружаются динамически и как правило несут в себе поторяющуюся от страницы к странице неуникальную информацию, делаем динамическую загрузку кода js, а код шаблона уже загружен на основную страницу, в моем случае я завернул его в <script>. Он будет проигнорирован поисковиками и правильно, ведь данные для него еще не загружены и смысла читать пустые тэги нет. Да, списки и ссылки которые формирует компонент будут недоступны Яндексу, но я к этому и не стремился. А если такая необходимость есть, то для каждого конкретного случая - свое решение, компромисное конечно и продиктованное исключительно несовершенством Яндекса. Я так понимаю, что гугл умеет получать полный контент страницы даже с динамической загрузкой, но не проверял, а Яндекс видит только то, что загружено сразу.


      1. supercat1337
        29.08.2026 11:29

        Благодарю за разъяснения, интересно.