Laravel 12 Basic Blog CMS (Admin Panel Only): A Step-by-Step Guide

Author

Kritim Yantra

May 04, 2025

Laravel 12 Basic Blog CMS (Admin Panel Only): A Step-by-Step Guide

In this tutorial, we'll build a complete Blog Content Management System (CMS) with Laravel 12, focusing exclusively on the admin panel functionality. This CMS will allow authorized users to manage blog posts, categories, and tags through a secure backend interface.

Whether you're a beginner looking to learn Laravel or an intermediate developer wanting to solidify your CMS-building skills, this guide will walk you through every step of creating a functional blog administration system.

Prerequisites

Before we begin, ensure you have:

  1. PHP 8.2 or higher installed
  2. Composer (PHP dependency manager)
  3. Node.js and npm (for frontend assets)
  4. A database server (MySQL, PostgreSQL, or SQLite)
  5. A code editor (VS Code, PHPStorm, etc.)
  6. Basic understanding of PHP and MVC concepts

Step 1: Install Laravel 12

Create a new Laravel project by running:

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

Step 2: Configure Environment Variables

Set up your environment:

  1. Rename .env.example to .env
  2. Generate an application key:
    php artisan key:generate
    
  3. Configure your database settings in .env:
    DB_CONNECTION=mysql
    DB_HOST=127.0.0.1
    DB_PORT=3306
    DB_DATABASE=laravel_blog_cms
    DB_USERNAME=root
    DB_PASSWORD=
    

Step 3: Set Up Authentication with Admin Role

We'll use Laravel Breeze for authentication with an admin role:

composer require laravel/breeze --dev
php artisan breeze:install
php artisan migrate
npm install && npm run dev

Now, let's add an admin role to our users:

  1. Create a migration for the admin role:

    php artisan make:migration add_is_admin_to_users_table
    
  2. Edit the migration file:

    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->boolean('is_admin')->default(false);
        });
    }
    
  3. Run the migration:

    php artisan migrate
    
  4. Add the is_admin to the fillable array in app/Models/User.php:

    protected $fillable = [
        'name',
        'email',
        'password',
        'is_admin'
    ];
    

Step 4: Create Database Migrations

Let's create the necessary tables for our blog CMS:

Posts Table

php artisan make:migration create_posts_table

Edit the migration:

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->string('slug')->unique();
    $table->text('excerpt');
    $table->longText('content');
    $table->string('featured_image')->nullable();
    $table->boolean('published')->default(false);
    $table->dateTime('published_at')->nullable();
    $table->foreignId('user_id')->constrained();
    $table->timestamps();
});

Categories Table

php artisan make:migration create_categories_table

Edit the migration:

Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->timestamps();
});

Tags Table

php artisan make:migration create_tags_table

Edit the migration:

Schema::create('tags', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->timestamps();
});

Pivot Tables

For the many-to-many relationships:

php artisan make:migration create_category_post_table
php artisan make:migration create_post_tag_table

Edit the category_post migration:

Schema::create('category_post', function (Blueprint $table) {
    $table->foreignId('category_id')->constrained();
    $table->foreignId('post_id')->constrained();
    $table->primary(['category_id', 'post_id']);
});

Edit the post_tag migration:

Schema::create('post_tag', function (Blueprint $table) {
    $table->foreignId('post_id')->constrained();
    $table->foreignId('tag_id')->constrained();
    $table->primary(['post_id', 'tag_id']);
});

Run all migrations:

php artisan migrate

Step 5: Create Models and Relationships

Post Model

php artisan make:model Post

Edit app/Models/Post.php:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'slug',
        'excerpt',
        'content',
        'featured_image',
        'published',
        'published_at',
        'user_id'
    ];

    protected $casts = [
        'published_at' => 'datetime',
        'published' => 'boolean'
    ];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function categories(): BelongsToMany
    {
        return $this->belongsToMany(Category::class);
    }

    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class);
    }
}

Category Model

php artisan make:model Category

Edit app/Models/Category.php:

<?php

namespace App\Models;

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

class Category extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'slug'];

    public function posts(): BelongsToMany
    {
        return $this->belongsToMany(Post::class);
    }
}

Tag Model

php artisan make:model Tag

