Project: Blog Sederhana dengan PHP Native (Bagian 2)

Di bagian pertama kita sudah menyiapkan database dan dashboard dasar. Sekarang saatnya membuat inti dari blog admin panel:

  • CRUD kategori
  • form tambah/edit artikel
  • status draft dan published
  • editor WYSIWYG agar penulisan artikel lebih nyaman

1. Kelola Kategori

Buat file admin/kategori.php:

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

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $namaKategori = trim($_POST['nama_kategori'] ?? '');
    $slug = slugify($namaKategori);

    if ($namaKategori !== '') {
        $stmt = $pdo->prepare("
            INSERT INTO kategori (nama_kategori, slug)
            VALUES (:nama_kategori, :slug)
        ");
        $stmt->execute([
            'nama_kategori' => $namaKategori,
            'slug' => $slug,
        ]);
    }

    header('Location: kategori.php');
    exit;
}

$kategori = $pdo->query("SELECT * FROM kategori ORDER BY nama_kategori ASC")->fetchAll();
?>

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <title>Kelola Kategori</title>
    <link rel="stylesheet" href="../assets/style.css">
</head>
<body>
    <div class="container">
        <h1>Kelola Kategori</h1>

        <form method="POST" class="card form-card">
            <label>Nama Kategori</label>
            <input type="text" name="nama_kategori" required>
            <button type="submit" class="btn">Simpan Kategori</button>
        </form>

        <div class="card">
            <table>
                <thead>
                    <tr>
                        <th>Nama</th>
                        <th>Slug</th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($kategori as $item): ?>
                        <tr>
                            <td><?= htmlspecialchars($item['nama_kategori']) ?></td>
                            <td><?= htmlspecialchars($item['slug']) ?></td>
                        </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
        </div>
    </div>
</body>
</html>

Di tahap ini, kita baru membuat fitur create + read. Delete bisa ditambahkan nanti lewat tombol POST agar lebih aman.

2. Membuat Form Artikel

Buat file admin/artikel-form.php:

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

$kategori = $pdo->query("SELECT id, nama_kategori FROM kategori ORDER BY nama_kategori ASC")->fetchAll();
?>

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <title>Artikel Baru</title>
    <link rel="stylesheet" href="../assets/style.css">
</head>
<body>
    <div class="container">
        <h1>Tulis Artikel Baru</h1>

        <form method="POST" action="proses-artikel.php" enctype="multipart/form-data" class="card form-card">
            <label>Judul</label>
            <input type="text" name="judul" required>

            <label>Kategori</label>
            <select name="kategori_id" required>
                <option value="">Pilih kategori</option>
                <?php foreach ($kategori as $item): ?>
                    <option value="<?= $item['id'] ?>">
                        <?= htmlspecialchars($item['nama_kategori']) ?>
                    </option>
                <?php endforeach; ?>
            </select>

            <label>Ringkasan</label>
            <textarea name="ringkasan" rows="3" required></textarea>

            <label>Thumbnail</label>
            <input type="file" name="thumbnail" accept=".jpg,.jpeg,.png">

            <label>Isi Artikel</label>
            <textarea name="isi" id="isi" rows="12" required></textarea>

            <label>Status</label>
            <select name="status" required>
                <option value="draft">Draft</option>
                <option value="published">Published</option>
            </select>

            <button type="submit" class="btn">Simpan Artikel</button>
        </form>
    </div>

    <script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js"></script>
    <script>
        tinymce.init({
            selector: '#isi',
            menubar: false,
            plugins: 'lists link code table',
            toolbar: 'undo redo | bold italic | bullist numlist | link | code',
            height: 360
        });
    </script>
</body>
</html>

Di sini kita mulai memakai WYSIWYG editor. User tidak perlu menulis HTML manual untuk paragraf, list, atau link.

TIP

Untuk Belajar, CDN Sudah Cukupno-api-key cukup untuk latihan local. Untuk production, baca dokumentasi resmi provider editor yang kamu pilih.

3. Memproses Simpan Artikel

