---
title: "Formatting & suffixes — Sluggable for Laravel | Roundly"
description: "How the pipeline transliterates, strips and truncates — plus separators, affixes, reserved words, custom sluggers and collision suffixes."
url: https://roundly-consulting.com/open-source/docs/sluggable-for-laravel/formatting
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Sluggable for Laravel](https://roundly-consulting.com/open-source/docs/sluggable-for-laravel.md)

# Formatting & suffixes

Every slug runs through one pure pipeline — no database involved. In order:

- Bound — scrub invalid UTF-8 and cut the source to limits.max\_source\_length before any regex runs.
- Clean — drop control characters and zero-width/bidi marks; unicode mode NFKC-normalises when ext-intl is loaded.
- Transliterate — Str::ascii() in the definition’s language (locale maps use each locale); skipped in unicode mode.
- Separator flip — the other of - and \_ becomes the separator.
- Dictionary — replacements such as @ → at, padded with the separator.
- Lowercase — before stripping, exactly in Str::slug()’s order.
- Strip and collapse — keep letters, numbers and the separator; collapse runs and trim.
- Words and length — keep the first maxWords words, then truncate to maxLength.

With the default format (ASCII, lowercase, -, \['@' => 'at'\]) the output matches Str::slug() for ordinary input of up to 255 characters. Two differences are deliberate:

- Zero-width and bidi control characters are stripped before transliterating — zero\\u{200B}width gives zerowidth, where Str::slug() gives zero-width.
- Slugs are capped at max\_length (255), built from the first max\_source\_length (2000) source characters; Str::slug() has no limit.

The facade exposes the pipeline directly:

```php
use RoundlyConsulting\Sluggable\Facades\Slugs;

Slugs::slugify('Žltý kôň @ home', language: 'sk');     // 'zlty-kon-at-home'
```

## Shaping the slug

```php
SlugDefinition::for('slug')
    ->from('title')
    ->language('de')                                  // or fn (?string $locale): ?string
    ->dictionary(['&' => 'and', '@' => 'at'])
    ->maxWords(8)
    ->prefix(fn (Post $post, ?string $locale): ?string => $post->published_at?->format('Y'))
    ->reserved(['admin', 'login']);
```

- separator() — 1–3 characters from - . \_ ~.
- maxLength() — 8–2048 characters including prefix, suffix and collision suffix; maxWords() caps the word count.
- language() — a language code or fn (?string $locale): ?string; dictionary() — replacements applied before stripping.
- unicode() keeps non-ASCII letters instead of transliterating; lowercase(false) keeps capitals.
- prefix() / suffix() — fixed strings or closures; they run through the same pipeline and are never cut by truncation. Affixes and a collision suffix that leave no room for the slug itself throw SlugGenerationException (noRoomForBody) instead of overflowing maxLength.
- reserved() — extra words that count as taken, on top of the global reserved config (compared case-insensitively).

Truncation is multibyte-safe: it cuts at the last separator when that lies in the second half of the budget, otherwise hard-cuts, then trims separators.

## Custom slugger

using() replaces the formatting steps with your own closure. Its output is still validated as a safe URL segment — letters, numbers, marks and the separator only — or SlugGenerationException (invalidCustomOutput) is thrown:

```php
use Illuminate\Support\Str;

SlugDefinition::for('slug')->from('name')->using(
    fn (string $source, ?string $locale): string => Str::of($source)->lower()->replace(' ', '-')->toString(),
);
```

## Collision suffixes

When the base slug is taken, a suffix is appended. Pick the strategy per definition:

```php
use RoundlyConsulting\Sluggable\Definitions\ResolvedSlugDefinition;

SlugDefinition::for('slug')->from('name');                              // chair, chair-2, chair-3 …
SlugDefinition::for('slug')->from('name')->sequentialSuffix(start: 1);  // chair, chair-1, chair-2 …
SlugDefinition::for('slug')->from('name')->randomSuffix(6);             // chair, then chair- + 6 random [a-z0-9]
SlugDefinition::for('slug')->from('name')->suffixUsing(
    fn (string $base, int $attempt, ResolvedSlugDefinition $definition): string => 'v'.$attempt,
);
```

Sequential suffixes (the default, starting at defaults.suffix\_start — 2 unless configured) probe up to limits.sequential\_probes candidates in batches, then fall back to random ones. randomSuffix() generates \[a-z0-9\] suffixes of randomLength characters (4–32). For anything else, implement the SuffixGenerator contract — it receives the base, the 1-based attempt and the resolved definition, and returns the suffix without the separator:

```php
use RoundlyConsulting\Sluggable\Contracts\SuffixGenerator;
use RoundlyConsulting\Sluggable\Definitions\ResolvedSlugDefinition;

final class YearSuffix implements SuffixGenerator
{
    public function suffix(string $base, int $attempt, ResolvedSlugDefinition $definition): string
    {
        return now()->year.'-'.$attempt;
    }
}

SlugDefinition::for('slug')->from('name')->suffixUsing(new YearSuffix);
```

A custom suffix is validated like custom slugger output — letters, numbers (marks in unicode mode) and the separator only — so it can never put /, ?, whitespace or other URL syntax into a slug; anything else throws SlugGenerationException (invalidSuffix).

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/sluggable-for-laravel.md)