Edit app/Models/Tag.php:

<?php

namespace App\Models;

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

class Tag extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'slug'];

    public function posts(): BelongsToMany
    {
        return $this->belongsToMany(Post::class);
    }
}

Step 6: Create Factories and Seed the Database

Let's create factories to generate test data:

Post Factory

php artisan make:factory PostFactory

Edit database/factories/PostFactory.php:

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'title' => $this->faker->sentence,
            'slug' => $this->faker->slug,
            'excerpt' => $this->faker->paragraph,
            'content' => $this->faker->text(2000),
            'published' => $this->faker->boolean,
            'published_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
            'user_id' => \App\Models\User::factory(),
        ];
    }
}

Category and Tag Factories

php artisan make:factory CategoryFactory
php artisan make:factory TagFactory

Edit database/factories/CategoryFactory.php:

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class CategoryFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name' => $this->faker->word,
            'slug' => $this->faker->slug,
        ];
    }
}

Edit database/factories/TagFactory.php:

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class TagFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name' => $this->faker->word,
            'slug' => $this->faker->slug,
        ];
    }
}

Create a database seeder:

php artisan make:seeder DatabaseSeeder

Edit database/seeders/DatabaseSeeder.php:

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        // Create admin user
        \App\Models\User::factory()->create([
            'name' => 'Admin User',
            'email' => 'admin@example.com',
            'password' => bcrypt('password'),
            'is_admin' => true,
        ]);

        // Create regular user
        \App\Models\User::factory()->create([
            'name' => 'Regular User',
            'email' => 'user@example.com',
            'password' => bcrypt('password'),
            'is_admin' => false,
        ]);

        // Create categories, tags, and posts
        $categories = \App\Models\Category::factory(5)->create();
        $tags = \App\Models\Tag::factory(10)->create();

        \App\Models\Post::factory(20)->create()->each(function ($post) use ($categories, $tags) {
            $post->categories()->attach(
                $categories->random(rand(1, 3))->pluck('id')->toArray()
            );
            
            $post->tags()->attach(
                $tags->random(rand(2, 5))->pluck('id')->toArray()
            );
        });
    }
}

Run the seeder:

php artisan db:seed

Step 7: Set Up Admin Middleware

We need to protect our admin routes so only admin users can access them:

  1. Create the middleware:

    php artisan make:middleware AdminMiddleware
    
  2. Edit app/Http/Middleware/AdminMiddleware.php:

    <?php
    
    namespace App\Http\Middleware;
    
    use Closure;
    use Illuminate\Http\Request;
    use Symfony\Component\HttpFoundation\Response;
    
    class AdminMiddleware
    {
        public function handle(Request $request, Closure $next): Response
        {
            if (!auth()->check() || !auth()->user()->is_admin) {
                abort(403, 'Unauthorized action.');
            }
    
            return $next($request);
        }
    }
    
  3. Register the laravel 12 middleware in bootstrap/app.php:

    ->withMiddleware(function ($middleware) {
        $middleware->alias([
            'admin' => \App\Http\Middleware\AdminMiddleware::class,
        ]);
    })
    

Step 8: Create Controllers

Let's create controllers for our admin panel:

Admin Dashboard Controller

php artisan make:controller Admin/DashboardController

Edit app/Http/Controllers/Admin/DashboardController.php:

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Post;
use App\Models\Category;
use App\Models\Tag;
use App\Models\User;

class DashboardController extends Controller
{
    public function index()
    {
        $stats = [
            'posts' => Post::count(),
            'categories' => Category::count(),
            'tags' => Tag::count(),
            'users' => User::count(),
        ];

        $recentPosts = Post::latest()->take(5)->get();

        return view('admin.dashboard', compact('stats', 'recentPosts'));
    }
}

Post Controller

php artisan make:controller Admin/PostController --resource

