Short links
A stable short URL per model and locale that always 301s to the current canonical URL,
so a printed or QR-coded link survives every slug rename.
Setup, in the order you need it
Three steps, and the second one is the one people miss — without it every short link
returns 404 and nothing says why.
1. A model that uses Polyslug
use Polyslug\Attributes\Polyslug;
use Polyslug\Concerns\HasPolyslug;
use Polyslug\Contracts\Sluggable;
#[Polyslug(source: 'title')]
final class Page extends Model implements Sluggable
{
use HasPolyslug;
}
See sluggable models if this part is new.
2. A bound URL resolver — the class you write yourself
The controller has to build the URL it redirects to, and only your application knows the
route a page lives on. So you supply one class implementing
PolyslugUrlResolver:
use Polyslug\Contracts\PolyslugUrlResolver;
use Polyslug\Contracts\Sluggable;
// AppServiceProvider::register()
$this->app->bind(PolyslugUrlResolver::class, fn () => new class implements PolyslugUrlResolver
{
public function url(Sluggable $model, string $locale): string
{
return route('pages.show', [
'locale' => $locale,
'page' => $model->polyslugRouteKeyForLocale($locale),
]);
}
});
Bind it once and sitemaps and the
laravel/head integration work too — it is the same contract.
The URL resolver covers it properly: multiple model types, nested paths,
and what each feature does when it is missing.
404The controller cannot guess a URL shape, so it returns 404 rather than a wrong redirect —
and that 404 is deliberately identical to the one an unknown token gets, so /go cannot be
used to probe which records exist. The cost is that a missing binding and a bad token look
exactly alike. php artisan polyslug:doctor tells them apart.
3. Route the shipped controller
use Polyslug\Http\Controllers\ShortLinkController;
Route::get('/go/{token}', ShortLinkController::class);
The path is yours to choose — /go/{token} is only the convention. The controller is
invokable, so it needs no wrapper.
Minting a link
$token = $page->shortLink(); // stable per (model, locale)
url('/go/'.$token); // → 301 → the model's current canonical URL
That is the whole API. The token is minted on the first call and returned unchanged afterwards.
Stability
shortLink() returns the same token for the same model and locale every time: the token
is stored once in polyslug_short_links and reused. That is what makes it safe to print
on a ticket, a poster, or a package insert — the target follows the model's canonical URL
forever, through every rename and every locale change to the slug.
Pass a locale to mint or fetch the token for a specific one:
$page->shortLink('de');
What returns 404
- An unknown token.
- A token whose model no longer exists.
- A token whose model the current request may not see. Short links resolve through
polyslugResolveQuery(), so/gois not a back door around a tenant or draft gate — and the404is indistinguishable from an unknown token, so it is not an existence oracle either. - No bound
PolyslugUrlResolver— step 2 above. This one is a setup mistake rather than a miss, and it is the only entry here that makes every link fail at once. If none of your short links have ever worked, start here.
The sharing and QR codes and events and ticketing recipes put this in context.
Choosing the token
A short link is its own token space with its own setting, because it is used differently from the token inside a URL: it gets printed, read out loud and put on a QR code, so it is the one most likely to want to be short.
// config/polyslug.php
'short_links' => [
'scheme' => 'random', // or 'sequential'
'length' => null, // null takes the scheme's default: 10 random, 1 counted
'alphabet' => null, // null uses 0-9a-z
],
Random (the default) draws ten characters, so the link says nothing about the record.
Sequential hands out the shortest token not yet taken — 0, 1, … z, then 00 —
which is the shortest a link can be, at the cost of being completely predictable. A /go
link resolves through the resolution gate like every other path, so
a counted link is not a way past authorization; it is a way to enumerate what exists.
length is a floor rather than a fixed width: a length whose space fills up yields to
one character more rather than failing to issue a link. polyslug:doctor reports how full
the space is before that happens.
To replace the scheme entirely — a checksum digit, a word list, an external service — bind
Polyslug\Contracts\TokenScheme in a service provider:
$this->app->bind(TokenScheme::class, fn () => new MyScheme);