Panduan Lengkap Cara Membuat Blog dengan Laravel 8 dan Bootstrap

Panduan Lengkap Cara Membuat Blog dengan Laravel 8 dan Bootstrap

Admin
Admin・ 30 Juli 2021
59 min read ・ 12096 views

Laravel 8 blog - Hi coders, di artikel ini saya akan coba share tutorial step by step bagaimana cara membuat blog dengan laravel 8 yang mudah dipahami untuk pemula. Dan di artikel ini juga, saya akan jelaskan bagaimana cara membuat CRUD manage categories, CRUD manage tags, CRUD manage posts (+ soft delete) dan bagaimana cara menerapkan SEO di blog laravel8.

Sama dengan artikel-artikel yang telah kami bagikan, di artikel ini saya juga akan menjelaskan step by stepnya mulai dari nol yaitu dimulai dengan install laravel versi terbaru. Dan sebelum masuk ke langkah pertama, ada baiknya saya jelaskan terlebih dahulu mengenai latar belakang kenapa kita perlu membuat blog dengan laravel.

Pendahuluan

Kenapa perlu membuat blog dengan laravel ? Blog dapat difungsikan sebagai media kita untuk mencatat apa yang kita ketahui dan mempublikasikannya. Dampak apa yang sudah kita tulis di blog tidak untuk kita saja, mungkin juga ada orang lain di luar sana yang membutuhkan seperti apa yang telah kita tulis di blog kita. Membuat blog dengan laravel juga bisa kita gunakan untuk portfolio kita. Sebenarnya kalau ingin membuat blog, akan jauh lebih mudah jika kita menggunakan platform blogger/blogspot atau menggunakan cms wordpress, tapi itu kan mudah sekali, tidak ada effort dari kita untuk membuatnya. Makanya, jika kamu seorang web developer yang memilik keahlian skill di laravel framework, kamu bisa coba membuat blog dengan laravel untuk personal website kamu atau sebagai portfolio kamu.

Untuk menerapkan SEO di laravel bukanlah perkara mudah, ada beberapa penyesuaian seperti menambahkan meta tag yang harus kita lakukan. Dan di tutorial ini, kita akan mencoba menerapkannya.

Membuat Blog dengan Laravel 8

Oke, kita akan mulai tutorial ini yang diawali dengan langkah-langkah cara install laravel versi terbaru.

Install Laravel Versi Terbaru

//via Laravel Installer
composer global require laravel/installer
laravel new laravel-blog

//via Composer
composer create-project laravel/laravel laravel-blog

Pada langkah yang pertama ini, kita perlu menginstall laravel versi terbaru (saat ini versi 8) yang akan kita coba untuk implementasi membuat blog di laravel 8. Untuk installasi laravel bisa menggunakan laravel installer atau menggunakan composer seperti contoh di atas.

Silahkan memilih salah satu cara yang ingin digunakan untuk installasi laravel. Dari kedua contoh perintah installasi laravel di atas, akan sama-sama menghasilkan atau generate laravel project dengan nama laravel-blog.

Tunggu hingga proses installasi selesai dan jika sudah selesai, jangan lupa untuk masuk ke direktori project menggunakan perintah cd laravel-blog.

Install Laravel UI Package

composer require laravel/ui
php artisan ui bootstrap --auth
npm install && npm run dev

Di tutorial ini kita juga akan menerapkan fitur authentication untuk mendapatkan akses untuk manage categories, manage tags dan manage posts. Untuk itu, kita akan menggunakan laravel ui package untuk generate fitur authentication. Silahkan jalankan perintah-perintah seperti di atas secara berurutuan untuk install laravel ui package. 

Setup Database

APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:brVarEvJfml0x6LL7VVxbCX1R/xpF6f2h7iVcG2Qavs=
APP_DEBUG=true
APP_URL=http://127.0.0.1:8000

LOG_CHANNEL=stack
LOG_LEVEL=debug

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

Buat database baru di phpMyAdmin, Laragon atau yang lainnya dengan nama laravel_blog atau nama yang lainnya. Jika kamu menggunakan laravel installer, DB_DATABASE di file .env sudah memiliki value laravel_blog tapi jika kamu menginstall laravel via composer, kamu perlu menyesuaikan DB_DATABASE dengan nama database yang baru saja dibuat.

Jika sudah setup database, sekarang silahkan jalankan perintah php artisan migrate. Kemudian jalankan laravel project menggunakan perintah php artisan serve. Buka di browser kamu, dan cobalah register satu akun user.

Manage Categories

Selanjutnya kita akan membuat CRUD untuk manage data category.

Membuat Category Model & Migration

php artisan make:model Category -m

Jalankan perintah seperti di atas untuk generate file model dan migration untuk Category. Dengan menjalankan perintah seperti di atas, kita akan mendapatkan dua file yaitu app/Models/Category.php dan databas/migrations/[timestamp] _create_categories_table.php.

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

Kemudian buka file [timestamp] _create_categories_table.php dan pada method up, edit menjadi seperti di atas. Di table categories, kita perlu menambahkan field name, slug serta keywords dan meta_desc untuk menerapkan SEO dan halaman category nantinya.

Jika sudah, jalankan perintah php artisan migrate, untuk memigrasi file migration yang baru saja dibuat.

protected $guarded = [];

Tambahkan kode di atas pada file Models/Category.php.

Define Route

Route::resource('categories', App\Http\Controllers\CategoryControllerController::class);

Tambahkan route baru di file routes/web.php seperti di atas. Disini kita akan membuat route resource untuk manage categories dan mengarahkannya ke file CategoryController.php yang akan kita buat di step selanjutnya.

Membuat CategoryController.php

php artisan make:controller CategoryController -r

Buat file controller baru untuk membuat logic CRUD Categories. Jalankan perintah artisan seperti di atas pada terminal. Perintah seperti di atas akan menghasilkan file CategoryController.php lengkap dengan method index, create, store, show, edit, update dan delete.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Category;

class CategoryController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $categories = Category::all();
        return view('categories.index',['categories'=> $categories]);
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        //
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $category = new Category();
        $category->name = $request->name;
        $category->slug = \Str::slug($request->name);
        $category->keywords = $request->keywords;
        $category->meta_desc = $request->meta_desc;
        $category->save();

        return redirect()->back()->with('success','Data added 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)
    {
        //
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        $category = Category::findOrFail($id);
        $category->name = $request->name;
        $category->slug = $request->slug;
        $category->keywords = $request->keywords;
        $category->meta_desc = $request->meta_desc;
        $category->save();
        
        return redirect()->back()->with('success','Data updated successfully');
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        $category = Category::findOrFail($id);
        $category->delete();
        
        return redirect()->back()->with('success', 'Data Deleted Successfully');
    }
} 

Kemudian buka file CategoryController.php tersebut, lalu edit menjadi seperti kode di atas. Disini kita tidak akan memakai method create, show dan edit karena di view nantinya kita akan menggunakan modal untuk tambah data category dan edit data category.

