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.
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
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:
- membuat kategori
- menulis artikel
- menambahkan thumbnail
- 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 →