Buat file admin/proses-artikel.php:

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

$judul = trim($_POST['judul'] ?? '');
$kategoriId = (int) ($_POST['kategori_id'] ?? 0);
$ringkasan = trim($_POST['ringkasan'] ?? '');
$isi = $_POST['isi'] ?? '';
$status = $_POST['status'] ?? 'draft';

if ($judul === '' || $kategoriId === 0 || $ringkasan === '' || trim(strip_tags($isi)) === '') {
    die('Semua field wajib diisi.');
}

$slug = slugify($judul);
$thumbnail = null;

if (!empty($_FILES['thumbnail']['name'])) {
    $file = $_FILES['thumbnail'];
    $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    $allowed = ['jpg', 'jpeg', 'png'];

    if (!in_array($ext, $allowed, true)) {
        die('Thumbnail harus berupa JPG, JPEG, atau PNG.');
    }

    $thumbnail = uniqid('thumb-', true) . '.' . $ext;
    $target = __DIR__ . '/../uploads/' . $thumbnail;

    if (!move_uploaded_file($file['tmp_name'], $target)) {
        die('Gagal upload thumbnail.');
    }
}

$stmt = $pdo->prepare("
    INSERT INTO artikel (kategori_id, judul, slug, ringkasan, isi, thumbnail, status)
    VALUES (:kategori_id, :judul, :slug, :ringkasan, :isi, :thumbnail, :status)
");

$stmt->execute([
    'kategori_id' => $kategoriId,
    'judul' => $judul,
    'slug' => $slug,
    'ringkasan' => $ringkasan,
    'isi' => $isi,
    'thumbnail' => $thumbnail,
    'status' => in_array($status, ['draft', 'published'], true) ? $status : 'draft',
]);

header('Location: index.php');
exit;

Poin penting di sini:

  • slug dibuat dari judul
  • isi artikel dari editor tetap dikirim lewat POST
  • thumbnail dipindahkan ke folder uploads/
  • status artikel dibatasi hanya draft atau published

4. Menambahkan Style untuk Form

Tambahkan style ini ke assets/style.css:

.card {
    background: white;
    border-radius: 12px;
    padding: 20px;
    box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
}

.form-card {
    display: grid;
    gap: 12px;
    margin-bottom: 24px;
}

input,
select,
textarea {
    width: 100%;
    padding: 12px;
    border: 1px solid #cbd5e1;
    border-radius: 8px;
    font: inherit;
}

5. Konsep Draft vs Published

Kenapa kita butuh dua status?

  • draft: artikel belum siap tampil ke publik
  • published: artikel sudah boleh muncul di halaman blog

Dengan pola ini, admin bisa menulis dan menyimpan artikel sedikit demi sedikit tanpa langsung menayangkannya.

6. Arah Pengembangan Berikutnya

Sampai titik ini, admin sudah bisa:

  1. membuat kategori
  2. menulis artikel
  3. menambahkan thumbnail
  4. memilih status publish

Yang belum ada:

  • tombol edit artikel
  • tombol hapus artikel
  • halaman publik untuk pembaca
  • pagination dan detail per artikel

Itulah yang akan kita selesaikan di bagian berikutnya.

Error Umum

Duplicate entry ... for key 'slug'

Dua artikel memiliki judul yang menghasilkan slug yang sama. Solusinya:

  • tambahkan pengecekan slug sebelum insert
  • atau tambahkan angka di belakang slug jika sudah ada

Isi artikel kosong padahal editor sudah muncul

Biasanya field textarea tidak punya atribut name="isi" atau form tidak benar-benar mengirim data editor.

Upload thumbnail selalu gagal

Pastikan folder uploads/ benar-benar ada dan writable oleh PHP.

HTML dari editor tampil mentah di dashboard

Itu normal selama kamu memang menyimpan HTML dari WYSIWYG. Nanti di halaman publik, HTML itu ditampilkan sebagai konten artikel.

Bacaan Terkait

Lanjut ke Blog Sederhana Bagian 3 →