Penjelasan singkat:

  1. Method index, berfungsi untuk mengarahkan ke file view categories/index.blade.php dan menampilkan semua data categories yang diambil dari model Category.php.
  2. Method store, berfungsi untuk menerima request yang dikirimkan dari view saat tambah data category dan menyimpannya di table categories atau menggunakan model Category. Di method store, kita juga menerapkan helper Str::slug dari laravel untuk mengkonversi request title menjadi format URL yang user friendly. Kemudian setelah semua request disimpan di database, kita akan diarahkan kembali ke halaman categories dengan tambahan alert success yang menampilkan pesan bahwa kita sudah berhasil menambahkan data category.
  3. Method update, berfungsi untuk mencari data category berdasarkan id data yang kita edit kemudian data dengan id tersebut akan diubah sesuai request yang diterima dari inputan di view. Di method edit, kita tidak akan menggunakan helper Str::slug karena saat edit kita memungkinkan untuk bisa edit slug secara manual. Setelah data category dari id yang diedit berhasil disimpan, kita akan diarahkan kembali ke halaman categories dengan tambahan alert success berisikan pesan bahwa kita telah berhasil memperbarui data category tersebut.
  4. Method destroy, berfungsi untuk mencari data category berdasarkan id yang dipilih dan menghapus data category tersebut. Kemudian setelah data tersebut berhasil dihapus, kita akan diarahkan kembali ke halaman categories dengan pesan kita sudah berhasil menghapus data tersebut. 

Membuat File View

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="table-responsive">
                <button type="button" class="btn btn-primary my-3" data-toggle="modal" data-target="#addModal">
                Add Category
                </button>
                @if (session('success'))
                <div class="alert alert-success alert-dismissible fade show my-1" role="alert">
                    {{ session('success') }}
                    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                @endif
                <table class="table">
                    <thead>
                        <tr>
                            <th scope="col">#</th>
                            <th scope="col">Name</th>
                            <th scope="col">Slug</th>
                            <th scope="col">Keywords</th>
                            <th scope="col">Action</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach ($categories as $key => $item)
                            <tr>
                                <th scope="row">{{ ++$key }}</th>
                                <td>{{ $item->name }}</td>
                                <td>{{ $item->slug }}</td>
                                <td>{{ $item->keywords }}</td>
                                <td>
                                    <button class="btn btn-primary btn-sm" data-toggle="modal" data-target="#editModal-{{ $item->id }}">
                                        Edit
                                    </button>
                                    <form method="POST" action="{{route('categories.destroy', [$item->id])}}" class="d-inline" onsubmit="return confirm('Delete this data permanently?')">
                                    @csrf
                                        <input type="hidden" name="_method" value="DELETE">
                                        <input type="submit" value="Delete" class="btn btn-danger btn-sm">
                                    </form>
                                </td>
                            </tr>
                            <!-- Edit Modal -->
                            <div class="modal fade" id="editModal-{{ $item->id }}" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
                                <div class="modal-dialog">
                                    <div class="modal-content">
                                        <div class="modal-header">
                                            <h5 class="modal-title" id="exampleModalLabel">Edit Category</h5>
                                            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                                            <span aria-hidden="true">&times;</span>
                                            </button>
                                        </div>
                                        <div class="modal-body">
                                            <form action="{{ route('categories.update', $item->id) }}" method="POST">
                                                <input type="hidden" name="_method" value="PUT">
                                                @csrf
                                                <div class="form-group">
                                                    <label for="name">Name</label>
                                                    <input type="text" name="name" class="form-control" id="name" value="{{ $item->name }}" required>   
                                                </div>
                                                <div class="form-group">
                                                    <label for="slug">Slug</label>
                                                    <input type="text" name="slug" class="form-control" id="slug" value="{{ $item->slug }}" required>   
                                                </div>
                                                <div class="form-group">
                                                    <label for="keywords">Keywords</label>
                                                    <input type="text" name="keywords" class="form-control" id="keywords" value="{{ $item->keywords }}" required>   
                                                </div>
                                                <div class="form-group">
                                                    <label for="meta_desc">Meta Description</label>
                                                    <input type="text" name="meta_desc" class="form-control" id="meta_desc" value="{{ $item->meta_desc }}" required>   
                                                </div>
                                                <button type="submit" class="btn btn-primary">Submit</button>
                                            </form>
                                        </div> 
                                    </div>
                                </div>
                            </div>
                        @endforeach   
                    </tbody>
                </table>
            </div>
        </div>
    </div>
    <!-- Add Modal -->
    <div class="modal fade" id="addModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="exampleModalLabel">Create Category</h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body">
                    <form action="{{ route('categories.store') }}" method="POST">
                        @csrf
                        <div class="form-group">
                            <label for="name">Name</label>
                            <input type="text" name="name" class="form-control" id="name" required>   
                        </div>
                        <div class="form-group">
                            <label for="keywords">Keywords</label>
                            <input type="text" name="keywords" class="form-control" id="keywords" required>   
                        </div>
                        <div class="form-group">
                            <label for="meta_desc">Meta Description</label>
                            <input type="text" name="meta_desc" class="form-control" id="meta_desc" required>   
                        </div>
                        <button type="submit" class="btn btn-primary">Submit</button>
                    </form>
                </div> 
            </div>
        </div>
    </div>
@endsection

Kemudian kita perlu membuat folder categories di direktori resources/views, kemudian kita juga perlu membuat file index.blade.php di folder categories yang baru dibuat tersebut. Copy semua kode di atas, lalu paste di file index.blade.php yang baru saja dibuat.

Seperti yang telah dijelaskan sebelumnya, di file resources/views/categories/index.blade.php ini kita akan membuat tampilan table categories dan menampilkan semua data category di table tersebut. Di file view ini, kita juga menambahkan button Add Category dengan target modal addModal yang berisikan form untuk kita menambahkan data category. Action dari form addModal tersebut akan diarahkan ke route categories.store yang artinya akan memanggil method store di file CategoryController.php.

Kemudian kita juga menambahkan button modal untuk edit data category dengan data-target="#editModal-{{ $item->id }}". Saat button edit tersebut diklik maka akan menampilkan modal yang berisikan data category dari id data yang dipilih. Modal edit tersebut berisikan form yang memiliki action mengarah ke {{ route('categories.update', $item->id) }}, yang artinya jika kita submit maka akan mengarahkan ke route dengan name categories.update dan memanggil method update di CategoryController.php untuk menjalankan logic update data category. Disini, kita bisa mengubah value atau data category berdasarkan id data category yang dipilih.

Selain itu kita juga menambahkan button delete yang dibungkus dengan form yang memiliki action mengarah ke {{route('categories.destroy', [$item->id])}} dan akan memanggil method destroy untuk logic menghapus data category berdasarkan id. Di button delete ini, kita juga menyisipkan script javascript onsubmit untuk menampilkan confirm alert yang berisikan pesan konfirmasi apakah kita yang menghapus data category tersebut secara permanen.

Pengujian CRUD Category

Pengujian CRUD Category

Oke, sekarang kita uji CRUD category yang telah kita buat. Buka laravel project di browser dengan URL /categories seperti gambar di atas. Cobalah untuk menambahkan data, edit data dan delete data.

Manage Tags

Setelah berhasil membuat CRUD untuk manage data category, selanjutnya kita akan membuat CRUD untuk manage tags. Di tutorial membuat blog di laravel 8 ini, kita akan coba menambahkan category dan tag saat kita membuat data post yang akan dijelaskan pada langkah selanjutnya.

Membuat Tag Model & Migration

php artisan make:model Tag -m

Hampir sama dengan langkah awal membuat CRUD untuk manage category, di langkah awal kita membuat CRUD untuk manage tag ini kita juga perlu membuat file model dan migration baru untuk tag. Jalankan perintah seperti di atas untuk generate file model dan migration untuk Tag. Dengan menjalankan perintah seperti di atas, kita akan mendapatkan dua file yaitu app/Models/Tag.php dan database/migrations/[timestamp] _create_tags_table.php.

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

Kemudian buka file [timestamp] _create_tags_table.php dan pada method up, edit menjadi seperti di atas. Di table tags, kita juga perlu menambahkan field name, slug serta keywords dan meta_desc untuk menerapkan SEO dan halaman tag nantinya.

