pushery / polyslug-for-laravel
Polymorphic, multilingual routable identity for Eloquent — leak-safe IDs, per-locale slugs, and hreflang out of the box.
Requires
- php: ^8.4
- ext-json: *
- illuminate/collections: ^13.0
- illuminate/console: ^13.0
- illuminate/container: ^13.0
- illuminate/contracts: ^13.0
- illuminate/database: ^13.0
- illuminate/filesystem: ^13.0
- illuminate/http: ^13.0
- illuminate/routing: ^13.0
- illuminate/support: ^13.0
- illuminate/view: ^13.0
- sqids/sqids: ^0.5.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/head: ^0.1.0
- laravel/pao: ^1.1
- laravel/pint: ^1.0
- orchestra/testbench: ^11.0
- pestphp/pest: ^5.0
- pestphp/pest-plugin-agent: ^5.0
- pestphp/pest-plugin-browser: ^5.0
- pestphp/pest-plugin-evals: ^5.0
- pestphp/pest-plugin-laravel: ^5.0
- pestphp/pest-plugin-mutate: ^5.0.1
- pestphp/pest-plugin-phpstan: ^5.0
- pestphp/pest-plugin-rector: ^5.0
- pestphp/pest-plugin-type-coverage: ^5.0
- rector/rector: ^2.0
- spaze/phpstan-disallowed-calls: ^4.12
Suggests
- laravel/head: Lets a Polyslug model describe its own <head>: Head::polyslug($model) writes the canonical URL, the reciprocal hreflang set, the Open Graph locale set and the robots directive from the model's own slug data.
README
Polyslug
Polymorphic, multilingual routable identity for Eloquent. Pretty URLs that are safe to expose, safe to rename, and correct across languages — with leak-free IDs, self-healing canonical redirects, and hreflang built in.
/blog/laravel-routing-explained_aB3xK
└──────── slug ─────────┘ └id─┘
changeable, localized, SEO-friendly stable, opaque, resolves the model
A slug should be free to change. A URL should never break. Those two goals usually fight each other. Polyslug settles the fight by splitting a URL into two independent parts — a human-readable slug (pretty, per-locale, editable) and a stable opaque identity (an encoded token that resolves the model). Rename the slug all you like: the identity still resolves, and old URLs redirect themselves to the new canonical one.
Install
composer require pushery/polyslug-for-laravel php artisan migrate
Mark a model as sluggable, point a route at it, and you are done:
#[Polyslug(source: 'title')] class Page extends Model implements Sluggable { use HasPolyslug; } Route::polyslug('/pages/{page}', [PageController::class, 'show'])->name('pages.show');
Rename the page and the old URL 301s to the new one, by itself.
Why Polyslug?
- 🔒 Leak-safe IDs by default. URLs carry an encoded token, not
/pages/1523. No exposed row counts, no enumerable primary keys. The default encoder issues a random, unguessable token per row — the only shipped encoder that makes that claim true end to end. The choice is yours, though: Sqids, UUID, ULID, the raw key, or your own. - ♻️ Self-healing URLs. Rename freely. A stale slug on a
GET/HEADrequest is301-redirected to the current canonical URL automatically — no redirect tables to hand-maintain, no dead links, no lost link equity. - 🌍 Multilingual with hreflang out of the box. One slug per locale, and a
reciprocal
hreflangset (plusx-default) generated from the same resolver that builds your canonical URL — so they can never drift apart. - 🧩 Polymorphic routing. Serve every content type — pages, articles, products —
through a single
{type}/{polyslug}route and one registry. - 🧭 Stable resolution. Route-model binding decodes the identity, not the slug, so
a mistyped or outdated slug still finds the right model (then redirects). An unknown
or malformed token is a clean
404— never a fuzzy match. - 🏢 Scoped uniqueness. Uniqueness can be scoped per tenant, per locale, per category — whatever columns you name.
- 🗂️ History, events & immutability. Superseded slugs are kept so old URLs keep
resolving; a
SlugChangedevent fires on every change; slugs can be frozen. - 🤝 Writes its own
<head>vialaravel/head. Install Laravel's<head>package andHead::polyslug($model)fills in the canonical URL, thehreflangset, the Open Graph locales and arobotsdirective for models your visibility gate hides. Optional — Polyslug requires nothing beyond slimilluminate/*components either way. - ✅ Proven on the databases you deploy to. The one-current-slug guarantee ships as a functional partial unique index on PostgreSQL and SQLite and as generated key columns on MySQL — two genuinely different mechanisms, and the suite proves both against real PostgreSQL 18 and MySQL 8.4 servers rather than inferring one from the other.
Documentation
Full docs at docs.pushery.com/polyslug-for-laravel.
- Installation — requirements, the migration, publishing
- Quick start — a sluggable model and a self-healing route
- Features — encoders, hreflang, nested paths, sitemaps, short links
- laravel/head — canonical URL, hreflang and indexability in Laravel's
<head>package - Recipes — twelve app shapes wired end to end
- Reference — every config key, option, command, event and contract
Requirements
- PHP 8.4+
- Laravel 13+
- PostgreSQL, MySQL 8.4+, or SQLite — the uniqueness guarantees are enforced natively on each, and the full suite runs against all three. So Polyslug works on Laravel Cloud (serverless Postgres + MySQL 8.4 LTS) with no extra configuration.
Security
Please review the security policy and report vulnerabilities privately rather than opening a public issue.
Built by Pushery
This package is built and maintained by Pushery — a Berlin-based studio building Laravel applications, SaaS products, and open-source tools.
Building a Laravel UI? WireKit, Pushery's open-source Livewire component kit, gives you a polished component library out of the box. Browse the rest of our work at pushery.com.
License
The MIT License (MIT). See LICENSE for details.