Edit app/Http/Controllers/Admin/PostController.php:

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Post;
use App\Models\Category;
use App\Models\Tag;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::with(['user', 'categories', 'tags'])->latest()->paginate(10);
        return view('admin.posts.index', compact('posts'));
    }

    public function create()
    {
        $categories = Category::all();
        $tags = Tag::all();
        return view('admin.posts.create', compact('categories', 'tags'));
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'excerpt' => 'required|string|max:500',
            'content' => 'required|string',
            'featured_image' => 'nullable|image|max:2048',
            'published' => 'boolean',
            'categories' => 'required|array',
            'categories.*' => 'exists:categories,id',
            'tags' => 'required|array',
            'tags.*' => 'exists:tags,id',
        ]);

        $post = new Post();
        $post->title = $validated['title'];
        $post->slug = Str::slug($validated['title']);
        $post->excerpt = $validated['excerpt'];
        $post->content = $validated['content'];
        $post->published = $request->has('published');
        $post->user_id = auth()->id();

        if ($request->hasFile('featured_image')) {
            $post->featured_image = $request->file('featured_image')->store('posts', 'public');
        }

        $post->save();

        $post->categories()->sync($validated['categories']);
        $post->tags()->sync($validated['tags']);

        return redirect()->route('admin.posts.index')->with('success', 'Post created successfully!');
    }

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

    public function edit(Post $post)
    {
        $categories = Category::all();
        $tags = Tag::all();
        return view('admin.posts.edit', compact('post', 'categories', 'tags'));
    }

    public function update(Request $request, Post $post)
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'excerpt' => 'required|string|max:500',
            'content' => 'required|string',
            'featured_image' => 'nullable|image|max:2048',
            'published' => 'boolean',
            'categories' => 'required|array',
            'categories.*' => 'exists:categories,id',
            'tags' => 'required|array',
            'tags.*' => 'exists:tags,id',
        ]);

        $post->title = $validated['title'];
        $post->slug = Str::slug($validated['title']);
        $post->excerpt = $validated['excerpt'];
        $post->content = $validated['content'];
        $post->published = $request->has('published');

        if ($request->hasFile('featured_image')) {
            // Delete old image if exists
            if ($post->featured_image) {
                Storage::disk('public')->delete($post->featured_image);
            }
            $post->featured_image = $request->file('featured_image')->store('posts', 'public');
        }

        $post->save();

        $post->categories()->sync($validated['categories']);
        $post->tags()->sync($validated['tags']);

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

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

        $post->delete();

        return redirect()->route('admin.posts.index')->with('success', 'Post deleted successfully!');
    }
}

Category Controller

php artisan make:controller Admin/CategoryController --resource

Edit app/Http/Controllers/Admin/CategoryController.php:

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

class CategoryController extends Controller
{
    public function index()
    {
        $categories = Category::latest()->paginate(10);
        return view('admin.categories.index', compact('categories'));
    }

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

    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255|unique:categories',
        ]);

        Category::create([
            'name' => $validated['name'],
            'slug' => Str::slug($validated['name']),
        ]);

        return redirect()->route('admin.categories.index')->with('success', 'Category created successfully!');
    }

    public function edit(Category $category)
    {
        return view('admin.categories.edit', compact('category'));
    }

    public function update(Request $request, Category $category)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255|unique:categories,name,' . $category->id,
        ]);

        $category->update([
            'name' => $validated['name'],
            'slug' => Str::slug($validated['name']),
        ]);

        return redirect()->route('admin.categories.index')->with('success', 'Category updated successfully!');
    }

    public function destroy(Category $category)
    {
        $category->delete();
        return redirect()->route('admin.categories.index')->with('success', 'Category deleted successfully!');
    }
}

Tag Controller

php artisan make:controller Admin/TagController --resource

Edit app/Http/Controllers/Admin/TagController.php:

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Tag;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

class TagController extends Controller
{
    public function index()
    {
        $tags = Tag::latest()->paginate(10);
        return view('admin.tags.index', compact('tags'));
    }

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

    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255|unique:tags',
        ]);

        Tag::create([
            'name' => $validated['name'],
            'slug' => Str::slug($validated['name']),
        ]);

        return redirect()->route('admin.tags.index')->with('success', 'Tag created successfully!');
    }

    public function edit(Tag $tag)
    {
        return view('admin.tags.edit', compact('tag'));
    }

    public function update(Request $request, Tag $tag)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255|unique:tags,name,' . $tag->id,
        ]);

        $tag->update([
            'name' => $validated['name'],
            'slug' => Str::slug($validated['name']),
        ]);

        return redirect()->route('admin.tags.index')->with('success', 'Tag updated successfully!');
    }

    public function destroy(Tag $tag)
    {
        $tag->delete();
        return redirect()->route('admin.tags.index')->with('success', 'Tag deleted successfully!');
    }
}

