Panduan Tutorial Laravel

Belajar Laravel Batch 9: SEO dan Publish Status - Membuat Artikel Lebih Optimal dan Teratur

Belajar Laravel Batch 9: SEO dan Publish Status - Membuat Artikel Lebih Optimal dan Teratur
Twitter / X WhatsApp Facebook LinkedIn Telegram
Apa yang kamu butuhkan

Di Batch 9 ini, PahamITian akan belajar cara mengoptimalkan artikel Laravel untuk mesin pencari (SEO) dengan slug dan meta data, serta mengelola status publikasi (draft/published) agar konten lebih terorganisir dan mudah ditemukan.

Halo, PahamITian! Selamat datang di Batch 9 dari seri tutorial Laravel dasar kita. Setelah di batch sebelumnya kita berhasil membuat fitur upload file, kini saatnya kita membuat artikel kita lebih "pintar" dan "terlihat" oleh dunia luar, terutama mesin pencari seperti Google. Kita akan membahas tentang SEO dasar dan bagaimana mengelola status publikasi artikel.

Mengapa SEO dan Publish Status Penting?

Bayangkan kamu punya toko kue. Kue-kue kamu enak, tapi kalau tokonya tersembunyi di gang sempit dan tidak ada papan nama, siapa yang tahu? Begitu juga dengan artikel di website kita. Sebagus apapun kontennya, kalau tidak dioptimalkan untuk mesin pencari (SEO) dan tidak diatur status publikasinya, maka akan sulit ditemukan oleh pembaca. Di batch ini, kita akan belajar bagaimana "memasang papan nama" dan "mengatur jam buka" toko kue kita.

1. Slug: URL yang Cantik dan SEO-Friendly

Apa itu Slug?

Slug adalah bagian dari URL yang mengidentifikasi sebuah halaman web dengan cara yang mudah dibaca oleh manusia dan mesin pencari. Contohnya, daripada URL https://pahamit.com/artikel/123, akan lebih baik jika menjadi https://pahamit.com/artikel/belajar-laravel-batch-9-seo-publish-status. Slug biasanya dibuat dari judul artikel, dengan spasi diganti tanda hubung (-) dan karakter khusus dihilangkan.

Mengapa Penting?

  • SEO: Mesin pencari lebih suka URL yang deskriptif dan mengandung kata kunci. Ini membantu mereka memahami isi halaman.
  • User Experience: Pengguna lebih mudah mengingat dan memahami isi halaman dari URL yang jelas.

Implementasi di Laravel

Kita perlu menambahkan kolom slug di tabel posts kita. Pastikan slug ini unik.

Langkah 1: Menambahkan Kolom slug ke Migration

Buat migration baru atau tambahkan ke migration create_posts_table jika belum ada data.

BASH
php artisan make:migration add_slug_to_posts_table --table=posts

Edit file migration yang baru dibuat:

PHP
// database/migrations/xxxx_xx_xx_xxxxxx_add_slug_to_posts_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->string('slug')->unique()->after('title');
        });
    }

    public function down(): void
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->dropColumn('slug');
        });
    }
};

Jalankan migration:

BASH
php artisan migrate

Langkah 2: Membuat Slug Otomatis Saat Menyimpan Artikel

Kita bisa menggunakan Str::slug() dari Laravel untuk membuat slug. Ini bisa dilakukan di Controller saat menyimpan atau mengupdate, atau lebih elegan lagi di Model menggunakan mutator atau event.

Untuk permulaan, kita lakukan di Controller PostController saat store dan update.

PHP
// app/Http/Controllers/PostController.php
use Illuminate\Support\Str; // Tambahkan ini di bagian atas

// ... dalam method store()
public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|max:255',
        'content' => 'required',
        // ... validasi lainnya
    ]);

    $post = new Post();
    $post->title = $validated['title'];
    $post->slug = Str::slug($validated['title']); // Membuat slug dari judul
    $post->content = $validated['content'];
    // ... simpan data lainnya
    $post->save();

    return redirect()->route('posts.index')->with('success', 'Artikel berhasil ditambahkan!');
}

// ... dalam method update()
public function update(Request $request, Post $post)
{
    $validated = $request->validate([
        'title' => 'required|max:255',
        'content' => 'required',
        // ... validasi lainnya
    ]);

    $post->title = $validated['title'];
    $post->slug = Str::slug($validated['title']); // Update slug juga
    $post->content = $validated['content'];
    // ... update data lainnya
    $post->save();

    return redirect()->route('posts.index')->with('success', 'Artikel berhasil diupdate!');
}

