0% прочитано

Laravel Ziggy: узгоджена робота в Blade, Vue та Inertia SSR

Ziggy може бездоганно працювати в браузері й водночас ламатися під час SSR. Розбираю, як побудувати єдиний routing contract для Laravel, Blade, Vue та Inertia SSR і не створювати різні route contexts у межах одного застосунку.

15 серпня 2026 р. 8 хв читанняЛаравель

Коли я почала інтегрувати logical routes у Vue та Inertia SSR, здавалося, що найскладніша частина вже позаду. Laravel знав маршрути, Ziggy працював у браузері, frontend збирався без помилок.

А потім SSR почав падати на маршрутах, які абсолютно нормально працювали після завантаження сторінки в браузері.

Саме в цей момент стало зрозуміло: мало мати правильні routes. Потрібно ще, щоб Blade, Vue і SSR дивилися на одну й ту саму Ziggy configuration.

У цій статті не про localization rules. Вони залишилися в попередній частині. Тут я хочу показати саме integration boundary між Laravel, Ziggy, Vue та Inertia SSR.

Коли browser працює, а SSR - ні

Найбільше мене збивало те, що помилка не виглядала як проблема з routing configuration. У браузері переходи працювали, потрібний route існував у Laravel, а client build був чистим.

Але SSR виконує Vue application окремо - у Node process. І якщо цей process отримав іншу Ziggy configuration, результат уже може бути зовсім іншим.

Типова помилка в моєму випадку виглядала так:

text
TypeError: Cannot read properties of undefined (reading 'posts.index')

posts.index існував. Проблема була не в самому route, а в тому, яку configuration бачив Ziggy під час SSR.

Один route helper ще не означає один routing context

На frontend дуже легко побачити однаковий виклик route('posts.index') і вирішити, що всюди використовується одна й та сама логіка.

Насправді route helper може працювати з різними configurations залежно від того, де його створили і як підключили Ziggy. Blade має свій context, browser - свій, а SSR - ще один.

Для мене правильна модель тепер виглядає так:

text
-----------------------    Laravel routes  Ziggy configuration┌──────────┬───────────┐ Blade     Vue      Inertia SSR    same route API

Один route() у коді ще не гарантує один routing contract.

ZiggyVue як спільна точка для Vue

Якщо application уже встановлює ZiggyVue, я не хочу, щоб кожен component окремо створював власний route context.

Логічніше один раз передати потрібну configuration в plugin, а components нехай використовують route helper, який уже надає Vue application.

У SSR setup це може виглядати так:

javascript
.use(ZiggyVue, initialProps.ziggy)

А в component route helper можна отримати через Vue dependency injection.

javascript
import { inject } from 'vue'; const route = inject('route');
Так component не вирішує сам, звідки взяти routes. Він використовує routing context поточного application instance.

Чому direct Ziggy import став проблемою

Спочатку direct import виглядає абсолютно природно: імпортувати route з Ziggy і використовувати його там, де потрібно.

Але в application з SSR це створює ризик, що component працюватиме не з тією configuration, яку ми передали в ZiggyVue. У browser цього можна навіть не помітити, особливо якщо потрібні routes доступні глобально або hydration уже відбувся.

Для мене стало простіше тримати одне правило: Vue components використовують route helper із Vue application context.

javascript
// Instead of creating another routing contextimport { route } from 'ziggy-js';
javascript
// Use the route helper provided by ZiggyVueimport { inject } from 'vue'; const route = inject('route');

Inertia props з’єднують Laravel і SSR

Для SSR потрібна точка, через яку Laravel передасть актуальну Ziggy configuration у Node process.

У моєму випадку цією точкою стали Inertia shared props. Laravel формує configuration на backend, вона потрапляє в initial page props, а SSR bootstrap передає її в ZiggyVue.

Це дозволяє не відтворювати routing logic ще раз у JavaScript.

php
'ziggy' => fn (): array => app(Ziggy::class)->toArray(),
javascript
const initialProps = props.initialPage.props || {}; return createSSRApp({  render: () => h(App, props),})  .use(plugin)  .use(ZiggyVue, initialProps.ziggy);

Context

Ziggy configuration

Laravel / Blade

Контекст маршрутизації Laravel