Jika sudah, jalankan perintah php artisan migrate, untuk memigrasi file migration yang baru saja dibuat.

protected $guarded = [];

Tambahkan kode di atas pada file Models/Category.php.

Define Route

Route::resource('tags', App\Http\Controllers\TagControllerController::class);

Tambahkan route baru di file routes/web.php seperti di atas. Disini kita akan membuat route resource untuk manage tags dan mengarahkannya ke file TagController.php yang akan kita buat di step selanjutnya.

Membuat TagController.php

php artisan make:controller TagController -r

Buat file controller baru untuk membuat logic CRUD Tags. Jalankan perintah artisan seperti di atas pada terminal. Perintah seperti di atas akan menghasilkan file TagController.php lengkap dengan method index, create, store, show, edit, update dan delete.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Tag;

class TagController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $tags = Tag::all();
        return view('tags.index',['tags'=> $tags]);
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        //
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $tag = new Tag();
        $tag->name = $request->name;
        $tag->slug = \Str::slug($request->name);
        $tag->keywords = $request->keywords;
        $tag->meta_desc = $request->meta_desc;
        $tag->save();

        return redirect()->back()->with('success','Data added 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)
    {
        //
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        $Tag = Tag::findOrFail($id);
        $Tag->name = $request->name;
        $Tag->slug = $request->slug;
        $Tag->keywords = $request->keywords;
        $Tag->meta_desc = $request->meta_desc;
        $Tag->save();
        
        return redirect()->back()->with('success','Data updated successfully');
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        $Tag = Tag::findOrFail($id);
        $Tag->delete();
        
        return redirect()->route('categories.index')->with('success', 'Data Deleted Successfully');
    }
} 

Kemudian buka file TagController.php tersebut, lalu edit menjadi seperti kode di atas. Disini kita tidak akan memakai method create, show dan edit karena di view nantinya kita akan menggunakan modal untuk tambah data tag dan edit data tag. Untuk penjelasan dari method-method di file TagController.php sama dengan penjelasan dari method-method di file CategoryController.php

Membuat File View

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="table-responsive">
                <button type="button" class="btn btn-primary my-3" data-toggle="modal" data-target="#addModal">
                Add Tag
                </button>
                @if (session('success'))
                <div class="alert alert-success alert-dismissible fade show my-1" role="alert">
                    {{ session('success') }}
                    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                @endif
                <table class="table">
                    <thead>
                        <tr>
                            <th scope="col">#</th>
                            <th scope="col">Name</th>
                            <th scope="col">Slug</th>
                            <th scope="col">Keywords</th>
                            <th scope="col">Action</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach ($tags as $key => $item)
                            <tr>
                                <th scope="row">{{ ++$key }}</th>
                                <td>{{ $item->name }}</td>
                                <td>{{ $item->slug }}</td>
                                <td>{{ $item->keywords }}</td>
                                <td>
                                    <button class="btn btn-primary btn-sm" data-toggle="modal" data-target="#editModal-{{ $item->id }}">
                                        Edit
                                    </button>
                                    <form method="POST" action="{{route('tags.destroy', [$item->id])}}" class="d-inline" onsubmit="return confirm('Delete this data permanently?')">
                                    @csrf
                                        <input type="hidden" name="_method" value="DELETE">
                                        <input type="submit" value="Delete" class="btn btn-danger btn-sm">
                                    </form>
                                </td>
                            </tr>
                            <!-- Edit Modal -->
                            <div class="modal fade" id="editModal-{{ $item->id }}" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
                                <div class="modal-dialog">
                                    <div class="modal-content">
                                        <div class="modal-header">
                                            <h5 class="modal-title" id="exampleModalLabel">Edit Tag</h5>
                                            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                                            <span aria-hidden="true">&times;</span>
                                            </button>
                                        </div>
                                        <div class="modal-body">
                                            <form action="{{ route('tags.update', $item->id) }}" method="POST">
                                                <input type="hidden" name="_method" value="PUT">
                                                @csrf
                                                <div class="form-group">
                                                    <label for="name">Name</label>
                                                    <input type="text" name="name" class="form-control" id="name" value="{{ $item->name }}" required>   
                                                </div>
                                                <div class="form-group">
                                                    <label for="slug">Slug</label>
                                                    <input type="text" name="slug" class="form-control" id="slug" value="{{ $item->slug }}" required>   
                                                </div>
                                                <div class="form-group">
                                                    <label for="keywords">Keywords</label>
                                                    <input type="text" name="keywords" class="form-control" id="keywords" value="{{ $item->keywords }}" required>   
                                                </div>
                                                <div class="form-group">
                                                    <label for="meta_desc">Meta Description</label>
                                                    <input type="text" name="meta_desc" class="form-control" id="meta_desc" value="{{ $item->meta_desc }}" required>   
                                                </div>
                                                <button type="submit" class="btn btn-primary">Submit</button>
                                            </form>
                                        </div> 
                                    </div>
                                </div>
                            </div>
                        @endforeach   
                    </tbody>
                </table>
            </div>
        </div>
    </div>
    <!-- Add Modal -->
    <div class="modal fade" id="addModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="exampleModalLabel">Create Tag</h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body">
                    <form action="{{ route('tags.store') }}" method="POST">
                        @csrf
                        <div class="form-group">
                            <label for="name">Name</label>
                            <input type="text" name="name" class="form-control" id="name" required>   
                        </div>
                        <div class="form-group">
                            <label for="keywords">Keywords</label>
                            <input type="text" name="keywords" class="form-control" id="keywords" required>   
                        </div>
                        <div class="form-group">
                            <label for="meta_desc">Meta Description</label>
                            <input type="text" name="meta_desc" class="form-control" id="meta_desc" required>   
                        </div>
                        <button type="submit" class="btn btn-primary">Submit</button>
                    </form>
                </div> 
            </div>
        </div>
    </div>
@endsection

Kemudian kita perlu membuat folder tags di direktori resources/views, kemudian kita juga perlu membuat file index.blade.php di folder tags yang baru dibuat tersebut. Copy semua kode di atas, lalu paste di file index.blade.php yang baru saja dibuat. Untuk penjelasan dari kode di atas juga sama dengan penjelasan kode file view untuk CRUD category di langkah sebelumnya. Di CRUD tags ini kita juga masih akan menggunakan modal untuk tambah data tag dan edit data tag.

Pengujian CRUD Tag

Tutorial Laravel Blog: CRUD tags

Oke, sekarang kita uji CRUD tag yang telah kita buat. Buka laravel project di browser dengan URL /tags seperti gambar di atas. Cobalah untuk menambahkan data, edit data dan delete data. Kita memang belum menambahkan menu navigasi dan akan kita tambahkan di akhir artikel tutorial membuat blog dengan laravel ini.

Manage Posts

Setelah selesai di materi membuat CRUD category dan CRUD tag, sekarang waktunya membuat CRUD untuk manage posts. Di materi ini nanti kita akan membuat file post model dan migration, kemudian membuat table pivot untuk post_tag dan membuat relasi eloquent antara model post-category, post-tag dan post-user. 

Membuat Post Model & Migration

php artisan make:model Post -m

Langkah pertama di materi membuat CRUD post, silahkan jalankan perintah artisan seperti di atas untuk generate file post model & migration.

 public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('category_id')
                    ->constrained()
                    ->onUpdate('cascade')
                    ->onDelete('cascade');
            $table->foreignId('user_id')
                    ->constrained()
                    ->onUpdate('cascade')
                    ->onDelete('cascade');
            $table->string('cover');
            $table->string('title');
            $table->string('slug');
            $table->longText('desc');
            $table->string('keywords');
            $table->text('meta_desc');
            $table->timestamps();
            $table->softDeletes();
        });
    }

