Laravel Blog Post CRUD
Creating the Post Model and Migration
Start by creating the Post model and database table using Artisan.
Command:
php artisan make:model Post -m
Migration Example:
// database/migrations/xxxx_xx_xx_create_posts_table.php
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug')->unique();
$table->text('body');
$table->string('image')->nullable();
$table->timestamps();
});
Then run:
php artisan migrate
Model Configuration
Add fillable properties and a slug generator.
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class Post extends Model
{
protected $fillable = ['title', 'slug', 'body', 'image'];
protected static function booted()
{
static::creating(function ($post) {
$post->slug = Str::slug($post->title);
});
}
}
Creating a Resource Controller
Laravel resource controllers map all CRUD routes automatically.
Command:
php artisan make:controller PostController --resource
Controller Example:
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class PostController extends Controller
{
public function index()
{
$posts = Post::latest()->paginate(6);
return view('posts.index', compact('posts'));
}
public function create()
{
return view('posts.create');
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|max:255',
'body' => 'required',
'image' => 'nullable|image|max:2048'
]);
if ($request->hasFile('image')) {
$validated['image'] = $request->file('image')->store('posts', 'public');
}
Post::create($validated);
return redirect()->route('posts.index')->with('success', 'Post created successfully!');
}
public function show(Post $post)
{
return view('posts.show', compact('post'));
}
public function edit(Post $post)
{
return view('posts.edit', compact('post'));
}
public function update(Request $request, Post $post)
{
$validated = $request->validate([
'title' => 'required|max:255',
'body' => 'required',
'image' => 'nullable|image|max:2048'
]);
if ($request->hasFile('image')) {
if ($post->image) Storage::disk('public')->delete($post->image);
$validated['image'] = $request->file('image')->store('posts', 'public');
}
$post->update($validated);
return redirect()->route('posts.index')->with('success', 'Post updated!');
}
public function destroy(Post $post)
{
if ($post->image) Storage::disk('public')->delete($post->image);
$post->delete();
return redirect()->route('posts.index')->with('success', 'Post deleted.');
}
}
Defining Routes
Use Laravelโs resource routes for cleaner code.
// routes/web.php
Route::resource('posts', PostController::class);
The Post Index Page
Display all posts with pagination and image thumbnails.
<!-- resources/views/posts/index.blade.php -->
@extends('layouts.app')
@section('content')
<div class="max-w-5xl mx-auto py-6">
<a href="{{ route('posts.create') }}" class="bg-blue-600 text-white px-4 py-2 rounded">+ New Post</a>
<div class="grid md:grid-cols-2 gap-6 mt-6">
@foreach($posts as $post)
<div class="bg-white rounded shadow p-4">
@if($post->image)
<img src="{{ asset('storage/'.$post->image) }}" class="w-full h-48 object-cover rounded">
@endif
<h3 class="text-lg font-bold mt-2">{{ $post->title }}</h3>
<p class="text-gray-600 text-sm mt-1">{{ Str::limit($post->body, 120) }}</p>
<a href="{{ route('posts.show', $post) }}" class="text-blue-600 hover:underline text-sm mt-2 inline-block">Read More</a>
</div>
@endforeach
</div>
<div class="mt-6">{{ $posts->links() }}</div>
</div>
@endsection
Create Post Form
<!-- resources/views/posts/create.blade.php -->
@extends('layouts.app')
@section('content')
<div class="max-w-xl mx-auto py-6">
<form action="{{ route('posts.store') }}" method="POST" enctype="multipart/form-data" class="space-y-4">
@csrf
<div>
<label class="block font-medium">Title</label>
<input type="text" name="title" class="w-full border px-3 py-2 rounded" required>
</div>
<div>
<label class="block font-medium">Body</label>
<textarea name="body" class="w-full border px-3 py-2 rounded" rows="5"></textarea>
</div>
<div>
<label class="block font-medium">Image</label>
<input type="file" name="image" class="w-full">
</div>
<button class="bg-blue-600 text-white px-4 py-2 rounded">Save Post</button>
</form>
</div>
@endsection
Show Post
<!-- resources/views/posts/show.blade.php -->
@extends('layouts.app')
@section('content')
<div class="max-w-3xl mx-auto py-8">
@if($post->image)
<img src="{{ asset('storage/'.$post->image) }}" class="w-full rounded mb-4">
@endif
<h1 class="text-2xl font-bold">{{ $post->title }}</h1>
<p class="mt-3 text-gray-700 leading-relaxed">{{ $post->body }}</p>
<a href="{{ route('posts.edit', $post) }}" class="inline-block mt-4 text-blue-600">โ๏ธ Edit Post</a>
</div>
@endsection
Edit Post
<!-- resources/views/posts/edit.blade.php -->
@extends('layouts.app')
@section('content')
<div class="max-w-xl mx-auto py-6">
<form action="{{ route('posts.update', $post) }}" method="POST" enctype="multipart/form-data" class="space-y-4">
@csrf
@method('PUT')
<div>
<label class="block font-medium">Title</label>
<input type="text" name="title" value="{{ $post->title }}" class="w-full border px-3 py-2 rounded" required>
</div>
<div>
<label class="block font-medium">Body</label>
<textarea name="body" class="w-full border px-3 py-2 rounded" rows="5">{{ $post->body }}</textarea>
</div>
<div>
<label class="block font-medium">Image</label>
<input type="file" name="image" class="w-full">
@if($post->image)
<img src="{{ asset('storage/'.$post->image) }}" class="w-32 mt-2 rounded">
@endif
</div>
<button class="bg-green-600 text-white px-4 py-2 rounded">Update Post</button>
</form>
</div>
@endsection
Delete Post Button
Add delete functionality with confirmation.
<form action="{{ route('posts.destroy', $post) }}" method="POST" onsubmit="return confirm('Are you sure?')" class="mt-4">
@csrf
@method('DELETE')
<button class="bg-red-600 text-white px-3 py-1 rounded">Delete Post</button>
</form>
Testing the CRUD
Now, you can:
- Create posts with title, body, and image.
- Edit and update them instantly.
- Delete with confirmation.
- View all posts paginated on the frontend.
Tip: Run:
php artisan storage:link
to make uploaded images visible.
English
Dutch