Author Login

Laravel Blog Post CRUD

๐Ÿ‘ค Shanta
20 Views
Oct 11, 2025
Contents

Creating the Post Model and Migration

Start by creating the Post model and database table using Artisan.

Command:

Language: BASH
php artisan make:model Post -m

Migration Example:

Language: PHP
// 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:

Language: BASH
php artisan migrate

Model Configuration

Add fillable properties and a slug generator.

Language: PHP
// 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:

Language: BASH
php artisan make:controller PostController --resource

Controller Example:

Language: TEXT
// 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.

Language: PHP
// routes/web.php
Route::resource('posts', PostController::class);

The Post Index Page

Display all posts with pagination and image thumbnails.

Language: BLADE
<!-- 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

Language: BLADE
<!-- 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

Language: BLADE
<!-- 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.

Language: BLADE
<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:

Language: BASH
php artisan storage:link

to make uploaded images visible.

About SuriSnippet

Know about our company more.

Contact Us

We are Here to Help

FAQ

Get all Answers