Troubleshooting
Start with php artisan polyslug:doctor — it rules out the two
configuration-level causes in one command.
Old URLs return 404 instead of redirecting
The middleware is not running, or it is running too early. polyslug.canonical must be
applied after SubstituteBindings, or the model is not bound yet and self-heal
silently does nothing — no error, just a feature that stopped working.
Use the Route::polyslug() macro,
which wires both in the right order, and assert the behavior with
assertSlugRedirects().
If the token itself is old, check whether the encoder changed: a URL encoded by a previous
encoder resolves only while that encoder is listed in
polyslug.legacy_decoders.
Nothing redirects on a form submission
By design. Self-healing runs on GET and HEAD only —
unsafe verbs pass through untouched
so a POST is never bounced.
The redirect goes to the wrong language
The middleware compared against the ambient application locale instead of the one in the
URL. Set
polyslug.locale.source to 'route'
so a /{locale}/… request is compared against — and redirected to — that locale's slug.
When you build URLs outside a request cycle (sitemaps, queued jobs, feeds), use
polyslugRouteKeyForLocale($locale) rather than polyslugRouteKey(); the latter reads the
ambient locale, which in a queue worker is whatever the worker booted with.
Slugs get a -2 suffix that should not be there
Uniqueness is per (type, locale, scope). If two records legitimately share a name under
different parents or tenants, they need a
scope — parent_id,
tenant_id, whatever separates them.
If the suffix should never appear at all because the id already disambiguates the URL, use
unique: false.
A single -2 on a name that looks unique may also be a
reserved word: a reserved base is
suffixed rather than rejected, and reserved.from_routes reserves the first segment of
every registered route.
A slug did not update after the title changed
Three possibilities:
- The model is
immutable: true, which is exactly what that option does.setSlug()is the deliberate override. - The source column did not actually change — regeneration is skipped when it did not.
- You are looking at a different locale.
currentSlug()reads the active locale; pass one explicitly to check another.
A save throws CouldNotWriteSlug
Repeated write conflicts on the same (type, id, locale, scope). See
the exception reference — raise
polyslug.write.max_attempts, or find out why the
same record is saved concurrently.
A CJK or emoji title saves with an empty slug
That is the emptyFallback: 'id-only' default, and the URL is just _{encodedId}. For
non-Latin markets you probably want
unicode: 'native'
instead, which keeps the script rather than transliterating it away.
Drafts show up in the sitemap
polyslugResolveQuery() and polyslugIsRoutable() answer different questions — the first
decides what may be reached, the second what is advertised. A draft blocked only by the
first is still announced. See Access control.
/go returns 404 for a link that used to work
Either no PolyslugUrlResolver is bound, or the model is no longer visible through the
resolution gate
— short links deliberately resolve through it, so an unpublished record's short link stops
working with the same 404 an unknown token returns.
Something else
Open an issue at github.com/pushery/polyslug-for-laravel. For a suspected vulnerability, use the security policy rather than a public issue.