Skip to main content

The URL resolver

Polyslug knows a model's slug. It does not know your routes — whether pages live at /pages/{slug}, at /{locale}/p/{slug}, or behind a subdomain — because that is your application's design, not the package's.

So there is exactly one class you write yourself: an implementation of Polyslug\Contracts\PolyslugUrlResolver. It answers a single question — given this model and this locale, what is its absolute URL? — and three features are built on that answer.

New here? This is the step people miss.

Nothing warns you when it is missing, and two of the three features simply go quiet. If short links return 404 or your sitemap comes out empty, this page is almost certainly the reason. php artisan polyslug:doctor reports it.

The interface

namespace Polyslug\Contracts;

interface PolyslugUrlResolver
{
public function url(Sluggable $model, string $locale): string;
}

One method. It returns an absolute URL, because a sitemap and a <link rel="canonical"> both require one, and a 301 target should be one.

Writing it

Most applications wrap route() and hand it the slug for the locale being asked about. polyslugRouteKeyForLocale() is what gives you that — not getRouteKey(), which answers for the current locale and would produce the same URL for every language.

// app/Support/AppUrlResolver.php
namespace App\Support;

use App\Models\Page;
use Polyslug\Contracts\PolyslugUrlResolver;
use Polyslug\Contracts\Sluggable;

final class AppUrlResolver implements PolyslugUrlResolver
{
public function url(Sluggable $model, string $locale): string
{
return match (true) {
$model instanceof Page => route('pages.show', [
'locale' => $locale,
'page' => $model->polyslugRouteKeyForLocale($locale),
]),
default => url('/'.$locale.'/'.$model->polyslugRouteKeyForLocale($locale)),
};
}
}

The match is there because one resolver serves every Polyslug model in your application. With a single model you can drop it and return the one route() call.

Binding it

In AppServiceProvider::register():

use App\Support\AppUrlResolver;
use Polyslug\Contracts\PolyslugUrlResolver;

public function register(): void
{
$this->app->bind(PolyslugUrlResolver::class, AppUrlResolver::class);
}

bind, not singleton: the resolver holds no state, and a bind keeps it out of the way if you ever need to swap it per request.

An anonymous class works too, and reads well when the whole thing is one route() call:

$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),
]);
}
});

What breaks without it, and how loudly

This is worth knowing before you go looking, because the three features fail differently:

FeatureWithout a bound resolver
Sitemapspolyslug:sitemap prints an error naming the contract and exits non-zero. It will not write an empty sitemap over a good one.
Short linksEvery /go/{token} returns 404 — deliberately indistinguishable from an unknown token, so the route is not an existence oracle. Nothing says why.
laravel/head integrationThe canonical tag, the hreflang set and og:locale are simply not written. The page renders fine and its <head> is quietly incomplete. The robots directive still ships — it needs no resolver, so a gated model stays out of the index either way.

Only the first one tells you. The other two look like ordinary behavior, which is exactly why this page exists — and why polyslug:doctor reports the binding.

warning
A 404 from /go is not always a bad token

The short-link controller returns 404 for an unknown token, for a model the current request may not see, and for a missing resolver. That sameness is on purpose — a different response for "exists but you may not see it" would leak the existence of the record. The cost is that a setup mistake and a genuine miss look identical, so check the binding first.

Checking it

php artisan polyslug:doctor

It reports whether the contract is bound, along with the encoder configuration, the unique indexes and the resolution gates. See Diagnostics.

Locales, if you do not have any

A single-language application still receives a $locale. Ignore it and return the URL:

public function url(Sluggable $model, string $locale): string
{
return route('pages.show', ['page' => $model->polyslugRouteKeyForLocale($locale)]);
}

Keep polyslugRouteKeyForLocale($locale) rather than getRouteKey() even here. It costs nothing today and is the line you would otherwise have to find again on the day a second language arrives.

Nested models

For a model with a parent, polyslugPath() composes the ancestors' slugs into the full path — use it where you would otherwise use the route key:

return url('/'.$locale.'/'.$model->polyslugPath($locale));