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 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}, or the encoded id alone on an id-less model. 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.