Implement Slug for post and lesson learned

Posted: 2025/03/11

Initially when I first built this blog, each single post is accessed via its id, which is convenient and easy. But it looks ugly and is also not SEO friendly. It's better to access the post via its slug.

1. Add new column slug for the posts tables

Create a new migrate file, edit it to add a new column called slug. It needs to be unique and nullable

Schema::table('posts', function (Blueprint $table) {
            $table->string('slug')->unique()->nullable();
        });

Then run php artisan migrate to update the database

2. Update the Post Model to generate slug upon saving post

First, update the $fillbale to include slug so that slug column can be updated.

protected $fillable = ['title', 'content', 'user_id', 'slug'];

Next, adding the boot() function to the Post Model to have it automatically generate slug based on the post's title, using Str::slug($post->title)

public static function boot()
{
    parent::boot(); // Call the parent boot method
    static::saving(function ($post) {
        if (empty($post->slug)) {
            $post->slug = Str::slug($post->title); // Generate slug from title
        }
    });
}

But with this code, the slug will be updated every time the post title is updated, which is not expected. In order to keep the slug static after the post is created, I need to spit static::saving into static::creating (when creating a post) and static::updating (when update a post)

public static function boot()
    {
        parent::boot();

        // Generate slug only when creating a post
        static::creating(function ($post) {
            if (empty($post->slug)) {
                $post->slug = Str::slug($post->title);
            }
        });

        // Prevent slug from changing when updating a post
        static::updating(function ($post) {
            $post->slug = $post->getOriginal('slug');
        });
    }

3. Update the route to replace id with slug

Route::get('/post/{slug}', [PostController::class, 'show'] )->name('post.show');;

4. Update the Post Controller

Find the post based on the $slug provided

public function show($slug) {
        $post = Post::where('slug', $slug)->firstorFail();
        $content = (new \Parsedown())->text($post->content);
        return view('post.single', compact('post', 'content'));
    }

5. Update the slug for already published posts

All the posts published before this slug functionality implemented does not have a slug generated yet. I need to update the slug for them.

Create a new route update-slugs

Route::get('/update-slugs', [PostController::class, 'update_slugs'])->middleware(['auth', 'verified']);

Then add new function in the Post Controller

    public function update_slugs() {
        //Get all posts
      $posts = Post::all();
        Post::withoutEvents(function () {
            foreach ($posts as $post) {
                if (empty($post->slug)) {
                    $post->slug = Str::slug($post->title);
                    $post->save();
                    //dump(Str::slug($post->title));
                }
            }
        });
            return 'Done. All the slugs is now generated';
    }

Good lesson learned here

The loop foreach needs to be wrapped in side Post::withoutEvents(function () { to disable the model event listeners (creating and updated events). Without this, static::updating() will take action and prevent slug from changing (from null to string).

Then access the route via browser to update the slugs for all the posts.

6. Update links in view blade files

Finally step, update all the links to single post from post/{{$post->id}} to `post/{{$post->slug)).

Done.

Next Goal: Implement Slug for Category

No comments yet

Leave your comment

Search
Side Widget
You can put anything you want inside of these side widgets. They are easy to use, and feature the Bootstrap 5 card component!