Catatan Penting: Untuk memastikan slug unik, kita bisa menambahkan logika pengecekan di controller atau menggunakan paket pihak ketiga seperti spatie/laravel-sluggable yang lebih canggih.

2. Status Draft/Published: Mengontrol Visibilitas Artikel

Apa itu Status Publikasi?

Status publikasi memungkinkan kita untuk menentukan apakah sebuah artikel sudah siap tayang (Published) atau masih dalam tahap pengerjaan (Draft). Ini sangat penting untuk alur kerja editorial.

Implementasi di Laravel

Kita akan menambahkan kolom is_published bertipe boolean ke tabel posts.

Langkah 1: Menambahkan Kolom is_published ke Migration

BASH
php artisan make:migration add_is_published_to_posts_table --table=posts

Edit file migration yang baru dibuat:

PHP
// database/migrations/xxxx_xx_xx_xxxxxx_add_is_published_to_posts_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->boolean('is_published')->default(false)->after('slug');
        });
    }

    public function down(): void
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->dropColumn('is_published');
        });
    }
};

Jalankan migration:

BASH
php artisan migrate

Langkah 2: Menambahkan Input di Form dan Menyimpan Status

Di form create.blade.php dan edit.blade.php, tambahkan checkbox untuk is_published.

HTML
<!-- resources/views/posts/create.blade.php atau edit.blade.php -->
<div class="mb-3 form-check">
    <input type="checkbox" class="form-check-input" id="is_published" name="is_published" value="1" {{ old('is_published', $post->is_published ?? false) ? 'checked' : '' }}>
    <label class="form-check-label" for="is_published">Publikasikan Artikel?</label>
</div>

Di PostController, pastikan is_published divalidasi dan disimpan.

PHP
// app/Http/Controllers/PostController.php
// ... dalam method store()
public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|max:255',
        'content' => 'required',
        'is_published' => 'boolean', // Tambahkan validasi ini
        // ... validasi lainnya
    ]);

    $post = new Post();
    $post->title = $validated['title'];
    $post->slug = Str::slug($validated['title']);
    $post->content = $validated['content'];
    $post->is_published = $request->has('is_published'); // Cek apakah checkbox dicentang
    // ... simpan data lainnya
    $post->save();

    return redirect()->route('posts.index')->with('success', 'Artikel berhasil ditambahkan!');
}

// ... dalam method update()
public function update(Request $request, Post $post)
{
    $validated = $request->validate([
        'title' => 'required|max:255',
        'content' => 'required',
        'is_published' => 'boolean', // Tambahkan validasi ini
        // ... validasi lainnya
    ]);

    $post->title = $validated['title'];
    $post->slug = Str::slug($validated['title']);
    $post->content = $validated['content'];
    $post->is_published = $request->has('is_published'); // Cek apakah checkbox dicentang
    // ... update data lainnya
    $post->save();

    return redirect()->route('posts.index')->with('success', 'Artikel berhasil diupdate!');
}

3. Meta Title dan Description: Wajah Artikel di Hasil Pencarian

Apa itu Meta Title dan Description?

  • Meta Title: Judul yang muncul di tab browser dan di hasil pencarian Google. Ini adalah salah satu faktor SEO terpenting.
  • Meta Description: Ringkasan singkat artikel yang muncul di bawah meta title di hasil pencarian. Ini tidak secara langsung mempengaruhi ranking, tapi sangat mempengaruhi click-through rate (CTR).

Implementasi di Laravel

Kita akan menambahkan kolom meta_title dan meta_description ke tabel posts.

Langkah 1: Menambahkan Kolom Meta ke Migration

BASH
php artisan make:migration add_meta_seo_to_posts_table --table=posts

Edit file migration yang baru dibuat:

PHP
// database/migrations/xxxx_xx_xx_xxxxxx_add_meta_seo_to_posts_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->string('meta_title')->nullable()->after('is_published');
            $table->text('meta_description')->nullable()->after('meta_title');
        });
    }

    public function down(): void
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->dropColumn(['meta_title', 'meta_description']);
        });
    }
};

Jalankan migration:

BASH
php artisan migrate

Langkah 2: Menambahkan Input di Form dan Menyimpan Data

Di form create.blade.php dan edit.blade.php, tambahkan input text untuk meta title dan textarea untuk meta description.

