Implement Slug for post and lesson learned
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.