Step 9: Create Admin Views

Let's create the admin panel views. We'll use Bootstrap for styling.

Admin Layout

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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Admin Panel | Laravel Blog CMS</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
    @vite(['resources/sass/app.scss', 'resources/js/app.js'])
</head>
<body>
    <div class="d-flex">
        <!-- Sidebar -->
        <div class="bg-dark text-white vh-100 p-3" style="width: 250px;">
            <h4 class="mb-4">Laravel Blog CMS</h4>
            <ul class="nav flex-column">
                <li class="nav-item">
                    <a class="nav-link text-white {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}" href="{{ route('admin.dashboard') }}">
                        <i class="bi bi-speedometer2 me-2"></i> Dashboard
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link text-white {{ request()->routeIs('admin.posts.*') ? 'active' : '' }}" href="{{ route('admin.posts.index') }}">
                        <i class="bi bi-file-earmark-post me-2"></i> Posts
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link text-white {{ request()->routeIs('admin.categories.*') ? 'active' : '' }}" href="{{ route('admin.categories.index') }}">
                        <i class="bi bi-tags me-2"></i> Categories
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link text-white {{ request()->routeIs('admin.tags.*') ? 'active' : '' }}" href="{{ route('admin.tags.index') }}">
                        <i class="bi bi-tag me-2"></i> Tags
                    </a>
                </li>
                <li class="nav-item mt-4">
                    <form method="POST" action="{{ route('logout') }}">
                        @csrf
                        <button type="submit" class="nav-link text-white bg-transparent border-0">
                            <i class="bi bi-box-arrow-right me-2"></i> Logout
                        </button>
                    </form>
                </li>
            </ul>
        </div>

        <!-- Main Content -->
        <div class="flex-grow-1 p-4">
            @if(session('success'))
                <div class="alert alert-success alert-dismissible fade show" role="alert">
                    {{ session('success') }}
                    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
                </div>
            @endif

            @if(session('error'))
                <div class="alert alert-danger alert-dismissible fade show" role="alert">
                    {{ session('error') }}
                    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
                </div>
            @endif

            @yield('content')
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
    @stack('scripts')
</body>
</html>

Dashboard View

Create resources/views/admin/dashboard.blade.php:

@extends('admin.layouts.app')

@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
    <h2>Dashboard</h2>
</div>

<div class="row mb-4">
    <div class="col-md-3">
        <div class="card bg-primary text-white">
            <div class="card-body">
                <h5 class="card-title">Posts</h5>
                <h2 class="card-text">{{ $stats['posts'] }}</h2>
            </div>
        </div>
    </div>
    <div class="col-md-3">
        <div class="card bg-success text-white">
            <div class="card-body">
                <h5 class="card-title">Categories</h5>
                <h2 class="card-text">{{ $stats['categories'] }}</h2>
            </div>
        </div>
    </div>
    <div class="col-md-3">
        <div class="card bg-info text-white">
            <div class="card-body">
                <h5 class="card-title">Tags</h5>
                <h2 class="card-text">{{ $stats['tags'] }}</h2>
            </div>
        </div>
    </div>
    <div class="col-md-3">
        <div class="card bg-warning text-dark">
            <div class="card-body">
                <h5 class="card-title">Users</h5>
                <h2 class="card-text">{{ $stats['users'] }}</h2>
            </div>
        </div>
    </div>
</div>

<div class="card">
    <div class="card-header">
        <h5>Recent Posts</h5>
    </div>
    <div class="card-body">
        <table class="table table-striped">
            <thead>
                <tr>
                    <th>Title</th>
                    <th>Status</th>
                    <th>Published At</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                @foreach($recentPosts as $post)
                <tr>
                    <td>{{ $post->title }}</td>
                    <td>
                        <span class="badge bg-{{ $post->published ? 'success' : 'secondary' }}">
                            {{ $post->published ? 'Published' : 'Draft' }}
                        </span>
                    </td>
                    <td>{{ $post->published_at ? $post->published_at->format('M d, Y') : '-' }}</td>
                    <td>
                        <a href="{{ route('admin.posts.edit', $post) }}" class="btn btn-sm btn-primary">
                            <i class="bi bi-pencil"></i>
                        </a>
                    </td>
                </tr>
                @endforeach
            </tbody>
        </table>
    </div>