HTML
<!-- resources/views/posts/create.blade.php atau edit.blade.php -->
<div class="mb-3">
    <label for="meta_title" class="form-label">Meta Title (untuk SEO)</label>
    <input type="text" class="form-control" id="meta_title" name="meta_title" value="{{ old('meta_title', $post->meta_title ?? '') }}" maxlength="60">
    <div class="form-text">Maksimal 60 karakter. Akan muncul di hasil pencarian.</div>
</div>
<div class="mb-3">
    <label for="meta_description" class="form-label">Meta Description (untuk SEO)</label>
    <textarea class="form-control" id="meta_description" name="meta_description" rows="3">{{ old('meta_description', $post->meta_description ?? '') }}</textarea>
    <div class="form-text">Maksimal 160 karakter. Ringkasan di hasil pencarian.</div>
</div>

Di PostController, pastikan meta_title dan meta_description divalidasi dan disimpan.

PHP
// app/Http/Controllers/PostController.php
// ... dalam method store()
public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|max:255',
        'content' => 'required',
        'is_published' => 'boolean',
        'meta_title' => 'nullable|string|max:60', // Tambahkan validasi ini
        'meta_description' => 'nullable|string|max:160', // Tambahkan validasi ini
        // ... validasi lainnya
    ]);

    $post = new Post();
    $post->title = $validated['title'];
    $post->slug = Str::slug($validated['title']);
    $post->content = $validated['content'];
    $post->is_published = $request->has('is_published');
    $post->meta_title = $validated['meta_title'];
    $post->meta_description = $validated['meta_description'];
    // ... simpan data lainnya
    $post->save();

    return redirect()->route('posts.index')->with('success', 'Artikel berhasil ditambahkan!');
}

// ... dalam method update()
public function update(Request $request, Post $post)
{
    $validated = $request->validate([
        'title' => 'required|max:255',
        'content' => 'required',
        'is_published' => 'boolean',
        'meta_title' => 'nullable|string|max:60',
        'meta_description' => 'nullable|string|max:160',
        // ... validasi lainnya
    ]);

    $post->title = $validated['title'];
    $post->slug = Str::slug($validated['title']);
    $post->content = $validated['content'];
    $post->is_published = $request->has('is_published');
    $post->meta_title = $validated['meta_title'];
    $post->meta_description = $validated['meta_description'];
    // ... update data lainnya
    $post->save();

    return redirect()->route('posts.index')->with('success', 'Artikel berhasil diupdate!');
}

Langkah 3: Menampilkan Meta Data di View

Di layout utama atau di view show.blade.php artikel, tambahkan tag <title> dan <meta name="description"> di bagian <head>.

HTML
<!-- resources/views/layouts/app.blade.php atau resources/views/posts/show.blade.php -->
<head>
    <!-- ... tag lainnya ... -->
    <title>{{ $post->meta_title ?? $post->title }} | Pahamit.com</title>
    <meta name="description" content="{{ $post->meta_description ?? Str::limit(strip_tags($post->content), 150) }}">
    <!-- ... tag lainnya ... -->
</head>

4. Published At: Kapan Artikel Dipublikasikan?

Apa itu published_at?

published_at adalah kolom timestamp yang menunjukkan kapan sebuah artikel benar-benar dipublikasikan. Ini berbeda dengan created_at yang menunjukkan kapan artikel dibuat. published_at bisa null jika artikel masih draft, dan akan terisi saat artikel dipublikasikan.

Implementasi di Laravel

Langkah 1: Menambahkan Kolom published_at ke Migration

BASH
php artisan make:migration add_published_at_to_posts_table --table=posts

Edit file migration yang baru dibuat:

PHP
// database/migrations/xxxx_xx_xx_xxxxxx_add_published_at_to_posts_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->timestamp('published_at')->nullable()->after('meta_description');
        });
    }

    public function down(): void
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->dropColumn('published_at');
        });
    }
};

Jalankan migration:

BASH
php artisan migrate

Langkah 2: Mengisi published_at Saat Artikel Dipublikasikan

Di PostController, saat is_published diatur menjadi true, kita akan mengisi published_at dengan waktu saat ini. Jika is_published menjadi false (kembali ke draft), kita bisa mengosongkan published_at.

PHP
// app/Http/Controllers/PostController.php
use Carbon\Carbon; // Tambahkan ini di bagian atas

