Skip to main content

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' => [
'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 the route_param segment, 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 _TOKEN key.
  • source and missing take only the values shown; any other value throws Polyslug\Exceptions\MisconfiguredPolyslug where 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 than missing: it is also the locale x-default points 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 own reserved option. A reserved base is suffixed (admin → admin-2) like any other collision.
  • from_routes — when true, 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.