Kemudian buka file migration [timestamp] _create_posts_table.php dan ubah kode pada method up menjadi seperti di atas. Di table posts, kita akan membuat foreign key yang dapat kita hubungkan antara table posts dengan table categories dan table posts dengan table users. Kemudian kita juga membuat beberapa field seperti cover, title, slug, desc, keywords dan meta_desc. Tidak lupa, kita juga menambahkan softdelete, yang akan terisi data timestamp ketika kita menghapus data posts.

Baca: Tutorial Lengkap Cara Menggunakan Soft Delete di Laravel

php artisan make:migration create_post_tag_table

Selanjutnya, kita buat file migration baru untuk membuat table pivot post_tag menggunakan perintah artisan seperti di atas.

public function up()
    {
        Schema::create('post_tag', function (Blueprint $table) {
            $table->id();
            $table->foreignId('post_id')->constrained('posts');
            $table->foreignId('tag_id')->constrained('tags');
            $table->timestamps();
        });
    }

Kemudian, buka file migration [timestamp] _create_post_tag_table.php. Pada bagian method up, ubah kodenya menjadi seperti di atas. Di table post_tag, kita hanya perlu dua field yaitu post_id dan tag_id.

Jika sudah, save dan jalankan perintah php artisan migrate.

Setup Relationship

Selanjutnya kita perlu setup relation antar model yang telah kita buat yaitu, User.php, Category.php, Tag.php dan Post.php

User.php

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

Untuk yang pertama, buka file model User.php, kemudian tambahkan method posts dengan kode seperti di atas. Di model User.php ini, kita menambahkan hasMany atau one to many ke model Post. Itu artinya, satu user dapat memiliki banyak post.

Category.php

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

Untuk yang kedua, buka file model Category.php dan tambahkan method posts seperti kode di atas. Di file Category.php kita juga menambahkan method posts dengan relasi one to many atau dengan menggunakan hasMany yang menghubungkan ke model Post. Artinya, satu category bisa memiliki banyak data post.

Tag.php

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

Kemudian untuk model Tag, kita buat relasi many-to-many dengan menggunakan belongsToMany ke model Post. Tambahkan kode di atas pada file model Tag.php

Post.php

<?php

namespace App\Models;

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

class Post extends Model
{
    use HasFactory, SoftDeletes;

    public function category()
    {
        return $this->belongsTo(Category::class);
    }

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

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

Dan model terakhir yaitu Post.php. Karena di Post ini nanti kita akan menerapkan softdelete dari laravel, jadi jangan lupa untuk menambahkan trait use Illuminate\Database\Eloquent\SoftDeletes;. Kemudian tambahkan tiga method baru untuk relasi ke model Category, User dan Tag. Sehingga secara keseluruhan, file model Post.php sekarang akan menjadi seperti di atas.

Define Route

// Manage Posts
    Route::get('posts/trash', [App\Http\Controllers\PostController::class , 'trash'])->name('posts.trash');
    Route::post('posts/trash/{id}/restore', [App\Http\Controllers\PostController::class , 'restore'])->name('posts.restore');
    Route::delete('posts/{id}/delete-permanent', [App\Http\Controllers\PostController::class,'deletePermanent'])->name('posts.deletePermanent');
    Route::resource('posts', App\Http\Controllers\PostController::class);

Next, tambahkan kode di atas pada file routes/web.php. Dengan kode seperti di atas, kita mendaftarkan route::resource untuk CRUD posts dan mengarahkannya ke file PostController.php yang akan kita buat di langkah berikutnya. Selain route::resource untuk posts, kita juga menambahkan 3 route lainnya yaitu route::get('posts/trash') untuk mengarahkan ke halaman data post dengan status softdelete, route::post('posts/trash/{id}/restore') yang dipanggil saat kita menjalankan perintah restore untuk mengembalikan data post dengan status softdelete dan route::delete('posts/{id}/delete-permanent') yang dipanggil saat kita menjalankan perintah hapus data post secara permanen atau bukan softdelete lagi (dihapus dari database atau table post).

Route::middleware(['auth'])->group(function () {
    Route::resource('categories', App\Http\Controllers\CategoryController::class);
    Route::resource('tags', App\Http\Controllers\TagController::class);

    // Manage Posts
    Route::get('posts/trash', [App\Http\Controllers\PostController::class , 'trash'])->name('posts.trash');
    Route::post('posts/trash/{id}/restore', [App\Http\Controllers\PostController::class , 'restore'])->name('posts.restore');
    Route::delete('posts/{id}/delete-permanent', [App\Http\Controllers\PostController::class,'deletePermanent'])->name('posts.deletePermanent');
    Route::resource('posts', App\Http\Controllers\PostController::class);
});

Kemudian, bungkus route untuk CRUD category, tag dan post menggunakan middleware auth agar untuk dapat melakukan CRUD category, tag dan post, user sudah melakukan login. Ketika user belum login tapi sudah mau mengakses data posts, maka user akan diarahkan ke halaman login.

Sehingga, route-route untuk CRUD category, tag dan post setelah dibungkus dengan middleware akan menjadi seperti kode di atas.

Membuat PostController.php

php artisan make:controller PostController -r

Di langkah sebelumnya kita telah menambahkan route baru yang mengarahkan ke file PostController, dan sekarang kita perlu membuat file PostController.php tersebut. Jalankan perintah artisan seperti di atas pada terminal. Perintah seperti di atas akan menghasilkan file PostController.php lengkap dengan method index, create, store, show, edit, update dan delete.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\{Category, Post, Tag};
use Auth;
use Illuminate\Support\Facades\Validator;

class PostController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $posts = Post::all();
        return view('posts.index', ['posts' => $posts]);
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        $categories = Category::all();
        $tags       = Tag::all();
        return view('posts.create', compact('categories','tags'));
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
            "title"     => "required|unique:posts,title",
            "cover"     => "required",
            "desc"      => "required",
            "category"  => "required",
            "tags"      => "array|required",  
            "keywords"  => "required",
            "meta_desc" => "required",
        ]);

        if ($validator->fails()) {
            return redirect()->back()
                        ->withErrors($validator)
                        ->withInput();
        }
        $post               = new Post();

        $cover              = $request->file('cover');
        if($cover){
            $cover_path     = $cover->store('images/blog', 'public');
            $post->cover    = $cover_path;
        }
        $post->title        = $request->title;
        $post->slug         = \Str::slug($request->title);
        $post->user_id      = Auth::user()->id;
        $post->category_id  = $request->category;
        $post->desc         = $request->desc;
        $post->keywords     = $request->keywords;
        $post->meta_desc    = $request->meta_desc;
        $post->save();

        $post->tags()->attach($request->tags);

        return redirect()->route('posts.index')->with('success', 'Data added 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)
    {
        $post = Post::findOrFail($id);
        $categories = Category::all();
        $tags = Tag::all();
        return view('posts.edit',compact('post','categories','tags'));
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        $validator = Validator::make($request->all(), [
            "title"     => "required|unique:posts,title,".$id,
            "desc"      => "required",
            "category"  => "required",
            "tags"      => "array|required",  
            "keywords"  => "required",
            "meta_desc" => "required",
        ]);

        if ($validator->fails()) {
            return redirect()->back()
                        ->withErrors($validator)
                        ->withInput();
        }

        $post = Post::findOrFail($id);

        $new_cover = $request->file('cover');

        if($new_cover){
            if($post->cover && file_exists(storage_path('app/public/' . $post->cover))){
                \Storage::delete('public/'. $post->cover);
            }

            $new_cover_path = $new_cover->store('images/blog', 'public');

            $post->cover = $new_cover_path;
        }

        $post->title        = $request->title;
        $post->slug         = $request->slug;
        $post->user_id      = Auth::user()->id;
        $post->category_id  = $request->category;
        $post->desc         = $request->desc;
        $post->keywords     = $request->keywords;
        $post->meta_desc    = $request->meta_desc;
        $post->save();

        $post->tags()->sync($request->tags);

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

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        $post = Post::findOrFail($id);

        $post->delete();

        return redirect()->route('posts.index')->with('success','Data moved to trash');
    }

    public function trash(){
        $posts = Post::onlyTrashed()->get();

        return view('posts.trash', compact('posts'));
    }

    public function restore($id) {
        $post = Post::withTrashed()->findOrFail($id);

        if ($post->trashed()) {
            $post->restore();
            return redirect()->back()->with('success','Data successfully restored');
        }else {
            return redirect()->back()->with('error','Data is not in trash');
        }
    }

    public function deletePermanent($id){
        
        $post = Post::withTrashed()->findOrFail($id);

        if (!$post->trashed()) {
        
            return redirect()->back()->with('error','Data is not in trash');
        
        }else {
        
            $post->tags()->detach();
            

            if($post->cover && file_exists(storage_path('app/public/' . $post->cover))){
                \Storage::delete('public/'. $post->cover);
            }

        $post->forceDelete();

        return redirect()->back()->with('success', 'Data deleted successfully');
        }
    }
}

