Configuration
Publish the configuration file to customize it:
php artisan vendor:publish --tag=polyslug-config
Every option in the published config/polyslug.php is documented inline. Nothing has to
be published to use Polyslug — the package config is merged, so an unpublished
installation runs on the defaults below.
encoder
'encoder' => \Polyslug\Encoders\RandomTokenEncoder::class,
The class that maps a model's key to and from the opaque token in its URL. Must implement
Polyslug\Contracts\IdentityEncoder. A model can override it with the encoder attribute
option.
The default stores an unguessable random token per key, so the URL reveals nothing.
SqidsEncoder was the default until 0.5.0 and remains supported — but its token decodes
back to the primary key, which leaks the key, the creation order and the growth rate.
See Identity encoders.
random_token
'random_token' => [
'length' => 16, // characters in a newly issued token
'alphabet' => null, // null uses 0-9a-z
],
Used by RandomTokenEncoder. length is a floor, not a fixed width: a length whose
space fills up yields to one character more rather than failing to issue a URL. Changing it
is safe at any time — tokens are stored, never recomputed from the key, so existing URLs
keep resolving and only new records use the new length.
A custom alphabet must be made of URL-unreserved characters (A-Z a-z 0-9 - . _ ~) and
must not repeat one. Per-model overrides go through the encoderOptions attribute option,
which takes the same two keys. See
Token length.
sequential_token
'sequential_token' => [
'length' => 1, // width the counting starts at
'alphabet' => null, // null uses 0-9a-z
],
Used by SequentialTokenEncoder, which hands out the shortest token not yet taken. length
is where the counting starts — it moves the first token, it does not scatter the ones after
it, and it must not be read as making a counted token unguessable. See
SequentialTokenEncoder.
short_links
'short_links' => [
'scheme' => 'random', // 'random' or 'sequential'
'length' => null, // null takes the scheme's own default: 10 random, 1 counted
'alphabet' => null,
],
The token space behind $model->shortLink() and the /go/{token} route — separate from the
identity token above, because a link that is printed, spoken or put on a QR code wants a
different trade from the one inside every URL.
length is null rather than a number on purpose: ten random characters is a short link,
ten counted ones is 0000000000 for the first record, so the sensible default belongs to
the scheme rather than to this section. Set it explicitly to override either.
Bind Polyslug\Contracts\TokenScheme yourself to replace the scheme entirely. See
Short links.
sqids
'sqids' => [
'alphabet' => null, // null uses the Sqids default alphabet
'min_length' => 0, // pad tokens to at least this many characters
],
Used only by SqidsEncoder. A custom alphabet shuffles the token space for this
application — set one and keep it stable, because changing it changes every previously
generated URL. Per-model overrides go through the encoderOptions attribute option.
legacy_decoders
'legacy_decoders' => [],
IdentityEncoder classes to try, in order, when the current encoder cannot decode a
token. This is what lets you switch encoders without breaking published URLs: old links
still resolve, and the canonical middleware 301s them to the new-format URL. See
migrating encoders.
write
'write' => [
'max_attempts' => 5,
],
How many times a slug write regenerates and retries after losing a race to a concurrent
writer, before throwing Polyslug\Exceptions\CouldNotWriteSlug. On failure the model keeps
its previous slug — restored in place, not rolled back. See
How it works.
locale
'locale' => [
'source' => 'app', // 'app' | 'route'
'route_param' => 'locale', // the route parameter holding the locale
'missing' => 'fallback', // 'fallback' | 'id-only'
'fallback_locale' => null, // null uses the application's fallback locale
],
source— how the canonical-redirect middleware decides which locale a request is for.'app'uses the active application locale;'route'reads theroute_paramsegment, so a/{locale}/…URL is compared against the right locale's slug even when the app locale differs.route_param— the name of that route parameter.missing— the route key when a locale has no slug yet:'fallback'uses the fallback locale's slug,'id-only'emits a slug-less_TOKENkey.fallback_locale— overrides the application's fallback locale. Note that this reaches further thanmissing: it is also the localex-defaultpoints at in the hreflang set.
See Locale-aware routing.
reserved
'reserved' => [
'global' => [],
'from_routes' => false,
],
global— app-wide slugs that may never be assigned to any model, merged with each model's ownreservedoption. A reserved base is suffixed (admin→admin-2) like any other collision.from_routes— whentrue, additionally reserves the static first segment of every registered route, so a generated slug can never shadow a real route.
See Reserved slugs.
gone
'gone' => [
'status' => 410,
'redirect_status' => 301,
],
The status returned for a model whose polyslugIsGone() is true, and the redirect
status used when polyslugSupersededBy() returns a successor. See
Gone and superseded content.
analytics
'analytics' => [
'enabled' => false,
],
When enabled, the canonical middleware dispatches a Polyslug\Events\SlugRedirected event
on every self-healing redirect. Off by default; nothing writes synchronously on the hot
path unless a listener does. See Events.
sitemap
'sitemap' => [
'types' => [
// \App\Models\Page::class,
],
'max_urls' => 50_000,
'max_bytes' => 50 * 1024 * 1024,
],
types is the sluggable models polyslug:sitemap includes. Each must be an Eloquent model
implementing Sluggable. A Polyslug\Contracts\PolyslugUrlResolver must be bound in the
container so the command can build absolute URLs.
max_urls and max_bytes are the sitemap protocol's own ceilings for one file: 50,000 URLs
and 50 MB uncompressed. Past either, the command writes numbered parts beside --path and a
<sitemapindex> at --path itself, so the limit is never something you have to notice.
Lower them if a CDN or a search console you use wants smaller files; raising them past the
protocol produces a document engines reject. See Sitemaps.
backfill
'backfill' => [
'connection' => null,
'queue' => null,
'tries' => null,
'timeout' => null,
],
Where polyslug:backfill --queue puts its jobs. A backfill walks an entire table, so on the
default queue it sits in front of every password reset and order confirmation the application
has, for as long as that takes. Name a queue your workers treat as bulk and the rest of the
app keeps moving.
null leaves the framework's own default in place for each. connection and queue can be
overridden per run with --on-connection= and --on-queue=. tries and timeout are the
job's, not the worker's: a chunk re-queries its own rows, so a retry is safe, and a large
chunk needs a timeout that admits it. See
Backfilling existing rows.
open_graph
'open_graph' => [
'locale_map' => [
// 'en' => 'en_US',
// 'de' => 'de_DE',
],
],
Open Graph writes a locale as language_TERRITORY (en_US, pt_BR). A plain en is
outside that format, and a scraper that cannot parse the value does not read a language from
it — it falls back to its own default. So a locale with no territory gets no og:locale
tag rather than an unparseable one.
The territory is not something the package can invent: en is en_US to one site and
en_GB to another, and asserting either would announce a regional variant nobody configured.
Name the pairs you want here. Locales that already carry a territory (pt_BR, de-AT) need
no entry. See laravel/head integration.
redirect
'redirect' => [
'status' => 301,
],
The status for the self-healing redirect from a stale slug to the canonical URL, on
GET/HEAD only. 301 is permanent; use 302 or 307 while a slug is still volatile so
the redirect is not cached. 308 is accepted as well, and since only GET and HEAD are
ever redirected, its method-preserving guarantee changes nothing here. See
Self-healing routes.
resolution
'resolution' => [
'require_scope' => false,
],
A slug-only URL carries no id, so the slug alone has to identify the record. On a model
scoped with #[Polyslug(scope: ...)] that only holds within a scope: the unique index
is scope-bound, so /@alice/toolkit and /@bob/toolkit may both hold toolkit
legitimately.
The resolution gate
does not separate them — it answers what the environment says is visible, while a scope in
a path segment is an argument of the resolution. Hand it over with
polyslugResolutionScope().
Turn this on and a scoped model whose caller names no scope is refused instead of resolving to whichever row sorts first. Off by default: switching it on refuses every scoped model that does not answer yet. See Slug-only URLs.
types
'types' => [
// 'page' => \App\Models\Page::class,
],
Maps a URL {type} segment to a sluggable model for the polymorphic resolver, so a single
/{type}/{slug} route serves every content type. Each value must be an Eloquent model
implementing Sluggable. See Polymorphic routing.