</div>
@endsection

Post Views

Create post views in resources/views/admin/posts/:

index.blade.php

@extends('admin.layouts.app')

@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
    <h2>Posts</h2>
    <a href="{{ route('admin.posts.create') }}" class="btn btn-primary">
        <i class="bi bi-plus-circle"></i> Create Post
    </a>
</div>

<div class="card">
    <div class="card-body">
        <table class="table table-striped">
            <thead>
                <tr>
                    <th>Title</th>
                    <th>Author</th>
                    <th>Categories</th>
                    <th>Status</th>
                    <th>Published At</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                @foreach($posts as $post)
                <tr>
                    <td>{{ $post->title }}</td>
                    <td>{{ $post->user->name }}</td>
                    <td>
                        @foreach($post->categories as $category)
                            <span class="badge bg-primary">{{ $category->name }}</span>
                        @endforeach
                    </td>
                    <td>
                        <span class="badge bg-{{ $post->published ? 'success' : 'secondary' }}">
                            {{ $post->published ? 'Published' : 'Draft' }}
                        </span>
                    </td>
                    <td>{{ $post->published_at ? $post->published_at->format('M d, Y') : '-' }}</td>
                    <td>
                        <a href="{{ route('admin.posts.edit', $post) }}" class="btn btn-sm btn-primary">
                            <i class="bi bi-pencil"></i>
                        </a>
                        <form action="{{ route('admin.posts.destroy', $post) }}" method="POST" class="d-inline">
                            @csrf
                            @method('DELETE')
                            <button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure?')">
                                <i class="bi bi-trash"></i>
                            </button>
                        </form>
                    </td>
                </tr>
                @endforeach
            </tbody>
        </table>
        {{ $posts->links() }}
    </div>
</div>
@endsection

create.blade.php

@extends('admin.layouts.app')

@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
    <h2>Create Post</h2>
    <a href="{{ route('admin.posts.index') }}" class="btn btn-secondary">Back</a>
</div>

<div class="card">
    <div class="card-body">
        <form action="{{ route('admin.posts.store') }}" method="POST" enctype="multipart/form-data">
            @csrf
            <div class="mb-3">
                <label for="title" class="form-label">Title</label>
                <input type="text" class="form-control" id="title" name="title" required>
            </div>
            <div class="mb-3">
                <label for="excerpt" class="form-label">Excerpt</label>
                <textarea class="form-control" id="excerpt" name="excerpt" rows="3" required></textarea>
            </div>
            <div class="mb-3">
                <label for="content" class="form-label">Content</label>
                <textarea class="form-control" id="content" name="content" rows="10" required></textarea>
            </div>
            <div class="mb-3">
                <label for="featured_image" class="form-label">Featured Image</label>
                <input type="file" class="form-control" id="featured_image" name="featured_image">
            </div>
            <div class="mb-3">
                <div class="form-check">
                    <input class="form-check-input" type="checkbox" id="published" name="published" value="1">
                    <label class="form-check-label" for="published">
                        Publish
                    </label>
                </div>
            </div>
            <div class="mb-3">
                <label class="form-label">Categories</label>
                <div class="row">
                    @foreach($categories as $category)
                    <div class="col-md-3">
                        <div class="form-check">
                            <input class="form-check-input" type="checkbox" 
                                   id="category_{{ $category->id }}" 
                                   name="categories[]" 
                                   value="{{ $category->id }}">
                            <label class="form-check-label" for="category_{{ $category->id }}">
                                {{ $category->name }}
                            </label>
                        </div>
                    </div>
                    @endforeach
                </div>
            </div>
            <div class="mb-3">
                <label class="form-label">Tags</label>
                <div class="row">
                    @foreach($tags as $tag)
                    <div class="col-md-3">
                        <div class="form-check">
                            <input class="form-check-input" type="checkbox" 
                                   id="tag_{{ $tag->id }}" 
                                   name="tags[]" 
                                   value="{{ $tag->id }}">
                            <label class="form-check-label" for="tag_{{ $tag->id }}">
                                {{ $tag->name }}
                            </label>
                        </div>
                    </div>
                    @endforeach
                </div>
            </div>
            <button type="submit" class="btn btn-primary">Save Post</button>
        </form>
    </div>