Kemudian, buka file PostController.php yang baru saja dibuat dan ubah semua kode atau replace kode yang ada dengan kode seperti di atas. Di file PostController.php ini, kita tidak hanya akan menggunakan method bawaan dari laravel yaitu index, create, store, edit, update dan delete, tapi juga menambahkan tiga method baru yaitu trash, restore dan deletePermanent.

Penjelasan singkat mengenai method-method yang ada di file PostController.php:

  1. Index, untuk mengarahkan ke file view yang terletak di direktori resources/views/posts/index.blade.php dan menampilkan data-data post atau blog yang telah dibuat.
  2. Create, untuk mengarahkan ke file view resources/views/posts/create.blade.php dan menyertakan data-data dari table atau model Category dan Tag yang akan digunakan untuk menambahkan category dan tag saat membuat post atau blog.
  3. Store, berfungsi untuk membuat logic validasi menggunakan validator dan jika terdapat kesalahan saat input data maka akan diarahkan kembali ke halaman create beserta pesan error. Tapi jika tidak terdapat kesalahan maka akan menerima semua request yang telah dikirim atau diinputkan saat menambahkan data post atau blog baru dan meneruskan data-data tersebut ke model Post yang selanjutnya akan disimpan di table posts. Selain itu, data array tags yang telah ditambahkan saat membuat post akan diteruskan ke table post_tag menggunakan $post->tags()->attach($request->tags);.
  4. Edit, berfungsi untuk mengarahkan ke halaman edit dengan file view dari posts/edit.blade.php dan menampilkan data post berdasarkan post id yang dipilih.
  5. Update, berfungsi untuk mencari data post berdasarkan id data yang kita edit kemudian data dengan id tersebut akan diubah sesuai request yang diterima dari inputan di view. Di method edit, kita tidak akan menggunakan helper Str::slug karena saat edit kita memungkinkan untuk bisa edit slug secara manual. Setelah data post dari id yang diedit berhasil disimpan, maka akan diarahkan kembali ke halaman posts dengan tambahan alert success berisikan pesan bahwa kita telah berhasil memperbarui data category tersebut. Kemudian data table post_tag berdasarkan post_id akan disinkronkan jika ada pembaruan tag pada data post tersebut menggunakan $post->tags()->sync($request->tags);. Di method update juga akan memerika jika ada request baru untuk data cover. Jika terdapat request baru untuk cover, maka akan diperiksa apakah data post dengan id yang yang diedit tersebut mempunyai nilai atau value untuk field cover dan apakah di storage path sudah terdapat file cover atau belum. Jika memang sudah terdapat cover pada post tersebut, maka data cover di table dan file cover di storage path akan dihapus dan digantikan dengan file cover yang baru.
  6. Destroy, berfungsi untuk mencari data post berdasarkan id yang dipilih dan menghapus data category tersebut. Kemudian setelah data tersebut berhasil dihapus, kita akan diarahkan kembali ke halaman posts dengan pesan kita sudah berhasil menghapus data tersebut. Karena untuk posts kita menerapkan softdelete, jadi ketika method destroy ini dipanggil, sebenarnya data post tersebut tidak benar dihapus dari table, melainkan hanya menambahkan value di field deleted_at dan data tersebut bisa kita kembalikan lagi menggunakan method restore.
  7. Trash, berfungsi untuk menampilkan halaman trash yang terdiri dari data-data yang mempunyai value deleted_at atau data-data yang dihapus sementara. Konsepnya mirip sekali dengan Recycle Bin di windows.
  8. Restore, berfungsi untuk mengembalikan atau menampilkan data post yang ada di trash kembali ke halaman posts/index.blade.php. Jika method ini dipanggil, maka value di field deleted_at juga akan dihapus.
  9. DeletePermanent, berfungsi untuk menghapus data post secara permanen dari table posts dan menghapus data di table post_tag dengan post_id sama dengan id data post yang dihapus. Setelah method ini dipanggil, maka data post di table posts sudah benar-benar dihapus dan tidak dapat direstore. 

Edit Master Layout

<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Fonts -->
    <link rel="dns-prefetch" href="//fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">

    <!-- Styles -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">

    {{-- @yield('styles') --}}
    <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
</head>
<body>
    <div id="app">
        <nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
            <div class="container">
                <a class="navbar-brand" href="{{ url('/') }}">
                    {{ config('app.name', 'Laravel') }}
                </a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
                    <span class="navbar-toggler-icon"></span>
                </button>

                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <!-- Left Side Of Navbar -->
                    <ul class="navbar-nav mr-auto">
                        <li class="nav-item {{ request()->is('tags') ? 'active' : '' }}">
                            <a class="nav-link" href="{{route('tags.index')}}">Tags</a>
                        </li>
                        <li class="nav-item {{ request()->is('categories') ? 'active' : '' }}">
                            <a class="nav-link" href="{{route('categories.index')}}">Categories</a>
                        </li>
                        <li class="nav-item dropdown {{ request()->is('posts') ? 'active' : '' }}">
                            <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                            Posts
                            </a>
                            <div class="dropdown-menu" aria-labelledby="navbarDropdown">
                                <a class="dropdown-item" href="{{route('posts.index')}}">Data</a>
                                <a class="dropdown-item" href="{{route('posts.trash')}}">Trash</a>
                            </div>
                        </li>
                    </ul>

                    <!-- Right Side Of Navbar -->
                    <ul class="navbar-nav ml-auto">
                        <!-- Authentication Links -->
                        @guest
                            @if (Route::has('login'))
                                <li class="nav-item">
                                    <a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
                                </li>
                            @endif

                            @if (Route::has('register'))
                                <li class="nav-item">
                                    <a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a>
                                </li>
                            @endif
                        @else
                            <li class="nav-item dropdown">
                                <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
                                    {{ Auth::user()->name }}
                                </a>

                                <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                                    <a class="dropdown-item" href="{{ route('logout') }}"
                                       onclick="event.preventDefault();
                                                     document.getElementById('logout-form').submit();">
                                        {{ __('Logout') }}
                                    </a>

                                    <form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
                                        @csrf
                                    </form>
                                </div>
                            </li>
                        @endguest
                    </ul>
                </div>
            </div>
        </nav>

        <main class="py-4">
            @yield('content')
        </main>
    </div>
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-Piv4xVNRyMGpqkS2by6br4gNJ7DXjqk09RmUpJ8jgGtD7zP9yug3goQfGII0yAns" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>

    <script>
    $(document).ready(function() {
        $('.select2').select2({
            placeholder: "Choose Some Tags"
        });
    });
