PHP and Laravel Slug Generation: Comprehensive Developer Guide

Learn to create SEO-friendly slugs with PHP and Laravel framework. Str::slug helper, Eloquent model events, and custom solutions for special character support.

Slugs are the human-readable part of a URL that identifies a page — for example my-first-post in example.com/blog/my-first-post. Clean slugs improve SEO, click-through rates and shareability. In this guide we cover every practical way to generate slugs in PHP and Laravel: a dependency-free vanilla function, Laravel's Str::slug helper, automatic slugs with Eloquent model events, the Spatie Sluggable package, and route model binding.

Vanilla PHP Slug Generation

Basic Slug Function

If you are not using a framework, a robust slug function needs three steps: transliterate non-ASCII characters, strip everything that is not alphanumeric, and collapse separators. iconv handles the transliteration on most systems:

function slugify(string $text, string $divider = '-'): string
{
    // Transliterate accents and special characters to ASCII
    $text = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text);

    // Replace everything that is not a letter or digit with the divider
    $text = preg_replace('~[^\pL\d]+~u', $divider, $text);

    // Trim, remove duplicate dividers, lowercase
    $text = trim($text, $divider);
    $text = preg_replace('~-+~', $divider, $text);

    return strtolower($text) ?: 'n-a';
}

echo slugify('Hello World! PHP & Laravel');
// hello-world-php-laravel

Note the ?: 'n-a' fallback: if the input contains only symbols, you still return a usable slug instead of an empty string.

Unique Slug Generation

Two posts titled "Hello World" must not share a URL. The standard pattern checks the database and appends a counter until the slug is free:

function uniqueSlug(PDO $pdo, string $title): string
{
    $slug = slugify($title);
    $original = $slug;
    $i = 2;

    $stmt = $pdo->prepare('SELECT COUNT(*) FROM posts WHERE slug = ?');
    while (true) {
        $stmt->execute([$slug]);
        if ((int) $stmt->fetchColumn() === 0) {
            return $slug;
        }
        $slug = $original . '-' . $i++;
    }
}

Laravel Slug Generation

The Str::slug Helper

Laravel ships with a battle-tested helper that transliterates Unicode, removes symbols and normalizes separators in one call:

use Illuminate\Support\Str;

Str::slug('Laravel 12 Performance Tips');
// laravel-12-performance-tips

Str::slug('Hello World', '_');
// hello_world

Language-Specific Character Support

Str::slug accepts a language argument that controls transliteration rules. This matters for languages like Turkish or German where the default ASCII mapping is wrong:

Str::slug('Büyük Türkçe Başlık', '-', 'tr');
// buyuk-turkce-baslik  (ı→i, ğ→g, ş→s handled correctly)

Str::slug('Größe Straße', '-', 'de');
// groesse-strasse  (ö→oe, ß→ss per German convention)

Automatic Slugs with Eloquent Models

Using Model Events

The cleanest zero-dependency approach: generate the slug in the model's creating event so every insert gets one automatically, and guarantee uniqueness with a counter:

class Post extends Model
{
    protected static function booted(): void
    {
        static::creating(function (Post $post) {
            $slug = Str::slug($post->title);
            $original = $slug;
            $i = 2;

            while (static::where('slug', $slug)->exists()) {
                $slug = $original . '-' . $i++;
            }

            $post->slug = $slug;
        });
    }
}

A Reusable HasSlug Trait

When several models need slugs, extract the event into a trait so the logic lives in one place:

trait HasSlug
{
    protected static function bootHasSlug(): void
    {
        static::creating(function ($model) {
            $model->slug = $model->generateUniqueSlug(
                Str::slug($model->{$model->slugSource ?? 'title'})
            );
        });
    }

    protected function generateUniqueSlug(string $slug): string
    {
        $original = $slug;
        $i = 2;
        while (static::where('slug', $slug)->exists()) {
            $slug = $original . '-' . $i++;
        }
        return $slug;
    }
}

The Spatie Laravel Sluggable Package

For production apps, spatie/laravel-sluggable adds slug regeneration control, custom sources and uniqueness out of the box:

composer require spatie/laravel-sluggable
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;

class Post extends Model
{
    use HasSlug;

    public function getSlugOptions(): SlugOptions
    {
        return SlugOptions::create()
            ->generateSlugsFrom('title')
            ->saveSlugsTo('slug')
            ->doNotGenerateSlugsOnUpdate(); // keep URLs stable
    }
}

doNotGenerateSlugsOnUpdate() is the important line: changing a published URL loses accumulated SEO value unless you add a 301 redirect.

Route Model Binding by Slug

Tell Laravel to resolve models by slug instead of ID either per-route or globally via getRouteKeyName:

// routes/web.php
Route::get('/blog/{post:slug}', [PostController::class, 'show']);

// or globally in the model
public function getRouteKeyName(): string
{
    return 'slug';
}

Best Practices

Conclusion

For quick scripts, a vanilla slugify() with iconv is enough. Inside Laravel, use Str::slug with the correct language argument, wire it into a creating event or a small trait for automatic generation, and reach for Spatie Sluggable when you need update policies and advanced options. Combine that with slug-based route model binding and a unique index, and your URLs stay clean, stable and SEO-friendly. Want to see the output before committing? Generate and preview slugs instantly with our free PHP slug generator.