Laravel 12 CRUD Application with Image Upload

Hey there! I’m excited to share my journey of building a CRUD (Create, Read, Update, Delete) application with image upload using Laravel 12, the latest version of this amazing PHP framework. If you’re new to Laravel or want to learn how to handle database operations and image uploads, this guide is for you.

I’ll walk you through setting up a simple blog post system where you can create, view, edit, and delete posts, complete with image uploads and SEO-friendly meta tags. My goal is to keep things simple, clear, and beginner-friendly, so let’s get started!

Step-by-Step Guide to Building a Laravel 12 CRUD Application with Image Upload

Laravel 12 CRUD Application with Image Upload

Step 1: Install and Set Up Laravel 12

First, I need to set up a fresh Laravel 12 project. Make sure you have PHP 8.2 or higher, Composer, and a database like MySQL installed. Open your terminal and run:

composer create-project laravel/laravel laravel-crud-app
cd laravel-crud-app

This creates a new Laravel project called laravel-crud-app. To check if it’s working, I start the development server:

php artisan serve

I visit http://localhost:8000 in my browser and see the Laravel welcome page—success!

Step 2: Configure the Database

Next, I set up a MySQL database. I create a database named laravel_crud using a tool like phpMyAdmin. Then, I update the .env file in the project root with my database details:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_crud
DB_USERNAME=root
DB_PASSWORD=

I replace DB_USERNAME and DB_PASSWORD with my actual MySQL credentials.

Step 3: Create the Post Model and Migration

To manage blog posts, I create a Post model with a migration and controller. Laravel’s Artisan command makes this easy:

php artisan make:model Post -mcr

This generates:

  • A Post model in app/Models/Post.php
  • A migration file in database/migrations
  • A PostController in app/Http/Controllers

I open the migration file (e.g., database/migrations/2025_07_13_XXXXXX_create_posts_table.php) and define the table structure:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content');
            $table->string('image')->nullable();
            $table->string('meta_keywords')->nullable();
            $table->string('meta_description')->nullable();
            $table->timestamps();
        });
    }

    public function down(): void {
        Schema::dropIfExists('posts');
    }
};

I run the migration to create the posts table:

php artisan migrate

Then, I update the Post model (app/Models/Post.php) to allow mass assignment:

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model {
    use HasFactory;
    protected $fillable = ['title', 'content', 'image', 'meta_keywords', 'meta_description'];
}

Step 4: Define Routes

I define routes for the CRUD operations in routes/web.php:

use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

Route::resource('posts', PostController::class);

This sets up routes for listing (index), creating (create, store), viewing (show), editing (edit, update), and deleting (destroy) posts.

Step 5: Create Blade Views

I need views to display and manage posts. First, I create a layout file at resources/views/layouts/app.blade.php with dynamic meta tags for SEO:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@yield('title', 'Laravel 12 CRUD App')</title>
    <meta name="keywords" content="@yield('meta_keywords', 'Laravel, CRUD, blog, image upload')">
    <meta name="description" content="@yield('meta_description', 'A simple Laravel 12 CRUD application with image upload')">
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-5">
        @yield('content')
    </div>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Now, I create views in resources/views/posts/:

  • index.blade.php (List all posts):
@extends('layouts.app')
@section('title', 'All Posts')
@section('content')
    <h1>All Posts</h1>
    <a href="{{ route('posts.create') }}" class="btn btn-primary mb-3">Create New Post</a>
    @if(session('success'))
        <div class="alert alert-success">{{ session('success') }}</div>
    @endif
    <table class="table">
        <thead>
            <tr>
                <th>Title</th>
                <th>Image</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
            @foreach($posts as $post)
                <tr>
                    <td>{{ $post->title }}</td>
                    <td>
                        @if($post->image)
                            <img src="{{ asset('storage/' . $post->image) }}" alt="{{ $post->title }}" width="100">
                        @endif
                    </td>
                    <td>
                        <a href="{{ route('posts.show', $post) }}" class="btn btn-info btn-sm">View</a>
                        <a href="{{ route('posts.edit', $post) }}" class="btn btn-warning btn-sm">Edit</a>
                        <form action="{{ route('posts.destroy', $post) }}" method="POST" style="display:inline;">
                            @csrf
                            @method('DELETE')
                            <button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Are you sure?')">Delete</button>
                        </form>
                    </td>
                </tr>
            @endforeach
        </tbody>
    </table>
@endsection
  • create.blade.php (Form to create a post):
@extends('layouts.app')
@section('title', 'Create Post')
@section('content')
    <h1>Create Post</h1>
    <form action="{{ route('posts.store') }}" method="POST" enctype="multipart/form-data">
        @csrf
        <div class="mb-3">
            <label for="title" class="form-label">Title</label>
            <input type="text" name="title" class="form-control" required>
            @error('title') <span class="text-danger">{{ $message }}</span> @enderror
        </div>
        <div class="mb-3">
            <label for="content" class="form-label">Content</label>
            <textarea name="content" class="form-control" required></textarea>
            @error('content') <span class="text-danger">{{ $message }}</span> @enderror
        </div>
        <div class="mb-3">
            <label for="image" class="form-label">Image</label>
            <input type="file" name="image" class="form-control">
            @error('image') <span class="text-danger">{{ $message }}</span> @enderror
        </div>
        <div class="mb-3">
            <label for="meta_keywords" class="form-label">Meta Keywords</label>
            <input type="text" name="meta_keywords" class="form-control">
        </div>
        <div class="mb-3">
            <label for="meta_description" class="form-label">Meta Description</label>
            <input type="text" name="meta_description" class="form-control">
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
@endsection
  • edit.blade.php (Form to edit a post):