</div>
@endsection

@push('scripts')
<script src="https://cdn.ckeditor.com/4.16.2/standard/ckeditor.js"></script>
<script>
    CKEDITOR.replace('content');
</script>
@endpush

edit.blade.php

@extends('admin.layouts.app')

@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
    <h2>Edit Post</h2>
    <a href="{{ route('admin.posts.index') }}" class="btn btn-secondary">Back</a>
</div>

<div class="card">
    <div class="card-body">
        <form action="{{ route('admin.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" class="form-control" id="title" name="title" value="{{ $post->title }}" required>
            </div>
            <div class="mb-3">
                <label for="excerpt" class="form-label">Excerpt</label>
                <textarea class="form-control" id="excerpt" name="excerpt" rows="3" required>{{ $post->excerpt }}</textarea>
            </div>
            <div class="mb-3">
                <label for="content" class="form-label">Content</label>
                <textarea class="form-control" id="content" name="content" rows="10" required>{{ $post->content }}</textarea>
            </div>
            <div class="mb-3">
                <label for="featured_image" class="form-label">Featured Image</label>
                @if($post->featured_image)
                    <div class="mb-2">
                        <img src="{{ asset('storage/' . $post->featured_image) }}" alt="Featured Image" class="img-thumbnail" style="max-height: 200px;">
                    </div>
                @endif
                <input type="file" class="form-control" id="featured_image" name="featured_image">
            </div>
            <div class="mb-3">
                <div class="form-check">
                    <input class="form-check-input" type="checkbox" id="published" name="published" value="1" {{ $post->published ? 'checked' : '' }}>
                    <label class="form-check-label" for="published">
                        Publish
                    </label>
                </div>
            </div>
            <div class="mb-3">
                <label class="form-label">Categories</label>
                <div class="row">
                    @foreach($categories as $category)
                    <div class="col-md-3">
                        <div class="form-check">
                            <input class="form-check-input" type="checkbox" 
                                   id="category_{{ $category->id }}" 
                                   name="categories[]" 
                                   value="{{ $category->id }}"
                                   {{ $post->categories->contains($category->id) ? 'checked' : '' }}>
                            <label class="form-check-label" for="category_{{ $category->id }}">
                                {{ $category->name }}
                            </label>
                        </div>
                    </div>
                    @endforeach
                </div>
            </div>
            <div class="mb-3">
                <label class="form-label">Tags</label>
                <div class="row">
                    @foreach($tags as $tag)
                    <div class="col-md-3">
                        <div class="form-check">
                            <input class="form-check-input" type="checkbox" 
                                   id="tag_{{ $tag->id }}" 
                                   name="tags[]" 
                                   value="{{ $tag->id }}"
                                   {{ $post->tags->contains($tag->id) ? 'checked' : '' }}>
                            <label class="form-check-label" for="tag_{{ $tag->id }}">
                                {{ $tag->name }}
                            </label>
                        </div>
                    </div>
                    @endforeach
                </div>
            </div>
            <button type="submit" class="btn btn-primary">Update Post</button>
        </form>
    </div>
</div>
@endsection

@push('scripts')
<script src="https://cdn.ckeditor.com/4.16.2/standard/ckeditor.js"></script>
<script>
    CKEDITOR.replace('content');
</script>
@endpush

show.blade.php

@extends('admin.layouts.app')

@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
    <h2>Post Details</h2>
    <a href="{{ route('admin.posts.index') }}" class="btn btn-secondary">Back</a>
</div>

