CMS dengan PHP Part 1: Menampilkan Daftar Artikel & Detail Artikel (Frontend Dasar Mendalam)

CMS dengan PHP Part 1: Menampilkan Daftar Artikel & Detail Artikel (Frontend Dasar Mendalam)

CMS dengan PHP Part 1: Menampilkan Daftar Artikel & Detail Artikel (Frontend Dasar Mendalam)

Novian Hidayat
2025-05-01

Mulai perjalanan membangun CMS sederhana Anda! Di bagian pertama ini, kita akan fokus membuat tampilan depan (frontend) untuk menampilkan daftar artikel dari database MySQL dan halaman detail untuk setiap artikel menggunakan PHP, dengan penjelasan mendalam setiap langkahnya.

Membangun CMS Sederhana dengan PHP & MySQL - Part 1: Menampilkan Daftar Artikel & Detail Artikel (Frontend Dasar Mendalam)

Selamat datang di seri tutorial “Membangun CMS Sederhana dari Nol dengan PHP & MySQL”! Dalam seri yang komprehensif ini, kita akan membedah proses pembuatan sistem manajemen konten (CMS) fungsional langkah demi langkah. Tujuan utama kita bukan hanya membuat aplikasi, tetapi juga memahami konsep-konsep inti di baliknya, mulai dari interaksi database, struktur aplikasi, hingga praktik pengembangan web dasar.

Di bagian pertama yang mendalam ini, kita akan meletakkan fondasi dengan membangun tampilan depan (frontend). Kita akan membuat halaman dinamis yang menampilkan daftar artikel langsung dari database MySQL dan halaman individual untuk membaca setiap artikel secara lengkap, semuanya ditenagai oleh PHP.

Prasyarat & Lingkungan Pengembangan

Sebelum kita menyelam ke dalam kode, pastikan lingkungan dan pengetahuan dasar Anda sudah siap:

  1. Lingkungan Pengembangan Web Lokal:
    • XAMPP, MAMP, WAMP, atau sejenisnya: Ini adalah paket perangkat lunak yang menyediakan server Apache (atau Nginx), PHP, dan MySQL. Pastikan sudah terinstal dan layanan Apache serta MySQL berjalan.
    • Akses ke phpMyAdmin: Alat berbasis web untuk mengelola database MySQL Anda, biasanya diakses melalui http://localhost/phpmyadmin.
  2. Pemahaman Dasar:
    • HTML & CSS: Struktur dasar halaman web dan styling visual. Kita tidak akan terlalu fokus pada desain yang rumit, tetapi pemahaman dasar sangat penting.
    • PHP: Sintaks dasar PHP, variabel, tipe data, struktur kontrol (if, else, while), array, fungsi, dan cara PHP berinteraksi dengan HTML.
    • MySQL: Konsep database relasional, tipe data SQL, dan query SQL dasar (SELECT, INSERT, UPDATE, DELETE).
  3. Teks Editor atau IDE:
    • Pilih editor yang nyaman bagi Anda, seperti VS Code (sangat direkomendasikan), Sublime Text, Atom, Notepad++, atau PHPStorm.

Persiapan Database dan Tabel posts