@extends('layouts.app')
@section('title', 'Edit Post')
@section('content')
    <h1>Edit Post</h1>
    <form action="{{ route('posts.update', $post) }}" method="POST" enctype="multipart/form-data">
        @csrf
        @method('PUT')
        <div class="mb-3">
            <label for="title" class="form-label">Title</label>
            <input type="text" name="title" class="form-control" value="{{ $post->title }}" required>
            @error('title') <span class="text-danger">{{ $message }}</span> @enderror
        </div>
        <div class="mb-3">
            <label for="content" class="form-label">Content</label>
            <textarea name="content" class="form-control" required>{{ $post->content }}</textarea>
            @error('content') <span class="text-danger">{{ $message }}</span> @enderror
        </div>
        <div class="mb-3">
            <label for="image" class="form-label">Image</label>
            <input type="file" name="image" class="form-control">
            @if($post->image)
                <img src="{{ asset('storage/' . $post->image) }}" alt="{{ $post->title }}" width="100" class="mt-2">
            @endif
            @error('image') <span class="text-danger">{{ $message }}</span> @enderror
        </div>
        <div class="mb-3">
            <label for="meta_keywords" class="form-label">Meta Keywords</label>
            <input type="text" name="meta_keywords" class="form-control" value="{{ $post->meta_keywords }}">
        </div>
        <div class="mb-3">
            <label for="meta_description" class="form-label">Meta Description</label>
            <input type="text" name="meta_description" class="form-control" value="{{ $post->meta_description }}">
        </div>
        <button type="submit" class="btn btn-primary">Update</button>
    </form>
@endsection
  • show.blade.php (View a single post):
@extends('layouts.app')
@section('title', $post->title)
@section('meta_keywords', $post->meta_keywords)
@section('meta_description', $post->meta_description)
@section('content')
    <h1>{{ $post->title }}</h1>
    @if($post->image)
        <img src="{{ asset('storage/' . $post->image) }}" alt="{{ $post->title }}" class="img-fluid mb-3">
    @endif
    <p>{{ $post->content }}</p>
    <a href="{{ route('posts.index') }}" class="btn btn-secondary">Back to Posts</a>
@endsection

Step 6: Implement the Controller

I update app/Http/Controllers/PostController.php to handle CRUD operations and image uploads:

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::all();
        return view('posts.index', compact('posts'));
    }

    public function create() {
        return view('posts.create');
    }

    public function store(Request $request) {
        $request->validate([
            'title' => 'required|string|max:255',
            'content' => 'required',
            'image' => 'nullable|image|max:2048',
            'meta_keywords' => 'nullable|string',
            'meta_description' => 'nullable|string',
        ]);

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

        Post::create($data);
        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) {
        $request->validate([
            'title' => 'required|string|max:255',
            'content' => 'required',
            'image' => 'nullable|image|max:2048',
            'meta_keywords' => 'nullable|string',
            'meta_description' => 'nullable|string',
        ]);

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

        $post->update($data);
        return redirect()->route('posts.index')->with('success', 'Post updated successfully!');
    }

    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 successfully!');
    }
}

Step 7: Configure Image Storage

To store images, I link the storage directory for public access:

php artisan storage:link

This creates a symlink from public/storage to storage/app/public.

Step 8: Test the Application

I restart the server (php artisan serve) and visit http://localhost:8000/posts. I can now:

  • Create a post with a title, content, image, and meta tags.
  • View all posts in a table.
  • Edit or delete existing posts.
  • View individual posts with SEO-optimized meta tags.

Conclusion

I’ve built a fully functional Laravel 12 CRUD application with image upload and SEO-friendly meta tags. From setting up the project to creating models, views, and controllers, I’ve learned how Laravel simplifies web development. The dynamic meta tags ensure each post is search engine-friendly, boosting visibility. This project is a great foundation—you can extend it with features like user authentication, pagination, or advanced image processing. I hope you found this guide as exciting to follow as I did to write!

Frequently Asked Questions(FAQs)

Q1: What is a CRUD application in Laravel?
A CRUD application allows you to Create, Read, Update, and Delete data in a database. In this guide, we built one for managing blog posts with images.

Q2: How does Laravel handle image uploads?
Laravel uses the Storage facade to store uploaded files. We validated images, stored them in the public disk, and linked the storage directory for access.

Q3: Why include meta tags in a Laravel app?
Meta tags like keywords and descriptions help search engines understand your content, improving your site’s ranking and click-through rates.

Q4: Can I add authentication to this app?
Absolutely! Use Laravel Breeze or Jetstream. Run php artisan breeze:install to add login and registration features easily.

Q5: How do I improve SEO in Laravel further?
Add unique slugs to URLs, use a sitemap, optimize page load speed with caching, and ensure mobile-friendliness with responsive design.


You might also like :

techsolutionstuff

Techsolutionstuff | The Complete Guide

I'm a software engineer and the founder of techsolutionstuff.com. Hailing from India, I craft articles, tutorials, tricks, and tips to aid developers. Explore Laravel, PHP, MySQL, jQuery, Bootstrap, Node.js, Vue.js, and AngularJS in our tech stack.

RECOMMENDED POSTS

FEATURE POSTS