When storing content in the database a typical pattern for retrieving it is to create a unique “slug” based on the title. This gives users and search engines a human-friendly way to access it through the URL.

For example, a typical URL might look like, site.com/post/1. The id in the URL gives no hints at what the content is about and is not user-friendly. Slugs, on the other hand, are designed to turn this into site.com/post/my-awesome-article

Creating slugs from an existing string is easy in Laravel as it includes a helper for just this use case:

$title = str_slug('My Awesome Title', '-');

The results of this would be a lower-cased string with a dash (-) separator between the words:

my-awesome-title

While this is a huge help it doesn’t account for situations where you have two pieces of content with the same title.

Laravel slug class

Let’s build a small utility class to handle creating unique slugs based on existing data so we can ensure that no two are the same.

For reference here is the full class:


<?php
namespace App\Services;
use App\Post;
class Slug
{
/**
* @param $title
* @param int $id
* @return string
* @throws \Exception
*/
public function createSlug($title, $id = 0)
{
// Normalize the title
$slug = str_slug($title);
// Get any that could possibly be related.
// This cuts the queries down by doing it once.
$allSlugs = $this->getRelatedSlugs($slug, $id);
// If we haven't used it before then we are all good.
if (! $allSlugs->contains('slug', $slug)){
return $slug;
}
// Just append numbers like a savage until we find not used.
for ($i = 1; $i <= 10; $i++) {
$newSlug = $slug.'-'.$i;
if (! $allSlugs->contains('slug', $newSlug)) {
return $newSlug;
}
}
throw new \Exception('Can not create a unique slug');
}
protected function getRelatedSlugs($slug, $id = 0)
{
return Post::select('slug')->where('slug', 'like', $slug.'%')
->where('id', '<>', $id)
->get();
}
}

What this does is give you a createSlug method that can be used in creating and editing by passing the existing record id.

Here is an example of generating for both:

// On create
$post->slug = $slug->createSlug($request->title);
// On update
if ($post->slug != $request->slug) {
 $post->slug = $slug->createSlug($request->slug, $id);
}

The Slug class itself is pretty simple. createSlug calls getRelatedSlugs which performs a single query selecting all the records that could possibly be a duplicate. Then uses the Laravel Collections class to see if it’s already used:

if (! $allSlugs->contains('slug', $slug)){
 return $slug;
}

If it still has a duplicate then it appends a number to the end and performs another check for uniqueness:

for ($i = 1; $i <= 10; $i++) {
    $newSlug = $slug.’-’.$i;
    if (! $allSlugs->contains(‘slug’, $newSlug)) {
        return $newSlug;
    }
}

Finally, if all the numbers are exhausted it just bails out by throwing an Exception.

By utilizing Laravel’s existing helpers and Collections generating unique title slugs is easy. With this implemented all that would be required next is a custom route and a query to pull out a single post by its slug:

Route::get('/post/{slug}', function(){
    $post = AppPost::where('slug', $slug)->firstOrFail(); 
});