Membangun CMS Sederhana dengan PHP & MySQL - Part 6: Upload Gambar Unggulan & Pagination
Selamat datang di Bagian 6 dari seri tutorial CMS PHP kita! Kita telah membuat kemajuan besar, mulai dari frontend (Part 1), sistem login (Part 2), manajemen artikel (Part 3), kategori (Part 4), hingga manajemen pengguna (Part 5). Sekarang, saatnya menambahkan beberapa fitur penting untuk meningkatkan fungsionalitas dan pengalaman pengguna: Upload Gambar Unggulan dan Pagination.
Dalam bagian ini, kita akan:
- Mengimplementasikan upload gambar unggulan saat menambah atau mengedit artikel.
- Menangani file upload menggunakan
$_FILES
di PHP. - Melakukan validasi dasar pada file yang diunggah (tipe, ukuran).
- Menyimpan path gambar ke database dan file gambar ke server.
- Menampilkan gambar unggulan di frontend.
- Menangani penggantian atau penghapusan gambar unggulan.
- Menangani file upload menggunakan
- Menerapkan pagination untuk daftar artikel di:
- Halaman utama frontend (
index.php
). - Halaman arsip kategori (
category.php
). - Halaman manajemen artikel di area admin (
admin/manage_posts.php
).
- Halaman utama frontend (
Fitur-fitur ini akan membuat CMS kita lebih profesional dan mampu menangani konten yang lebih banyak.
Prasyarat
Pastikan Anda telah mengikuti bagian-bagian sebelumnya. Kita akan memodifikasi formulir artikel dan halaman daftar. Anda juga perlu memastikan direktori uploads/
di root proyek Anda dapat ditulis oleh server web (permission 755
atau 777
untuk pengembangan, perhatikan keamanan di produksi).
Persiapan: Folder Upload
Pastikan Anda memiliki folder uploads/
di root direktori proyek Anda. Di dalam uploads/
, buat subfolder misalnya posts/
untuk menyimpan gambar-gambar unggulan artikel. Jadi path-nya akan menjadi uploads/posts/
.
Bagian A: Upload Gambar Unggulan
Kita akan memodifikasi form tambah dan edit artikel untuk menyertakan field input file gambar.
Langkah A1: Modifikasi Form Tambah Artikel (admin/add_post.php
)
-
Update Tag
<form>
: Tambahkanenctype="multipart/form-data"
ke tag<form>
. Ini penting untuk file upload.<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post" class="admin-form" enctype="multipart/form-data">
-
Tambahkan Field Input File: Di dalam form, sebelum field kategori atau status, tambahkan:
<!-- ... (field content sebelumnya) ... --> <div class="form-group"> <label for="featured_image">Gambar Unggulan (Opsional):</label> <input type="file" name="featured_image" id="featured_image" accept="image/jpeg, image/png, image/gif"> <small>Format yang diizinkan: JPG, PNG, GIF. Ukuran maks: 2MB.</small> </div> <!-- ... (field kategori/status berikutnya) ... -->
-
Proses Upload di PHP: Modifikasi bagian proses PHP di
admin/add_post.php
.<?php // admin/add_post.php (bagian proses PHP) // ... (variabel dan error yang sudah ada) ... $image_path_to_db = null; // Path gambar yang akan disimpan ke DB if ($_SERVER["REQUEST_METHOD"] == "POST") { // ... (ambil title, content, status, slug_custom, category_id_selected) ... // --- Proses Upload Gambar --- if (isset($_FILES['featured_image']) && $_FILES['featured_image']['error'] == UPLOAD_ERR_OK) { $file = $_FILES['featured_image']; $file_name = $file['name']; $file_tmp_name = $file['tmp_name']; $file_size = $file['size']; $file_error = $file['error']; $file_type = $file['type']; $file_ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION)); $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif']; if (in_array($file_ext, $allowed_extensions)) { if ($file_error === 0) { if ($file_size <= 2000000) { // Maks 2MB (2 * 1024 * 1024) // Buat nama file unik untuk menghindari penimpaan $new_file_name = uniqid('postimg_', true) . '.' . $file_ext; $file_destination = '../uploads/posts/' . $new_file_name; // Pastikan folder uploads/posts/ ada dan writable if (move_uploaded_file($file_tmp_name, $file_destination)) { $image_path_to_db = 'uploads/posts/' . $new_file_name; // Path relatif dari root proyek } else { $errors[] = "Gagal memindahkan file yang diunggah."; } } else { $errors[] = "Ukuran file terlalu besar (maks 2MB)."; } } else { $errors[] = "Terjadi error saat mengunggah file: " . $file_error; } } else { $errors[] = "Tipe file tidak diizinkan (hanya JPG, PNG, GIF)."; } } elseif (isset($_FILES['featured_image']) && $_FILES['featured_image']['error'] != UPLOAD_ERR_NO_FILE) { // Jika ada file diupload tapi ada error selain "no file" $errors[] = "Error saat mengunggah gambar: " . $_FILES['featured_image']['error']; } // --- Akhir Proses Upload Gambar --- // ... (validasi title, content, status, slug, kategori) ... // Pastikan validasi lain dijalankan SETELAH proses upload, // karena jika upload gagal, kita mungkin tidak mau melanjutkan. // Atau, jalankan validasi lain dulu, baru upload jika validasi lain lolos. // Untuk sekarang, kita proses upload dulu. if (empty($errors)) { // Hanya lanjut jika tidak ada error dari upload DAN validasi lain // Modifikasi SQL INSERT dan bind_param untuk menyertakan image_path $sql_insert = "INSERT INTO posts (title, content, slug, status, author_id, category_id, image_path) VALUES (?, ?, ?, ?, ?, ?, ?)"; if ($stmt = mysqli_prepare($conn, $sql_insert)) { // ... (author_id, category_id_to_db) ... // Sesuaikan tipe data untuk bind_param jika image_path ditambahkan // Jika image_path bisa NULL, dan $image_path_to_db adalah null, ini aman. mysqli_stmt_bind_param($stmt, "ssssiis", $title, $content, $slug, $status, $author_id, $category_id_to_db, $image_path_to_db); if (mysqli_stmt_execute($stmt)) { set_flash_message('post_action', "Artikel \"" . htmlspecialchars($title) . "\" berhasil ditambahkan!", "success"); header("Location: manage_posts.php"); exit; } else { $errors[] = "Gagal menyimpan artikel: " . mysqli_stmt_error($stmt); // Jika gagal simpan DB, mungkin hapus file yang sudah terupload? (Lebih advance) if ($image_path_to_db && file_exists('../' . $image_path_to_db)) { unlink('../' . $image_path_to_db); } } mysqli_stmt_close($stmt); } else { $errors[] = "Gagal mempersiapkan statement: " . mysqli_error($conn); if ($image_path_to_db && file_exists('../' . $image_path_to_db)) { unlink('../' . $image_path_to_db); } } } elseif ($image_path_to_db && file_exists('../' . $image_path_to_db)) { // Jika ada error validasi lain SETELAH gambar berhasil diupload, hapus gambar yang sudah terupload unlink('../' . $image_path_to_db); } } // ... ?>
Penjelasan Proses Upload:
$_FILES['featured_image']
: Superglobal PHP yang berisi informasi file yang diunggah.['error'] == UPLOAD_ERR_OK
: Memastikan tidak ada error dasar saat upload.UPLOAD_ERR_NO_FILE
berarti tidak ada file yang dipilih, ini oke karena gambar opsional.- Validasi Tipe File: Mengecek ekstensi file.
- Validasi Ukuran File.
uniqid('postimg_', true)
: Membuat nama file yang unik untuk mencegah konflik nama.move_uploaded_file()
: Memindahkan file dari direktori sementara PHP ke folderuploads/posts/
kita.$image_path_to_db
: Menyimpan path relatif gambar yang akan disimpan ke database.- Penting (Rollback Sederhana): Jika ada error validasi lain setelah gambar berhasil diunggah, atau jika penyimpanan ke DB gagal, kita mencoba menghapus file gambar yang sudah terlanjur diunggah menggunakan
unlink()
. Ini adalah bentuk rollback sederhana.
Langkah A2: Modifikasi Form Edit Artikel (admin/edit_post.php
)
-
Update Tag
<form>
: Tambahkanenctype="multipart/form-data"
. -
Tambahkan Field Input File dan Tampilkan Gambar Saat Ini:
<!-- ... (field content sebelumnya) ... --> <div class="form-group"> <label for="featured_image">Gambar Unggulan Baru (Opsional):</label> <input type="file" name="featured_image" id="featured_image" accept="image/jpeg, image/png, image/gif"> <small>Biarkan kosong jika tidak ingin mengubah gambar. Format: JPG, PNG, GIF. Maks: 2MB.</small> <?php if (!empty($post['image_path'])): // $post adalah data asli dari DB ?> <div class="current-image-preview"> <p>Gambar saat ini:</p> <img src="<?php echo site_url(htmlspecialchars($post['image_path'])); ?>" alt="Gambar Unggulan Saat Ini" style="max-width: 200px; height: auto; margin-top: 10px;"> <label style="display:block; margin-top:5px;"> <input type="checkbox" name="remove_current_image" value="1"> Hapus gambar saat ini </label> </div> <?php endif; ?> </div> <!-- ... (field kategori/status berikutnya) ... -->
-
Proses Upload dan Hapus Gambar di PHP: Modifikasi bagian proses PHP di
admin/edit_post.php
.<?php // admin/edit_post.php (bagian proses PHP) // ... (ambil data asli $post) ... $image_path_for_update = $post['image_path']; // Default ke gambar lama if ($_SERVER["REQUEST_METHOD"] == "POST") { // ... (ambil title_new, content_new, status_new, slug_new_custom, category_id_new_selected) ... $remove_current_image = isset($_POST['remove_current_image']) ? (int)$_POST['remove_current_image'] : 0; // --- Proses Hapus Gambar Lama jika dicentang ATAU ada gambar baru diupload --- if ($remove_current_image == 1 || (isset($_FILES['featured_image']) && $_FILES['featured_image']['error'] == UPLOAD_ERR_OK)) { if (!empty($post['image_path']) && file_exists('../' . $post['image_path'])) { unlink('../' . $post['image_path']); // Hapus file gambar lama } $image_path_for_update = null; // Set path jadi null, akan diisi jika ada upload baru } // --- Proses Upload Gambar Baru (jika ada) --- // Logika upload sama seperti di add_post.php if (isset($_FILES['featured_image']) && $_FILES['featured_image']['error'] == UPLOAD_ERR_OK) { $file = $_FILES['featured_image']; /* ... (validasi dan move_uploaded_file seperti di add_post.php) ... */ // Jika berhasil upload: // $image_path_for_update = 'uploads/posts/' . $new_file_name; // Jika gagal upload dan ada error, tambahkan ke $errors[] dan JANGAN set $image_path_for_update // Salin logika validasi & move_uploaded_file dari add_post.php ke sini // ... (Kode upload disalin dari add_post.php) ... // Jika upload baru berhasil, $new_uploaded_path akan berisi path baru. // Contoh setelah menyalin kode upload: $new_uploaded_path = null; if (in_array($file_ext, $allowed_extensions)) { /* ... */ if (move_uploaded_file($file_tmp_name, $file_destination)) { $new_uploaded_path = 'uploads/posts/' . $new_file_name; } /* ... else error ... */ } /* ... else error ... */ if ($new_uploaded_path) { $image_path_for_update = $new_uploaded_path; } else if (!empty($errors)) { // Jika upload gagal dan menghasilkan error // Jangan update image_path, biarkan nilai $image_path_for_update sebelumnya (bisa jadi null jika remove dicentang) // Error sudah ditambahkan ke $errors[] } } // --- Akhir Proses Upload Gambar Baru --- // ... (validasi title, content, status, slug, kategori) ... if (empty($errors)) { // Modifikasi SQL UPDATE dan bind_param untuk menyertakan image_path $sql_update = "UPDATE posts SET title = ?, content = ?, slug = ?, status = ?, category_id = ?, image_path = ?, updated_at = NOW() WHERE id = ?"; if ($stmt_update = mysqli_prepare($conn, $sql_update)) { // ... (category_id_to_db_update) ... mysqli_stmt_bind_param($stmt_update, "ssssisi", $title_new, $content_new, $final_slug, $status_new, $category_id_to_db_update, $image_path_for_update, $post_id); if (mysqli_stmt_execute($stmt_update)) { set_flash_message('post_action', "Artikel \"" . htmlspecialchars($title_new) . "\" berhasil diperbarui!", "success"); header("Location: manage_posts.php"); exit; } else { $errors[] = "Gagal memperbarui artikel: " . mysqli_stmt_error($stmt_update); // Rollback upload baru jika DB update gagal if ($new_uploaded_path && file_exists('../' . $new_uploaded_path)) { unlink('../' . $new_uploaded_path); } } mysqli_stmt_close($stmt_update); } else { $errors[] = "Gagal mempersiapkan statement UPDATE: " . mysqli_error($conn); if ($new_uploaded_path && file_exists('../' . $new_uploaded_path)) { unlink('../' . $new_uploaded_path); } } } elseif ($new_uploaded_path && file_exists('../' . $new_uploaded_path) && !$remove_current_image) { // Jika ada error validasi lain SETELAH gambar baru berhasil diupload (dan tidak ada perintah remove gambar lama), hapus gambar BARU yang sudah terupload unlink('../' . $new_uploaded_path); } } // ... ?>
Penjelasan Edit:
- Menampilkan preview gambar yang ada dan checkbox untuk menghapusnya.
- Jika checkbox “Hapus gambar saat ini” dicentang ATAU ada file baru yang diunggah, file gambar lama di server akan dihapus (
unlink()
) dan$image_path_for_update
diatur kenull
(atau path gambar baru jika ada upload baru). - Logika upload gambar baru sama dengan di
add_post.php
. - Nilai
$image_path_for_update
(bisa path baru, path lama, ataunull
) yang akan disimpan ke database.
Langkah A3: Menghapus Gambar Saat Artikel Dihapus (admin/delete_post.php
)
Saat artikel dihapus, kita juga harus menghapus file gambar unggulannya dari server.
Modifikasi admin/delete_post.php
:
<?php
// admin/delete_post.php
// ... (require_once) ...
if ($post_id > 0) {
// Ambil path gambar dan judul sebelum menghapus dari DB
$image_path_to_delete = null;
$title_to_delete = "Artikel";
$sql_get_info = "SELECT title, image_path FROM posts WHERE id = ?";
if($stmt_get_info = mysqli_prepare($conn, $sql_get_info)) {
mysqli_stmt_bind_param($stmt_get_info, "i", $post_id);
mysqli_stmt_execute($stmt_get_info);
$result_info = mysqli_stmt_get_result($stmt_get_info);
if($row_info = mysqli_fetch_assoc($result_info)) {
$title_to_delete = $row_info['title'];
$image_path_to_delete = $row_info['image_path'];
} else {
set_flash_message('post_action', "Gagal menghapus: Artikel dengan ID $post_id tidak ditemukan.", "error");
header("Location: manage_posts.php");
exit;
}
mysqli_stmt_close($stmt_get_info);
}
// Query untuk menghapus artikel
$sql_delete = "DELETE FROM posts WHERE id = ?";
if ($stmt_delete = mysqli_prepare($conn, $sql_delete)) {
mysqli_stmt_bind_param($stmt_delete, "i", $post_id);
if (mysqli_stmt_execute($stmt_delete)) {
if (mysqli_stmt_affected_rows($stmt_delete) > 0) {
// Hapus file gambar jika ada
if (!empty($image_path_to_delete) && file_exists('../' . $image_path_to_delete)) {
unlink('../' . $image_path_to_delete);
}
set_flash_message('post_action', "Artikel \"" . htmlspecialchars($title_to_delete) . "\" berhasil dihapus.", "success");
} else { /* ... (error message) ... */ }
} else { /* ... (error message) ... */ }
mysqli_stmt_close($stmt_delete);
} else { /* ... (error message) ... */ }
} else { /* ... (error message) ... */ }
header("Location: manage_posts.php");
exit;
?>
Perubahan: Mengambil image_path
dari database sebelum menghapus record, lalu jika path ada dan file ada di server, panggil unlink()
untuk menghapus file tersebut.
Bagian B: Pagination
Pagination membagi daftar item yang panjang menjadi beberapa halaman.
Langkah B1: Pagination di Frontend (index.php
dan category.php
)
Kita akan membuat fungsi helper untuk pagination.
1. Tambahkan fungsi pagination di includes/functions.php
:
<?php
// includes/functions.php (Tambahkan fungsi baru ini)
// ... (fungsi generate_excerpt dan flash message yang sudah ada) ...
/**
* Membuat link pagination.
*
* @param int $current_page Halaman saat ini.
* @param int $total_pages Jumlah total halaman.
* @param string $base_url URL dasar untuk link pagination (misal, 'index.php?page=').
* @param int $links_to_show Jumlah link halaman yang ditampilkan di sekitar halaman saat ini.
* @return string HTML untuk navigasi pagination.
*/
function generate_pagination($current_page, $total_pages, $base_url = 'index.php', $query_params = []) {
if ($total_pages <= 1) {
return ''; // Tidak perlu pagination jika hanya 1 halaman atau kurang
}
$output = '<nav class="pagination-nav"><ul class="pagination">';
$links_to_show = 2; // Jumlah link sebelum dan sesudah halaman saat ini
// Tombol Sebelumnya
if ($current_page > 1) {
$prev_page = $current_page - 1;
$query_params['page'] = $prev_page;
$output .= '<li class="page-item"><a class="page-link" href="' . $base_url . '?' . http_build_query($query_params) . '">« Sebelumnya</a></li>';
} else {
$output .= '<li class="page-item disabled"><span class="page-link">« Sebelumnya</span></li>';
}
// Link halaman angka
for ($i = 1; $i <= $total_pages; $i++) {
if ($i == 1 || $i == $total_pages || ($i >= $current_page - $links_to_show && $i <= $current_page + $links_to_show)) {
if ($i == $current_page) {
$output .= '<li class="page-item active"><span class="page-link">' . $i . '</span></li>';
} else {
$query_params['page'] = $i;
$output .= '<li class="page-item"><a class="page-link" href="' . $base_url . '?' . http_build_query($query_params) . '">' . $i . '</a></li>';
}
} elseif (strpos($output, '<li class="page-item ellipsis">') === false && ($i == $current_page - $links_to_show - 1 || $i == $current_page + $links_to_show + 1) ) {
// Tambahkan elipsis hanya sekali per sisi
if (($i == $current_page - $links_to_show - 1 && $current_page - $links_to_show > 2) ||
($i == $current_page + $links_to_show + 1 && $current_page + $links_to_show < $total_pages - 1)) {
$output .= '<li class="page-item ellipsis"><span class="page-link">...</span></li>';
}
}
}
// Tombol Berikutnya
if ($current_page < $total_pages) {
$next_page = $current_page + 1;
$query_params['page'] = $next_page;
$output .= '<li class="page-item"><a class="page-link" href="' . $base_url . '?' . http_build_query($query_params) . '">Berikutnya »</a></li>';
} else {
$output .= '<li class="page-item disabled"><span class="page-link">Berikutnya »</span></li>';
}
$output .= '</ul></nav>';
return $output;
}
?>
2. Tambahkan CSS untuk Pagination di css/style.css
:
/* css/style.css (Tambahkan) */
/* ... (CSS sebelumnya) ... */
.pagination-nav {
margin-top: 30px;
text-align: center;
}
.pagination {
display: inline-flex; /* Menggunakan inline-flex agar bisa ditengahkan */
list-style: none;
padding: 0;
margin: 0;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.pagination .page-item {
margin: 0;
}
.pagination .page-link {
display: block;
padding: 10px 15px;
color: #007bff;
text-decoration: none;
background-color: #fff;
border: 1px solid #dee2e6;
border-left-width: 0; /* Hindari double border */
transition: background-color 0.2s ease, color 0.2s ease;
}
.pagination .page-item:first-child .page-link {
border-left-width: 1px;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.pagination .page-item:last-child .page-link {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.pagination .page-item.active .page-link {
z-index: 1;
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.pagination .page-item.disabled .page-link {
color: #6c757d;
pointer-events: none;
background-color: #e9ecef;
border-color: #dee2e6;
}
.pagination .page-item:not(.disabled) .page-link:hover {
background-color: #e9ecef;
color: #0056b3;
}
.pagination .page-item.ellipsis .page-link {
color: #6c757d;
background-color: #fff;
border-color: #dee2e6;
pointer-events: none;
}
3. Implementasi di index.php
:
<?php
// index.php (Modifikasi bagian logika dan loop)
// ... (require_once, $page_title, $page_description) ...
// require_once 'includes/header.php'; // Pindah setelah logika pagination
// --- Logika Pagination ---
$posts_per_page = 3; // Jumlah artikel per halaman
$current_page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($current_page < 1) $current_page = 1;
// Hitung total artikel yang published untuk pagination
$sql_total_posts = "SELECT COUNT(id) as total FROM posts WHERE status = 'published'";
$result_total = mysqli_query($conn, $sql_total_posts);
$total_rows = ($result_total) ? mysqli_fetch_assoc($result_total)['total'] : 0;
$total_pages = ceil($total_rows / $posts_per_page);
// Pastikan current_page tidak melebihi total_pages
if ($current_page > $total_pages && $total_pages > 0) $current_page = $total_pages;
$offset = ($current_page - 1) * $posts_per_page;
// --- Akhir Logika Pagination ---
// Sertakan file header HTML (Setelah $page_title, dll. diset)
require_once 'includes/header.php';
// Query untuk mengambil artikel dengan LIMIT dan OFFSET
$sql = "SELECT p.id, p.title, p.content, p.slug, p.created_at, p.image_path,
c.name as category_name, c.slug as category_slug,
u.username as author_name
FROM posts p
LEFT JOIN categories c ON p.category_id = c.id
LEFT JOIN users u ON p.author_id = u.id
WHERE p.status = 'published'
ORDER BY p.created_at DESC
LIMIT ? OFFSET ?"; // Gunakan placeholder untuk prepared statement
$posts_data = []; // Array untuk menyimpan hasil
if ($stmt = mysqli_prepare($conn, $sql)) {
mysqli_stmt_bind_param($stmt, "ii", $posts_per_page, $offset);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
while($row = mysqli_fetch_assoc($result)){
$posts_data[] = $row;
}
mysqli_stmt_close($stmt);
} else {
// Handle error jika prepare statement gagal
echo "<p class='error-message'>Terjadi kesalahan saat mempersiapkan data artikel: " . mysqli_error($conn) . "</p>";
error_log("Error prepare di index.php: " . mysqli_error($conn), 0);
}
if (!empty($posts_data)) {
?>
<section class="post-listing-section">
<h2 class="section-title">Artikel Terbaru</h2>
<div class="post-grid">
<?php foreach ($posts_data as $post): ?>
<!-- ... (HTML untuk post-item tetap sama) ... -->
<?php endforeach; ?>
</div>
<?php
// Tampilkan pagination
echo generate_pagination($current_page, $total_pages, 'index.php');
?>
</section>
<?php
} else if ($total_rows == 0) { // Jika tidak ada artikel sama sekali
echo "<p class='no-posts'>Belum ada artikel yang dipublikasikan saat ini.</p>";
} else { // Jika ada artikel tapi halaman saat ini kosong (misal, page > total_pages)
echo "<p class='no-posts'>Tidak ada artikel di halaman ini.</p>";
echo generate_pagination($current_page, $total_pages, 'index.php');
}
// ... (require_once 'includes/footer.php'; dan mysqli_close($conn);) ...
?>
4. Implementasi di category.php
(Mirip dengan index.php
):
Modifikasi category.php
untuk menghitung total artikel dalam kategori tersebut dan menerapkan LIMIT
serta OFFSET
pada query pengambilan artikel. Kemudian panggil generate_pagination()
dengan base_url
yang menyertakan slug kategori.
<?php
// category.php (Modifikasi)
// ... (require_once, ambil $category_slug) ...
$posts_per_page_cat = 3; // Atau ambil dari setting global
$current_page_cat = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($current_page_cat < 1) $current_page_cat = 1;
$total_posts_in_category = 0;
$total_pages_cat = 0;
if (!empty($category_slug) && $cat_info) { // $cat_info dari query info kategori
$category_id = $cat_info['id'];
// Hitung total artikel dalam kategori ini
$sql_total_cat_posts = "SELECT COUNT(id) as total FROM posts WHERE category_id = ? AND status = 'published'";
if($stmt_total_cat = mysqli_prepare($conn, $sql_total_cat_posts)){
mysqli_stmt_bind_param($stmt_total_cat, "i", $category_id);
mysqli_stmt_execute($stmt_total_cat);
$result_total_cat = mysqli_stmt_get_result($stmt_total_cat);
$total_posts_in_category = ($result_total_cat) ? mysqli_fetch_assoc($result_total_cat)['total'] : 0;
$total_pages_cat = ceil($total_posts_in_category / $posts_per_page_cat);
mysqli_stmt_close($stmt_total_cat);
}
if ($current_page_cat > $total_pages_cat && $total_pages_cat > 0) $current_page_cat = $total_pages_cat;
$offset_cat = ($current_page_cat - 1) * $posts_per_page_cat;
// Modifikasi query untuk mengambil artikel dengan LIMIT dan OFFSET
$sql_posts = "SELECT p.id, p.title, p.content, p.slug, p.created_at, p.image_path
FROM posts p
WHERE p.category_id = ? AND p.status = 'published'
ORDER BY p.created_at DESC
LIMIT ? OFFSET ?";
if($stmt_posts = mysqli_prepare($conn, $sql_posts)) {
mysqli_stmt_bind_param($stmt_posts, "iii", $category_id, $posts_per_page_cat, $offset_cat);
mysqli_stmt_execute($stmt_posts);
$result_posts = mysqli_stmt_get_result($stmt_posts);
while($row_post = mysqli_fetch_assoc($result_posts)){
$posts_in_category[] = $row_post;
}
mysqli_stmt_close($stmt_posts);
}
}
// ... (set $page_title, $page_description) ...
require_once 'includes/header.php';
// ... (HTML untuk menampilkan header kategori) ...
if (!empty($posts_in_category)) {
?>
<section class="post-listing-section">
<h2 class="section-title">Artikel dalam Kategori "<?php echo htmlspecialchars($cat_info['name']); ?>"</h2>
<div class="post-grid">
<?php foreach ($posts_in_category as $post): ?>
<!-- ... (HTML untuk post-item tetap sama) ... -->
<?php endforeach; ?>
</div>
<?php
// Tampilkan pagination
echo generate_pagination($current_page_cat, $total_pages_cat, 'category.php', ['slug' => $category_slug]);
?>
</section>
<?php
} else if ($total_posts_in_category == 0) {
echo "<p class='no-posts'>Belum ada artikel dalam kategori ini.</p>";
} else {
echo "<p class='no-posts'>Tidak ada artikel di halaman ini.</p>";
echo generate_pagination($current_page_cat, $total_pages_cat, 'category.php', ['slug' => $category_slug]);
}
// ... (require_once 'includes/footer.php'; dan mysqli_close($conn);) ...
?>
Langkah B2: Pagination di Area Admin (admin/manage_posts.php
)
Logikanya sama, terapkan pada tabel daftar artikel di admin.
<?php
// admin/manage_posts.php (Modifikasi)
// ... (require_once, $page_title_admin) ...
// --- Logika Pagination Admin ---
$posts_per_page_admin = 10; // Jumlah artikel per halaman di admin
$current_page_admin = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($current_page_admin < 1) $current_page_admin = 1;
// Hitung total semua artikel (termasuk draft)
$sql_total_all_posts = "SELECT COUNT(id) as total FROM posts";
$result_total_all = mysqli_query($conn, $sql_total_all_posts);
$total_all_rows = ($result_total_all) ? mysqli_fetch_assoc($result_total_all)['total'] : 0;
$total_pages_admin = ceil($total_all_rows / $posts_per_page_admin);
if ($current_page_admin > $total_pages_admin && $total_pages_admin > 0) $current_page_admin = $total_pages_admin;
$offset_admin = ($current_page_admin - 1) * $posts_per_page_admin;
// --- Akhir Logika Pagination Admin ---
// ... (HTML header, nav, judul, flash message, tombol tambah) ...
?>
<table class="admin-table">
<!-- ... (thead) ... -->
<tbody>
<?php
// Query untuk mengambil artikel dengan LIMIT dan OFFSET
$sql = "SELECT id, title, slug, status, created_at FROM posts ORDER BY created_at DESC LIMIT ? OFFSET ?";
$posts_admin_data = [];
if($stmt = mysqli_prepare($conn, $sql)){
mysqli_stmt_bind_param($stmt, "ii", $posts_per_page_admin, $offset_admin);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
while($row = mysqli_fetch_assoc($result)){
$posts_admin_data[] = $row;
}
mysqli_stmt_close($stmt);
}
if (!empty($posts_admin_data)) {
foreach ($posts_admin_data as $post) {
// ... (echo <tr> dan <td> seperti sebelumnya) ...
}
} else {
echo "<tr><td colspan='6' style='text-align:center;'>Belum ada artikel.</td></tr>";
}
?>
</tbody>
</table>
<?php
// Tampilkan pagination
if ($total_all_rows > 0) {
echo generate_pagination($current_page_admin, $total_pages_admin, 'manage_posts.php');
}
?>
</div>
<?php // ... (HTML footer dan mysqli_close) ... ?>
Menguji Fitur Baru
- Upload Gambar:
- Buka
admin/add_post.php
. Coba unggah gambar dengan tipe dan ukuran yang valid dan tidak valid. - Pastikan gambar tersimpan di folder
uploads/posts/
dan path-nya tersimpan di database. - Buka
admin/edit_post.php
. Gambar saat ini seharusnya muncul. Coba ganti gambar atau hapus gambar. - Lihat frontend (
index.php
dansingle.php
), gambar unggulan seharusnya tampil. - Coba hapus artikel yang memiliki gambar unggulan. File gambar di server juga seharusnya terhapus.
- Buka
- Pagination:
- Pastikan Anda memiliki cukup banyak artikel dummy (lebih dari
posts_per_page
yang Anda set). - Buka
index.php
. Anda seharusnya melihat link pagination jika total artikel melebihi batas per halaman. Klik link halaman untuk bernavigasi. - Lakukan hal yang sama untuk
category.php
(jika ada kategori dengan banyak artikel) danadmin/manage_posts.php
. - Pastikan parameter
page
di URL berfungsi dengan benar.
- Pastikan Anda memiliki cukup banyak artikel dummy (lebih dari
Refleksi Part 6 dan Apa Selanjutnya?
Selamat! Dengan fitur upload gambar dan pagination, CMS Anda kini jauh lebih fungsional dan siap menangani konten yang lebih kaya serta banyak. Pengalaman pengguna, baik di frontend maupun backend, juga meningkat.
Poin Kunci yang Telah Dicapai:
- Implementasi upload file gambar unggulan dengan validasi dasar.
- Penanganan penyimpanan file di server dan path di database.
- Kemampuan untuk mengganti dan menghapus gambar unggulan.
- Pembuatan fungsi pagination yang reusable.
- Penerapan pagination di halaman daftar artikel frontend dan admin.
Apa Selanjutnya? CMS dasar kita sudah memiliki banyak fitur inti. Beberapa ide untuk pengembangan lebih lanjut atau seri berikutnya bisa meliputi:
- Editor WYSIWYG: Mengganti
<textarea>
dengan editor teks kaya seperti TinyMCE atau CKEditor. - Sistem Komentar: Memungkinkan pengunjung berinteraksi dengan artikel.
- Fitur Pencarian: Fungsi pencarian artikel di frontend.
- Pengaturan Situs Dasar: Halaman admin untuk mengubah judul situs, deskripsi, item per halaman, dll.
- Keamanan Lanjutan: Perlindungan XSS yang lebih kuat (misalnya dengan HTML Purifier), CSRF token, rate limiting.
- Optimasi & Caching: Untuk performa yang lebih baik.
- REST API: Jika ingin membuat frontend terpisah (misalnya dengan JavaScript framework).
- User Profile: Halaman bagi user admin untuk mengubah profil dan password mereka sendiri.
Untuk seri tutorial dasar ini, kita mungkin bisa menutup dengan Part 7: Penyempurnaan Akhir, Editor WYSIWYG, dan Kesimpulan. Ini akan memberikan sentuhan akhir yang bagus.
Terima kasih telah mengikuti sejauh ini. Teruslah bereksperimen dan belajar!