Technology

Mastering CRUD in Laravel 12 Build a Functional Blog With Authentication, Slugs and File Uploads

This article walks through building a full CRUD blog application in Laravel 12, starting from environment setup and Tailwind integration to migrations, controllers, Blade templates, validation and image uploads. You’ll also learn how to generate unique slugs, load author data with Eloquent relationships and secure post management using authentication.

2 weeks ago · 7 mins read
Summarize and analyze this article with:
Share this

Building a blog is one of the best ways to understand the full life cycle of a Laravel application. You work with routing, controllers, views, models, migrations, relationships, validation, middleware, file uploads, and security. In this article, I walk you through all of these parts in a structured way. It is written to help young and new developers build a practical blog that supports creating, reading, updating, and deleting posts while also handling images, generating slugs, and securing admin actions through authentication.

Before going into every step, keep in mind a common mistake. Many tutorials jump into writing controllers before setting the environment correctly. That causes version mismatches, Tailwind not compiling, or unexpected PHP errors later. So the first section focuses on laying the groundwork in a predictable and reproducible way.


1. Environment Setup

1.1 Check PHP and Composer Versions

Laravel 12 expects modern PHP. Running outdated versions produces subtle errors that waste hours later.
Run these commands:

php -v
composer -v

Make sure PHP is at least 8.2 and Composer is up to date.

1.2 Create the Laravel Project

composer create-project laravel/laravel blog-laravel
cd blog-laravel

Once done, launch your server:

php artisan serve

You should see the default Laravel welcome page.

1.3 Install and Configure Tailwind CSS

Many skip this and end up styling with inline CSS. Tailwind gives the flexibility of utility classes, which is cleaner for component based Laravel Blade.

npm install
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Update tailwind.config.js:

content: [
    "./resources/**/*.blade.php",
    "./resources/**/*.js",
]

Start the build watcher:

npm run dev

Your Laravel app now supports Tailwind.


2. Planning the Database Schema

A blog normally has these fields for posts:

  • id
  • title
  • slug
  • body
  • image
  • user_id
  • created_at and updated_at

Slug generation is often ignored until URLs break. Plan for it now because it affects routing.

2.1 Create the Migration

php artisan make:migration create_posts_table

Inside the migration:

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->string('slug')->unique();
    $table->text('body');
    $table->string('image')->nullable();
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->timestamps();
});

This foreign key forces every post to belong to a valid user.

Run the migration:

php artisan migrate

3. Creating the Post Model and Relationship

php artisan make:model Post

Inside Post.php:

public function author() {
    return $this->belongsTo(User::class, 'user_id');
}

This relationship powers author names in the frontend without needing extra queries.


4. Building the Resource Controller

Image
Image

Laravel resource controllers reduce repetitive code. They also map cleanly to CRUD.

php artisan make:controller PostController --resource

This generates methods for index, create, store, show, edit, update, and destroy.

4.1 Add Resource Routes

In routes/web.php:

Route::resource('posts', PostController::class)->middleware('auth');

Now only logged in users can manage posts. Readers can still view public blog pages, which you will handle later with public routes.


5. Working With Blade Templates

Many new developers cram all HTML into one file. A better approach is to structure templates with layouts and components.

5.1 Create a Layout

resources/views/layouts/app.blade.php:

<html>
<head>
    <title>{{ $title ?? 'My Blog' }}</title>
    @vite('resources/css/app.css')
</head>
<body class="bg-gray-100 text-slate-800">

<nav class="bg-white shadow p-4 flex justify-between">
    <a href="/" class="font-bold">My Blog</a>
    @auth
        <a href="/posts/create">New Post</a>
    @endauth
</nav>

<div class="max-w-4xl mx-auto mt-10">
    {{ $slot }}
</div>

</body>
</html>

Now you can wrap any page inside this layout:

<x-layouts.app title="Create Post">
   // content here
</x-layouts.app>

6. Implementing the Post Creation Form

6.1 Create Blade File

resources/views/posts/create.blade.php:

<form action="{{ route('posts.store') }}" method="POST" enctype="multipart/form-data">
    @csrf

    <input type="text" name="title" class="border p-2 w-full" placeholder="Title">
    <textarea name="body" class="border p-2 w-full mt-4" placeholder="Body"></textarea>

    <input type="file" name="image" class="mt-4">

    <button class="bg-blue-600 text-white px-4 py-2 mt-4">Publish</button>
</form>

7. Handling Validation, Slugs, and Image Uploads

Inside PostController@store:

$data = request()->validate([
    'title' => 'required|min:3',
    'body' => 'required|min:10',
    'image' => 'nullable|image|max:2048'
]);

if (request()->hasFile('image')) {
    $data['image'] = request()->file('image')->store('images', 'public');
}

$data['slug'] = Str::slug($data['title']) . '-' . uniqid();
$data['user_id'] = auth()->id();

Post::create($data);

Key notes that developers often ignore:

  • Slugs must be unique or you break routing
  • Store images in storage/public for easy CDN use later
  • Avoid trusting user input for author id

8. Displaying Posts on the Frontend

PostController@index:

$posts = Post::with('author')->latest()->paginate(10);
return view('posts.index', compact('posts'));

Blade file:

@foreach ($posts as $post)
    <a href="{{ route('posts.show', $post->slug) }}">
        <h2 class="text-xl font-semibold">{{ $post->title }}</h2>
        <p class="text-gray-500 text-sm">By {{ $post->author->name }}</p>
    </a>
@endforeach

This uses eager loading to avoid N+1 query problems. A surprising number of devs miss this until the site slows down.


9. Creating the Show Page

public function show(Post $post) {
    return view('posts.show', compact('post'));
}

Blade file:

@if ($post->image)
    <img src="{{ asset('storage/' . $post->image) }}" class="w-full mb-4">
@endif

<h1 class="text-3xl font-bold">{{ $post->title }}</h1>
<p class="mt-2">{{ $post->body }}</p>

10. Editing and Updating Posts

Update form pre fills values:

<input type="text" name="title" value="{{ $post->title }}">
<textarea name="body">{{ $post->body }}</textarea>

Update logic:

$data = request()->validate([
    'title' => 'required',
    'body' => 'required',
    'image' => 'nullable|image'
]);

if (request()->hasFile('image')) {
    Storage::disk('public')->delete($post->image);
    $data['image'] = request()->file('image')->store('images', 'public');
}

$post->update($data);

Note the delete step. Leaving old images behind bloats disk usage quickly.


11. Deleting Posts Safely

public function destroy(Post $post) {
    Storage::disk('public')->delete($post->image);
    $post->delete();

    return redirect()->route('posts.index');
}

Soft deletes are optional but useful for recovering lost content.


12. Securing Actions With Authentication

All admin actions are already behind middleware due to resource route protection:

Route::resource('posts', PostController::class)->middleware('auth');

If you want extra rules like authors only editing their posts, add this in update and destroy:

if ($post->user_id !== auth()->id()) {
    abort(403);
}

13. Testing the CRUD Flow

Check each part:

  • Create post
  • Slug generated correctly
  • Image uploads work
  • Author name displays
  • Edit preserves slug or regenerates it depending on your logic
  • Delete removes file from storage
  • Authentication blocks anonymous users

Testing this cycle is the best way to catch indexing or routing mistakes.


Closing Thoughts

This project covers the full spectrum of Laravel skills. If you build it cleanly, you understand routing, Blade, Tailwind, migrations, relationships, middleware, validation, and file handling. Most real world projects follow similar patterns, just with more models and relationships. The key is not writing code fast, but structuring it so it is readable and predictable.

Read next

Before You Chase AI SEO, Fix Your Page Titles: Laravel Tools That Help

SEO in 2025 feels crowded with AI tools, automation, and buzzwords. But the truth is simpler. Page titles, meta descriptions, and Open Graph tags still decide whether someone clicks your link or ignores it. In this article, we look at practical Laravel packages that help you handle SEO fundamentals properly, without overengineering things.

Dec 17 · 1 min read