Project: Blog Sederhana dengan PHP Native (Bagian 3)

Admin panel kita sudah siap. Sekarang kita selesaikan sisi yang dilihat pengunjung:

  • daftar artikel publik
  • pagination
  • halaman detail per artikel
  • filter hanya artikel published

Di bagian ini, blog sederhana kita benar-benar menjadi aplikasi yang usable.

1. Menampilkan Daftar Artikel Publik

Buat file public/index.php:

<?php
require __DIR__ . '/../config/database.php';
require __DIR__ . '/../functions.php';

$limit = 5;
$page = max(1, (int) ($_GET['page'] ?? 1));
$offset = ($page - 1) * $limit;

$stmtTotal = $pdo->query("SELECT COUNT(*) AS total FROM artikel WHERE status = 'published'");
$totalArtikel = (int) $stmtTotal->fetch()['total'];
$totalPages = max(1, (int) ceil($totalArtikel / $limit));

if ($page > $totalPages) {
    $page = $totalPages;
    $offset = ($page - 1) * $limit;
}

$stmt = $pdo->prepare("
    SELECT artikel.*, kategori.nama_kategori
    FROM artikel
    INNER JOIN kategori ON kategori.id = artikel.kategori_id
    WHERE artikel.status = 'published'
    ORDER BY artikel.created_at DESC
    LIMIT :limit OFFSET :offset
");
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$artikels = $stmt->fetchAll();
?>

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <title>Blog Sederhana</title>
    <link rel="stylesheet" href="../assets/style.css">
</head>
<body>
    <div class="container">
        <h1>Blog Sederhana</h1>
        <p>Belajar PHP lewat artikel yang ditulis sendiri.</p>

        <div class="post-list">
            <?php foreach ($artikels as $artikel): ?>
                <article class="card post-card">
                    <small><?= htmlspecialchars($artikel['nama_kategori']) ?></small>
                    <h2>
                        <a href="detail.php?slug=<?= urlencode($artikel['slug']) ?>">
                            <?= htmlspecialchars($artikel['judul']) ?>
                        </a>
                    </h2>
                    <p><?= htmlspecialchars($artikel['ringkasan']) ?></p>
                    <a class="read-more" href="detail.php?slug=<?= urlencode($artikel['slug']) ?>">
                        Baca selengkapnya โ†’
                    </a>
                </article>
            <?php endforeach; ?>
        </div>

        <nav class="pagination">
            <?php for ($i = 1; $i <= $totalPages; $i++): ?>
                <a class="<?= $i === $page ? 'active' : '' ?>" href="?page=<?= $i ?>">
                    <?= $i ?>
                </a>
            <?php endfor; ?>
        </nav>
    </div>
</body>
</html>

Perhatikan dua hal penting:

  1. hanya artikel dengan status = 'published' yang ditampilkan
  2. query list dan query total data sama-sama memakai filter yang sama

Kalau dua query ini tidak sinkron, pagination akan rusak.

2. Membuat Halaman Detail Artikel

Buat file public/detail.php:

<?php
require __DIR__ . '/../config/database.php';

$slug = trim($_GET['slug'] ?? '');

$stmt = $pdo->prepare("
    SELECT artikel.*, kategori.nama_kategori
    FROM artikel
    INNER JOIN kategori ON kategori.id = artikel.kategori_id
    WHERE artikel.slug = :slug
      AND artikel.status = 'published'
    LIMIT 1
");
$stmt->execute(['slug' => $slug]);
$artikel = $stmt->fetch();

if (!$artikel) {
    http_response_code(404);
    die('Artikel tidak ditemukan.');
}
?>

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <title><?= htmlspecialchars($artikel['judul']) ?></title>
    <link rel="stylesheet" href="../assets/style.css">
</head>
<body>
    <div class="container article-detail">
        <a href="index.php">โ† Kembali ke daftar artikel</a>
        <p class="article-meta">
            <?= htmlspecialchars($artikel['nama_kategori']) ?> ยท
            <?= htmlspecialchars($artikel['created_at']) ?>
        </p>
        <h1><?= htmlspecialchars($artikel['judul']) ?></h1>
        <p class="article-summary"><?= htmlspecialchars($artikel['ringkasan']) ?></p>

        <div class="article-body">
            <?= $artikel['isi'] ?>
        </div>
    </div>
</body>
</html>

Di halaman detail, isi artikel ditampilkan apa adanya karena kita memang menyimpan HTML dari editor WYSIWYG.

3. Menambahkan Style untuk Halaman Publik

Tambahkan ke assets/style.css:

.post-list {
    display: grid;
    gap: 18px;
}

.post-card h2 {
    margin: 8px 0 12px;
}

.post-card a {
    color: inherit;
    text-decoration: none;
}

.read-more {
    color: #2563eb;
    font-weight: 600;
}

.pagination {
    display: flex;
    gap: 8px;
    margin-top: 24px;
}

.pagination a {
    padding: 8px 12px;
    border-radius: 8px;
    background: white;
    color: #0f172a;
    text-decoration: none;
}

.pagination a.active {
    background: #2563eb;
    color: white;
}

.article-detail {
    max-width: 760px;
}

.article-meta,
.article-summary {
    color: #64748b;
}

.article-body {
    margin-top: 24px;
    line-height: 1.8;
}

4. Kenapa Pagination Penting?

Tanpa pagination, semua artikel akan dimuat sekaligus. Saat jumlah artikel masih 2 atau 3, itu tidak terasa. Tetapi saat artikel sudah 50 atau 100, halaman jadi:

  • lebih berat
  • lebih sulit dibaca
  • lebih susah diurutkan

Pagination memecah daftar panjang menjadi halaman kecil yang lebih nyaman untuk user dan lebih ringan untuk query database.

5. Tips Agar Blog Lebih Rapi

Setelah versi dasar ini selesai, kamu bisa menambah:

  1. filter artikel per kategori
  2. halaman pencarian
  3. related posts
  4. slug otomatis yang dijaga unik
  5. halaman admin edit dan delete

Kalau semua itu mulai terasa banyak, itu pertanda bagus: kamu sedang mendekati alasan kenapa framework seperti Laravel sangat membantu.

Error Umum

Halaman detail selalu 404

Biasanya:

  • slug di URL salah
  • artikel masih draft
  • query WHERE tidak cocok dengan data database

Nomor halaman tampil, tetapi artikel kosong

Cek rumus offset:

<?php
$offset = ($page - 1) * $limit;

HTML artikel berantakan

Kalau admin menempel konten dari editor yang aneh, HTML yang tersimpan juga bisa berantakan. Untuk project besar, kamu biasanya perlu sanitasi HTML dengan library khusus.

Draft ikut muncul di halaman publik

Pastikan query list dan query detail sama-sama memakai:

WHERE artikel.status = 'published'

Evaluasi Akhir

Selamat, kamu sekarang sudah punya blog sederhana yang mencakup:

  • relasi kategori dan artikel
  • admin input konten
  • WYSIWYG editor
  • upload thumbnail
  • halaman publik
  • pagination
  • detail artikel berbasis slug

Ini bukan sekadar latihan CRUD kecil. Polanya sangat mirip dengan CMS mini versi awal.

Bacaan Terkait