<div class="card-body">
        <div class="mb-4">
            <h3>{{ $post->title }}</h3>
            <div class="text-muted mb-2">
                <span class="me-3">By {{ $post->user->name }}</span>
                <span>Published: {{ $post->published_at ? $post->published_at->format('M d, Y') : 'Not published' }}</span>
            </div>
            <div class="mb-3">
                @foreach($post->categories as $category)
                    <span class="badge bg-primary me-1">{{ $category->name }}</span>
                @endforeach
                @foreach($post->tags as $tag)
                    <span class="badge bg-secondary me-1">{{ $tag->name }}</span>
                @endforeach
            </div>
            @if($post->featured_image)
                <img src="{{ asset('storage/' . $post->featured_image) }}" alt="Featured Image" class="img-fluid mb-4">
            @endif
            <div class="mb-4">
                <h5>Excerpt</h5>
                <p>{{ $post->excerpt }}</p>
            </div>
            <div>
                <h5>Content</h5>
                <div>{!! $post->content !!}</div>
            </div>
        </div>
        <div class="d-flex justify-content-end">
            <a href="{{ route('admin.posts.edit', $post) }}" class="btn btn-primary me-2">
                <i class="bi bi-pencil"></i> Edit
            </a>
            <form action="{{ route('admin.posts.destroy', $post) }}" method="POST">
                @csrf
                @method('DELETE')
                <button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure?')">
                    <i class="bi bi-trash"></i> Delete
                </button>
            </form>
        </div>
    </div>
</div>
@endsection

Category Views

Create category views in resources/views/admin/categories/:

index.blade.php

@extends('admin.layouts.app')

@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
    <h2>Categories</h2>
    <a href="{{ route('admin.categories.create') }}" class="btn btn-primary">
        <i class="bi bi-plus-circle"></i> Create Category
    </a>
</div>

<div class="card">
    <div class="card-body">
        <table class="table table-striped">
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Slug</th>
                    <th>Posts</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                @foreach($categories as $category)
                <tr>
                    <td>{{ $category->name }}</td>
                    <td>{{ $category->slug }}</td>
                    <td>{{ $category->posts_count }}</td>
                    <td>
                        <a href="{{ route('admin.categories.edit', $category) }}" class="btn btn-sm btn-primary">
                            <i class="bi bi-pencil"></i>
                        </a>
                        <form action="{{ route('admin.categories.destroy', $category) }}" method="POST" class="d-inline">
                            @csrf
                            @method('DELETE')
                            <button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure?')">
                                <i class="bi bi-trash"></i>
                            </button>
                        </form>
                    </td>
                </tr>
                @endforeach
            </tbody>
        </table>
        {{ $categories->links() }}
    </div>
</div>
@endsection

create.blade.php

@extends('admin.layouts.app')

@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
    <h2>Create Category</h2>
    <a href="{{ route('admin.categories.index') }}" class="btn btn-secondary">Back</a>
</div>

<div class="card">
    <div class="card-body">
        <form action="{{ route('admin.categories.store') }}" method="POST">
            @csrf
            <div class="mb-3">
                <label for="name" class="form-label">Name</label>
                <input type="text" class="form-control" id="name" name="name" required>
            </div>
            <button type="submit" class="btn btn-primary">Save Category</button>
        </form>
    </div>
</div>
@endsection

edit.blade.php

@extends('admin.layouts.app')

@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
    <h2>Edit Category</h2>
    <a href="{{ route('admin.categories.index') }}" class="btn btn-secondary">Back</a>
</div>

<div class="card">
    <div class="card-body">
        <form action="{{ route('admin.categories.update', $category) }}" method="POST">
            @csrf
            @method('PUT')
            <div class="mb-3">
                <label for="name" class="form-label">Name</label>
                <input type="text" class="form-control" id="name" name="name" value="{{ $category->name }}" required>
            </div>
            <button type="submit" class="btn btn-primary">Update Category</button>
        </form>
    </div>
</div>
@endsection

Tag Views

Create tag views in resources/views/admin/tags/ (similar structure to categories):

index.blade.php

@extends('admin.layouts.app')

@section('content')
<div class="d-flex justify-content-between align-items-center mb-4">
    <h2>Tags</h2>
    <a href="{{ route('admin.tags.create') }}" class="btn btn-primary">
        <i class="bi bi-plus-circle"></i> Create Tag
    </a>
</div>

