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-laravelNote 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_worldLanguage-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-sluggableuse 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
- Index the slug column (
$table->string('slug')->unique()) — every page load queries it. - Never regenerate slugs on title edits; if a slug must change, 301-redirect the old URL.
- Keep slugs under ~60 characters and remove stop words for cleaner URLs.
- Always lowercase:
/Blog/Postand/blog/postare different URLs to crawlers. - Prefer hyphens over underscores — Google treats hyphens as word separators.
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.