tightenco/ziggy
Ziggy gives you a JavaScript route() function that works just like Laravel's, so you can use your named Laravel routes in JavaScript. Add the @routes Blade directive to your layout and the route() helper is available globally.
Install the PHP side, then drop the Blade directive into your layout:
composer require tightenco/ziggy
{{-- resources/views/layouts/app.blade.php --}}
<head>
@routes
@vite('resources/js/app.js')
</head>
That's it for the classic Blade + bundled-JS setup. route() is now a global and accepts the same arguments as Laravel's helper:
route('posts.index'); // https://myapp.test/posts
route('posts.show', 1); // https://myapp.test/posts/1
route('posts.show', { post: 1 }); // same
Where to look first: the README's "Usage" and "Filtering Routes" sections — filtering matters more than it looks, because @routes inlines every named route (names, URIs, methods, domains) into the HTML by default. Your first real task is almost always replacing a hardcoded /api/posts/1 in an axios call with route('posts.show', post), immediately followed by trimming what @routes emits.
If you're building a Vue/React SPA or using a separate frontend repo, skip the directive and generate a config file instead:
php artisan ziggy:generate # resources/js/ziggy.js
php artisan ziggy:generate --types # + TypeScript route/param types
Then alias the vendor path in Vite so imports read cleanly:
// vite.config.js
resolve: { alias: { 'ziggy-js': path.resolve('vendor/tightenco/ziggy') } }
Blade + axios. Ziggy shines when the server owns routing and JS just consumes it. Wrap it once so URL building and 422 handling live in one place:
const post = { id: 1, slug: 'ziggy-stardust' };
axios.get(route('posts.show', post)).then(r => r.data);
axios.post(route('posts.store'), payload);
Route-model binding. Pass the whole model object — Ziggy reads the route's binding key (getRouteKeyName(), or an inline {photo:uuid}) and falls back to id. This means no .slug/.uuid juggling in JS when the model's key changes.
Navigation state. route().current() is the reason many teams adopt Ziggy at all — it keeps "active link" logic in one place instead of comparing window.location strings:
route().current(); // 'events.index'
route().current('events.*'); // true
route().current('venues.events.show', { venue: 1 }); // param-aware, incl. query
route().has('admin.dashboard'); // gate UI on route existence
route().params; // { venue: '1', event: '2' } — always strings
Framework glue. Vue: app.use(ZiggyVue) and route() works in templates; inject('route') inside <script setup>. React: const route = useRoute() (pass Ziggy if not using @routes). Both are thin wrappers, so prefer them over hand-importing the function.
Per-page scoping. @routes accepts a group name defined in config/ziggy.php, so admin pages ship admin routes and public pages don't. Combine with only/except (config or Ziggy::filter() in a service provider) to cap payload size and reduce surface area.
TypeScript. Generate types, declare the global, and opt into strict names per project:
// types/ziggy.d.ts
import { route as routeFn } from 'ziggy-js';
declare global { var route: typeof routeFn; }
declare module 'ziggy-js' {
interface TypeConfig { strictRouteNames: true }
}
Add export {}; if the file isn't picked up.
Testing. Assert URLs on the PHP side (where route() already works) rather than asserting in JS. JS tests can construct a Router with a trimmed config object — faster and no Blade render required.
@routes output is visible in page source. Filter aggressively; never assume route names are secret.ziggy:generate is a build artifact. Re-run it on every deploy, or SPA links silently 404 after a route rename. Put it in your deploy script / post-autoload-dump.ziggy-js on npm can drift. Pick one strategy: either alias to vendor/tightenco/ziggy (always matched) or pin both. Mixing a cached npm ziggy-js with a newer PHP config is a classic "route exists but route().has() says no" bug.@routes before your bundle, and exactly once. Two directives (e.g. layout + partial) re-declare Ziggy and can produce confusing behavior._query. Booleans are coerced to 0/1, matching Laravel.URL::defaults(['locale' => ...]) only takes effect if the middleware runs on the request that renders the page. If Ziggy emits a literal {locale}, check middleware ordering, not Ziggy.APP_URL. Behind a proxy or in Docker, a stale APP_URL yields wrong hosts in JS links. The third argument toggles absoluteness: route('posts.index', {}, false).route:cache, but filter callbacks that reference request state or closures can break caching — keep filters deterministic or use the config file's static only/except.route().params is strings. Cast before numeric comparison; route().current() does the parsing for you.<meta name="csrf-token"> and set axios's header yourself.@routes only renders in Blade/HTML. For a config endpoint, render the directive in a tiny view or serialize new Ziggy from PHP and hand it to route(name, params, absolute, config).Ziggy object, or run php artisan ziggy:generate and inspect the file — mismatches between expected and emitted routes are almost always filtering or route-cache staleness.How can I help you explore Laravel packages today?