</script>
</body>
</html>

Oke, next kita edit file master layout kita atau file app.blade.php yang terletak di dalam folder resources/views/layouts. Edit atau replace kode yang ada saat ini dengan kode seperti di atas. Di file master layout ini, kita menambahkan jquery dan plugin select2 untuk mempercantik tampilan multiple select tag saat di halaman create atau edit post nantinya. Selain itu, kita juga menambahkan navigasi bar untuk categories, tags dan posts.

Membuat File Post Index

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="table-responsive">
                <a href="{{ route('posts.create') }}" class="btn btn-primary my-3">
                Add Post
                </a>
                @if (session('success'))
                <div class="alert alert-success alert-dismissible fade show my-1" role="alert">
                    {{ session('success') }}
                    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                @endif
                @if (session('error'))
                <div class="alert alert-danger alert-dismissible fade show my-1" role="alert">
                    {{ session('error') }}
                    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                @endif
                <table class="table">
                    <thead>
                        <tr>
                            <th scope="col">#</th>
                            <th scope="col">Title</th>
                            <th scope="col">Desc</th>
                            <th scope="col">Category</th>
                            <th scope="col">Action</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach ($posts as $key => $item)
                            <tr>
                                <th scope="row">{{ ++$key }}</th>
                                <td>{{ $item->title }}</td>
                                <td>{{  Str::limit( strip_tags( $item->desc ), 60 ) }}</td>
                                <td>{{ $item->category->name }}</td>
                                <td>
                                    <a href="{{ route('posts.edit', $item->id) }}" class="btn btn-success btn-sm">Edit</a>
                                    <form method="POST" action="{{route('posts.destroy', [$item->id])}}" class="d-inline" onsubmit="return confirm('Move post to trash ?')">
                                        @csrf
                                        <input type="hidden" name="_method" value="DELETE">
                                        <input type="submit" value="Delete" class="btn btn-danger btn-sm">
                                    </form>
                                </td>
                            </tr>
                        @endforeach   
                    </tbody>
                </table>
            </div>
        </div>
    </div>
@endsection

Kemudian kita perlu membuat folder baru untuk menampung file-file yang digunakan untuk menampilkan data posts, trash, create dan edit post. Untuk iu, silahkan buat folder baru dengan posts di dalam direktori resources/views. Setelah itu, buat file baru dengan nama index.blade.php di dalam folder posts yang baru saja dibuat. Jika file index.blade.php sudah dibuat, copy semua kode di atas dan paste di file index.blade.php yang baru saja dibuat.

Membuat File Post Trash

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="table-responsive">
                @if (session('success'))
                <div class="alert alert-success alert-dismissible fade show my-1" role="alert">
                    {{ session('success') }}
                    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                @endif
                @if (session('error'))
                <div class="alert alert-danger alert-dismissible fade show my-1" role="alert">
                    {{ session('error') }}
                    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                @endif
                <table class="table">
                    <thead>
                        <tr>
                            <th scope="col">#</th>
                            <th scope="col">Title</th>
                            <th scope="col">Desc</th>
                            <th scope="col">Category</th>
                            <th scope="col">Action</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach ($posts as $key => $item)
                            <tr>
                                <th scope="row">{{ ++$key }}</th>
                                <td>{{ $item->title }}</td>
                                <td>{{  Str::limit( strip_tags( $item->desc ), 60 ) }}</td>
                                <td>{{ $item->category->name }}</td>
                                <td>
                                    <form method="POST" action="{{route('posts.restore', $item->id)}}" class="d-inline">
                                    @csrf
                                        <input type="submit" value="Restore" class="btn btn-success btn-sm"/>
                                    </form>
                                    <form method="POST" action="{{route('posts.deletePermanent', $item->id)}}" class="d-inline" onsubmit="return confirm('Delete this data permanently?')">
                                    @csrf
                                        <input type="hidden" name="_method" value="DELETE">
                                        <input type="submit" value="Delete" class="btn btn-danger btn-sm">
                                    </form>
                                </td>
                            </tr>
                        @endforeach   
                    </tbody>
                </table>
            </div>
        </div>
    </div>
@endsection

Selanjutnya, silahkan buat file baru lagi di dalam folder posts dengan nama trash.blade.php dan copy semua kode di atas lalu paste di file trash.blade.php yang baru saja dibuat.

Membuat File Post Create.

@extends('layouts.app')
@section('content')
<div class="container">
    <div class="row">
        <div class="col-12">
            @if (session('error'))
            <div class="alert alert-danger alert-dismissible fade show my-1" role="alert">
                {{ session('error') }}
                <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                <span aria-hidden="true">&times;</span>
                </button>
            </div>
            @endif
            <form action="{{route('posts.store')}}" method="POST" enctype="multipart/form-data">
                @csrf
                <div class="form-group">
                    <label for="cover">Cover</label>
                    <input type="file" name="cover" class="form-control @error('cover') is-invalid @enderror" value="{{old('cover')}}" id="cover" required>
                    @error('cover')
                    <div class="invalid-feedback">
                        {{ $message }}    
                    </div>
                    @enderror
                </div>
                <div class="form-group">
                    <label for="title">Title</label>
                    <input type="text" name="title" class="form-control @error('title') is-invalid @enderror" value="{{old('title')}}" required>
                    @error('title')
                    <div class="invalid-feedback">
                        {{ $message }}    
                    </div>
                    @enderror
                </div>
                <div class="form-group">
                    <label for="desc">Description</label>
                    <textarea name="desc" id="desc" cols="30" rows="10" class="form-control @error('desc') is-invalid @enderror" required>{{old('desc')}}</textarea>
                    @error('desc')
                    <div class="invalid-feedback">
                        {{ $message }}    
                    </div>
                    @enderror
                </div>
                <div class="form-group">
                    <label for="category">Category</label>
                    <select name="category" id="category" class="form-control @error('category') is-invalid @enderror" required>
                        <option value="" disabled selected>Choose one</option>
                        @foreach ($categories as $category)
                        <option value="{{ $category->id }}">{{ $category->name }}</option>
                        @endforeach
                    </select>
                    @error('category')
                    <div class="invalid-feedback">
                        {{ $message }}    
                    </div>
                    @enderror
                </div>
                <div class="form-group">
                    <label for="tag">Tags</label>
                    <select name="tags[]" id="tag" class="form-control select2 @error('tags') is-invalid @enderror" required multiple>
                        @foreach ($tags as $tag)
                        <option value="{{ $tag->id }}">{{ $tag->name }}</option>
                        @endforeach
                    </select>
                    @error('tags')
                    <div class="invalid-feedback">
                        {{ $message }}    
                    </div>
                    @enderror
                </div>
                <div class="form-group">
                    <label for="keywords">Keywords</label>
                    <input type="text" name="keywords" class="form-control @error('keywords') is-invalid @enderror" value="{{old('keywords')}}" required>
                    @error('keywords')
                    <div class="invalid-feedback">
                        {{ $message }}    
                    </div>
                    @enderror
                </div>
                <div class="form-group">
                    <label for="meta_desc">Meta Desc</label>
                    <input type="text" name="meta_desc" class="form-control @error('meta_desc') is-invalid @enderror" value="{{old('meta_desc')}}" required>
                    @error('meta_desc')
                    <div class="invalid-feedback">
                        {{ $message }}    
                    </div>
                    @enderror
                </div>
                <button type="submit" class="btn btn-primary">Submit</button>
            </form>
        </div>
    </div>