<div class="card">
    <div class="card-body">
        <table class="table table-striped">
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Slug</th>
                    <th>Posts</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                @foreach($tags as $tag)
                <tr>
                    <td>{{ $tag->name }}</td>
                    <td>{{ $tag->slug }}</td>
                    <td>{{ $tag->posts_count }}</td>
                    <td>
                        <a href="{{ route('admin.tags.edit', $tag) }}" class="btn btn-sm btn-primary">
                            <i class="bi bi-pencil"></i>
                        </a>
                        <form action="{{ route('admin.tags.destroy', $tag) }}" method="POST" class="d-inline">
                            @csrf
                            @method('DELETE')
                            <button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure?')">
                                <i class="bi bi-trash"></i>
                            </button>
                        </form>
                    </td>
                </tr>
                @endforeach
            </tbody>
        </table>
        {{ $tags->links() }}
    </div>
</div>
@endsection

create.blade.php and edit.blade.php

These views would be nearly identical to the category versions, just changing "Category" to "Tag" in the text.

Step 10: Set Up Admin Routes

Edit routes/web.php to include our admin routes:

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Admin\DashboardController;
use App\Http\Controllers\Admin\PostController;
use App\Http\Controllers\Admin\CategoryController;
use App\Http\Controllers\Admin\TagController;

Route::get('/', function () {
    return view('welcome');
});

Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
    
    // Posts
    Route::resource('posts', PostController::class)->except(['show']);
    Route::get('posts/{post}', [PostController::class, 'show'])->name('posts.show');
    
    // Categories
    Route::resource('categories', CategoryController::class)->except(['show']);
    
    // Tags
    Route::resource('tags', TagController::class)->except(['show']);
});

require __DIR__.'/auth.php';

Step 11: Add Some Styling

Create resources/sass/app.scss:

// Fonts
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap');

// Variables
@import 'variables';

// Bootstrap
@import 'bootstrap/scss/bootstrap';

body {
    font-family: 'Open Sans', sans-serif;
}

// Sidebar navigation
.nav-link {
    padding: 0.5rem 1rem;
    border-radius: 0.25rem;
    margin-bottom: 0.25rem;
    
    &.active {
        background-color: rgba(255, 255, 255, 0.1);
    }
    
    &:hover:not(.active) {
        background-color: rgba(255, 255, 255, 0.05);
    }
}

// Tables
.table {
    th, td {
        vertical-align: middle;
    }
}

// CKEditor content
.ck-content {
    min-height: 300px;
}

// Badges
.badge {
    font-weight: normal;
}

Step 12: Test the Admin Panel

Now you can test your admin panel:

  1. Start the development server:
    php artisan serve
    
  2. Visit http://localhost:8000/login and log in with:
  3. You should be redirected to the admin dashboard
  4. Explore all the features:
    • Create, edit, and delete posts
    • Manage categories and tags
    • View statistics

Step 13: Optional Enhancements

Here are some additional features you could add to make your CMS more robust:

  1. User Management: Add CRUD for users with different roles
  2. Media Library: Implement a file manager for images and documents
  3. Post Comments: Add comment management
  4. SEO Tools: Add meta tags and SEO-friendly URLs
  5. Post Scheduling: Allow scheduling posts for future publication
  6. Advanced Search: Implement search functionality for posts
  7. Export/Import: Add data export/import capabilities
  8. Activity Log: Track admin activities

Conclusion

Congratulations! You've successfully built a basic Blog CMS admin panel with Laravel 12. This includes:

  • Secure authentication with admin role
  • Complete post management (CRUD)
  • Category and tag management
  • Rich text editing with CKEditor
  • File uploads for featured images
  • Clean admin interface with Bootstrap

This CMS provides a solid foundation that you can extend with more advanced features as needed. Remember to always:

  • Validate all user input
  • Implement proper error handling
  • Follow security best practices
  • Write tests for your application
  • Keep your dependencies updated

Happy coding with Laravel!

Ajay Yadav

Ajay Yadav

Senior Full-Stack Engineer

7 + Years Experience

Transforming Ideas Into Digital Solutions

I architect and build high-performance web applications with modern tech:

Laravel PHP 8+ Vue.js React.js Flask Python MySQL

Response time: under 24 hours • 100% confidential

Tags

Comments

No comments yet. Be the first to comment!

Please log in to post a comment:

Sign in with Google

Related Posts