// ... dalam method store()
public function store(Request $request)
{
    $validated = $request->validate([
        // ... validasi lainnya
        'is_published' => 'boolean',
    ]);

    $post = new Post();
    // ... simpan data lainnya
    $post->is_published = $request->has('is_published');
    $post->published_at = $post->is_published ? Carbon::now() : null; // Set published_at
    $post->save();

    return redirect()->route('posts.index')->with('success', 'Artikel berhasil ditambahkan!');
}

// ... dalam method update()
public function update(Request $request, Post $post)
{
    $validated = $request->validate([
        // ... validasi lainnya
        'is_published' => 'boolean',
    ]);

    // Jika status publikasi berubah dari tidak published menjadi published
    if ($request->has('is_published') && !$post->is_published) {
        $post->published_at = Carbon::now();
    } elseif (!$request->has('is_published') && $post->is_published) {
        // Jika status publikasi berubah dari published menjadi tidak published
        $post->published_at = null;
    }
    $post->is_published = $request->has('is_published');
    // ... update data lainnya
    $post->save();

    return redirect()->route('posts.index')->with('success', 'Artikel berhasil diupdate!');
}

5. Filter Artikel Published: Hanya Menampilkan yang Siap Tayang

Sekarang, kita punya kolom is_published dan published_at. Kita bisa menggunakannya untuk hanya menampilkan artikel yang benar-benar siap tayang di halaman publik.

Di PostController atau di mana pun kamu menampilkan daftar artikel publik (misalnya di HomeController atau PostController@index jika itu untuk publik):

