Коли я почала інтегрувати logical routes у Vue та Inertia SSR, здавалося, що найскладніша частина вже позаду. Laravel знав маршрути, Ziggy працював у браузері, frontend збирався без помилок.
А потім SSR почав падати на маршрутах, які абсолютно нормально працювали після завантаження сторінки в браузері.
Саме в цей момент стало зрозуміло: мало мати правильні routes. Потрібно ще, щоб Blade, Vue і SSR дивилися на одну й ту саму Ziggy configuration.
Попередня частина: Laravel Localization: логічні назви маршрутів для локалізованих URL.
Коли browser працює, а SSR - ні
Найбільше мене збивало те, що помилка не виглядала як проблема з routing configuration. У браузері переходи працювали, потрібний route існував у Laravel, а client build був чистим.
Але SSR виконує Vue application окремо - у Node process. І якщо цей process отримав іншу Ziggy configuration, результат уже може бути зовсім іншим.
Типова помилка в моєму випадку виглядала так:
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 - ще один.
Для мене правильна модель тепер виглядає так:
----------------------- 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 це може виглядати так:
.use(ZiggyVue, initialProps.ziggy)А в component route helper можна отримати через Vue dependency injection.
import { inject } from 'vue'; const route = inject('route');Чому direct Ziggy import став проблемою
Спочатку direct import виглядає абсолютно природно: імпортувати route з Ziggy і використовувати його там, де потрібно.
Але в application з SSR це створює ризик, що component працюватиме не з тією configuration, яку ми передали в ZiggyVue. У browser цього можна навіть не помітити, особливо якщо потрібні routes доступні глобально або hydration уже відбувся.
Для мене стало простіше тримати одне правило: Vue components використовують route helper із Vue application context.
// Instead of creating another routing contextimport { route } from 'ziggy-js';// 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.
'ziggy' => fn (): array => app(Ziggy::class)->toArray(),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.
$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.
Що в результаті спрацювало
У результаті рішення виявилося не в одному магічному рядку.
Потрібно було зробити routing flow послідовним:
Backend формує правильну Ziggy configuration;
Inertia передає її в page props;
SSR використовує ці props під час bootstrap;
Vue components беруть
routeіз ZiggyVue context.
Після цього browser та SSR перестали жити у двох різних routing worlds.
--------------------------- Laravel ↓ Logical 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 виявився дуже корисним інструментом для такої перевірки.
wc -l storage/logs/laravel-2026-08-12.logПісля контрольного request я читаю тільки нові рядки.
tail -n +121 storage/logs/laravel-2026-08-12.logПрості правила, які я залишила собі
один 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.



