Transliteration and Unicode
ASCII transliteration
By default non-ASCII source text is folded to a URL-safe ASCII slug. Pick the profile that matches your audience:
#[Polyslug(source: 'title', transliterate: TransliterationProfile::Din)]
| Profile | Größe → | Über → |
|---|---|---|
Simple (default) | grosse | uber |
Din | groesse | ueber |
Simple drops the umlaut; Din expands it the German-standard way. Neither is more
correct in general — pick the one your readers expect to see in a URL, and keep it
consistent, because changing the profile changes future slugs (existing ones stay put
and keep resolving through their history).
Native Unicode slugs
For non-Latin markets, transliteration destroys the slug's whole purpose: a Chinese
headline folded to ASCII is not readable by anyone. Set unicode: 'native' to keep
Unicode letters and numbers instead:
#[Polyslug(source: 'headline', unicode: 'native')]
class Article extends Model implements Sluggable
{
use HasPolyslug;
}
Everything that is not a letter, a number or a mark attached to one is collapsed into the
separator, so the grammar of a slug is unchanged — only its character set widens. Chinese,
Cyrillic, Greek and accented Latin all survive, and so do the scripts that write vowels and
tones as combining marks: Devanagari, Bengali, Thai, vocalized Arabic. The variation
selectors and enclosing marks an emoji or a keycap carries are dropped, and maxLength
cuts between grapheme clusters, so no vowel sign is parted from its letter.
Slugs are lower-cased at generation, mb-aware, so the case-insensitive unique index behaves identically on PostgreSQL, SQLite and MySQL — the three disagree on how to lower-case non-ASCII letters, and folding at write time is what removes that disagreement from the equation. Input is assumed to be NFC-normalized.
pin_polyslug_key_collation migrationOn MySQL the slug columns inherit the connection's collation unless a migration pins them,
and the default collations ignore accents. Until pin_polyslug_key_collation has run,
café and cafe are one name to every lookup but two to the unique index: a second
record titled Cafe gets cafe-2, an id-less URL for cafe resolves to
the record holding café, and a reclaimActive takeover
of cafe retires café from a record that was never competing for it. php artisan migrate runs it.
Titles with nothing to slugify
A title made only of emoji or of characters the profile strips leaves an empty slug.
emptyFallback decides what happens:
'id-only'(default) — store an empty slug, so the URL is just_{encodedId}and the save never fails. Every such record gets its own_{encodedId}: an empty slug is no name, so it is not held unique and never gets a counter suffix. An id-less URL has no id to fall back to, so there the encoded id becomes the slug: the URL is/{encodedId}.'throw'— raisePolyslug\Exceptions\CouldNotGenerateSlug.
The default is deliberate: a user saving a CJK-only or emoji-only title should get a
working (if plain) URL, not a 500. Choose 'throw' only where an empty slug is a data
problem you want surfaced at write time.
Note that with unicode: 'native' a CJK title is not empty at all — it becomes a CJK
slug, and the fallback never triggers.
Length
maxLength trims a slug to at most that many characters and never leaves it ending
mid-separator. It applies after slugification, so it counts the characters that actually
appear in the URL. A uniqueness suffix such as -2 is appended after the trim, so the second
record with the same title can be longer by the suffix.