PHP
// app/Http/Controllers/PostController.php (untuk daftar publik)
public function indexPublic()
{
    $posts = Post::where('is_published', true)
                 ->whereNotNull('published_at')
                 ->where('published_at', '<=', Carbon::now())
                 ->orderBy('published_at', 'desc')
                 ->paginate(10);

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

Penjelasan filternya:
* where('is_published', true): Hanya ambil artikel yang statusnya published.
* whereNotNull('published_at'): Pastikan published_at tidak kosong (sudah pernah dipublikasikan).
* where('published_at', '<=', Carbon::now()): Pastikan tanggal publikasi sudah tiba atau sudah lewat. Ini berguna untuk artikel yang dijadwalkan publikasinya di masa depan.

Latihan: Halaman Artikel Publik Berdasarkan Slug

Mari kita buat halaman untuk menampilkan artikel secara individual, yang bisa diakses oleh publik menggunakan slug.

Langkah 1: Tambahkan Route Baru

Di routes/web.php, tambahkan route untuk artikel publik. Letakkan di luar group middleware auth jika kamu punya.

PHP
// routes/web.php
use App\Http\Controllers\PostController;

Route::get('/artikel/{slug}', [PostController::class, 'showPublic'])->name('artikel.show');

Langkah 2: Tambahkan Method showPublic di PostController

PHP
// app/Http/Controllers/PostController.php
use App\Models\Post;
use Carbon\Carbon;

// ...

public function showPublic(string $slug)
{
    $post = Post::where('slug', $slug)
                ->where('is_published', true)
                ->whereNotNull('published_at')
                ->where('published_at', '<=', Carbon::now())
                ->firstOrFail(); // Jika tidak ditemukan, akan otomatis 404

    return view('posts.public_show', compact('post'));
}

Langkah 3: Buat View public_show.blade.php

Buat file resources/views/posts/public_show.blade.php.

HTML
<!-- resources/views/posts/public_show.blade.php -->
@extends('layouts.app') {{-- Asumsikan Anda punya layout dasar --}}

@section('title', $post->meta_title ?? $post->title)
@section('description', $post->meta_description ?? Str::limit(strip_tags($post->content), 150))

@section('content')
    <div class="container mt-5">
        <article>
            <h1 class="mb-3">{{ $post->title }}</h1>
            <p class="text-muted">Dipublikasikan pada: {{ $post->published_at->format('d M Y H:i') }}</p>
            @if ($post->featured_image)
                <img src="{{ asset('storage/' . $post->featured_image) }}" class="img-fluid mb-4" alt="{{ $post->title }}">
            @endif
            <div class="blog-content">
                {!! $post->content !!}
            </div>
            @if ($post->category)
                <p class="mt-4">Kategori: <a href="#">{{ $post->category->name }}</a></p>
            @endif
            @if ($post->user)
                <p>Penulis: {{ $post->user->name }}</p>
            @endif
        </article>
        <a href="{{ url('/') }}" class="btn btn-primary mt-5">Kembali ke Beranda</a>
    </div>
@endsection

Pastikan layouts.app Anda memiliki @yield('title') dan @yield('description') di bagian <head>.

HTML
<!-- resources/views/layouts/app.blade.php -->
<head>
    <!-- ... -->
    <title>@yield('title', 'Pahamit.com')</title>
    <meta name="description" content="@yield('description', 'Pahamit.com - Bukan Sekadar Belajar. Tutorial IT, berita teknologi, dan solusi digital.')">
    <!-- ... -->
</head>

Ringkasan Singkat Batch 9

Di batch ini, kita telah mempelajari bagaimana membuat artikel kita lebih ramah SEO dan terorganisir dengan baik:
* Slug: URL yang bersih dan deskriptif untuk SEO dan pengalaman pengguna.
* Status Publikasi (is_published): Mengontrol apakah artikel sudah siap tayang atau masih dalam draft.
* Meta Title & Description: Judul dan deskripsi khusus untuk hasil pencarian Google.
* published_at: Timestamp kapan artikel benar-benar dipublikasikan, berguna untuk penjadwalan.
* Filter Artikel Publik: Hanya menampilkan artikel yang sudah dipublikasikan dan sudah waktunya tayang.
* Halaman Artikel Publik Berdasarkan Slug: Mengimplementasikan tampilan artikel tunggal yang bisa diakses publik melalui slugnya.

Latihan Praktik Batch 9

  1. Implementasikan Kolom Baru: Pastikan semua kolom slug, is_published, meta_title, meta_description, dan published_at sudah ada di tabel posts Anda dan terisi dengan benar saat membuat/mengedit artikel.
  2. Uji Pembuatan Slug: Buat beberapa artikel dengan judul yang berbeda, pastikan slugnya terbentuk dengan benar dan unik.
  3. Uji Status Publikasi: Buat artikel dalam status draft dan published. Pastikan hanya yang published yang muncul di halaman publik.
  4. Uji Meta SEO: Isi meta title dan description. Cek di browser (View Page Source) apakah tag <title> dan <meta name="description"> sudah terisi sesuai data artikel.
  5. Uji Halaman Publik: Akses artikel yang sudah dipublikasikan menggunakan URL dengan slug-nya (misal: http://localhost:8000/artikel/judul-artikel-anda). Pastikan hanya artikel yang is_published = true dan published_at sudah lewat yang bisa diakses.

Pertanyaan Cek Pemahaman Batch 9

  1. Apa perbedaan utama antara created_at dan published_at pada sebuah artikel?
  2. Mengapa slug penting untuk SEO dan bagaimana cara membuatnya di Laravel?
  3. Apa fungsi meta_title dan meta_description? Di mana keduanya akan terlihat oleh pengguna?
  4. Bagaimana cara memfilter artikel agar hanya menampilkan yang sudah dipublikasikan dan sudah waktunya tayang?
  5. Jika sebuah artikel memiliki is_published = true tetapi published_at diatur ke tanggal di masa depan, apakah artikel tersebut akan muncul di halaman publik dengan filter yang kita buat?

Kesalahan Umum Pemula Batch 9

  • Slug Tidak Unik: Lupa menambahkan validasi atau logika untuk memastikan slug unik, sehingga bisa terjadi error database atau artikel tidak bisa diakses dengan benar.
  • Lupa Str::slug(): Langsung menyimpan judul ke kolom slug tanpa mengonversinya menjadi format slug yang benar.
  • Masalah published_at: Tidak mengatur published_at menjadi null saat artikel kembali ke draft, atau tidak mengisi Carbon::now() saat dipublikasikan.
  • Filter Kurang Lengkap: Hanya memfilter is_published = true tanpa mempertimbangkan published_at atau published_at yang masih di masa depan.
  • Meta Data Tidak Tampil: Lupa menambahkan @yield('title') dan @yield('description') di layout utama atau di view artikel.

Persiapan untuk Batch Berikutnya

Selamat, PahamITian! Kamu sudah semakin mahir dalam mengelola artikel. Di Batch 10, kita akan menggabungkan semua yang sudah kita pelajari untuk membangun Mini Project Akhir: Blog Laravel Sederhana. Kita akan membuat sebuah blog yang fungsional dengan fitur login admin, CRUD artikel, kategori, upload gambar, status publish, dan halaman publik. Ini akan menjadi puncak dari perjalanan belajar dasar Laravel kita! Siapkan semangatmu!