Skip to main content

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 scopeparent_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.

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.