</div>
@endsection

Di file index.blade.php kita telah menambahkan button create post, maka selanjutnya kita perlu file view baru untuk membuat tampilan dan fungsi create post. Silahkan buat file view baru lagi di dalam folder posts dengan nama filenya create.blade.php, kemudian copy semua kode di atas dan paste di file create.blade.php yang baru saja dibuat.

Membuat File Post Edit

@extends('layouts.app')
@section('content')
<div class="container">
    <div class="row">
        <div class="col-12">
            @if (session('error'))
            <div class="alert alert-danger alert-dismissible fade show my-1" role="alert">
                {{ session('error') }}
                <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                <span aria-hidden="true">&times;</span>
                </button>
            </div>
            @endif
            <form action="{{route('posts.update', $post->id)}}" method="POST" enctype="multipart/form-data">
                <input type="hidden" name="_method" value="PUT">
                @csrf
                <div class="form-group">
                    <label for="cover">Cover</label>
                    <input type="file" name="cover" class="form-control @error('cover') is-invalid @enderror" id="cover" value="{{old('cover') ? old('cover') : $post->cover}}">
                    @error('cover')
                    <div class="invalid-feedback">
                        {{ $message }}    
                    </div>
                    @enderror
                </div>
                <div class="form-group">
                    <label for="title">Title</label>
                    <input type="text" name="title" class="form-control @error('title') is-invalid @enderror" value="{{old('title') ? old('title') : $post->title}}" required>
                    @error('title')
                    <div class="invalid-feedback">
                        {{ $message }}    
                    </div>
                    @enderror
                </div>
                <div class="form-group">
                    <label for="slug">Slug</label>
                    <input type="text" name="slug" class="form-control @error('slug') is-invalid @enderror" value="{{old('slug') ? old('slug') : $post->slug}}" required>
                    @error('slug')
                    <div class="invalid-feedback">
                        {{ $message }}    
                    </div>
                    @enderror
                </div>
                <div class="form-group">
                    <label for="desc">Description</label>
                    <textarea name="desc" id="desc" cols="30" rows="10" class="form-control @error('desc') is-invalid @enderror" required>{{old('desc') ? old('desc') : $post->desc}}</textarea>
                    @error('desc')
                    <div class="invalid-feedback">
                        {{ $message }}    
                    </div>
                    @enderror
                </div>
                <div class="form-group">
                    <label for="category">Category</label>
                    <select name="category" id="category" class="form-control @error('category') is-invalid @enderror" required>
                        <option value="" disabled selected>Choose one</option>
                        @foreach ($categories as $category)
                        <option {{ $category->id == $post->category_id ? 'selected' : '' }} value="{{ $category->id }}">{{ $category->name }}</option>
                        @endforeach
                    </select>
                    @error('category')
                    <div class="invalid-feedback">
                        {{ $message }}    
                    </div>
                    @enderror
                </div>
                <div class="form-group">
                    <label for="tag">Tags</label>
                    <select name="tags[]" id="tag" class="form-control select2 @error('tags') is-invalid @enderror" required multiple>
                        @foreach ($post->tags as $tag)
                        <option selected value="{{ $tag->id }}">{{ $tag->name }}</option>
                        @endforeach
                        @foreach ($tags as $tags)
                        <option value="{{ $tags->id }}">{{ $tags->name }}</option>
                        @endforeach
                    </select>
                    @error('tags')
                    <div class="invalid-feedback">
                        {{ $message }}    
                    </div>
                    @enderror
                </div>
                <div class="form-group">
                    <label for="keywords">Keywords</label>
                    <input type="text" name="keywords" class="form-control @error('keywords') is-invalid @enderror" value="{{old('keywords') ? old('keywords') : $post->keywords}}" required>
                    @error('keywords')
                    <div class="invalid-feedback">
                        {{ $message }}    
                    </div>
                    @enderror
                </div>
                <div class="form-group">
                    <label for="meta_desc">Meta Desc</label>
                    <input type="text" name="meta_desc" class="form-control @error('meta_desc') is-invalid @enderror" value="{{old('meta_desc') ? old('meta_desc') : $post->meta_desc}}" required>
                    @error('meta_desc')
                    <div class="invalid-feedback">
                        {{ $message }}    
                    </div>
                    @enderror
                </div>
                <button type="submit" class="btn btn-primary">Submit</button>
            </form>
        </div>
    </div>
</div>
@endsection

File view terakhir yang akan kita buat di folder posts yaitu file dengan nama edit.blade.php. Silahkan buat file baru dengan nama file tersebut dan copy semua kode di atas lalu paste di file edit.blade.php tersebut.

testing; CRUD post laravel 8

Oke, sekarang kita bisa uji coba CRUD post yang telah kita buat. Silahkan jalan laravel project kita dengan perintah artisan php artisan serve. Kemudian jangan lupa untuk login terlebih dahulu. Jika sudah berhasil login, sekarang masuklah ke halaman posts dan cobalah untuk menambahkan data post atau add post, edit, delete, restore dan delete permanent.

Sampai disini kita sudah berhasil membuat tiga CRUD yaitu manage tags, manage categories dan manage posts. Selanjutnya kita akan menampilkan data-data post yang telah dibuat dan menampilkan detail dari data post tersebut.

Menampilkan Data Post di Front End

Oke, di langkah ini kita akan menampilkan data-data post, category dan tag ke halaman front end atau halaman yang biasanya dilihat pengunjung blog atau website.

Define Route

Route::get('/', [App\Http\Controllers\FrontController::class, 'index'])->name('homepage');
Route::get('post/{slug}', [App\Http\Controllers\FrontController::class, 'show'])->name('show');
Route::get('category/{category:slug}', [App\Http\Controllers\FrontController::class, 'category'])->name('category');
Route::get('tag/{tag:slug}', [App\Http\Controllers\FrontController::class, 'tag'])->name('tag');

Buka file routes/web.php, kemudian ubah route::('/') bawaan dari laravel dan tambahkan tiga route baru untuk menampilkan detail post atau blog, menampilkan post atau blog berdasarkan category dan menampilkan data post atau blog berdasarkan tag. 

Membuat FrontController.php

php artisan make:controller FrontController

Selanjunya, kita buat file controller baru menggunakan perintah artisan seperti di atas. File controller yang akan kita buat, kita namakan dengan FrontController.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\{Category, Tag, Post};

class FrontController extends Controller
{
    public function index()
    {
        $posts = Post::latest()->get();
        return view('welcome', compact('posts'));
    }

    public function show($slug)
    {
        $post = Post::where('slug', $slug)->first();
        return view('show', compact('post'));
    }

    public function category(Category $category)
    {
        $posts = $category->posts()->latest()->get();
        return view ('welcome',compact('category','posts'));
    }

    public function tag(Tag $tag)
    {
        $posts = $tag->posts()->latest()->get();
        return view ('welcome',compact('tag','posts'));
    }

}

Oke, selanjutnya buka file FrontController.php yang baru saja dibuat dan ubah atau replace kode yang ada dengan kode seperti di atas. Di file FrontController.php ini kita menambahkan method index yang berfungsi untuk menampilkan semua data post atau  blog, method show berfungsi untuk menampilkan detail post atau blog, method category untuk menampilkan data blog atau post berdasarkan slug category dan method tag untuk menampilkan data post atau blog berdasarkan tag.

Edit File Welcome.blade.php

