In this article we will take a look at laravel breeze starter kit and the features provides and then we will utilize this to build a simple CMS.
Laravel 8 comes with two nice starter kits, which are the Laravel Breeze which we will talk about in this article, and the JetStream.
Both starter kits represent a starting point if you need to build a fresh laravel application as it provides the basic features like authentication features through api and cookies, login, registration, password reset, email verification, and password confirmation.
So instead of wasting time of building all these features in any application we can start with this starter kit.
The laravel breeze use the blade template as the default view template and the Tailwindcss. Of course you can use Vuejs or Reactjs instead of blade but in this article we will be using blade.
To use and install laravel breeze, let’s install a new laravel 8 project:
composer create-project laravel/laravel lara-breeze-cms --prefer-dist
Then install laravel breeze package:
composer require laravel/breeze
After installation complete run this command:
php artisan breeze:install
This command publishes the required authentication views, routes, controllers.
If you checked the project after running breeze:install, you will see a bunch of files created:
- The app/Http/Controllers/Auth contains all the necessary controllers for authentication features.
- app/View directory which contains two class based components for layouts.
- The auth routes in routes/auth.php
- The resources/views/auth, resources/views/components, resources/views/layouts directories.
Now compile the assets that come with laravel breeze using npm:
npm install npm run dev
Next migrate the database but first create a new mysql database named “lara_breeze_cms” and update the .env as follows:
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=lara_breeze_cms DB_USERNAME=<db username> DB_PASSWORD=<db password>
Finally migrate the database:
php artisan migrate
Now if you go to the <project base url>/public you will see laravel home and the login links at the top.
Extending Laravel Breeze
The next step after installing laravel breeze is that you may need to extend it according to your project to add new features. So how can we modify or add new features?
The process is so simple, laravel breeze made up of blade views and controllers. Let’s for example modify the login or register pages. To do this you can go to resources/views/auth directory which contains all files regarding authentication.
Also there are two layouts available in resources/views/layouts which are the app layout in app.blade.php and this layout for authenticated users and the guest.blade.php and this layout for guest users.
You might need to modify the top header navigation links and logo. This is in layouts/navigation.blade.php. This view file contains the <x-application-logo /> component and top nav links. For example if need to modify the application logo just go to views/components/application-logo.blade.php and set your own logo.
You will encounter this directive x-data={} in navigation.blade.php this is not blade directive instead it’s alpinejs directive and allows you set minimal declarative rendering like Vuejs. Here are the alpinejs docs if you need to learn more.
Now for the purpose of this article we will add two modules for the cms, the categories and posts. At first create the migrations and models:
php artisan make:model Category -m
php artisan make:model Post -m
Those commands will create the models and migration together using -m option. Next modify the migration files
Update the up() method in migrations/create_categories_table.php
public function up() { Schema::create('categories', function (Blueprint $table) { $table->id(); $table->string("title"); $table->timestamps(); }); }
I just added on field which is the title.
Also update the up() method in migrations/create_posts_table.php
public function up() { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string("title"); $table->text("content"); $table->string("image"); $table->bigInteger("category_id"); $table->timestamps(); }); }
The post contains the title, image, content, and related category_id.
Now migrate the two tables:
php artisan migrate
Each category contains many posts and each post belong to one category so let’s add those relationship in Category and Post models.
Models/Category.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Category extends Model { use HasFactory; public function posts() { return $this->hasMany(Post::class, "category_id"); } }
Models/Post.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Post extends Model { use HasFactory; public function category() { return $this->belongsTo(Category::class, "category_id"); } }
After updating the models, let’s move to the controllers and views.
Create a new directory in app/Http/Controllers/Admin and then create two resource controllers for category and post like so:
php artisan make:Controller Admin/CategoriesController -r php artisan make:Controller Admin/PostsController -r
Then add the routes for this controllers:
In routes/web.php underneath Route::get(‘dashboard’) add those lines:
Route::resource("/category", App\Http\Controllers\Admin\CategoriesController::class)->middleware("auth"); Route::resource("/post", App\Http\Controllers\Admin\PostsController::class)->middleware("auth");
Also add the navigation links in navigation.blade.php
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100"> <!-- Primary Navigation Menu --> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="flex h-16 grid-cols-6 grid"> <div class="flex"> <!-- Logo --> <div class="flex-shrink-0 flex items-center"> <a href="{{ route('dashboard') }}"> <x-application-logo class="block h-10 w-auto fill-current text-gray-600" /> </a> </div> <!-- Navigation Links --> <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex"> <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')"> {{ __('Dashboard') }} </x-nav-link> </div> </div> <div></div> <div></div> <div class="flex justify-self-end items-center"> <a href="{{route('category.create')}}" class="flex mx-2 text-green-900 underline text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out"> Creat Category </a> <a href="{{route('category.index')}}" class="flex text-green-900 underline text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out"> All Categories </a> </div> <div class="flex justify-self-end items-center"> <a href="{{route('post.create')}}" class="flex mx-2 text-green-900 underline text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out"> Creat Post </a> <a href="{{route('post.index')}}" class="flex text-green-900 underline text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out"> All Posts </a> </div> <!-- Settings Dropdown --> <div class="hidden sm:flex sm:items-center sm:ml-6 justify-self-end"> <x-dropdown align="right" width="48"> <x-slot name="trigger"> <button class="flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out"> <div>{{ Auth::user()->name }}</div> <div class="ml-1"> <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /> </svg> </div> </button> </x-slot> <x-slot name="content"> <!-- Authentication --> <form method="POST" action="{{ route('logout') }}"> @csrf <x-dropdown-link :href="route('logout')" onclick="event.preventDefault(); this.closest('form').submit();"> {{ __('Log Out') }} </x-dropdown-link> </form> </x-slot> </x-dropdown> </div> <!-- Hamburger --> <div class="-mr-2 flex items-center sm:hidden"> <button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out"> <svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24"> <path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> <path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> </svg> </button> </div> </div> </div> <!-- Responsive Navigation Menu --> <div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden"> <div class="pt-2 pb-3 space-y-1"> <x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')"> {{ __('Dashboard') }} </x-responsive-nav-link> </div> <!-- Responsive Settings Options --> <div class="pt-4 pb-1 border-t border-gray-200"> <div class="px-4"> <div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div> <div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div> </div> <div class="mt-3 space-y-1"> <!-- Authentication --> <form method="POST" action="{{ route('logout') }}"> @csrf <x-responsive-nav-link :href="route('logout')" onclick="event.preventDefault(); this.closest('form').submit();"> {{ __('Log Out') }} </x-responsive-nav-link> </form> </div> </div> </div> </nav>
If you click the links you will see blank pages but let’s update each module separatly. I will start with the categories so open the CategoriesController.php and update like so:
Admin/CategoriesController.php
<?php namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Category; use Illuminate\Http\Request; class CategoriesController extends Controller { /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { $categories = Category::paginate(10); return view("admin.category.index", compact('categories')); } /** * Show the form for creating a new resource. * * @return \Illuminate\Http\Response */ public function create() { return view("admin.category.add"); } /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { $this->validate($request, [ "title" => "required" ]); $category = new Category(); $category->title = $request->title; $category->save(); return redirect(route('category.index'))->with('message', 'Category created with success'); } /** * Display the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function show($id) { // } /** * Show the form for editing the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function edit($id) { $category = Category::find($id); return view("admin.category.edit", compact('category')); } /** * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request * @param int $id * @return \Illuminate\Http\Response */ public function update(Request $request, $id) { $this->validate($request, [ "title" => "required" ]); $category = Category::find($id); $category->title = $request->title; $category->save(); return redirect(route('category.index'))->with('message', 'Category updated with success'); } /** * Remove the specified resource from storage. * * @param int $id * @return \Illuminate\Http\Response */ public function destroy($id) { Category::find($id)->delete(); return redirect(route('category.index'))->with('message', 'Category deleted with success'); } }
This is a CRUD for categories, i will not explain every step as this a simple controller that creates and displays categories.
Create the views, so create a new directory admin/ in resources/views that will contain the admin modules. Inside it create category/ directory and add the below files for add, edit and index:
resources/views/admin/category/index.blade.php
<x-app-layout> <x-slot name="header"> <h2 class="font-semibold text-xl text-gray-800 leading-tight"> Categories </h2> </x-slot> <div class="py-12 px-20"> @if(Session::has('message')) <div class="bg-green-300 text-green-700 rounded px-2 py-3"> {{Session::get('message')}} </div> @endif <div class="grid justify-items-stretch my-3"> <a href="{{route('category.create')}}" class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 active:bg-gray-900 focus:outline-none focus:border-gray-900 focus:ring ring-gray-300 disabled:opacity-25 transition ease-in-out duration-150 flex justify-self-end">Create Category</a> </div> <table class="min-w-full divide-y divide-gray-200"> <thead class="bg-gray-50"> <tr> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">#</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Title</th> <th class="relative px-6 py-3"></th> <th class="relative px-6 py-3"></th> </tr> </thead> <tbody class="bg-white divide-y divide-gray-200"> @forelse($categories as $category) <tr> <td class="px-6 py-4 whitespace-nowrap">{{$category->id}}</td> <td class="px-6 py-4 whitespace-nowrap">{{$category->title}}</td> <td class="px-6 py-4 whitespace-nowrap"> <a href="{{route('category.edit', [$category->id])}}" class="text-blue-500">Edit</a> </td> <td class="px-6 py-4 whitespace-nowrap"> <form method="post" action="{{route('category.destroy', [$category->id])}}" id="deleteForm{{$category->id}}"> @csrf @method('DELETE') <button type="submit" class="text-red-500" onclick="event.preventDefault(); if(confirm('Are you sure to delete?')) {document.getElementById('deleteForm{{$category->id}}').submit();} else {return false;} ">Delete</button> </form> </td> </tr> @empty <tr><td colspan="5" class="px-6 py-4 whitespace-nowrap">No categories to display</td></tr> @endforelse </tbody> </table> <div class="my-3"> {{$categories->links()}} </div> </div> </x-app-layout>
resources/views/admin/category/add.blade.php
<x-app-layout> <x-slot name="header"> <h2 class="font-semibold text-xl text-gray-800 leading-tight"> Create Category </h2> </x-slot> <div class="py-12 px-20"> <form method="post" action="{{route('category.store')}}"> @csrf <div class="shadow sm:rounded-md sm:overflow-hidden"> <div class="px-4 py-5 bg-white space-y-6 sm:p-6"> <div class="grid grid-cols-3 gap-6"> <div class="col-span-3 sm:col-span-2"> <label class="block text-sm font-medium text-gray-700"> Title </label> <div class="mt-1 flex rounded-md shadow-sm"> <input type="text" name="title" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300 @error('title') border-red-500 @enderror" placeholder="Title"> </div> @error('title') <div class="text-red-600">{{$message}}</div> @enderror </div> </div> </div> <div class="px-4 py-3 bg-gray-50 text-right sm:px-6"> <button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> Create Category </button> </div> </div> </form> </div> </x-app-layout>
resources/views/admin/category/edit.blade.php
<x-app-layout> <x-slot name="header"> <h2 class="font-semibold text-xl text-gray-800 leading-tight"> Update Category </h2> </x-slot> <div class="py-12 px-20"> <form method="post" action="{{route('category.update', [$category->id])}}"> @csrf @method('PUT') <div class="shadow sm:rounded-md sm:overflow-hidden"> <div class="px-4 py-5 bg-white space-y-6 sm:p-6"> <div class="grid grid-cols-3 gap-6"> <div class="col-span-3 sm:col-span-2"> <label class="block text-sm font-medium text-gray-700"> Title </label> <div class="mt-1 flex rounded-md shadow-sm"> <input type="text" name="title" value="{{$category->title}}" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300 @error('title') border-red-500 @enderror" placeholder="Title"> </div> @error('title') <div class="text-red-600">{{$message}}</div> @enderror </div> </div> </div> <div class="px-4 py-3 bg-gray-50 text-right sm:px-6"> <button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> Update Category </button> </div> </div> </form> </div> </x-app-layout>
Now if you can create categories and display all categories by clicking the top links.
In ths same way let’s add the code for posts so update PostsController like so:
Admin/PostsController.php
<?php namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Category; use App\Models\Post; use Illuminate\Http\Request; class PostsController extends Controller { /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { $posts = Post::paginate(10); return view("admin.post.index", compact('posts')); } /** * Show the form for creating a new resource. * * @return \Illuminate\Http\Response */ public function create() { $categories = Category::all(); return view("admin.post.add", compact('categories')); } /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { $this->validate($request, [ "title" => "required", "content" => "required|min:10", "category_id" => "required", "image" => "required|mimes:jpeg,jpg,png,gif" ]); $post = new Post(); $post->title = $request->input('title'); $post->content = $request->input('content'); $post->category_id = $request->input('category_id'); $image = $request->file("image"); $filename = md5(uniqid()) . "." . $image->getClientOriginalExtension(); $image->move(public_path("uploads"), $filename); $post->image = $filename; $post->save(); return redirect(route('post.index'))->with("message", "Post created successfully"); } /** * Display the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function show($id) { // } /** * Show the form for editing the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function edit($id) { $categories = Category::all(); $post = Post::find($id); return view("admin.post.edit", compact('categories', 'post')); } /** * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request * @param int $id * @return \Illuminate\Http\Response */ public function update(Request $request, $id) { $this->validate($request, [ "title" => "required", "content" => "required|min:10", "category_id" => "required", "image" => "mimes:jpeg,jpg,png,gif" ]); $post = Post::find($id); $post->title = $request->input('title'); $post->content = $request->input('content'); $post->category_id = $request->input('category_id'); if($request->hasFile("image")) { $image = $request->file("image"); $filename = md5(uniqid()) . "." . $image->getClientOriginalExtension(); $image->move(public_path("uploads"), $filename); $post->image = $filename; } $post->save(); return redirect(route('post.index'))->with("message", "Post updated successfully"); } /** * Remove the specified resource from storage. * * @param int $id * @return \Illuminate\Http\Response */ public function destroy($id) { Post::find($id)->delete(); return redirect(route('post.index'))->with("message", "Post deleted successfully"); } }
Don’t forget to create a writeable uploads/ directory in public/ that hold the post images/
Then add the views files for posts as shown below:
resources/views/admin/post/index.blade.php
<x-app-layout> <x-slot name="header"> <h2 class="font-semibold text-xl text-gray-800 leading-tight"> Posts </h2> </x-slot> <div class="py-12 px-20"> @if(Session::has('message')) <div class="bg-green-300 text-green-700 rounded px-2 py-3"> {{Session::get('message')}} </div> @endif <div class="grid justify-items-stretch my-3"> <a href="{{route('post.create')}}" class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 active:bg-gray-900 focus:outline-none focus:border-gray-900 focus:ring ring-gray-300 disabled:opacity-25 transition ease-in-out duration-150 flex justify-self-end">Create Post</a> </div> <table class="min-w-full divide-y divide-gray-200"> <thead class="bg-gray-50"> <tr> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">#</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Title</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Category</th> <th class="relative px-6 py-3"></th> <th class="relative px-6 py-3"></th> </tr> </thead> <tbody class="bg-white divide-y divide-gray-200"> @forelse($posts as $post) <tr> <td class="px-6 py-4 whitespace-nowrap">{{$post->id}}</td> <td class="px-6 py-4 whitespace-nowrap"> <img src="{{ url('/') . '/uploads/' . $post->image }}" width="200" /> </td> <td class="px-6 py-4 whitespace-nowrap">{{$post->title}}</td> <td class="px-6 py-4 whitespace-nowrap">{{$post->category->title}}</td> <td class="px-6 py-4 whitespace-nowrap"> <a href="{{route('post.edit', [$post->id])}}" class="text-blue-500">Edit</a> </td> <td class="px-6 py-4 whitespace-nowrap"> <form method="post" action="{{route('post.destroy', [$post->id])}}" id="deleteForm{{$post->id}}"> @csrf @method('DELETE') <button type="submit" class="text-red-500" onclick="event.preventDefault(); if(confirm('Are you sure to delete?')) {document.getElementById('deleteForm{{$post->id}}').submit();} else {return false;} ">Delete</button> </form> </td> </tr> @empty <tr><td colspan="5" class="px-6 py-4 whitespace-nowrap">No posts to display</td></tr> @endforelse </tbody> </table> <div class="my-3"> {{$posts->links()}} </div> </div> </x-app-layout>
resources/views/admin/post/add.blade.php
<x-app-layout> <x-slot name="header"> <h2 class="font-semibold text-xl text-gray-800 leading-tight"> Create Post </h2> </x-slot> <div class="py-12 px-20"> <form method="post" action="{{route('post.store')}}" enctype="multipart/form-data"> @csrf <div class="shadow sm:rounded-md sm:overflow-hidden"> <div class="px-4 py-5 bg-white space-y-6 sm:p-6"> <div class="grid grid-cols-3 gap-6"> <div class="col-span-3 sm:col-span-2"> <label class="block text-sm font-medium text-gray-700"> Title </label> <div class="mt-1 flex rounded-md shadow-sm"> <input type="text" name="title" value="{{old('title')}}" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300 @error('title') border-red-500 @enderror" placeholder="Title"> </div> @error('title') <div class="text-red-600">{{$message}}</div> @enderror </div> <div class="col-span-3 sm:col-span-2"> <label class="block text-sm font-medium text-gray-700"> Content </label> <div class="mt-1 flex rounded-md shadow-sm"> <textarea name="content" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300 @error('content') border-red-500 @enderror">{{old('content')}}</textarea> </div> @error('content') <div class="text-red-600">{{$message}}</div> @enderror </div> <div class="col-span-3 sm:col-span-2"> <label class="block text-sm font-medium text-gray-700"> Category </label> <div class="mt-1 flex rounded-md shadow-sm"> <select name="category_id" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300 @error('category_id') border-red-500 @enderror"> <option value="" {{old('category_id')=='' ? 'selected':''}}>select</option> @foreach($categories as $category) <option value="{{$category->id}}" {{old('category_id')==$category->id ? 'selected':''}}>{{$category->title}}</option> @endforeach </select> </div> @error('category_id') <div class="text-red-600">{{$message}}</div> @enderror </div> <div class="col-span-3 sm:col-span-2"> <label class="block text-sm font-medium text-gray-700"> Image </label> <div class="mt-1 flex rounded-md shadow-sm"> <input type="file" name="image" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300 @error('title') border-red-500 @enderror"> </div> @error('image') <div class="text-red-600">{{$message}}</div> @enderror </div> </div> </div> <div class="px-4 py-3 bg-gray-50 text-right sm:px-6"> <button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> Create Post </button> </div> </div> </form> </div> </x-app-layout>
resources/views/admin/post/edit.blade.php
<x-app-layout> <x-slot name="header"> <h2 class="font-semibold text-xl text-gray-800 leading-tight"> Update Post </h2> </x-slot> <div class="py-12 px-20"> <form method="post" action="{{route('post.update', [$post->id])}}" enctype="multipart/form-data"> @csrf @method('PUT') <div class="shadow sm:rounded-md sm:overflow-hidden"> <div class="px-4 py-5 bg-white space-y-6 sm:p-6"> <div class="grid grid-cols-3 gap-6"> <div class="col-span-3 sm:col-span-2"> <label class="block text-sm font-medium text-gray-700"> Title </label> <div class="mt-1 flex rounded-md shadow-sm"> <input type="text" name="title" value="{{$post->title}}" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300 @error('title') border-red-500 @enderror" placeholder="Title"> </div> @error('title') <div class="text-red-600">{{$message}}</div> @enderror </div> <div class="col-span-3 sm:col-span-2"> <label class="block text-sm font-medium text-gray-700"> Content </label> <div class="mt-1 flex rounded-md shadow-sm"> <textarea name="content" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300 @error('content') border-red-500 @enderror">{{$post->content}}</textarea> </div> @error('content') <div class="text-red-600">{{$message}}</div> @enderror </div> <div class="col-span-3 sm:col-span-2"> <label class="block text-sm font-medium text-gray-700"> Category </label> <div class="mt-1 flex rounded-md shadow-sm"> <select name="category_id" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300 @error('category_id') border-red-500 @enderror"> <option value="" {{$post->category_id=='' ? 'selected':''}}>select</option> @foreach($categories as $category) <option value="{{$category->id}}" {{$post->category_id==$category->id ? 'selected':''}}>{{$category->title}}</option> @endforeach </select> </div> @error('category_id') <div class="text-red-600">{{$message}}</div> @enderror </div> <div class="col-span-3 sm:col-span-2"> <label class="block text-sm font-medium text-gray-700"> Image </label> @if($post->image) <img src="{{url('/') . '/uploads/' . $post->image}}" width="200" /> @endif <div class="mt-1 flex rounded-md shadow-sm"> <input type="file" name="image" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300 @error('title') border-red-500 @enderror"> </div> @error('image') <div class="text-red-600">{{$message}}</div> @enderror </div> </div> </div> <div class="px-4 py-3 bg-gray-50 text-right sm:px-6"> <button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> Update Post </button> </div> </div> </form> </div> </x-app-layout>
Now you can create posts too and display all posts by clicking on create post and all posts on the top header.
Display Categories and Posts On Frontend
Now the guest users should be able to view created posts and categories so i will create should three pages:
- A home page to display latest posts.
- Category page to display posts for this category
- Post detail page
I will create just one controller which is the HomeController.php
app/Http/Controllers/HomeController.php
<?php namespace App\Http\Controllers; use App\Models\Category; use App\Models\Post; use Illuminate\Http\Request; class HomeController extends Controller { public function index() { $categories = Category::all(); $posts = Post::latest()->limit(20)->get(); return view("home", compact('categories', 'posts')); } public function getCategory($id, $slug) { $category = Category::find($id); $posts = $category->posts()->paginate(20); return view("category", compact('category', 'posts')); } public function getPost($id, $slug) { $post = Post::find($id); return view("post", compact('post')); } }
As you see the controller contains three methods, index which display the home page, getCategory which display the category page, and the getPost which display the post detail page.
Next update routes in routes/web.php like so:
<?php use App\Http\Controllers\Admin\CategoriesController; use App\Http\Controllers\HomeController; use App\Http\Controllers\Admin\PostsController; use Illuminate\Support\Facades\Route; Route::get('/', [HomeController::class, 'index']); Route::get('/dashboard', function () { return view('dashboard'); })->middleware(['auth'])->name('dashboard'); Route::resource("/category", CategoriesController::class)->middleware("auth"); Route::resource("/post", PostsController::class)->middleware("auth"); Route::get('/c/{id}/{slug}', [HomeController::class, 'getCategory'])->name('category.single'); Route::get('/p/{id}/{slug}', [HomeController::class, 'getPost'])->name('post.single'); require __DIR__.'/auth.php';
Finally create the below views and update as shown:
resources/views/home.blade.php
<x-guest-layout> <div class="container"> <nav class="my-5 mx-5"> <ul> @foreach($categories as $category) <li class="inline-block mx-1"> <a href="{{route('category.single', [$category->id, Str::slug($category->title)])}}" class="bg-yellow-500 text-center align-middle rounded p-3 text-xs underline hover:bg-yellow-800">{{$category->title}}</a> </li> @endforeach </ul> </nav> <div class="grid flex m-12"> <h2 class="text-2xl my-2">Latest Posts</h2> @foreach($posts as $post) <x-post-single :post="$post" /> @endforeach </div> </div> </x-guest-layout>
resources/views/category.blade.php
<x-guest-layout> <div class="container"> <div class="grid flex m-12"> <h2 class="text-2xl my-2">Posts in {{$category->title}}</h2> @forelse($posts as $post) <x-post-single :post="$post" /> @empty <article class="text-left">No posts found</article> @endforelse {{$posts->links()}} </div> </div> </x-guest-layout>
resources/views/post.blade.php
<x-guest-layout> <div class="container"> <div class="grid flex m-12"> <h2 class="text-2xl my-2">{{$post->title}}</h2> <article> <div class="bg-white overflow-hidden border-b-4 border-blue-500"> <img src="{{url('/') . '/uploads/' . $post->image}}" class="w-full object-cover h-32 sm:h-48 md:h-64"> <div class="p-4 md:p-6"> <p class="text-blue-500 font-semibold text-xs mb-1 leading-none">{{$post->category->title}}</p> <h3 class="font-semibold mb-2 text-xl leading-tight sm:leading-normal">{{$post->title}}</h3> <div class="text-sm flex items-center"> <svg class="opacity-75 mr-2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="12" height="12" viewBox="0 0 97.16 97.16" style="enable-background:new 0 0 97.16 97.16;" xml:space="preserve"> <path d="M48.58,0C21.793,0,0,21.793,0,48.58s21.793,48.58,48.58,48.58s48.58-21.793,48.58-48.58S75.367,0,48.58,0z M48.58,86.823 c-21.087,0-38.244-17.155-38.244-38.243S27.493,10.337,48.58,10.337S86.824,27.492,86.824,48.58S69.667,86.823,48.58,86.823z"/> <path d="M73.898,47.08H52.066V20.83c0-2.209-1.791-4-4-4c-2.209,0-4,1.791-4,4v30.25c0,2.209,1.791,4,4,4h25.832 c2.209,0,4-1.791,4-4S76.107,47.08,73.898,47.08z"/> </svg> <p class="leading-none"> {{date("d M Y", strtotime($post->created_at))}}</p> </div> </div> <div>{{$post->content}}</div> </div> </article> </div> </div> </x-guest-layout>
As you see the three views use <x-guest-layout /> that comes with laravel breeze. At this point you can view all posts and categories.
As you see above how we extended laravel breeze with other modules using just blade.
If you need to work with vuejs or reactjs in laravel breeze you have to install it with either of those commands:
php artisan breeze:install vue
or
php artisan breeze:install react
As an assignment to you is to add a module for users and module for permissions in the admin dashboard.
I’m unable to show the posts on the front-end. The error I receive is that it’s Unable to locate a class or view for component [post-single]. Does anyone know a solution?
Just follow every step in the tutorial carefully, it’s working on my side
I’d like to separate the posts from each other, so that every post gets it’s own page. Any advise?
I suggest that you can make different layout for each post type, take wordpress as an example
Not for me