Setelah tiga proyek CLI sebelumnya, sekarang kita menyelam ke medium yang membuat PHP terkenal: halaman web yang diminta lewat HTTP. Pada Bagian 1 ini kita membangun kerangka blog tanpa framework — tidak ada Laravel, tidak ada Symfony, tidak satu pun dependensi Composer. Yang kita rakit adalah tiga fondasi yang nanti dipakai semua bagian berikutnya: front controller sebagai satu pintu masuk seluruh request, tabel rute yang memetakan path ke handler, dan layout HTML yang membungkus konten setiap halaman. Artikelnya masih ditulis tetap di dalam template; justru itu yang membuat kita bisa fokus pada struktur sebelum data datang pada Bagian 2 dan Bagian 3.
Arsitektur Proyek #
Blog ini mengikuti pola yang dipakai hampir semua framework modern: hanya ada satu berkas PHP yang boleh diakses web, sisanya di-import dari dalamnya. Struktur direktori pada tahap ini:
website-blog/
├── public/
│ └── index.php front controller — satu pintu masuk semua request
└── templates/
├── layout.php kerangka HTML: <head>, header, footer
└── home.php konten beranda: daftar artikelDirektori public/ sengaja dipisahkan supaya kelak bisa dijadikan document root — berkas di luar public/ (nanti: src/ dan database/) tidak pernah bisa diminta langsung lewat HTTP. Alur satu request dari masuk sampai keluar terdiri dari empat langkah pendek:
Browser mengirim request ke public/index.php, front controller mengambil path dari $_SERVER['REQUEST_URI'], mencocokkannya dengan tabel rute, lalu handler yang cocok merender template dan membungkusnya dengan layout. Path yang tidak dikenal jatuh ke handler 404 — tetap berupa halaman HTML yang rapi, bukan error mentah server.
Front Controller dan Penjaga CLI #
Berikut isi lengkap public/index.php pada tahap ini:
<?php
declare(strict_types=1);
/**
* public/index.php — front controller website blog.
*
* Ini satu-satunya berkas PHP yang diakses langsung oleh web server.
* Setiap request HTTP masuk ke berkas ini, dicocokkan dengan tabel rute,
* lalu template dirender dan dikirim sebagai respons.
*/
// ---------------------------------------------------------------------------
// Penjaga CLI
// ---------------------------------------------------------------------------
// Dijalankan langsung dari terminal tanpa request apa pun (misalnya
// `php index.php` oleh skrip verifikasi) harus berakhir rapi: tampilkan
// petunjuk pemakaian lalu keluar dengan status sukses (0), bukan error.
if (PHP_SAPI === 'cli' && !isset($_SERVER['REQUEST_URI'])) {
echo "Blog PHP — front controller\n\n";
echo "Berkas ini menangani request HTTP, bukan dijalankan langsung.\n";
echo "Menyalakan server pengembangan dari root proyek:\n";
echo " php -S 127.0.0.1:8093 public/index.php\n\n";
echo "Setelah server menyala, buka http://127.0.0.1:8093/ di browser.\n";
exit(0);
}
// ---------------------------------------------------------------------------
// Helper bersama
// ---------------------------------------------------------------------------
if (!function_exists('e')) {
/** Escape teks untuk HTML — dipakai di semua template. */
function e(?string $nilai): string
{
return htmlspecialchars($nilai ?? '', ENT_QUOTES, 'UTF-8');
}
}
/**
* Render satu file template menjadi string.
*
* Variabel pada $data diekstrak menjadi variabel lokal di dalam template,
* lalu output yang dihasilkan template ditangkap dengan output buffering
* (ob_start) dan dikembalikan sebagai string.
*/
function render(string $template, array $data = []): string
{
extract($data, EXTR_SKIP);
ob_start();
require __DIR__ . '/../templates/' . $template;
return (string) ob_get_clean();
}
// ---------------------------------------------------------------------------
// Handler halaman — masing-masing mengembalikan [status, html]
// ---------------------------------------------------------------------------
function halamanBeranda(): array
{
$konten = render('home.php');
return [200, render('layout.php', ['judul' => 'Beranda', 'konten' => $konten])];
}
function halamanTentang(): array
{
$konten = <<<'HTML'
<article class="halaman">
<h2>Tentang Blog Ini</h2>
<p>Blog latihan yang dibangun dengan PHP murni — tanpa framework,
tanpa dependensi eksternal. Seluruh kode penyusunnya bisa Anda baca
dari awal sampai akhir.</p>
</article>
HTML;
return [200, render('layout.php', ['judul' => 'Tentang', 'konten' => $konten])];
}
function halamanTidakDitemukan(string $path): array
{
$konten = '<article class="halaman">'
. '<h2>404 — Halaman Tidak Ditemukan</h2>'
. '<p>Rute <code>' . e($path) . '</code> tidak dikenali oleh blog ini.</p>'
. '<p>Kembali ke <a href="/">beranda</a>?</p>'
. '</article>';
return [404, render('layout.php', ['judul' => '404', 'konten' => $konten])];
}
// ---------------------------------------------------------------------------
// dispatch — inti routing: request mana yang menghasilkan respons apa
// ---------------------------------------------------------------------------
/**
* Cocokkan path dari request dengan tabel rute.
*
* Murni tanpa efek samping: tidak mencetak apa pun dan tidak mengirim
* header, hanya MENGEMBALIKAN pasangan [status, html]. Titik masuk di
* bawah yang menerapkan status dan mencetak html — pemisahan inilah yang
* membuat dispatch bisa diuji berulang kali dari skrip CLI.
*
* @param array<string,mixed> $server isi $_SERVER (asli atau tiruan)
* @return array{0: int, 1: string} pasangan [status HTTP, badan respons]
*/
function dispatch(array $server): array
{
// parse_url dapat mengembalikan false pada URI yang ganjil (mis. "//"),
// jadi kita pakai ?: — bukan ?? — agar false pun jatuh ke '/'.
$path = (string) (parse_url((string) ($server['REQUEST_URI'] ?? '/'), PHP_URL_PATH) ?: '/');
$path = rtrim($path, '/') ?: '/'; // '/tentang/' juga diterima
$rute = [
'/' => 'halamanBeranda',
'/tentang' => 'halamanTentang',
];
if (isset($rute[$path])) {
$handler = $rute[$path];
return $handler();
}
return halamanTidakDitemukan($path);
}
// ---------------------------------------------------------------------------
// Titik masuk — satu-satunya tempat dengan efek samping
// ---------------------------------------------------------------------------
[$status, $html] = dispatch($_SERVER);
http_response_code($status);
echo $html;Mari kita bedah berkas ini per blok, karena keenam bagian berikutnya terus menumpuk di atasnya:
- Penjaga CLI: konstanta
PHP_SAPIbernilai"cli"ketika berkas dijalankan lewat terminal, dan terminal tidak mengisi$_SERVER['REQUEST_URI']. Dua syarat bersama-sama menjadi tanda “ini bukan request web”, sehingga kita mencetak petunjuk ramah laluexit(0)— bukan error menakutkan. Pemeriksaan ganda ini juga membuka pintu trik penting: skrip pengujian bisa mengisi$_SERVER['REQUEST_URI']dengan nilai tiruan sebelum memuat berkas ini, dan penjaga CLI pun meloloskannya sebagai request palsu. - Helper
e(): pembungkushtmlspecialchars()yang mengubah<,>,",', dan&menjadi entitas HTML. Setiap nilai dinamis yang dicetak ke halaman wajib melewati helper ini — kebiasaan yang melindungi blog dari XSS sejak halaman pertama. - Helper
render(): intinya tiga baris.extract($data, EXTR_SKIP)mengubah kunci array menjadi variabel lokal (['judul' => ...]menjadi$judul),ob_start()menyalakan output buffering agarrequiretemplate tidak langsung menulis ke respons, laluob_get_clean()menangkap seluruh output template sebagai string. FlagEXTR_SKIPmencegahextractmenimpa variabel yang sudah ada — tanpa flag itu, array dari pemanggil bisa menyusupkan variabel tak terduga ke dalam template. - Handler halaman: setiap handler murni membangun nilai — mengembalikan pasangan
[status, html]tanpa menyentuhechomaupunheader(). Berkat itu, handler bisa dipanggil dari skrip CLI untuk diperiksa hasilnya. dispatch(): mengambil path bersih dari URI (membuang query string lewatparse_url), menormalkan garis miring berlebih denganrtrim, lalu mencocokkannya dengan tabel rute berupa array asosiatif sederhana. Router framework pada dasarnya adalah tabel seperti ini ditambah dukungan parameter — yang kita tambahkan pada Bagian 2.- Titik masuk: tiga baris terakhir adalah satu-satunya tempat dengan efek samping nyata — menerapkan status lewat
http_response_code()dan mencetak HTML. Semua logika di atasnya tetap murni dan bisa diuji.
Penjaga CLI bisa dilihat langsung hasilnya tanpa web server:
<?php
// Menjalankan front controller dari terminal: penjaga CLI mencetak
// petunjuk penggunaan lalu keluar dengan exit code 0 — tanpa error.
require __DIR__ . '/public/index.php';Output yang Dihasilkan:
Blog PHP — front controller
Berkas ini menangani request HTTP, bukan dijalankan langsung.
Menyalakan server pengembangan dari root proyek:
php -S 127.0.0.1:8093 public/index.php
Setelah server menyala, buka http://127.0.0.1:8093/ di browser.Routing Tanpa Database Rute #
Tabel rute sebesar dua baris memang terlihat sederhana, tetapi bentuknya sudah benar: routing adalah pencarian nilai berdasarkan kunci, dan isset($rute[$path]) adalah pencarian O(1) paling cepat di PHP. Karena dispatch() tidak menyentuh superglobal asli, kita bisa menguji seluruh keputusan routing dari CLI dengan array $_SERVER tiruan. Muat front controller sekali (sambil menyamar sebagai request web supaya penjaga CLI meloloskannya), lalu panggil dispatch() berulang kali:
<?php
// Muat front controller sambil menyamar sebagai request GET /.
$_SERVER['REQUEST_URI'] = '/';
ob_start();
require __DIR__ . '/public/index.php';
ob_end_clean(); // hasil render ditampilkan pada cuplikan berikutnya
// dispatch() kini terdefinisi — uji beberapa path dengan array $_SERVER tiruan.
foreach (['/', '/?halaman=2', '/tentang', '/tentang/', '/halo'] as $uri) {
[$status, $html] = dispatch(['REQUEST_URI' => $uri]);
printf("%-16s -> %d (%d byte html)\n", $uri, $status, strlen($html));
}Output yang Dihasilkan:
/ -> 200 (1744 byte html)
/?halaman=2 -> 200 (1744 byte html)
/tentang -> 200 (825 byte html)
/tentang/ -> 200 (825 byte html)
/halo -> 404 (756 byte html)Tiga perilaku routing terbaca langsung dari tabel di atas. Pertama, query string dibuang — /?halaman=2 dan / menghasilkan halaman yang sama karena parse_url(..., PHP_URL_PATH) hanya mengambil bagian path. Kedua, garis miring penutup diterima — /tentang/ dinormalkan menjadi /tentang oleh rtrim. Ketiga, path yang tidak dikenal seperti /halo jatuh ke handler 404 dengan status 404 dan halaman HTML yang tetap utuh.
Layout dan Template dengan Sintaks Alternatif #
Sekarang lapisan tampilan. templates/layout.php adalah kerangka yang dipakai bersama seluruh halaman — ia menerima judul dan fragmen konten, lalu membingungkai keduanya dengan header navigasi dan footer:
<?php
/**
* templates/layout.php — kerangka HTML yang membingkai setiap halaman.
*
* Template ini tidak menghasilkan konten sendiri; ia menerima dua variabel:
* $judul (untuk elemen <title>) dan $konten (fragmen HTML dari template
* halaman). Keduanya punya nilai bawaan supaya berkas ini tetap aman
* di-include tanpa data — misalnya saat diperiksa langsung lewat CLI.
*/
if (!function_exists('e')) {
/** Escape teks untuk HTML — dipakai di semua template. */
function e(?string $nilai): string
{
return htmlspecialchars($nilai ?? '', ENT_QUOTES, 'UTF-8');
}
}
$judul ??= 'Blog PHP';
$konten ??= '';
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= e($judul) ?> — Blog PHP</title>
</head>
<body>
<header class="site-header">
<strong><a href="/">Blog PHP</a></strong>
<nav>
<a href="/">Beranda</a> |
<a href="/tentang">Tentang</a>
</nav>
</header>
<main class="site-main">
<?= $konten ?>
</main>
<footer class="site-footer">
<p>Dibangun dengan PHP murni — proyek kursus "Website Blog".</p>
</footer>
</body>
</html>
Dua keputusan desain di sini penting. Pertama, $judul ??= 'Blog PHP' dan $konten ??= '' memberi nilai bawaan ketika template di-include tanpa data — itulah sebabnya berkas ini aman dijalankan langsung tanpa peringatan “Undefined variable”. Kedua, $konten sengaja dicetak mentah tanpa e(): variabel tersebut bukan teks dari pengguna, melainkan fragmen HTML hasil render template lain; meng-escape-nya akan menghancurkan struktur halaman. Data asli di dalam template konten-lah yang di-escape satu per satu.
Adapun templates/home.php menghasilkan fragmen daftar artikel untuk beranda. Data artikel masih ditulis tetap di dalam berkas — pada Bagian 2 ia pindah ke class PostRepository dan template cukup menerima variabel $posts:
<?php
/**
* templates/home.php — halaman beranda: daftar artikel terbaru.
*
* Data artikel masih di-hardcode di sini. Pada Bagian 2 data ini pindah
* ke class PostRepository, jadi template cukup menerima variabel $posts
* dari pemanggilnya. Baris `$posts ??= [...]` memberi nilai bawaan agar
* berkas ini tetap aman dijalankan langsung tanpa data dari luar.
*/
if (!function_exists('e')) {
/** Escape teks untuk HTML — dipakai di semua template. */
function e(?string $nilai): string
{
return htmlspecialchars($nilai ?? '', ENT_QUOTES, 'UTF-8');
}
}
$posts ??= [
[
'slug' => 'kenapa-php-masih-relevan',
'judul' => 'Kenapa PHP Masih Relevan di 2026',
'ringkasan' => 'Bahasa yang menggerakkan sebagian besar web ternyata masih terus berkembang: rilis besar tiap tahun, ekosistem yang matang, dan kinerja yang jauh melampaui reputasi lamanya.',
'dibuat_pada' => '2026-01-03',
],
[
'slug' => 'membangun-router-sederhana',
'judul' => 'Membangun Router Sederhana Tanpa Framework',
'ringkasan' => 'Router pada framework favorit Anda sebenarnya hanyalah array asosiatif dan pencocokan string. Kita buktikan dengan membangunnya sendiri dalam satu berkas.',
'dibuat_pada' => '2026-01-10',
],
[
'slug' => 'tips-aman-menampilkan-data-pengguna',
'judul' => 'Tips Aman Menampilkan Data Pengguna',
'ringkasan' => 'Setiap nilai yang berasal dari pengguna harus di-escape sebelum ditampilkan. Satu helper kecil bernama e() menyelamatkan seluruh template Anda.',
'dibuat_pada' => '2026-01-17',
],
];
?>
<section class="post-list">
<h2>Artikel Terbaru</h2>
<?php foreach ($posts as $post): ?>
<article class="post">
<h3><a href="/post/<?= e($post['slug']) ?>"><?= e($post['judul']) ?></a></h3>
<p><?= e($post['ringkasan']) ?></p>
<small>Diterbitkan <?= e($post['dibuat_pada']) ?></small>
</article>
<?php endforeach; ?>
</section>
Perhatikan gaya penulisan templatenya: foreach (...): … endforeach; — ini disebut sintaks alternatif
dan wajib tradisi di dunia template. Dibanding kurung kurawal, kata kunci penutup eksplisit membuat blok HTML bertingkat tetap terbaca ketika editor melipat tag. Tag <?= ... ?> adalah bentuk pendek <?php echo ... ?> yang aman dipakai karena selalu aktif di PHP modern. Setiap nilai dinamis — slug, judul, ringkasan, tanggal — melewati e() sebelum dicetak; hanya struktur HTML statis yang ditulis mentah.
Komposisi layout dan konten bisa dilihat utuh hasilnya. Cuplikan berikut menyamar sebagai request GET /, menangkap respons front controller dengan output buffering, lalu mencetaknya:
<?php
// Menyamar sebagai request web GET /, lalu menangkap seluruh HTML-nya.
$_SERVER['REQUEST_URI'] = '/';
ob_start();
require __DIR__ . '/public/index.php';
echo (string) ob_get_clean();Output yang Dihasilkan:
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Beranda — Blog PHP</title>
</head>
<body>
<header class="site-header">
<strong><a href="/">Blog PHP</a></strong>
<nav>
<a href="/">Beranda</a> |
<a href="/tentang">Tentang</a>
</nav>
</header>
<main class="site-main">
<section class="post-list">
<h2>Artikel Terbaru</h2>
<article class="post">
<h3><a href="/post/kenapa-php-masih-relevan">Kenapa PHP Masih Relevan di 2026</a></h3>
<p>Bahasa yang menggerakkan sebagian besar web ternyata masih terus berkembang: rilis besar tiap tahun, ekosistem yang matang, dan kinerja yang jauh melampaui reputasi lamanya.</p>
<small>Diterbitkan 2026-01-03</small>
</article>
<article class="post">
<h3><a href="/post/membangun-router-sederhana">Membangun Router Sederhana Tanpa Framework</a></h3>
<p>Router pada framework favorit Anda sebenarnya hanyalah array asosiatif dan pencocokan string. Kita buktikan dengan membangunnya sendiri dalam satu berkas.</p>
<small>Diterbitkan 2026-01-10</small>
</article>
<article class="post">
<h3><a href="/post/tips-aman-menampilkan-data-pengguna">Tips Aman Menampilkan Data Pengguna</a></h3>
<p>Setiap nilai yang berasal dari pengguna harus di-escape sebelum ditampilkan. Satu helper kecil bernama e() menyelamatkan seluruh template Anda.</p>
<small>Diterbitkan 2026-01-17</small>
</article>
</section>
</main>
<footer class="site-footer">
<p>Dibangun dengan PHP murni — proyek kursus "Website Blog".</p>
</footer>
</body>
</html>Baca ulang output di atas dari atas ke bawah: kerangka dari layout.php (doctype, head, header navigasi, footer) menyelimuti fragmen dari home.php (elemen <section> beserta tiga <article>). Inilah seluruh sihir komposisinya — render('home.php') menghasilkan string, string itu dikirim sebagai $konten ke render('layout.php'), dan hasil akhirnya menjadi body respons.
Menguji dengan Web Server Bawaan #
Pengujian sungguhan memakai web server bawaan PHP. Karena seluruh request harus melewati front controller, kita memakai bentuk perintah yang menjadikan public/index.php sebagai router — bukan -t public yang akan melayani berkas statis secara terpisah:
# terminal 1 — dari direktori root proyek
$ php -S 127.0.0.1:8093 public/index.phpServer berjalan di foreground dan mencatat setiap request yang masuk. Dari terminal kedua, kita meminta halaman beranda beserta header-nya:
# terminal 2
$ curl -sS -i http://127.0.0.1:8093/ | head -n 22HTTP/1.1 200 OK
Host: 127.0.0.1:8093
Date: Tue, 13 Oct 2026 21:36:57 GMT
Connection: close
X-Powered-By: PHP/8.4.23
Content-type: text/html; charset=UTF-8
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Beranda — Blog PHP</title>
</head>
<body>
<header class="site-header">
<strong><a href="/">Blog PHP</a></strong>
<nav>
<a href="/">Beranda</a> |
<a href="/tentang">Tentang</a>Status 200, Content-type: text/html, dan body HTML yang persis sama dengan hasil simulasi CLI — kali ini lewat jalur HTTP sungguhan; baris Date akan menyesuaikan jam permintaan Anda. Giliran halaman yang tidak dikenal:
$ curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8093/halusinasi404$ curl -sS http://127.0.0.1:8093/halusinasi | grep '<code>'<p>Rute <code>/halusinasi</code> tidak dikenali oleh blog ini.</p>Handler 404 mengembalikan status yang benar dan menyisipkan path yang diminta ke halaman — sudah di-escape, karena path adalah data yang datang dari luar. Matikan server dengan Ctrl+C pada terminal pertama setelah selesai mencoba.
Apa yang Berubah #
Dibandingkan dengan sebelum proyek dimulai, tahap ini menambahkan tiga berkas yang menjadi tulang punggung seluruh proyek:
public/index.php— front controller dengan penjaga CLI, helpere()danrender(), tiga handler halaman, sertadispatch()berbasis tabel rute.templates/layout.php— kerangka HTML dengan nilai bawaan defensif untuk$juduldan$konten.templates/home.php— fragmen beranda dengan data artikel tetap dan sintaks alternatifforeach/endforeach.
Belum ada halaman detail artikel, belum ada sumber data selain hardcode, dan tabel rute belum mengenal parameter. Bagian 2 memperkenalkan class pertama proyek ini — PostRepository — beserta rute parameter /post/{slug}.
Cara Menjalankan #
Dari direktori bagian-1-routing-dan-layout/ (folder bundle halaman ini), tiga lapisan pemeriksaan tersedia:
# 1. Penjaga CLI — petunjuk pemakaian, exit code 0
php public/index.php
# 2. Web server bawaan + curl (terminal terpisah)
php -S 127.0.0.1:8093 public/index.php
curl -sS -i http://127.0.0.1:8093/
curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8093/halusinasi
# 3. Pemeriksaan sintaks seluruh berkas
php -l public/index.php && php -l templates/layout.php && php -l templates/home.phpTiga berkas pendukung halaman ini — public/index.php, templates/layout.php, dan templates/home.php — tersedia di bagian bawah halaman dan dapat diunduh langsung. Untuk pendalaman, baca dokumentasi resmi tentang superglobal $_SERVER
, parse_url()
, output buffering pada ob_start()
, serta http_response_code()
. Dengan kerangka yang berdiri, kita lanjut ke Bagian 2 untuk memberi blog ini halaman detail artikel dan sumber data yang layak.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
templates/home.php
Unduh berkas<?php
/**
* templates/home.php — halaman beranda: daftar artikel terbaru.
*
* Data artikel masih di-hardcode di sini. Pada Bagian 2 data ini pindah
* ke class PostRepository, jadi template cukup menerima variabel $posts
* dari pemanggilnya. Baris `$posts ??= [...]` memberi nilai bawaan agar
* berkas ini tetap aman dijalankan langsung tanpa data dari luar.
*/
if (!function_exists('e')) {
/** Escape teks untuk HTML — dipakai di semua template. */
function e(?string $nilai): string
{
return htmlspecialchars($nilai ?? '', ENT_QUOTES, 'UTF-8');
}
}
$posts ??= [
[
'slug' => 'kenapa-php-masih-relevan',
'judul' => 'Kenapa PHP Masih Relevan di 2026',
'ringkasan' => 'Bahasa yang menggerakkan sebagian besar web ternyata masih terus berkembang: rilis besar tiap tahun, ekosistem yang matang, dan kinerja yang jauh melampaui reputasi lamanya.',
'dibuat_pada' => '2026-01-03',
],
[
'slug' => 'membangun-router-sederhana',
'judul' => 'Membangun Router Sederhana Tanpa Framework',
'ringkasan' => 'Router pada framework favorit Anda sebenarnya hanyalah array asosiatif dan pencocokan string. Kita buktikan dengan membangunnya sendiri dalam satu berkas.',
'dibuat_pada' => '2026-01-10',
],
[
'slug' => 'tips-aman-menampilkan-data-pengguna',
'judul' => 'Tips Aman Menampilkan Data Pengguna',
'ringkasan' => 'Setiap nilai yang berasal dari pengguna harus di-escape sebelum ditampilkan. Satu helper kecil bernama e() menyelamatkan seluruh template Anda.',
'dibuat_pada' => '2026-01-17',
],
];
?>
<section class="post-list">
<h2>Artikel Terbaru</h2>
<?php foreach ($posts as $post): ?>
<article class="post">
<h3><a href="/post/<?= e($post['slug']) ?>"><?= e($post['judul']) ?></a></h3>
<p><?= e($post['ringkasan']) ?></p>
<small>Diterbitkan <?= e($post['dibuat_pada']) ?></small>
</article>
<?php endforeach; ?>
</section>
templates/layout.php
Unduh berkas<?php
/**
* templates/layout.php — kerangka HTML yang membingkai setiap halaman.
*
* Template ini tidak menghasilkan konten sendiri; ia menerima dua variabel:
* $judul (untuk elemen <title>) dan $konten (fragmen HTML dari template
* halaman). Keduanya punya nilai bawaan supaya berkas ini tetap aman
* di-include tanpa data — misalnya saat diperiksa langsung lewat CLI.
*/
if (!function_exists('e')) {
/** Escape teks untuk HTML — dipakai di semua template. */
function e(?string $nilai): string
{
return htmlspecialchars($nilai ?? '', ENT_QUOTES, 'UTF-8');
}
}
$judul ??= 'Blog PHP';
$konten ??= '';
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= e($judul) ?> — Blog PHP</title>
</head>
<body>
<header class="site-header">
<strong><a href="/">Blog PHP</a></strong>
<nav>
<a href="/">Beranda</a> |
<a href="/tentang">Tentang</a>
</nav>
</header>
<main class="site-main">
<?= $konten ?>
</main>
<footer class="site-footer">
<p>Dibangun dengan PHP murni — proyek kursus "Website Blog".</p>
</footer>
</body>
</html>