Kritim Yantra
May 04, 2025
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.
Before we begin, ensure you have:
Create a new Laravel project by running:
composer create-project laravel/laravel laravel-blog-cms
cd laravel-blog-cms
Set up your environment:
.env.example
to .env
php artisan key:generate
.env
:DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_blog_cms
DB_USERNAME=root
DB_PASSWORD=
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:
Create a migration for the admin role:
php artisan make:migration add_is_admin_to_users_table
Edit the migration file:
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_admin')->default(false);
});
}
Run the migration:
php artisan migrate
Add the is_admin
to the fillable
array in app/Models/User.php
:
protected $fillable = [
'name',
'email',
'password',
'is_admin'
];
Let's create the necessary tables for our blog CMS:
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();
});
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();
});
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();
});
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
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);
}
}
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);
}
}
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);
}
}
Let's create factories to generate test data:
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(),
];
}
}
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
We need to protect our admin routes so only admin users can access them:
Create the middleware:
php artisan make:middleware AdminMiddleware
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);
}
}
Register the laravel 12 middleware in bootstrap/app.php
:
->withMiddleware(function ($middleware) {
$middleware->alias([
'admin' => \App\Http\Middleware\AdminMiddleware::class,
]);
})
Let's create controllers for our admin panel:
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'));
}
}
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!');
}
}
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!');
}
}
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!');
}
}
Let's create the admin panel views. We'll use Bootstrap for styling.
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>
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
Create post views in resources/views/admin/posts/
:
@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
@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
@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
@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
Create category views in resources/views/admin/categories/
:
@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
@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
@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
Create tag views in resources/views/admin/tags/
(similar structure to categories):
@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
These views would be nearly identical to the category versions, just changing "Category" to "Tag" in the text.
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';
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;
}
Now you can test your admin panel:
php artisan serve
http://localhost:8000/login
and log in with:Here are some additional features you could add to make your CMS more robust:
Congratulations! You've successfully built a basic Blog CMS admin panel with Laravel 12. This includes:
This CMS provides a solid foundation that you can extend with more advanced features as needed. Remember to always:
Happy coding with Laravel!
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google