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 may use A-Z a-z 0-9 - ~ and must not repeat one: _ separates the
slug from the token, and a . token can be a path segment a browser removes. Per-model overrides go through the encoderOptions attribute option,
which takes the same two keys. See
Token length.
Every token length is an integer, and every alphabet a string. A value of another kind
keeps the default, here, under sequential_token and short_links and in encoderOptions
alike, and polyslug:doctor names it. To read a length from the environment, cast it the way
the shipped file does for the Sqids padding below:
'length' => is_numeric($length = env('POLYSLUG_TOKEN_LENGTH')) && (int) $length >= 1 ? (int) $length : 16,
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. A scheme other than random or sequential
throws Polyslug\Exceptions\MisconfiguredPolyslug when the scheme is built.
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. It is held to the characters of any other token alphabet. Per-model overrides go through the encoderOptions attribute option.
A model's Sqids options are a space of their own: an option the model leaves out takes the
Sqids default, not the value here, because every URL that model has issued was encoded that
way. polyslug:doctor names a model that leaves out an option this section sets.
min_length reads POLYSLUG_SQIDS_MIN_LENGTH, so you can lengthen short tokens without
publishing this file. A value that is not a number, or one below 0, keeps the default.
The alphabet reads no variable on purpose: it has to stay the same for as long as any URL
issued with it is in circulation.
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.
max_attempts reads POLYSLUG_WRITE_MAX_ATTEMPTS: contention depends on how many workers
write slugs at once, which is a property of your application. A value that is not a number,
or one below 1, keeps the default of 5.
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.sourceandmissingtake only the values shown; any other value throwsPolyslug\Exceptions\MisconfiguredPolyslugwhere the setting is read, and an unset or null value means the default.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.
status takes a client error from 400 to 499, and redirect_status one of 301, 302,
303, 307 or 308. Any other value, or one that is not an integer, gives the default:
a withdrawn record answered with 200 would read as a live page, and a redirect cannot carry
a status that is no redirect. polyslug:doctor names such a value.
retired
'retired' => [
'status' => 410,
],
The status a request for a slug retired with retireSlug() answers, in place of the redirect
a former slug gets. 410 by default, 404 if you prefer. A value that is no client error
from 400 to 499 gives 410. See
Retiring a single slug.
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.
timeout reads POLYSLUG_BACKFILL_TIMEOUT, in seconds, because how long a chunk takes depends
on your table and your database. tries reads POLYSLUG_BACKFILL_TRIES, the number of attempts
a chunk gets before it fails for good. For each, a value that is not a number, or one below 1,
keeps null.
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 and 303 are accepted as well: since only GET and HEAD
are ever redirected, what either says about the method changes nothing here. Any other
value, or one that is not an integer, gives 301, because a redirect cannot carry a status
that is no redirect. 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.
require_scope reads POLYSLUG_REQUIRE_SCOPE. Write POLYSLUG_REQUIRE_SCOPE=true: the value
is compared against true, and Laravel's env() reads 1 as the string "1", which does not
enable it.
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.
models
'models' => [
// \Polyslug\Models\PolyslugSlug::class => \App\Models\Slug::class,
// \Polyslug\Models\PolyslugShortLink::class => \App\Models\ShortLink::class,
],
Replaces Polyslug's own models with your subclasses, keyed by the package class. Use it when you need your own relations, scopes or casts on a slug or short-link row:
namespace App\Models;
use Polyslug\Models\PolyslugSlug;
class Slug extends PolyslugSlug
{
// your relations, scopes and casts
}
Polyslug then uses your class on every path: every query it runs, every row it writes, and the
slugs relation on your sluggable models. A class that does not exist, does not extend the
package class or cannot be instantiated, such as an abstract one, is ignored and the package
class is used instead — a typo here costs you the customization, not a failed request or queued
job. polyslug:doctor names every such entry.
Your subclass inherits the package's table name, so it reads and writes the same rows. Keep it
that way: a subclass that sets its own $table points Polyslug at a table its migrations never
created.
Switching an existing installation over needs no data migration, in either direction. Every
type column Polyslug writes (sluggable_type on slugs and short links, key_type on tokens)
holds the type of the model that owns the row, never the class of the row itself, so changing
the class that reads the table changes no stored value.
In your own code, PolyslugSlug::model() returns the configured class and
PolyslugSlug::resolve() a new instance of it.