<!doctype html>
<html lang="en">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
        @isset($category)
        <title>{{ $category->name }}</title>
        <meta name="title" content="{{ $category->name }}">
        <meta name="description" content="{{ $category->meta_desc }}">
        <meta name="keywords" content="{{ $category->keywords }}">
        @endisset
        @isset($tag)
        <title>{{ $tag->name }}</title>
        <meta name="title" content="{{ $tag->name }}">
        <meta name="description" content="{{ $tag->meta_desc }}">
        <meta name="keywords" content="{{ $tag->keywords }}">
        @endisset
        @if (!isset($tag) && !isset($category))
        <title>Panduan Lengkap Cara Membuat Blog dengan Laravel 8 dan Bootstrap untuk Pemula</title>
        @endif
    </head>
    <body>
        <div class="container mt-5">
            @isset($category)
            <h1 class="text-center my-3">Blog Category: {{ $category->name }}</h1>
            @endisset
            @isset($tag)
            <h1 class="text-center my-3">Blog Tag: {{ $tag->name }}</h1>
            @endisset
            @if (!isset($tag) && !isset($category))
            <h1 class="text-center my-3">Blog</h1>
            @endif
            <div class="row">
                @foreach ($posts as $item)
                <div class="col-4 mt-3">
                    <div class="card">
                        <img src="{{ asset('storage/'.$item->cover) }}" class="card-img-top" alt="...">
                        <div class="card-body">
                            <a href="{{ route('show', $item->slug) }}" class="text-dark">
                                <h5 class="card-title">{{ $item->title }}</h5>
                            </a>
                        </div>
                        <div class="card-footer">
                            <div class="d-flex mx-auto">
                                @foreach ($item->tags as $tags)
                                <a href="{{ route('tag', $tags->slug) }}" class="badge badge-secondary mr-1">{{ $tags->name }}</a>
                                @endforeach
                                <small class="text-muted ml-auto">{{ Carbon\Carbon::parse($item->created_at)->diffForHumans()}}</small>
                            </div>
                        </div>
                    </div>
                </div>
                @endforeach
            </div>
        </div>
        <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-Piv4xVNRyMGpqkS2by6br4gNJ7DXjqk09RmUpJ8jgGtD7zP9yug3goQfGII0yAns" crossorigin="anonymous"></script>
    </body>
</html>

Kemudian buka file welcome.blade.php. Pada langkah ini, kita akan mengubah semua kode yang ada dengan kode seperti di atas. Di file welcome.blade.php ini, di dalam tag <head></head> kita menambahkan pengkondisian menggunakan isset untuk mengubah data meta tag (SEO) yang bisa berubah nilainya sesuai dengan halaman yang kita buka.

Contohnya jika membuka halaman category atau menampilkan data blog atau post berdasarkan category maka meta tags title, description dan keywords juga akan menampilkan data title, meta description dan keywords dari category tersebut.

Kemudian dibagian heading kita juga membuat hal yang sama seperti di dalam tag <head></head> yaitu menambahkan kondisi isset. Sementara dibagian content atau card data blog atau post kita menggunakan perulangan dengan foreach.

Membuat File Show.blade.php

<!doctype html>
<html lang="en">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <!-- Primary Meta Tags -->
        <title>{{ $post->title }}</title>
        <meta name="title" content="{{ $post->title }}">
        <meta name="description" content="{{ $post->meta_desc }}">
        <meta name="keywords" content="{{ $post->keywords }}">
        <!-- Open Graph / Facebook -->
        <meta property="og:type" content="article">
        <meta property="og:url" content="{{ URL::current() }}">
        <meta property="og:title" content="{{ $post->title }}">
        <meta property="og:description" content="{{ $post->meta_desc }}">
        <meta property="og:image" content="{{ asset('storage/'.$post->cover) }}">
        <!-- Twitter -->
        <meta property="twitter:card" content="summary_large_image">
        <meta property="twitter:url" content="{{ URL::current() }}">
        <meta property="twitter:title" content="{{ $post->title }}">
        <meta property="twitter:description" content="{{ $post->meta_desc }}">
        <meta property="twitter:image" content="{{ asset('storage/'.$post->cover) }}">
        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
    </head>
    <body>
        <div class="container my-5">
            <div class="row">
                <div class="card">
                    <img src="{{ asset('storage/'.$post->cover) }}" alt="" class="img-fluid">
                    <div class="card-body">
                        <h1 class="card-title">{{ $post->title }}</h1>
                        <div class="d-flex my-2">
                            <small class="text-muted">by {{ $post->user->name }} ・ {{ Carbon\Carbon::parse($post->created_at)->isoFormat('D MMMM Y'); }}</small>
                        </div>
                        <p>{{ $post->desc }}</p>
                        <div class="card-footer bg-transparent d-flex mx-auto">
                            <a href="{{ route('category',$post->category->slug) }}" class="text-dark">{{ $post->category->name }}</a>
                            <div class="d-flex ml-auto">
                                @foreach ($post->tags as $item)
                                <a href="{{ route('tag', $item->slug) }}" class="badge badge-secondary mr-1">{{ $item->name }}</a>
                                @endforeach
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-Piv4xVNRyMGpqkS2by6br4gNJ7DXjqk09RmUpJ8jgGtD7zP9yug3goQfGII0yAns" crossorigin="anonymous"></script>
    </body>
</html>

Selanjutnya, kita perlu satu file view baru lagi untuk menampilkan detail data post atau blog yang dipilih. Silahkan buat file di dalam folder views dengan nama show.blade.php kemudian copy semua kode di atas dan paste di file show.blade.php yang baru saja dibuat.

Pengujian Blog Laravel 8

Kita telah sampai di akhir artikel tutorial membuat blog dengan laravel 8 dan bootstrap. Untuk mengujinya, pastikan kamu sudah membuat atau menambahkan data post atau blog lengkap dengan category dan tag. Silahkan jalankan laravel project dengan perintah php artisan serve, lalu buka project di browser. 

Untuk melihat tampilan detail post atau blog, silahkan klik salah satu data post atau blog, maka kamu akan diarahkan ke halaman detail blog tersebut. Kemudian untuk melihat data-data post atau blog berdasarkan category, di bagian card footer terdapat data category dari post tersebut. Silahkan klik category tersebut, maka kamu akan diarahkan ke halaman category yang menampilkan data-data post atau blog berdasarkan category tersebut. Untuk melihat data-data post berdasarkan tag, di halaman detail blog atau tepatnya di bagian card footer juga terdapat data tag dari post tersebut. Silahkan klik data tag tersebut, maka kamu akan di arahkan ke halaman tag yang menampilkan data-data post atau blog berdasarkan tag tersebut.

Kesimpulan

Kita telah menyelesaikan tutorial cara membuat blog dengan laravel 8 dan bootstrap. Mungkin terlihat cukup panjang langkah-langkah di artikel ini, itu karena di artikel ini saya menjadikan satu artikel untuk langkah-langkah membuat CRUD category, CRUD tag dan CRUD post. Dari segi tampilan, mungkin terlihat cukup sederhana, tapi saya rasa point dari artikel ini sudah cukup jelas yaitu membuat blog sederhana dengan laravel versi 8 dan bootstrap, serta menerapkan SEO sederhana di setiap halaman di front end.

Kamu bisa improve kode-kode yang telah didapat dari artikel ini untuk membuat fungsinya lebih baik atau lebih kompleks dan dengan tampilan yang lebih menarik lagi.

Sekian artikel tutorial membuat blog dengan laravel 8 kali ini, semoga artikel ini bisa membantu. Selamat mencoba dan sampai jumpa di artikel berikutnya. 😊 🚀 👨‍🚀

 

Credit: Internet illustrations by Storyset

Tinggalkan Komentar
Loading Comments