Vue in browser

Inertia page props

Inertia SSR

Ті самі props початкової сторінки

Programmatic Ziggy теж не можна забувати

Ще одна деталь, яку легко пропустити: Ziggy використовується не тільки через Blade directive або Vue plugin.

Configuration можна запросити безпосередньо в Laravel code. І якщо application має custom routing projection, programmatic usage теж повинно бачити той самий результат.

Тому я окремо перевіряла, що container повертає потрібну Ziggy implementation.

php
$ziggy = app(\Tighten\Ziggy\Ziggy::class); $data = $ziggy->toArray();

У моєму випадку posts.index був у цій configuration. Це стало важливою підказкою: проблема була не в route registration.

Як я звужувала проблему

Тут найбільше допомогло не змінювати одразу весь routing code, а перевіряти по одному рівню.

Спочатку я підтвердила, що Laravel бачить logical route. Потім, що container повертає правильну Ziggy configuration. Після цього вже можна було дивитися, що саме доходить до Vue та SSR.

Так значно легше відрізнити проблему route registration від проблеми передачі configuration.

  • перевірити, що route існує в Laravel;

  • перевірити Ziggy configuration на backend;

  • перевірити initialProps.ziggy;

  • перевірити, звідки component отримує route;

  • перевірити browser окремо від SSR;

  • після зміни перебудувати client і SSR bundles.

Найгірший варіант у такій ситуації - почати додавати fallback routes у components. Це може приховати симптом, але не виправляє різні routing contexts.

Що в результаті спрацювало

У результаті рішення виявилося не в одному магічному рядку.

Потрібно було зробити routing flow послідовним:

  1. Backend формує правильну Ziggy configuration;

  2. Inertia передає її в page props;

  3. SSR використовує ці props під час bootstrap;

  4. Vue components беруть route із ZiggyVue context.

Після цього browser та SSR перестали жити у двох різних routing worlds.

text
---------------------------        LaravelLogical Ziggy configuration     Inertia props         ZiggyVue      Vue components      Browser + SSR

Я не хотіла виправляти кожен component. Я хотіла виправити місце, де application отримує routing context.

Що я тепер перевіряю після таких змін

Після routing або SSR змін одного успішного npm run build для мене вже недостатньо.

Схожу ситуацію я вже бачила з meta tags в Inertia SSR: у браузері все виглядало правильно, але потрібних meta tags не було в початковому HTML.

  • client build проходить;

  • SSR build проходить;

  • SSR process запущений;

  • звичайна public page відкривається;

  • перемикання locale працює;

  • Vue navigation працює без console errors;

  • admin navigation не має regressions;

  • після smoke test у Laravel log немає нових SsrException.

Окремо корисно ставити мітку в log перед smoke test. Старі SSR errors легко сплутати зі свіжими, особливо коли кілька разів перезапускаєш process і перевіряєш різні сторінки.

Я тепер дивлюся тільки на записи, які з’явилися після конкретного контрольного request.

Log marker замість здогадок

Простий wc -l виявився дуже корисним інструментом для такої перевірки.

bash
wc -l storage/logs/laravel-2026-08-12.log

Після контрольного request я читаю тільки нові рядки.

bash
tail -n +121 storage/logs/laravel-2026-08-12.log
Якщо output порожній - це набагато надійніше, ніж дивитися останні 300 рядків і намагатися згадати, яка помилка була свіжою.

Прості правила, які я залишила собі

  • один routing contract для browser і SSR;

  • одна Ziggy configuration на request;

  • Vue components не створюють власний Ziggy context;

  • localization details залишаються на backend boundary;

  • build success не замінює SSR smoke test;

  • logs перевіряються тільки після контрольної мітки.

Висновок

Ziggy сам по собі тут не був проблемою. Проблемою було те, що один application може непомітно отримати кілька різних routing contexts.

Коли я звела Laravel, Inertia props, ZiggyVue та SSR до одного flow, debugging став набагато простішим. І тепер я дивлюся на route() не просто як на helper, а як на споживач конкретної routing конфігурації.

Але після цього залишився ще один дуже цікавий edge case: звичайні сторінки вже працювали чисто, а 404 все ще міг зламати SSR.