Inti dari setiap CMS adalah database tempat konten disimpan. Mari kita siapkan database dan tabel posts yang akan menyimpan artikel-artikel kita.

  1. Membuat Database:

    • Buka phpMyAdmin di browser Anda.
    • Klik “New” atau “Database” di panel kiri.
    • Masukkan nama database, misalnya cms_sederhana_db (gunakan nama yang konsisten).
    • Pilih collation utf8mb4_general_ci atau utf8mb4_unicode_ci untuk dukungan karakter internasional dan emoji yang baik. Klik “Create”.
  2. Membuat Tabel posts:

    • Setelah database dibuat, pilih database tersebut dari panel kiri.
    • Klik tab “SQL”.
    • Salin dan tempel query SQL berikut untuk membuat tabel posts, lalu klik “Go”:
    CREATE TABLE `posts` (
      `id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
      `title` VARCHAR(255) NOT NULL,
      `content` TEXT NOT NULL,
      `slug` VARCHAR(255) NOT NULL UNIQUE,
      `status` VARCHAR(20) NOT NULL DEFAULT 'published' COMMENT 'Possible values: published, draft, archived',
      `image_path` VARCHAR(255) NULL COMMENT 'Path to the featured image',
      `author_id` INT(11) UNSIGNED NULL COMMENT 'Foreign key to users table (akan ditambahkan nanti)',
      `category_id` INT(11) UNSIGNED NULL COMMENT 'Foreign key to categories table (akan ditambahkan nanti)',
      `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
      `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

    Penjelasan Struktur Tabel posts:

    • id: Primary Key, auto-increment, untuk identifikasi unik setiap post. UNSIGNED berarti hanya bilangan positif.
    • title: Judul artikel, tidak boleh kosong.
    • content: Isi artikel, menggunakan tipe TEXT untuk konten yang panjang.
    • slug: Versi judul yang ramah URL (misalnya, ini-judul-artikel), harus unik. Ini akan kita gunakan untuk URL artikel.
    • status: Status artikel (misalnya, ‘published’, ‘draft’). Defaultnya ‘published’. COMMENT membantu mendokumentasikan tabel.
    • image_path: (Baru) Path opsional untuk gambar unggulan artikel.
    • author_id, category_id: Kolom untuk foreign key ke tabel users dan categories yang akan kita buat di bagian selanjutnya. Untuk sekarang, bisa NULL.
    • created_at: Timestamp kapan artikel dibuat.
    • updated_at: Timestamp kapan artikel terakhir diubah, otomatis terupdate.
    • ENGINE=InnoDB: Mesin penyimpanan yang mendukung foreign key dan transaksi.
    • CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci: Pengaturan karakter yang baik.
  3. Mengisi Data Dummy (Contoh): Agar kita punya sesuatu untuk ditampilkan, mari masukkan beberapa data contoh melalui tab “Insert” di phpMyAdmin untuk tabel posts, atau jalankan query SQL:

    INSERT INTO `posts` (`title`, `content`, `slug`, `status`) VALUES
    ('Selamat Datang di Blog Saya!', 'Ini adalah artikel pertama di blog sederhana yang dibangun dengan PHP dan MySQL. Semoga Anda menikmati konten yang akan datang!', 'selamat-datang-blog-saya', 'published'),
    ('Tutorial PHP untuk Pemula', 'PHP adalah bahasa pemrograman sisi server yang populer. Di artikel ini, kita akan membahas dasar-dasar PHP yang perlu diketahui oleh pemula. Mulai dari variabel, tipe data, hingga struktur kontrol.', 'tutorial-php-pemula', 'published'),
    ('Mengapa Belajar Pemrograman Web?', 'Pemrograman web membuka banyak peluang. Dari membuat website pribadi hingga membangun aplikasi skala besar, kemungkinannya tak terbatas. Mari kita lihat beberapa alasan mengapa Anda harus mulai belajar hari ini.', 'mengapa-belajar-pemrograman-web', 'published'),
    ('Artikel Rahasia (Draft)', 'Ini adalah artikel yang masih dalam tahap penulisan dan belum siap untuk dipublikasikan.', 'artikel-rahasia-draft', 'draft');

    Perhatikan bahwa kita hanya mengisi kolom yang NOT NULL atau yang tidak memiliki DEFAULT. id, created_at, dan updated_at akan diisi otomatis.

Perencanaan Struktur Folder Proyek

Organisasi file yang baik akan memudahkan pengembangan dan pemeliharaan. Buat struktur folder berikut di dalam direktori server web lokal Anda (misalnya, xampp/htdocs/ atau mamp/www/):

cms_sederhana_project/  // Nama folder proyek utama Anda
├── index.php             // Halaman utama (daftar artikel)
├── single.php            // Halaman detail artikel
├── includes/
│   ├── db_connect.php    // Skrip koneksi database
│   ├── header.php        // Bagian header HTML yang reusable
│   └── footer.php        // Bagian footer HTML yang reusable
│   └── functions.php     // (Opsional untuk sekarang) Fungsi-fungsi bantuan
├── css/
│   └── style.css         // File CSS utama untuk styling
├── js/
│   └── script.js         // (Opsional untuk sekarang) File JavaScript
└── images/                 // Untuk menyimpan gambar statis (logo, dll.)
    └── (kosongkan dulu)
└── uploads/                // Untuk menyimpan gambar yang diunggah pengguna (akan digunakan nanti)
    └── (kosongkan dulu)

Langkah 1: Membuat Koneksi Database yang Aman (includes/db_connect.php)

Koneksi ke database adalah jantung dari aplikasi dinamis kita. Kita akan menggunakan ekstensi mysqli (MySQL Improved) yang merupakan cara modern untuk berinteraksi dengan MySQL di PHP.

Buat file includes/db_connect.php dan masukkan kode berikut:

<?php
// includes/db_connect.php

// Pengaturan Error Reporting untuk Pengembangan
// Aktifkan ini selama pengembangan untuk melihat semua error.
// Matikan atau set ke level yang lebih rendah di lingkungan produksi.
error_reporting(E_ALL);
ini_set('display_errors', 1);

// Definisikan konstanta untuk kredensial database
// Menggunakan konstanta membuat konfigurasi lebih mudah diubah di satu tempat.
define('DB_HOST', 'localhost');      // Biasanya 'localhost' atau '127.0.0.1'
define('DB_USER', 'root');         // Username database Anda (default XAMPP/MAMP adalah 'root')
define('DB_PASS', '');             // Password database Anda (default XAMPP/MAMP kosong)
define('DB_NAME', 'cms_sederhana_db'); // Nama database yang Anda buat

// Membuat koneksi ke database menggunakan mysqli
// Tanda @ di depan mysqli_connect() digunakan untuk menekan pesan error default PHP,
// karena kita akan menangani error secara manual.
$conn = @mysqli_connect(DB_HOST, DB_USER, DB_PASS, DB_NAME);

// Periksa koneksi dan tangani error jika gagal
if (!$conn) {
    // Jika koneksi gagal, hentikan skrip dan tampilkan pesan error yang lebih ramah
    // atau log error ke file di lingkungan produksi.
    // mysqli_connect_error() memberikan detail error koneksi.
    // mysqli_connect_errno() memberikan nomor error koneksi.
    error_log("Koneksi database gagal: " . mysqli_connect_error(), 0); // Log error ke server log
    die("Maaf, terjadi masalah saat menghubungkan ke database. Silakan coba lagi nanti.");
    // Untuk debugging, Anda bisa menampilkan error langsung:
    // die("ERROR: Tidak bisa terhubung. " . mysqli_connect_error() . " (Error No: " . mysqli_connect_errno() . ")");
}

// Set karakter set koneksi ke utf8mb4
// Ini penting untuk mendukung berbagai karakter, termasuk emoji.
if (!mysqli_set_charset($conn, "utf8mb4")) {
    error_log("Error loading character set utf8mb4: " . mysqli_error($conn), 0);
    // Untuk debugging:
    // printf("Error loading character set utf8mb4: %s\n", mysqli_error($conn));
}

// Opsional: Anda bisa uncomment baris ini saat pertama kali setup untuk memastikan koneksi berhasil.
// echo "Koneksi ke database '" . DB_NAME . "' berhasil.";

// Catatan: $conn adalah variabel koneksi yang akan kita gunakan di file lain.
// Tidak perlu menutup koneksi di sini karena file ini akan di-include.
// Koneksi biasanya ditutup di akhir skrip utama yang menggunakannya.
?>

Penjelasan Lebih Detail (db_connect.php):

  • Error Reporting: Sangat berguna selama pengembangan. Di produksi, error harus dicatat ke log, bukan ditampilkan ke pengguna.
  • Konstanta: define() membuat kode lebih mudah dibaca dan dikelola. Jika kredensial berubah, Anda hanya perlu mengubahnya di satu tempat.
  • @mysqli_connect(): Tanda @ (operator penekan error) mencegah PHP menampilkan pesan error teknis langsung ke browser jika koneksi gagal. Ini memberi kita kontrol untuk menampilkan pesan yang lebih ramah pengguna atau mencatat error secara internal.
  • Penanganan Error Koneksi: if (!$conn) memeriksa apakah koneksi berhasil. Jika tidak, mysqli_connect_error() memberikan deskripsi error dan mysqli_connect_errno() memberikan kode error. die() menghentikan eksekusi skrip.
  • mysqli_set_charset($conn, "utf8mb4"): Mengatur encoding karakter yang akan digunakan untuk transfer data antara PHP dan MySQL. utf8mb4 adalah pilihan yang baik untuk dukungan karakter internasional yang luas.
  • Variabel $conn akan menjadi objek koneksi kita yang akan digunakan di seluruh aplikasi saat kita perlu berinteraksi dengan database.

Untuk menjaga konsistensi dan menghindari pengulangan kode HTML, kita akan membuat file terpisah untuk bagian header dan footer.

File includes/header.php:

<?php
// includes/header.php

// Mendefinisikan URL dasar situs untuk path yang absolut (lebih baik untuk SEO dan konsistensi)
// Pastikan path ini sesuai dengan lokasi proyek Anda di server lokal.
// Sesuaikan 'cms_sederhana_project' jika nama folder Anda berbeda.
define('BASE_URL', 'http://' . $_SERVER['HTTP_HOST'] . '/cms_sederhana_project/');

// Fungsi sederhana untuk membuat URL absolut
function site_url($path = '') {
    return BASE_URL . ltrim($path, '/');
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="<?php echo isset($page_description) ? htmlspecialchars($page_description) : 'CMS Sederhana dibangun dengan PHP dan MySQL'; ?>">
    <title><?php echo isset($page_title) ? htmlspecialchars($page_title) . ' - CMS Saya' : 'CMS Sederhana Saya'; ?></title>
    <link rel="stylesheet" href="<?php echo site_url('css/style.css'); ?>?v=<?php echo time(); ?>">
    <!-- Tambahkan "?v=<timestamp>" untuk cache busting CSS selama pengembangan -->
    <!-- Anda juga bisa menambahkan favicon, font, atau stylesheet lain di sini -->
    <!-- Contoh: <link rel="icon" href="<?php echo site_url('images/favicon.ico'); ?>" type="image/x-icon"> -->
</head>
<body>
    <div class="page-wrapper"> <!-- Wrapper utama untuk seluruh halaman -->
        <header class="site-header">
            <div class="container">
                <div class="logo">
                    <!-- Ganti dengan logo Anda jika ada, atau biarkan teks -->
                    <h1><a href="<?php echo site_url(); ?>">CMS Keren</a></h1>
                </div>
                <nav class="main-navigation">
                    <ul>
                        <li><a href="<?php echo site_url(); ?>" class="<?php echo (basename($_SERVER['PHP_SELF']) == 'index.php') ? 'active' : ''; ?>">Beranda</a></li>
                        <li><a href="<?php echo site_url('tentang.php'); // Contoh halaman statis ?>" class="<?php echo (basename($_SERVER['PHP_SELF']) == 'tentang.php') ? 'active' : ''; ?>">Tentang</a></li>
                        <li><a href="<?php echo site_url('kontak.php'); // Contoh halaman statis ?>" class="<?php echo (basename($_SERVER['PHP_SELF']) == 'kontak.php') ? 'active' : ''; ?>">Kontak</a></li>
                        <!-- Link ke area admin akan ditambahkan nanti -->
                    </ul>
                </nav>
            </div>
        </header>

        <main class="site-main">
            <div class="container main-content-area">

Penjelasan Lebih Detail (header.php):

  • BASE_URL dan site_url(): Mendefinisikan URL dasar dan membuat fungsi helper untuk menghasilkan URL absolut. Ini sangat berguna agar link dan path ke aset (CSS, JS, gambar) tetap berfungsi dengan benar terlepas dari struktur URL atau jika proyek dipindahkan. $_SERVER['HTTP_HOST'] mengambil nama domain.
  • Meta Description: Menambahkan meta description untuk SEO, bisa di-set per halaman.
  • Cache Busting CSS: Menambahkan ?v=<?php echo time(); ?> ke link CSS memaksa browser untuk mengambil versi terbaru file CSS setiap kali halaman dimuat selama pengembangan. Untuk produksi, gunakan metode cache busting yang lebih baik (misalnya, hash file).
  • Struktur Semantik: Menggunakan tag <header>, <nav>, <main>.
  • Navigasi Aktif: Contoh sederhana untuk menandai link navigasi yang aktif berdasarkan nama file PHP saat ini (basename($_SERVER['PHP_SELF'])).

File includes/footer.php:

<?php
// includes/footer.php
?>
            </div> <!-- Penutup .main-content-area dari header.php -->
        </main> <!-- Penutup .site-main dari header.php -->

        <footer class="site-footer">
            <div class="container">
                <p>&copy; <?php echo date('Y'); ?> CMS Keren. Semua Hak Dilindungi.</p>
                <!-- Anda bisa menambahkan link sosial media atau informasi lain di sini -->
            </div>
        </footer>
    </div> <!-- Penutup .page-wrapper dari header.php -->

    <!-- File JavaScript bisa diletakkan di sini untuk performa yang lebih baik -->
    <!-- Contoh: <script src="<?php echo site_url('js/script.js'); ?>?v=<?php echo time(); ?>"></script> -->
</body>
</html>

Penjelasan Lebih Detail (footer.php):

  • Menutup tag-tag yang dibuka di header.php.
  • Menampilkan tahun hak cipta secara dinamis menggunakan date('Y').
  • Memberikan tempat untuk meletakkan link file JavaScript sebelum tag </body> (praktik baik untuk performa).

Langkah 3: Halaman Utama (index.php) - Menampilkan Daftar Artikel yang Dinamis

Halaman index.php akan menjadi etalase utama blog kita, menampilkan ringkasan dari artikel-artikel terbaru yang sudah dipublikasikan.

Buat file index.php di root direktori proyek Anda:

<?php
// index.php - Halaman utama untuk menampilkan daftar artikel

// 1. Sertakan file konfigurasi dan koneksi database
require_once 'includes/db_connect.php'; // Ini sudah termasuk error reporting dari file tsb
require_once 'includes/functions.php';  // (Kita akan buat file ini untuk fungsi bantuan)

// 2. Set variabel untuk metadata halaman (digunakan di header.php)
$page_title = "Selamat Datang di Blog Kami";
$page_description = "Kumpulan artikel terbaru tentang berbagai topik menarik.";

// 3. Sertakan file header HTML
require_once 'includes/header.php';

// 4. Logika untuk mengambil data artikel dari database
$posts_per_page = 5; // Jumlah artikel per halaman (untuk pagination nanti)
// Untuk sekarang, kita ambil semua artikel published
$sql = "SELECT id, title, content, slug, created_at, image_path 
        FROM posts 
        WHERE status = 'published' 
        ORDER BY created_at DESC";
        // LIMIT $posts_per_page; // (Tambahkan ini jika sudah implementasi pagination)

$result = mysqli_query($conn, $sql);

// Periksa apakah query berhasil dan ada hasilnya
if ($result) {
?>
    <section class="post-listing-section">
        <h2 class="section-title">Artikel Terbaru</h2>

        <?php if (mysqli_num_rows($result) > 0): ?>
            <div class="post-grid">
                <?php while($post = mysqli_fetch_assoc($result)): ?>
                    <article class="post-item">
                        <?php if (!empty($post['image_path'])): ?>
                            <div class="post-thumbnail">
                                <a href="<?php echo site_url('single.php?slug=' . htmlspecialchars($post['slug'])); ?>">
                                    <img src="<?php echo site_url(htmlspecialchars($post['image_path'])); ?>" alt="<?php echo htmlspecialchars($post['title']); ?>">
                                </a>
                            </div>
                        <?php endif; ?>
                        <div class="post-content-summary">
                            <h3 class="post-title">
                                <a href="<?php echo site_url('single.php?slug=' . htmlspecialchars($post['slug'])); ?>">
                                    <?php echo htmlspecialchars($post['title']); ?>
                                </a>
                            </h3>
                            <p class="post-meta">
                                <span class="post-date">Dipublikasikan pada: <?php echo date('j F Y', strtotime($post['created_at'])); ?></span>
                                <!-- Tambahkan nama penulis jika ada nanti -->
                            </p>
                            <div class="post-excerpt">
                                <?php echo htmlspecialchars(generate_excerpt($post['content'], 150)); // Fungsi helper dari functions.php ?>
                            </div>
                            <a href="<?php echo site_url('single.php?slug=' . htmlspecialchars($post['slug'])); ?>" class="read-more-btn">Baca Selengkapnya &raquo;</a>
                        </div>
                    </article>
                <?php endwhile; ?>
            </div>
        <?php else: ?>
            <p class="no-posts">Belum ada artikel yang dipublikasikan saat ini. Silakan kembali lagi nanti!</p>
        <?php endif; ?>
    </section>
<?php
} else {
    // Jika query gagal, tampilkan pesan error atau log
    echo "<p class='error-message'>Terjadi kesalahan saat mengambil data artikel: " . mysqli_error($conn) . "</p>";
    error_log("Error query di index.php: " . mysqli_error($conn), 0);
}

// 5. Sertakan file footer HTML
require_once 'includes/footer.php';

// 6. Tutup koneksi database (opsional di akhir skrip, tapi praktik baik)
if (isset($conn)) {
    mysqli_close($conn);
}
?>

File includes/functions.php (Baru): Buat file ini untuk fungsi-fungsi bantuan.

<?php
// includes/functions.php

/**
 * Membuat excerpt (ringkasan) dari teks.
 *
 * @param string $text Teks sumber.
 * @param int $length Panjang maksimal excerpt dalam karakter.
 * @param string $suffix Sufiks yang ditambahkan jika teks dipotong (misal, '...').
 * @return string Excerpt yang dihasilkan.
 */
function generate_excerpt($text, $length = 200, $suffix = '...') {
    $text = strip_tags($text); // Hapus tag HTML
    if (strlen($text) > $length) {
        $text = substr($text, 0, $length);
        $text = substr($text, 0, strrpos($text, ' ')); // Potong sampai spasi terakhir
        $text .= $suffix;
    }
    return $text;
}

// Anda bisa menambahkan fungsi lain di sini seiring berkembangnya proyek
// Misalnya, fungsi untuk sanitasi input, format tanggal, dll.
?>

Penjelasan Lebih Detail (index.php dan functions.php):

  • Struktur File: File PHP dimulai dengan logika PHP (inklusi, query), lalu disisipi HTML, dan ditutup dengan PHP lagi.
  • require_once 'includes/functions.php';: Kita memisahkan fungsi generate_excerpt ke file terpisah agar kode lebih terorganisir.
  • Query SQL: Masih mengambil artikel dengan status published. ORDER BY created_at DESC memastikan artikel terbaru muncul di atas.
  • Error Handling untuk Query: if ($result) memeriksa apakah mysqli_query berhasil. Jika tidak, pesan error ditampilkan dan dicatat.
  • mysqli_num_rows(): Mengecek apakah ada baris data yang dikembalikan.
  • mysqli_fetch_assoc(): Digunakan dalam loop while untuk mengambil setiap baris data sebagai array asosiatif (nama kolom sebagai kunci).
  • generate_excerpt(): Fungsi kustom di functions.php untuk membuat ringkasan konten. Ini lebih baik daripada substr sederhana karena mencoba memotong di spasi terakhir agar kata tidak terpenggal. strip_tags() penting untuk menghilangkan HTML dari konten sebelum membuat excerpt.
  • htmlspecialchars(): KRUSIAL untuk Keamanan! Selalu gunakan htmlspecialchars() saat menampilkan data dari database (atau input pengguna) ke HTML. Ini mencegah serangan XSS (Cross-Site Scripting) dengan mengubah karakter khusus HTML (seperti <, >, &) menjadi entitas HTML.
  • Link ke Detail (single.php): Kita membuat link dinamis ke halaman single.php dengan menyertakan slug artikel sebagai parameter URL.
  • Tampilan Gambar Unggulan (Baru): Ada blok if untuk menampilkan gambar jika image_path ada di database.
  • Pesan “Belum ada artikel”: Tampilan yang lebih ramah jika tidak ada konten.
  • mysqli_close($conn): Menutup koneksi database secara eksplisit adalah praktik yang baik, meskipun PHP akan melakukannya secara otomatis di akhir skript.

Langkah 4: Halaman Detail Artikel (single.php) - Menampilkan Konten Penuh

Halaman ini akan menampilkan satu artikel secara mendalam berdasarkan slug yang diterima dari URL.

Buat file single.php di root direktori proyek:

<?php
// single.php - Halaman untuk menampilkan detail satu artikel

// 1. Sertakan file konfigurasi, koneksi, dan fungsi
require_once 'includes/db_connect.php';
require_once 'includes/functions.php'; // Mungkin ada fungsi yang berguna nanti

$post = null; // Inisialisasi variabel post
$page_title = "Artikel Tidak Ditemukan"; // Default title
$page_description = "Maaf, artikel yang Anda cari tidak tersedia."; // Default description

// 2. Validasi dan Ambil Parameter 'slug' dari URL
if (isset($_GET['slug']) && !empty(trim($_GET['slug']))) {
    // Sanitasi slug untuk keamanan sebelum digunakan dalam query
    // mysqli_real_escape_string penting untuk mencegah SQL Injection
    $slug = mysqli_real_escape_string($conn, trim($_GET['slug']));

    // 3. Query untuk mengambil artikel spesifik berdasarkan slug
    // Kita juga bisa mengambil nama penulis dan kategori jika tabel sudah di-join nanti
    $sql = "SELECT id, title, content, slug, created_at, updated_at, image_path 
            FROM posts 
            WHERE slug = '$slug' AND status = 'published'";
    
    $result = mysqli_query($conn, $sql);

    if ($result && mysqli_num_rows($result) == 1) {
        $post = mysqli_fetch_assoc($result);
        // Set metadata halaman berdasarkan data artikel
        $page_title = htmlspecialchars($post['title']);
        // Buat deskripsi singkat dari konten untuk meta tag
        $page_description = htmlspecialchars(generate_excerpt($post['content'], 160));
    } else {
        // Jika query gagal atau artikel tidak ditemukan, catat error (jika ada)
        if (!$result) {
            error_log("Error query di single.php: " . mysqli_error($conn), 0);
        }
        // $post tetap null, pesan "Artikel Tidak Ditemukan" akan ditampilkan
        // Mungkin redirect ke halaman 404 kustom di masa depan
        // header("HTTP/1.0 404 Not Found"); // Kirim header 404
    }
} else {
    // Jika tidak ada slug, redirect ke halaman utama atau tampilkan pesan error
    // Untuk sekarang, kita redirect.
    header("Location: " . site_url()); // Gunakan fungsi site_url() jika sudah ada di header
    exit; // Pastikan skrip berhenti setelah redirect
}

// 4. Sertakan file header HTML
require_once 'includes/header.php';
?>

<div class="single-post-container">
    <?php if ($post): // Periksa apakah variabel $post berisi data (artikel ditemukan) ?>
        <article class="single-post-article">
            <header class="post-header-single">
                <h1 class="post-title-single"><?php echo htmlspecialchars($post['title']); ?></h1>
                <p class="post-meta-single">
                    <span class="post-date-single">Dipublikasikan: <?php echo date('l, j F Y H:i', strtotime($post['created_at'])); ?></span>
                    <?php if ($post['created_at'] != $post['updated_at']): ?>
                        <br><span class="post-update-date-single">Diperbarui: <?php echo date('l, j F Y H:i', strtotime($post['updated_at'])); ?></span>
                    <?php endif; ?>
                    <!-- Nanti bisa ditambahkan: Oleh: Nama Penulis | Kategori: Nama Kategori -->
                </p>
            </header>

            <?php if (!empty($post['image_path'])): ?>
                <div class="post-featured-image-single">
                    <img src="<?php echo site_url(htmlspecialchars($post['image_path'])); ?>" alt="<?php echo htmlspecialchars($post['title']); ?>">
                </div>
            <?php endif; ?>

            <div class="post-content-full">
                <?php
                // Tampilkan konten. Jika konten disimpan sebagai HTML murni, tampilkan langsung.
                // Jika konten adalah teks biasa dengan newline, gunakan nl2br().
                // Untuk keamanan, jika konten bisa diinput pengguna dan mengandung HTML,
                // Anda perlu menggunakan library sanitasi HTML seperti HTML Purifier
                // sebelum menyimpannya ke database atau saat menampilkannya.
                // Untuk tutorial ini, kita asumsikan konten aman atau teks biasa.
                echo $post['content']; // Jika konten sudah HTML
                // echo nl2br(htmlspecialchars($post['content'])); // Jika konten teks biasa dan ingin aman + format newline
                ?>
            </div>
        </article>
        
        <!-- Tambahkan bagian komentar di sini nanti -->
        <!-- <section class="comments-section"> ... </section> -->

    <?php else: // Jika $post adalah null (artikel tidak ditemukan) ?>
        <div class="post-not-found">
            <h2>Oops! Artikel Tidak Ditemukan</h2>
            <p>Maaf, artikel yang Anda coba akses dengan slug tersebut tidak dapat ditemukan atau mungkin belum dipublikasikan.</p>
            <p>Anda bisa mencoba mencari artikel lain atau kembali ke <a href="<?php echo site_url(); ?>">halaman utama</a>.</p>
        </div>
    <?php endif; ?>
</div>

<?php
// 5. Sertakan file footer HTML
require_once 'includes/footer.php';

// 6. Tutup koneksi database
if (isset($conn)) {
    mysqli_close($conn);
}
?>

Penjelasan Lebih Detail (single.php):

  • Validasi $_GET['slug']: isset() memeriksa apakah parameter ada, !empty(trim($_GET['slug'])) memeriksa apakah tidak kosong setelah menghapus spasi di awal/akhir.
  • Sanitasi slug: mysqli_real_escape_string($conn, trim($_GET['slug'])) adalah langkah keamanan kritis untuk mencegah SQL Injection. Selalu sanitasi data yang datang dari pengguna sebelum memasukkannya ke dalam query SQL.
  • Error Handling Query: Sama seperti di index.php, kita memeriksa apakah query berhasil dan apakah ada baris yang dikembalikan.
  • Header 404 (Opsional Lanjutan): Jika artikel tidak ditemukan, idealnya server mengirimkan status HTTP 404. Ini bisa dilakukan dengan header("HTTP/1.0 404 Not Found"); sebelum output HTML apapun.
  • Redirect jika slug Tidak Ada: Jika parameter slug tidak ada sama sekali di URL, pengguna diarahkan kembali ke halaman utama. exit; penting setelah header("Location: ...") untuk memastikan tidak ada kode lain yang dieksekusi.
  • Menampilkan Konten:
    • echo $post['content'];: Jika Anda menyimpan konten artikel sebagai HTML di database (misalnya, dari editor WYSIWYG), Anda bisa menampilkannya langsung. PERHATIAN: Ini hanya aman jika Anda yakin sumber HTML tersebut terpercaya atau sudah disanitasi sebelum disimpan.
    • echo nl2br(htmlspecialchars($post['content']));: Jika konten adalah teks biasa dan Anda ingin menjaga format newline serta aman dari XSS, gunakan kombinasi ini. Namun, ini akan mengubah tag HTML menjadi teks biasa.
    • Praktik Terbaik untuk Konten dari Pengguna: Jika konten bisa dimasukkan oleh pengguna (misalnya, melalui area admin nanti) dan boleh mengandung HTML, Anda harus menggunakan library sanitasi HTML yang kuat seperti HTML Purifier sebelum menyimpan ke database atau saat menampilkannya untuk mencegah XSS. Untuk tutorial awal ini, kita akan menjaga kompleksitas tetap rendah.
  • Tanggal Update: Menampilkan tanggal kapan artikel terakhir diperbarui jika berbeda dari tanggal pembuatan.

Langkah 5: Styling Lanjutan dengan CSS (css/style.css)

Mari kita tambahkan dan perbaiki beberapa gaya di css/style.css agar lebih menarik dan fungsional.

/* css/style.css (Lanjutan dan Perbaikan) */
/* Reset dasar dan Box Sizing */
*, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: 'Helvetica Neue', Arial, sans-serif; /* Font lebih modern */
    line-height: 1.7;
    background-color: #f8f9fa; /* Warna latar belakang lebih lembut */
    color: #343a40; /* Warna teks utama lebih gelap */
    font-size: 16px;
}

.page-wrapper {
    display: flex;
    flex-direction: column;
    min-height: 100vh; /* Pastikan footer selalu di bawah */
}

.container {
    width: 90%;
    max-width: 1140px; /* Lebar kontainer umum */
    margin-left: auto;
    margin-right: auto;
    padding-left: 15px;
    padding-right: 15px;
}

/* Header */
.site-header {
    background-color: #ffffff; /* Header putih */
    padding: 15px 0;
    border-bottom: 1px solid #e9ecef; /* Garis bawah halus */
    box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}

.site-header .container {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.site-header .logo h1 {
    margin: 0;
}
.site-header .logo h1 a {
    font-size: 1.8rem; /* Ukuran logo lebih besar */
    color: #007bff; /* Warna logo primer */
    text-decoration: none;
    font-weight: bold;
}

.main-navigation ul {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex; /* Navigasi flex */
}

.main-navigation ul li {
    margin-left: 25px; /* Jarak antar item nav */
}

.main-navigation ul li a {
    text-decoration: none;
    color: #495057; /* Warna link nav */
    font-weight: 500;
    padding: 5px 0;
    position: relative;
    transition: color 0.3s ease;
}
.main-navigation ul li a:hover,
.main-navigation ul li a.active {
    color: #007bff; /* Warna hover/aktif */
}
.main-navigation ul li a.active::after { /* Garis bawah untuk link aktif */
    content: '';
    position: absolute;
    bottom: -2px;
    left: 0;
    width: 100%;
    height: 2px;
    background-color: #007bff;
}

/* Main Content Area */
.site-main {
    padding: 30px 0;
    flex-grow: 1; /* Agar main content mengisi ruang dan footer tetap di bawah */
}

.main-content-area {
    background-color: #ffffff;
    padding: 30px;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}

.section-title {
    font-size: 2rem;
    color: #343a40;
    border-bottom: 3px solid #007bff;
    padding-bottom: 10px;
    margin-bottom: 30px;
}

/* Post Listing (index.php) */
.post-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); /* Grid responsif */
    gap: 30px;
}

.post-item {
    background-color: #fff; /* Sudah diatur di .main-content-area, bisa dihapus jika duplikat */
    border: 1px solid #e9ecef;
    border-radius: 8px;
    overflow: hidden; /* Agar gambar tidak meluber dari border-radius */
    display: flex;
    flex-direction: column;
    transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.post-item:hover {
    transform: translateY(-5px);
    box-shadow: 0 8px 16px rgba(0,0,0,0.1);
}

.post-thumbnail img {
    width: 100%;
    height: 200px; /* Tinggi gambar thumbnail tetap */
    object-fit: cover; /* Pastikan gambar mengisi area tanpa distorsi */
    display: block;
}

.post-content-summary {
    padding: 20px;
    flex-grow: 1; /* Agar konten mengisi ruang dan tombol di bawah */
    display: flex;
    flex-direction: column;
}

.post-title a {
    text-decoration: none;
    color: #007bff;
    font-size: 1.4rem;
    font-weight: 600;
}
.post-title a:hover {
    text-decoration: underline;
    color: #0056b3;
}

.post-meta {
    font-size: 0.85rem;
    color: #6c757d;
    margin-bottom: 15px;
    margin-top: 5px;
}

.post-excerpt {
    font-size: 0.95rem;
    color: #495057;
    margin-bottom: 20px;
    flex-grow: 1;
}

.read-more-btn {
    display: inline-block;
    background-color: #007bff;
    color: #fff;
    padding: 10px 18px;
    text-decoration: none;
    border-radius: 5px;
    font-weight: 500;
    align-self: flex-start; /* Tombol di kiri bawah */
    transition: background-color 0.2s ease;
}
.read-more-btn:hover {
    background-color: #0056b3;
}

.no-posts, .error-message, .post-not-found {
    padding: 20px;
    background-color: #f8d7da;
    color: #721c24;
    border: 1px solid #f5c6cb;
    border-radius: 5px;
    margin-top: 20px;
}
.post-not-found a {
    color: #721c24;
    font-weight: bold;
}


/* Single Post (single.php) */
.single-post-container {
    /* Sudah diatur di .main-content-area */
}

.post-header-single {
    margin-bottom: 30px;
    padding-bottom: 20px;
    border-bottom: 1px solid #e9ecef;
}

.post-title-single {
    font-size: 2.5rem; /* Judul lebih besar di halaman single */
    color: #343a40;
    margin-bottom: 10px;
    line-height: 1.2;
}

.post-meta-single {
    font-size: 0.9rem;
    color: #6c757d;
}
.post-meta-single span {
    display: block; /* Setiap meta di baris baru */
    margin-bottom: 5px;
}

.post-featured-image-single {
    margin-bottom: 30px;
    text-align: center; /* Tengahkan gambar jika tidak full width */
}
.post-featured-image-single img {
    max-width: 100%;
    height: auto;
    border-radius: 8px;
    box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}

.post-content-full {
    font-size: 1.1rem; /* Konten lebih mudah dibaca */
    color: #343a40;
}
.post-content-full p,
.post-content-full ul,
.post-content-full ol,
.post-content-full blockquote {
    margin-bottom: 1.5em;
}
.post-content-full h2, .post-content-full h3, .post-content-full h4 { /* Styling untuk heading di dalam konten */
    margin-top: 1.8em;
    margin-bottom: 0.8em;
    color: #343a40;
}
.post-content-full a {
    color: #007bff;
    text-decoration: none;
}
.post-content-full a:hover {
    text-decoration: underline;
}
.post-content-full img { /* Gambar di dalam konten agar responsif */
    max-width: 100%;
    height: auto;
    border-radius: 4px;
    margin: 10px 0;
}


/* Footer */
.site-footer {
    background-color: #343a40; /* Footer lebih gelap */
    color: #adb5bd; /* Teks footer lebih terang */
    text-align: center;
    padding: 30px 0;
    margin-top: auto; /* Mendorong footer ke bawah jika konten pendek */
}
.site-footer p {
    margin: 0;
}

Penjelasan Tambahan CSS:

  • Reset dan Box Sizing: * { box-sizing: border-box; } adalah praktik modern yang membuat layout lebih intuitif.
  • Font Lebih Modern: Mengganti font default.
  • Struktur .page-wrapper: Menggunakan flexbox untuk memastikan footer menempel di bagian bawah halaman, bahkan jika kontennya pendek.
  • Layout Grid untuk Daftar Artikel: Menggunakan CSS Grid (display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));) untuk membuat tata letak kartu artikel yang responsif secara otomatis.
  • Efek Hover Halus: Menambahkan transisi pada item artikel dan tombol.
  • Styling Gambar Unggulan: Memberikan tinggi tetap untuk thumbnail di daftar artikel dan memastikan gambar di halaman detail responsif.
  • Konsistensi Warna dan Spasi: Mencoba menggunakan palet warna yang lebih konsisten dan spasi yang lebih teratur.

Menguji Ulang Halaman Frontend

Setelah semua perubahan dan penambahan detail:

  1. Refresh Browser: Hapus cache browser Anda (Ctrl+Shift+R atau Cmd+Shift+R) untuk memastikan Anda melihat versi terbaru CSS dan PHP.
  2. Periksa Tampilan: Lihat halaman index.php. Daftar artikel seharusnya sekarang menggunakan layout grid dan memiliki gambar (jika Anda menambahkan image_path ke data dummy dan mengaturnya di kode).
  3. Klik Artikel: Buka halaman single.php. Periksa tampilan judul, meta, gambar unggulan (jika ada), dan konten.
  4. Responsivitas (Dasar): Ubah ukuran jendela browser Anda. Daftar artikel (grid) dan gambar di halaman detail seharusnya menyesuaikan diri dengan baik.
  5. Periksa Kode Sumber (View Source): Lihat kode sumber HTML yang dihasilkan oleh PHP di browser. Pastikan path ke CSS benar, meta tags terisi, dan tidak ada error PHP yang terlihat.
  6. Periksa Konsol Browser (Developer Tools): Buka Developer Tools (biasanya F12) dan periksa tab “Console” untuk error JavaScript (seharusnya tidak ada saat ini) dan tab “Network” untuk memastikan semua aset (CSS, gambar) termuat dengan benar.

Refleksi Part 1 dan Persiapan untuk Part 2

Selamat! Anda telah menyelesaikan Bagian 1 dari seri tutorial ini dengan lebih mendalam. Kita telah membangun fondasi frontend yang solid, lengkap dengan:

  • Koneksi database yang aman dan terstruktur.
  • Header dan footer yang reusable.
  • Halaman daftar artikel dinamis dengan excerpt dan gambar.
  • Halaman detail artikel yang menampilkan konten penuh.
  • Styling CSS yang lebih baik dan responsif dasar.
  • Penggunaan htmlspecialchars() untuk keamanan output.
  • Penanganan error dasar untuk query database.

Ini adalah langkah awal yang sangat penting. Memahami bagaimana data diambil dari database dan ditampilkan di browser menggunakan PHP adalah inti dari banyak aplikasi web.

Di Part 2 mendatang, kita akan beralih ke sisi backend dan mulai membangun Area Admin. Fokus utama kita adalah membuat sistem Login yang aman untuk administrator dan Dashboard Admin sebagai pusat kontrol CMS kita. Ini akan melibatkan penggunaan sesi PHP, hashing password, dan melindungi halaman admin dari akses tidak sah.

Pastikan Anda memahami semua konsep di Bagian 1 ini, karena Bagian 2 akan membangun di atasnya. Jangan ragu untuk bereksperimen, mencoba memodifikasi kode, atau menambahkan fitur kecil sendiri!

Tutorial Terkait