Categories & Tags
LaravelNow we'll build the categories and tags management in the admin panel, and display them on the public blog.
Category Controller
app/Http/Controllers/Admin/CategoryController.php
<?php
namespace App\Http\Controllers\Admin;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class CategoryController extends Controller
{
public function index()
{
$categories = Category::withCount('posts')->get();
return view('admin.categories.index', compact('categories'));
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:100|unique:categories',
'description' => 'nullable|string|max:500',
'color' => 'nullable|string|size:7',
]);
$validated['slug'] = Str::slug($validated['name']);
Category::create($validated);
return redirect()
->route('admin.categories.index')
->with('success', 'Category created!');
}
public function destroy(Category $category)
{
// Set posts category to null before deleting
$category->posts()->update(['category_id' => null]);
$category->delete();
return redirect()
->route('admin.categories.index')
->with('success', 'Category deleted!');
}
}Categories View
resources/views/admin/categories/index.blade.php
@extends('layouts.admin')
@section('title', 'Categories')
@section('content')
<div class="grid grid-cols-2 gap-6">
<!-- Category List -->
<div class="bg-white rounded shadow">
<div class="p-4 border-b font-semibold">All Categories</div>
<ul class="divide-y">
@forelse($categories as $category)
<li class="p-4 flex justify-between items-center">
<div class="flex items-center gap-3">
<span class="w-3 h-3 rounded-full"
style="background: {{ $category->color }}"></span>
<div>
<div class="font-medium">{{ $category->name }}</div>
<div class="text-xs text-gray-400">{{ $category->posts_count }} posts</div>
</div>
</div>
<form method="POST" action="{{ route('admin.categories.destroy', $category) }}"
onsubmit="return confirm('Delete this category?')">
@csrf @method('DELETE')
<button class="text-red-500 text-sm hover:underline">Delete</button>
</form>
</li>
@empty
<li class="p-4 text-gray-400">No categories yet.</li>
@endforelse
</ul>
</div>
<!-- Add Category Form -->
<div class="bg-white rounded shadow p-6">
<h3 class="font-semibold mb-4">Add New Category</h3>
<form method="POST" action="{{ route('admin.categories.store') }}">
@csrf
<div class="mb-4">
<label class="block text-sm font-medium mb-1">Name</label>
<input type="text" name="name" class="w-full border rounded p-2"
value="{{ old('name') }}" required />
@error('name')
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
<div class="mb-4">
<label class="block text-sm font-medium mb-1">Color</label>
<input type="color" name="color" value="#6366f1" class="h-10 w-20 border rounded" />
</div>
<div class="mb-4">
<label class="block text-sm font-medium mb-1">Description</label>
<textarea name="description" rows="3"
class="w-full border rounded p-2">{{ old('description') }}</textarea>
</div>
<button type="submit"
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Add Category
</button>
</form>
</div>
</div>
@endsectionTag Management
app/Http/Controllers/Admin/TagController.php
<?php
class TagController extends Controller
{
public function index()
{
$tags = Tag::withCount('posts')->orderBy('posts_count', 'desc')->get();
return view('admin.tags.index', compact('tags'));
}
public function store(Request $request)
{
$request->validate(['name' => 'required|unique:tags|max:50']);
Tag::create([
'name' => $request->name,
'slug' => Str::slug($request->name),
]);
return back()->with('success', 'Tag created!');
}
public function destroy(Tag $tag)
{
$tag->posts()->detach(); // Remove from pivot table
$tag->delete();
return back()->with('success', 'Tag deleted!');
}
}💡
withCount() is very useful!
Category::withCount('posts') adds a posts_count attribute to each category without writing a subquery. This is much more efficient than loading all posts just to count them.Stuck? Need help?
Review the previous lessons or check the Laravel documentation.