<?xml version="1.0"?>
<rss version="2.0"><channel><title>Articles: Platform web: Blogger, WordPress, forum, dan template</title><link>https://lalu.pro/articles/platform-web/?d=1</link><description>Articles: Platform web: Blogger, WordPress, forum, dan template</description><language>en</language><item><title>Vibe Coding: Ilusi Produktivitas yang Menguras Waktu dan Biaya</title><link>https://lalu.pro/articles/platform-web/vibe-coding-ilusi-produktivitas-yang-menguras-waktu-dan-biaya-r6/</link><description><![CDATA[
<p><img src="https://lalu.pro/uploads/monthly_2026_07/vibe_coding_hobi_mahal.avif.949bce75e353578d136499a8147a7c88.avif" /></p>
<h3>Pendahuluan: Mengapa Artikel Ini Penting Dibaca</h3><p><em>Vibe coding</em>—praktik menggunakan AI untuk menghasilkan kode tanpa pemahaman mendalam—telah menjadi tren signifikan di kalangan developer dan <em>entrepreneur</em>. Namun, realitas di lapangan menunjukkan bahwa mayoritas proyek yang dihasilkan melalui metode ini tidak pernah mencapai tahap produksi. Bahkan jika berhasil dirilis, banyak yang gagal mendapatkan adopsi pasar. Artikel ini menguraikan mengapa fenomena ini terjadi dan berapa biaya sebenarnya yang harus dibayar oleh para praktisinya.</p><h3>Poin Utama: Vibe Coding Bukan Solusi, Melainkan Hobi Mahal</h3><p>Kesalahpahaman terbesar seputar <em>vibe coding</em> adalah persepsi bahwa AI telah mengeliminasi semua hambatan untuk membangun sebuah produk. Kenyataannya, AI hanya menghilangkan hambatan teknis—bukan hambatan pasar.</p><p>Meskipun seseorang kini bisa menghasilkan kode dengan jauh lebih cepat, mereka rata-rata menghabiskan $1.200 hingga $4.000 per tahun untuk menghasilkan produk yang berakhir dengan tiga nasib tragis:</p><ul><li><p>Mangkrak di tengah jalan (terjebak di tahap pengembangan).</p></li><li><p>Tidak pernah dirilis sama sekali.</p></li><li><p>Dirilis, tetapi tidak ada permintaan pasar.</p></li></ul><p>Ini bukan masalah ketidakmampuan teknis. Ini tentang jebakan psikologis yang menganggap kesibukan (<em>motion</em>) sebagai sebuah kemajuan (<em>progress</em>).</p><h3>Bagian 1: Dua Nasib yang Menunggu Produk Hasil Vibe-Coding</h3><h4>Nasib Pertama: Ditinggalkan di Tengah Jalan</h4><p>Mayoritas proyek <em>vibe-coded</em> tidak pernah selesai. Pola umum yang sering teramati adalah:</p><ul><li><p><strong>Fase Awal:</strong> Antusiasme tinggi, produksi kode berjalan sangat cepat.</p></li><li><p><strong>Fase Tengah:</strong> Momentum mulai menurun setelah mencapai target (<em>milestone</em>) tertentu dan mulai menemui eror yang rumit.</p></li><li><p><strong>Fase Akhir:</strong> Proyek ditinggalkan begitu saja demi mengejar ide baru yang terasa lebih "segar".</p></li></ul><p>Fenomena ini menciptakan apa yang disebut <em>“vibe cemetery”</em>—kumpulan proyek yang hampir selesai tetapi tidak pernah diluncurkan. Dampaknya bukan hanya pemborosan finansial, melainkan juga memicu siklus kegagalan berulang yang mengikis kesehatan mental pelakunya.</p><p><img class="ipsImage ipsImage_thumbnailed ipsRichText__align--block" data-fileid="8" src="https://lalu.pro/uploads/monthly_2026_07/Vibe_Coding.thumb.avif.058af756f5bc82d8ad4d292be9923f4c.avif" alt="Vibe_Coding.avif" title="Vibe_Coding.avif" width="1000" height="454" data-full-image="https://lalu.pro/uploads/monthly_2026_07/Vibe_Coding.avif.c13b6f12b5b815b4ee126f684cd4986c.avif" data-extension="core_Attachment" loading="lazy"></p><h4>Nasib Kedua: Peluncuran Tanpa Pengguna</h4><p>Skenario alternatifnya—di mana produk benar-benar dirilis—justru bisa terasa lebih menyakitkan. Setelah menginvestasikan waktu dan sumber daya yang besar, sang pencipta produk harus menghadapi kenyataan bahwa:</p><ul><li><p>Pasar tidak membutuhkan solusi yang mereka tawarkan.</p></li><li><p>Kompetitor yang sudah mapan telah mendominasi ceruk (<em>niche</em>) tersebut.</p></li><li><p>Pengguna dapat merasakan pendekatan instan (<em>low-effort approach</em>) dalam desain dan fungsionalitas produk.</p></li></ul><p>Tren terbaru menunjukkan bahwa pengguna mulai mengalami <em>“vibe fatigue”</em>—kelelahan menghadapi banjiran produk baru yang terasa seperti cetakan <em>template</em> AI, alih-alih sebuah solusi yang dipikirkan matang-matang.</p><h3>Bagian 2: Analisis Biaya Tersembunyi yang Jarang Diperhitungkan</h3><h4>Biaya Finansial</h4><p>Seorang praktisi <em>vibe coding</em> yang serius biasanya mengalokasikan anggaran yang tidak sedikit:</p><ul><li><p><strong>Kelas Pemula:</strong> ~$100/bulan untuk langganan berbagai alat AI (sekitar $1.200 setahun).</p></li><li><p><strong>Kelas Menengah:</strong> Langganan beberapa lisensi sekaligus yang bisa mencapai $2.400 setahun.</p></li><li><p><strong>Kelas Ekstrem:</strong> Menggunakan <em>agentic loops</em> terparalel dengan pengeluaran token mencapai $2.000–$4.000 per bulan.</p></li></ul><p>Itu perkiraan untuk wilayah Eropa dan Amerika yang menggunakan mata uang dolar. Sementara wilayah lain yang tidak menggunakan mata uang dolar menurut saya menghabiskan lebih sedikit.</p><p>Sering kali pengeluaran ini menghasilkan nol konversi yang berhasil dikomersialkan.</p><h4>Biaya Waktu dan Kesehatan Mental</h4><p><em>Vibe coding</em> memiliki karakteristik yang adiktif. Sensasi produktivitas semu (<em>false productivity</em>) yang dihasilkan—melihat baris kode terbangkitkan secara otomatis dengan cepat—memberikan letupan dopamin yang mirip seperti bermain <em>video game</em>, bukan seperti bekerja nyata. Konsekuensinya:</p><ul><li><p>Waktu berharga teralihkan dari interaksi sosial, aktivitas fisik, dan relaksasi.</p></li><li><p>Siklus proyek mangkrak yang berulang menciptakan beban psikologis negatif.</p></li><li><p>Munculnya rasa pencapaian semu (<em>false sense of accomplishment</em>) yang tidak pernah diterjemahkan menjadi keberhasilan nyata.</p></li></ul><h4>Biaya Peluang (<em>Opportunity Cost</em>)</h4><p>Sumber daya yang dihabiskan untuk proyek <em>vibe-coded</em>—baik waktu, fokus, maupun uang—seharusnya bisa dialokasikan untuk aktivitas lain yang jauh lebih produktif, bernilai, atau menghasilkan keuntungan nyata.</p><h3>Bagian 3: Mengapa Pasar Menolak Produk Vibe-Coded?</h3><h4>Kejenuhan Pasar dan Hilangnya Keunikan</h4><p>Premis fundamental <em>vibe coding</em> adalah bahwa AI menghilangkan hambatan untuk membangun (<em>building</em>). Namun, premis ini mengabaikan hukum ekonomi dasar: <strong>jika semua orang bisa membuat suatu produk dengan mudah, maka produk tersebut tidak lagi memiliki keunikan.</strong></p><p>Ketika perusahaan AI mendorong adopsi massal, dampaknya adalah:</p><ul><li><p>Ribuan produk serupa muncul secara bersamaan dengan tawaran nilai (<em>value proposition</em>) yang identik.</p></li><li><p>Pengguna akhir mengalami kelumpuhan pilihan (<em>choice paralysis</em>), yang membuat mereka cenderung tetap memilih produk lama yang sudah tepercaya.</p></li><li><p><em>Startup</em> dengan modal besar dan tim berpengalaman tetap mendominasi setiap kategori.</p></li></ul><h4>Pentingnya Momentum dan Konteks Pasar</h4><p>Kesuksesan produk digital sering kali tidak terletak pada kecanggihan teknis atau keaslian ide, melainkan pada momentum (<em>timing</em>) masuk ke pasar.</p><p>Sebagai gambaran, seorang pengembang aplikasi untuk iOS bisa dengan mudah mendatangkan ratusan ribu unduhan, dulu ketika pasar masih baru dan sepi. Namun, mencoba hal yang sama hari ini akan menghasilkan angka yang mendekati nol karena saturasi pasar yang luar biasa padat. Secepat apa pun AI menghasilkan kode Anda, teknologi tersebut tidak bisa mempercepat kesiapan pasar (<em>market readiness</em>).</p><h3>Bagian 4: Satu-Satunya Model yang Viable: Bangun untuk Diri Sendiri</h3><h4>Kriteria Produk yang Layak Dipertahankan</h4><p>Analisis mendalam terhadap produk-produk independen yang bertahan lama menunjukkan satu pola universal: produk yang terus berkembang adalah produk yang <strong>menyelesaikan masalah nyata dari si pembuatnya sendiri.</strong></p><p>Tanda bahwa proyek Anda layak dilanjutkan:</p><ul><li><p><strong>Kebutuhan Nyata:</strong> Anda menggunakan produk tersebut setiap hari untuk produktivitas pribadi.</p></li><li><p><strong>Didorong Gairah (<em>Passion-Driven</em>):</strong> Anda rela melakukan pemeliharaan (<em>maintenance</em>) dan perbaikan bug tanpa kompensasi finansial instan.</p></li><li><p><strong>Pengembangan Iteratif:</strong> Pembaruan fitur didorong oleh pengalaman penggunaan personal, bukan sekadar menebak-nebak keinginan pasar.</p></li></ul><h4>Jebakan Pola Pikir Optimalisasi</h4><p>Banyak orang terjebak dalam logika bahwa peningkatan efisiensi kecil (misalnya membuat alur kerja 15% lebih cepat) adalah alasan kuat untuk terus mengembangkan sebuah produk komersial.</p><p>Kenyataannya, antusiasme awal akan selalu menurun seiring berjalannya waktu, sementara tingkat kesulitan teknis yang muncul belakangan justru semakin meningkat. Produk yang hanya menawarkan nilai tambah kecil (<em>incremental value</em>) lambat laun akan ditinggalkan oleh pembuatnya sendiri karena kelelahan emosional.</p><h3>Bagian 5: Konteks Industri dan Langkah ke Depan</h3><h4>Komoditisasi AI dan Dampak Jangka Panjang</h4><p>Model AI saat ini bukan lagi benteng pertahanan bisnis (<em>competitive moat</em>). Kehadiran model-model sumber terbuka (<em>open-source</em>) yang semakin kuat dan efisien dari berbagai negara membuat kemampuan dasar menghasilkan kode menjadi komoditas murah.</p><p>Bagi para <em>vibe coders</em>, ini berarti:</p><ul><li><p>Kualitas keluaran (<em>output</em>) dari model gratisan/lokal akan segera menyamai model komersial terkemuka dalam waktu dekat.</p></li><li><p>Saturasi pasar akan semakin parah, memperburuk masalah diferensiasi produk yang dialami oleh produk <em>vibe-coded</em>.</p></li></ul><h4>Jalan Keluar yang Berkelanjutan (<em>Sustainable Path Forward</em>)</h4><p>Bagi Anda yang tetap ingin berkreasi menggunakan bantuan AI, ubah strateginya:</p><ol><li><p><strong>Turunkan Ekspektasi:</strong> Fokuslah membangun alat (<em>tools</em>) yang nyata-nyata Anda gunakan sendiri, bukan alat yang "Anda pikir" akan dibeli orang lain.</p></li><li><p><strong>Terima Realitas Pasar:</strong> Probabilitas membangun perusahaan bernilai miliaran dolar secara instan lewat <em>vibe coding</em> sangatlah kecil. Alihkan fokus pada utilitas personal yang konkret.</p></li><li><p><strong>Diversifikasi Penggunaan AI:</strong> Gunakan AI untuk pemecahan masalah yang memiliki dampak nyata pada efisiensi kerja harian Anda, alih-alih terus-menerus memproduksi aplikasi konsumen (<em>consumer apps</em>) baru yang generik.</p></li></ol><h3>Kesimpulan: Mengevaluasi Ulang Nilai Vibe Coding</h3><p><em>Vibe coding</em> tidak gagal karena teknologi AI-nya buruk. Sebaliknya, fenomena ini gagal karena memberikan <strong>ilusi</strong> bahwa menyingkirkan hambatan teknis sama dengan menyingkirkan hambatan pasar.</p><p>Kenyataan pahit yang harus diterima:</p><ul><li><p>Hambatan pasar jauh lebih besar dan kejam daripada hambatan menulis kode.</p></li><li><p>Sibuk menghasilkan baris kode (<em>motion</em>) tidak sama dengan membuat kemajuan produk (<em>progress</em>).</p></li><li><p>Tingkat penyelesaian (<em>completion rate</em>) hingga produk sukses dari metode <em>vibe-coding</em> murni mendekati nol persen.</p></li></ul><p>Sebelum Anda memulai proyek koding berbasis AI berikutnya, jawablah pertanyaan ini dengan jujur:</p><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><em>"Apakah saya membangun ini karena saya benar-benar membutuhkannya, atau saya hanya ingin merasa menjadi seorang entrepreneur yang terlihat produktif di media sosial?"</em></p></div></blockquote><p>Jika jawabannya adalah yang kedua, artikel ini adalah pengingat untuk menata ulang prioritas Anda. Namun, jika jawabannya adalah yang pertama, silakan lanjutkan—tetapi dengan ekspektasi yang realistis dan fokus pada kebermanfaatan bagi diri sendiri.</p><p>Apakah Anda sedang terjebak atau justru berkembang dalam tren <em>vibe coding</em> ini? Mari refleksikan strategi Anda. Bagikan pengalaman, insights, atau sudut pandang Anda di kolom komentar!</p><p>Saya juga menulis di Blog tentang topik ini.</p><div class="ipsEmbeddedOther" data-og-user_text="https://lalu.pro/blogs/entry/1-apakah-saya-vibe-coding/"><iframe src="https://lalu.pro/blogs/entry/1-apakah-saya-vibe-coding/?do=embed" data-embedcontent="" data-internalembed="" data-controller="core.front.core.autosizeiframe" data-embedauthorid="1" data-ipsembed-contentapp="blog" data-ipsembed-contentclass="blog_Entry" data-ipsembed-contentid="1" data-ipsembed-timestamp="1790027533" allowfullscreen="" data-og-user_text="https://lalu.pro/blogs/entry/1-apakah-saya-vibe-coding/" loading="lazy"></iframe></div>]]></description><guid isPermaLink="false">6</guid><pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate></item><item><title>Membuat Template Blogger Mulai dari Nol</title><link>https://lalu.pro/articles/platform-web/membuat-template-blogger-mulai-dari-nol-r5/</link><description><![CDATA[<blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Tujuan:</strong> Membangun template Blogger dari kerangka kosong secara bertahap — mulai dari header, footer, sidebar, hingga area postingan — sehingga pemula bisa melihat langsung perubahan yang terjadi di blog mereka.</p></div></blockquote><hr><h2>Prasyarat</h2><ul><li><p>Akun Google &amp; blog Blogger yang sudah dibuat</p></li><li><p>Akses ke <strong>Tema → Edit HTML</strong> di dashboard Blogger</p></li><li><p>Browser (disarankan Chrome/Firefox)</p></li><li><p>Semangat belajar</p></li></ul><hr><h2>Cara Mengganti Template di Blogger</h2><ol><li><p>Login ke <a rel="external nofollow" href="https://www.blogger.com/">blogger.com</a></p></li><li><p>Pilih blog Anda → klik <strong>Tema</strong> di menu kiri</p></li><li><p>Klik tombol <strong>Edit HTML</strong> (ikon pensil)</p></li><li><p>Hapus semua kode yang ada</p></li><li><p>Tempel kode baru dari tutorial ini</p></li><li><p>Klik <strong>Simpan tema</strong></p></li><li><p>Buka blog Anda di tab baru untuk melihat hasilnya</p></li></ol><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><span class="ipsEmoji" title="">💡</span> <strong>Tips:</strong> Buka blog Anda di tab terpisah, lalu setiap selesai menyimpan, refresh tab tersebut untuk melihat perubahannya langsung.</p></div></blockquote><hr><h2>Tahap 1 — Kerangka Dasar (Hanya Tulang)</h2><p>Kita mulai dari kerangka paling sederhana: hanya struktur HTML tanpa konten apapun. Ini untuk memastikan template terbaca oleh Blogger tanpa error.</p><pre spellcheck="" class="ipsCode language-xml" data-language="HTML/XML"><code>&lt;?xml version="1.0" encoding="UTF-8" ?&gt;
&lt;!DOCTYPE html&gt;
&lt;html b:css='false' b:layoutsversion='3'
  xmlns='http://www.w3.org/1999/xhtml'
  xmlns:b='http://www.google.com/2005/gml/b'
  xmlns:data='http://www.google.com/2005/gml/data'
  xmlns:expr='http://www.google.com/2005/gml/expr'&gt;

&lt;head&gt;
  &lt;meta charset='utf-8'/&gt;
  &lt;meta content='width=device-width,initial-scale=1' name='viewport'/&gt;
  &lt;title&gt;&lt;data:view.title.escape/&gt;&lt;/title&gt;

  &lt;b:skin&gt;&lt;![CDATA[
    body {
      font-family: sans-serif;
      margin: 0;
      padding: 0;
      background-color: #f4f4f4;
      color: #333;
    }
    .wrapper {
      max-width: 1200px;
      margin: 0 auto;
      padding: 20px;
    }
  ]]&gt;&lt;/b:skin&gt;
&lt;/head&gt;

&lt;body&gt;
  &lt;div class='wrapper'&gt;
    &lt;p&gt;Blog saya sedang dibangun...&lt;/p&gt;
  &lt;/div&gt;
&lt;/body&gt;

&lt;/html&gt;
</code></pre><h3>Apa yang terjadi?</h3><p>Simpan dan refresh blog Anda. Anda akan melihat halaman putih dengan tulisan <strong>"Blog saya sedang dibangun..."</strong></p><p>Ini artinya:</p><ul><li><p>Blogger menerima template Anda</p></li><li><p>Judul tab browser sudah menggunakan nama blog Anda (lihat <code>&lt;data:view.title.escape/&gt;</code>)</p></li><li><p>CSS dasar sudah aktif</p></li></ul><h3>Penjelasan Kode Penting</h3><div class="ipsRichText__table-wrapper"><table style="width: 692px;"><colgroup><col style="width:336px;"><col style="width:356px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Kode</p></th><th colspan="1" rowspan="1"><p>Fungsi</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>b:css='false'</code></p></td><td colspan="1" rowspan="1"><p>Mematikan CSS bawaan Blogger agar kita pakai CSS sendiri</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>b:layoutsversion='3'</code></p></td><td colspan="1" rowspan="1"><p>Menggunakan versi layout Blogger terbaru</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>xmlns:b</code>, <code>xmlns:data</code>, <code>xmlns:expr</code></p></td><td colspan="1" rowspan="1"><p>Namespace yang dibutuhkan agar tag Blogger (<code>b:section</code>, <code>data:</code>, dll) bisa dibaca</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>&lt;data:view.title.escape/&gt;</code></p></td><td colspan="1" rowspan="1"><p>Tag Blogger untuk menampilkan judul halaman secara otomatis</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>&lt;b:skin&gt;&lt;![CDATA[ ... ]]&gt;&lt;/b:skin&gt;</code></p></td><td colspan="1" rowspan="1"><p>Tempat menulis CSS utama template</p></td></tr></tbody></table></div><hr><h2>Tahap 2 — Menambahkan Header</h2><p>Sekarang kita tambahkan <strong>header</strong> — bagian paling atas blog yang biasanya berisi nama blog dan deskripsi.</p><p>Ganti seluruh kode dengan yang berikut:</p><pre spellcheck="" class="ipsCode language-xml" data-language="HTML/XML"><code>&lt;?xml version="1.0" encoding="UTF-8" ?&gt;
&lt;!DOCTYPE html&gt;
&lt;html b:css='false' b:layoutsversion='3'
  xmlns='http://www.w3.org/1999/xhtml'
  xmlns:b='http://www.google.com/2005/gml/b'
  xmlns:data='http://www.google.com/2005/gml/data'
  xmlns:expr='http://www.google.com/2005/gml/expr'&gt;

&lt;head&gt;
  &lt;meta charset='utf-8'/&gt;
  &lt;meta content='width=device-width,initial-scale=1' name='viewport'/&gt;
  &lt;title&gt;&lt;data:view.title.escape/&gt;&lt;/title&gt;

  &lt;b:skin&gt;&lt;![CDATA[
    * { box-sizing: border-box; }
    body {
      font-family: sans-serif;
      margin: 0;
      padding: 0;
      background-color: #f4f4f4;
      color: #333;
    }
    .wrapper {
      max-width: 1200px;
      margin: 0 auto;
      padding: 20px;
    }

    /* === HEADER === */
    header {
      background: #2c3e50;
      color: #fff;
      padding: 30px 20px;
      margin-bottom: 20px;
      border-radius: 6px;
    }
    header a {
      color: #fff;
      text-decoration: none;
    }
  ]]&gt;&lt;/b:skin&gt;
&lt;/head&gt;

&lt;body&gt;
  &lt;div class='wrapper'&gt;

    &lt;header role='banner'&gt;
      &lt;b:section class='header-section' id='header-section' maxwidgets='1' showaddelement='yes'/&gt;
    &lt;/header&gt;

  &lt;/div&gt;
&lt;/body&gt;

&lt;/html&gt;
</code></pre><h3>Cara Menambahkan Widget Header</h3><p>Setelah menyimpan dan refresh blog, Anda akan melihat <strong>area gelap di atas</strong> (header). Tapi masih kosong! Sekarang:</p><ol><li><p>Kembali ke dashboard Blogger → <strong>Tata Letak</strong></p></li><li><p>Anda akan melihat kotak bertuliskan <strong>"header-section"</strong></p></li><li><p>Klik tombol <strong>Tambah Widget</strong> di kotak header</p></li><li><p>Pilih widget <strong>Header</strong> → Simpan</p></li><li><p>Refresh blog Anda</p></li></ol><p><strong>Hasilnya:</strong> Nama blog dan deskripsi muncul di header dengan latar belakang gelap.</p><h3>Penjelasan <code>&lt;b:section&gt;</code></h3><pre spellcheck="" class="ipsCode language-xml" data-language="HTML/XML"><code>&lt;b:section id='header-section' maxwidgets='1' showaddelement='yes'/&gt;
</code></pre><div class="ipsRichText__table-wrapper"><table style="width: 554px;"><colgroup><col style="width:207px;"><col style="width:347px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Atribut</p></th><th colspan="1" rowspan="1"><p>Fungsi</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>id</code></p></td><td colspan="1" rowspan="1"><p>Nama unik untuk section ini (wajib, tidak boleh sama)</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>maxwidgets='1'</code></p></td><td colspan="1" rowspan="1"><p>Membatasi hanya 1 widget yang bisa ditambahkan di sini</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>showaddelement='yes'</code></p></td><td colspan="1" rowspan="1"><p>Menampilkan tombol "Tambah Widget" di mode Tata Letak</p></td></tr></tbody></table></div><hr><h2>Tahap 3 — Menambahkan Footer</h2><p>Footer adalah bagian paling bawah blog. Mari kita tambahkan sekarang.</p><p>Ganti seluruh kode dengan:</p><pre spellcheck="" class="ipsCode language-xml" data-language="HTML/XML"><code>&lt;?xml version="1.0" encoding="UTF-8" ?&gt;
&lt;!DOCTYPE html&gt;
&lt;html b:css='false' b:layoutsversion='3'
  xmlns='http://www.w3.org/1999/xhtml'
  xmlns:b='http://www.google.com/2005/gml/b'
  xmlns:data='http://www.google.com/2005/gml/data'
  xmlns:expr='http://www.google.com/2005/gml/expr'&gt;

&lt;head&gt;
  &lt;meta charset='utf-8'/&gt;
  &lt;meta content='width=device-width,initial-scale=1' name='viewport'/&gt;
  &lt;title&gt;&lt;data:view.title.escape/&gt;&lt;/title&gt;

  &lt;b:skin&gt;&lt;![CDATA[
    * { box-sizing: border-box; }
    body {
      font-family: sans-serif;
      margin: 0;
      padding: 0;
      background-color: #f4f4f4;
      color: #333;
    }
    .wrapper {
      max-width: 1200px;
      margin: 0 auto;
      padding: 20px;
    }

    /* === HEADER === */
    header {
      background: #2c3e50;
      color: #fff;
      padding: 30px 20px;
      margin-bottom: 20px;
      border-radius: 6px;
    }
    header a {
      color: #fff;
      text-decoration: none;
    }

    /* === FOOTER === */
    footer {
      background: #2c3e50;
      color: #ccc;
      padding: 20px;
      margin-top: 20px;
      border-radius: 6px;
      text-align: center;
      font-size: 0.9em;
    }
    footer a {
      color: #aed6f1;
      text-decoration: none;
    }
  ]]&gt;&lt;/b:skin&gt;
&lt;/head&gt;

&lt;body&gt;
  &lt;div class='wrapper'&gt;

    &lt;header role='banner'&gt;
      &lt;b:section class='header-section' id='header-section' maxwidgets='1' showaddelement='yes'/&gt;
    &lt;/header&gt;

    &lt;!-- Area tengah (isi &amp; sidebar) akan ditambahkan nanti --&gt;
    &lt;div style='background:#fff; padding:20px; border-radius:6px; min-height:200px; margin-bottom:20px;'&gt;
      &lt;p style='color:#999; text-align:center;'&gt;[ Area Postingan &amp;amp; Sidebar — segera hadir ]&lt;/p&gt;
    &lt;/div&gt;

    &lt;footer role='contentinfo'&gt;
      &lt;b:section class='footer-section' id='footer-section' showaddelement='yes'/&gt;
    &lt;/footer&gt;

  &lt;/div&gt;
&lt;/body&gt;

&lt;/html&gt;
</code></pre><h3>Cara Menambahkan Widget Footer</h3><ol><li><p>Kembali ke <strong>Tata Letak</strong></p></li><li><p>Di area <strong>footer-section</strong>, klik <strong>Tambah Widget</strong></p></li><li><p>Pilih <strong>Teks</strong> → ketik "<span class="ipsEmoji">©</span> 2025 Blog Saya. Hak cipta dilindungi." → Simpan</p></li><li><p>Refresh blog</p></li></ol><p><strong>Hasilnya:</strong> Sekarang blog Anda sudah punya header di atas, area kosong di tengah (placeholder), dan footer di bawah.</p><hr><h2>Tahap 4 — Menambahkan Sidebar</h2><p>Sekarang kita bentuk layout dua kolom: <strong>konten utama</strong> di kiri dan <strong>sidebar</strong> di kanan.</p><p>Ganti seluruh kode dengan:</p><pre spellcheck="" class="ipsCode language-xml" data-language="HTML/XML"><code>&lt;?xml version="1.0" encoding="UTF-8" ?&gt;
&lt;!DOCTYPE html&gt;
&lt;html b:css='false' b:layoutsversion='3'
  xmlns='http://www.w3.org/1999/xhtml'
  xmlns:b='http://www.google.com/2005/gml/b'
  xmlns:data='http://www.google.com/2005/gml/data'
  xmlns:expr='http://www.google.com/2005/gml/expr'&gt;

&lt;head&gt;
  &lt;meta charset='utf-8'/&gt;
  &lt;meta content='width=device-width,initial-scale=1' name='viewport'/&gt;
  &lt;title&gt;&lt;data:view.title.escape/&gt;&lt;/title&gt;

  &lt;b:skin&gt;&lt;![CDATA[
    * { box-sizing: border-box; }
    body {
      font-family: sans-serif;
      margin: 0;
      padding: 0;
      background-color: #f4f4f4;
      color: #333;
    }
    .wrapper {
      max-width: 1200px;
      margin: 0 auto;
      padding: 20px;
    }

    /* === HEADER === */
    header {
      background: #2c3e50;
      color: #fff;
      padding: 30px 20px;
      margin-bottom: 20px;
      border-radius: 6px;
    }
    header a {
      color: #fff;
      text-decoration: none;
    }

    /* === LAYOUT TENGAH === */
    .main-container {
      display: flex;
      flex-direction: column;
      gap: 20px;
      margin-bottom: 20px;
    }
    @media (min-width: 768px) {
      .main-container {
        flex-direction: row;
      }
    }

    /* === AREA KONTEN UTAMA === */
    main {
      flex: 3;
      background: #fff;
      padding: 20px;
      border-radius: 6px;
      min-height: 300px;
    }

    /* === SIDEBAR === */
    aside {
      flex: 1;
      background: #fff;
      padding: 20px;
      border-radius: 6px;
    }
    aside h2, aside h3 {
      font-size: 1em;
      border-bottom: 2px solid #2c3e50;
      padding-bottom: 6px;
      margin-top: 0;
    }

    /* === FOOTER === */
    footer {
      background: #2c3e50;
      color: #ccc;
      padding: 20px;
      border-radius: 6px;
      text-align: center;
      font-size: 0.9em;
    }
    footer a {
      color: #aed6f1;
      text-decoration: none;
    }
  ]]&gt;&lt;/b:skin&gt;
&lt;/head&gt;

&lt;body&gt;
  &lt;div class='wrapper'&gt;

    &lt;header role='banner'&gt;
      &lt;b:section class='header-section' id='header-section' maxwidgets='1' showaddelement='yes'/&gt;
    &lt;/header&gt;

    &lt;div class='main-container'&gt;

      &lt;main id='main-content' role='main'&gt;
        &lt;!-- Postingan akan ditambahkan di tahap berikutnya --&gt;
        &lt;p style='color:#999; text-align:center;'&gt;[ Area Postingan — segera hadir ]&lt;/p&gt;
      &lt;/main&gt;

      &lt;aside id='sidebar' role='complementary'&gt;
        &lt;b:section class='sidebar-section' id='sidebar-section' showaddelement='yes'/&gt;
      &lt;/aside&gt;

    &lt;/div&gt;

    &lt;footer role='contentinfo'&gt;
      &lt;b:section class='footer-section' id='footer-section' showaddelement='yes'/&gt;
    &lt;/footer&gt;

  &lt;/div&gt;
&lt;/body&gt;

&lt;/html&gt;
</code></pre><h3>Cara Mengisi Sidebar</h3><ol><li><p>Buka <strong>Tata Letak</strong> di dashboard</p></li><li><p>Di kotak <strong>sidebar-section</strong>, klik <strong>Tambah Widget</strong></p></li><li><p>Coba tambahkan:</p><ul><li><p><strong>Arsip Blog</strong> — menampilkan arsip postingan per bulan</p></li><li><p><strong>Label</strong> — menampilkan semua label/kategori</p></li><li><p><strong>Tentang Saya</strong> — profil singkat Anda</p></li></ul></li><li><p>Simpan dan refresh blog</p></li></ol><p><strong>Hasilnya:</strong> Blog Anda sekarang punya dua kolom — kiri untuk konten, kanan untuk sidebar. Di layar HP (&lt; 768px), keduanya akan tampil berurutan ke bawah secara otomatis berkat CSS <code>flexbox</code> + <code>@media</code>.</p><h3>Memahami Flexbox yang Digunakan</h3><pre spellcheck="" class="ipsCode language-css" data-language="CSS"><code>.main-container {
  display: flex;        /* aktifkan flexbox */
  flex-direction: column; /* default: kolom (untuk HP) */
  gap: 20px;
}
@media (min-width: 768px) {
  .main-container {
    flex-direction: row; /* di layar lebar: jadi baris (kolom-kolom berdampingan) */
  }
}

main  { flex: 3; } /* ambil 3 bagian dari ruang tersedia */
aside { flex: 1; } /* ambil 1 bagian dari ruang tersedia */
</code></pre><p>Artinya: main mendapat <strong>75%</strong> lebar, sidebar mendapat <strong>25%</strong> lebar.</p><hr><h2>Tahap 5 — Menambahkan Area Postingan (Blog1)</h2><p>Ini tahap terakhir dan paling penting. Kita akan mengganti placeholder dengan <code>&lt;b:widget id='Blog1'&gt;</code> yang menampilkan postingan sungguhan.</p><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><span class="ipsEmoji" title="">⚠️</span> <strong>Perhatian:</strong> Widget Blog1 memang menghasilkan kode yang panjang. Tapi di tahap ini Anda sudah paham strukturnya, jadi tidak perlu khawatir.</p></div></blockquote><p>Ganti seluruh kode dengan versi <strong>final</strong> berikut:</p><pre spellcheck="" class="ipsCode language-xml" data-language="HTML/XML"><code>&lt;?xml version="1.0" encoding="UTF-8" ?&gt;
&lt;!DOCTYPE html&gt;
&lt;html b:css='false' b:layoutsversion='3'
  xmlns='http://www.w3.org/1999/xhtml'
  xmlns:b='http://www.google.com/2005/gml/b'
  xmlns:data='http://www.google.com/2005/gml/data'
  xmlns:expr='http://www.google.com/2005/gml/expr'&gt;

&lt;head&gt;
  &lt;meta charset='utf-8'/&gt;
  &lt;meta content='width=device-width,initial-scale=1' name='viewport'/&gt;
  &lt;title&gt;&lt;data:view.title.escape/&gt;&lt;/title&gt;

  &lt;b:skin&gt;&lt;![CDATA[
    * { box-sizing: border-box; }
    body {
      font-family: sans-serif;
      margin: 0;
      padding: 0;
      background-color: #f4f4f4;
      color: #333;
    }
    .wrapper {
      max-width: 1200px;
      margin: 0 auto;
      padding: 20px;
    }

    /* === HEADER === */
    header {
      background: #2c3e50;
      color: #fff;
      padding: 30px 20px;
      margin-bottom: 20px;
      border-radius: 6px;
    }
    header a {
      color: #fff;
      text-decoration: none;
    }

    /* === LAYOUT TENGAH === */
    .main-container {
      display: flex;
      flex-direction: column;
      gap: 20px;
      margin-bottom: 20px;
    }
    @media (min-width: 768px) {
      .main-container {
        flex-direction: row;
      }
    }

    /* === AREA KONTEN UTAMA === */
    main {
      flex: 3;
      background: #fff;
      padding: 20px;
      border-radius: 6px;
    }

    /* === POSTINGAN === */
    .post {
      margin-bottom: 40px;
      padding-bottom: 40px;
      border-bottom: 1px solid #eee;
    }
    .post:last-child {
      border-bottom: none;
      margin-bottom: 0;
    }
    .post-title {
      font-size: 1.5em;
      margin: 0 0 8px 0;
    }
    .post-title a {
      color: #2c3e50;
      text-decoration: none;
    }
    .post-title a:hover {
      color: #e74c3c;
    }
    .post-meta {
      font-size: 0.85em;
      color: #888;
      margin-bottom: 14px;
    }
    .post-body {
      line-height: 1.7;
    }
    .post-labels {
      margin-top: 12px;
      font-size: 0.85em;
    }
    .post-labels a {
      background: #eaf2ff;
      color: #2980b9;
      padding: 2px 8px;
      border-radius: 12px;
      text-decoration: none;
      margin-right: 4px;
    }

    /* === NAVIGASI HALAMAN === */
    .blog-pager {
      display: flex;
      justify-content: space-between;
      margin-top: 20px;
      font-size: 0.9em;
    }
    .blog-pager a {
      background: #2c3e50;
      color: #fff;
      padding: 8px 16px;
      border-radius: 4px;
      text-decoration: none;
    }
    .blog-pager a:hover {
      background: #e74c3c;
    }

    /* === SIDEBAR === */
    aside {
      flex: 1;
      background: #fff;
      padding: 20px;
      border-radius: 6px;
    }
    aside h2, aside h3 {
      font-size: 1em;
      border-bottom: 2px solid #2c3e50;
      padding-bottom: 6px;
      margin-top: 0;
    }

    /* === FOOTER === */
    footer {
      background: #2c3e50;
      color: #ccc;
      padding: 20px;
      border-radius: 6px;
      text-align: center;
      font-size: 0.9em;
    }
    footer a {
      color: #aed6f1;
      text-decoration: none;
    }
  ]]&gt;&lt;/b:skin&gt;
&lt;/head&gt;

&lt;body&gt;
  &lt;div class='wrapper'&gt;

    &lt;header role='banner'&gt;
      &lt;b:section class='header-section' id='header-section' maxwidgets='1' showaddelement='yes'/&gt;
    &lt;/header&gt;

    &lt;div class='main-container'&gt;

      &lt;main id='main-content' role='main'&gt;
        &lt;b:section class='main-section' id='main-section' showaddelement='yes'&gt;

          &lt;b:widget id='Blog1' locked='false' title='Postingan Blog' type='Blog' version='2'&gt;
            &lt;b:widget-settings&gt;
              &lt;b:widget-setting name='showDateHeader'&gt;false&lt;/b:widget-setting&gt;
            &lt;/b:widget-settings&gt;
            &lt;b:includable id='main' var='top'&gt;

              &lt;!-- Loop semua postingan --&gt;
              &lt;b:loop values='data:posts' var='post'&gt;
                &lt;article class='post' expr:id='"post-" + data:post.id'&gt;

                  &lt;!-- Judul postingan --&gt;
                  &lt;h2 class='post-title'&gt;
                    &lt;a expr:href='data:post.url'&gt;&lt;data:post.title/&gt;&lt;/a&gt;
                  &lt;/h2&gt;

                  &lt;!-- Meta: tanggal &amp; penulis --&gt;
                  &lt;div class='post-meta'&gt;
                    &lt;data:post.dateHeader/&gt; &amp;#8226; oleh &lt;data:post.author/&gt;
                  &lt;/div&gt;

                  &lt;!-- Isi postingan --&gt;
                  &lt;div class='post-body'&gt;
                    &lt;b:if cond='data:view.isPost'&gt;
                      &lt;!-- Di halaman postingan tunggal: tampilkan semua isi --&gt;
                      &lt;data:post.body/&gt;
                    &lt;b:else/&gt;
                      &lt;!-- Di halaman beranda: tampilkan ringkasan / snippet --&gt;
                      &lt;data:post.snippets/&gt;
                      &lt;a expr:href='data:post.url'&gt;Baca selengkapnya &amp;#8594;&lt;/a&gt;
                    &lt;/b:if&gt;
                  &lt;/div&gt;

                  &lt;!-- Label / kategori --&gt;
                  &lt;b:if cond='data:post.labels'&gt;
                    &lt;div class='post-labels'&gt;
                      Label:
                      &lt;b:loop values='data:post.labels' var='label'&gt;
                        &lt;a expr:href='data:label.url'&gt;&lt;data:label.name/&gt;&lt;/a&gt;
                      &lt;/b:loop&gt;
                    &lt;/div&gt;
                  &lt;/b:if&gt;

                &lt;/article&gt;
              &lt;/b:loop&gt;

              &lt;!-- Navigasi halaman (Lebih Baru / Lebih Lama) --&gt;
              &lt;div class='blog-pager'&gt;
                &lt;b:if cond='data:newerPageUrl'&gt;
                  &lt;a expr:href='data:newerPageUrl'&gt;&amp;#8592; Postingan lebih baru&lt;/a&gt;
                &lt;/b:if&gt;
                &lt;b:if cond='data:olderPageUrl'&gt;
                  &lt;a expr:href='data:olderPageUrl'&gt;Postingan lebih lama &amp;#8594;&lt;/a&gt;
                &lt;/b:if&gt;
              &lt;/div&gt;

            &lt;/b:includable&gt;
          &lt;/b:widget&gt;

        &lt;/b:section&gt;
      &lt;/main&gt;

      &lt;aside id='sidebar' role='complementary'&gt;
        &lt;b:section class='sidebar-section' id='sidebar-section' showaddelement='yes'/&gt;
      &lt;/aside&gt;

    &lt;/div&gt;

    &lt;footer role='contentinfo'&gt;
      &lt;b:section class='footer-section' id='footer-section' showaddelement='yes'/&gt;
    &lt;/footer&gt;

  &lt;/div&gt;
&lt;/body&gt;

&lt;/html&gt;
</code></pre><h3>Cara Kerja Widget Blog1</h3><p>Widget ini berisi <strong>tag-tag Blogger</strong> untuk menampilkan data dari database blog Anda:</p><div class="ipsRichText__table-wrapper"><table style="width: 752px;"><colgroup><col style="width:389px;"><col style="width:363px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Tag</p></th><th colspan="1" rowspan="1"><p>Fungsi</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>&lt;b:loop values='data:posts' var='post'&gt;</code></p></td><td colspan="1" rowspan="1"><p>Mengulang (loop) untuk setiap postingan yang ada</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>&lt;data:post.title/&gt;</code></p></td><td colspan="1" rowspan="1"><p>Menampilkan judul postingan</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>&lt;data:post.url/&gt;</code></p></td><td colspan="1" rowspan="1"><p>URL / link ke postingan</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>&lt;data:post.dateHeader/&gt;</code></p></td><td colspan="1" rowspan="1"><p>Tanggal postingan</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>&lt;data:post.author/&gt;</code></p></td><td colspan="1" rowspan="1"><p>Nama penulis</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>&lt;data:post.body/&gt;</code></p></td><td colspan="1" rowspan="1"><p>Isi lengkap postingan</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>&lt;data:post.snippets/&gt;</code></p></td><td colspan="1" rowspan="1"><p>Ringkasan/preview postingan (untuk beranda)</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>&lt;b:if cond='data:view.isPost'&gt;</code></p></td><td colspan="1" rowspan="1"><p>Kondisi: apakah ini halaman postingan tunggal?</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>&lt;b:loop values='data:post.labels'&gt;</code></p></td><td colspan="1" rowspan="1"><p>Loop untuk setiap label postingan</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>data:newerPageUrl</code></p></td><td colspan="1" rowspan="1"><p>URL halaman postingan lebih baru</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>data:olderPageUrl</code></p></td><td colspan="1" rowspan="1"><p>URL halaman postingan lebih lama</p></td></tr></tbody></table></div><hr><h2>Ringkasan</h2><div class="ipsRichText__table-wrapper"><table style="min-width: 611px;"><colgroup><col style="min-width:20px;"><col style="width:269px;"><col style="width:322px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Tahap</p></th><th colspan="1" rowspan="1"><p>Yang Dilakukan</p></th><th colspan="1" rowspan="1"><p>Hasilnya</p></th></tr><tr><td colspan="1" rowspan="1"><p>1</p></td><td colspan="1" rowspan="1"><p>Kerangka HTML dasar</p></td><td colspan="1" rowspan="1"><p>Halaman terbaca, judul dari nama blog</p></td></tr><tr><td colspan="1" rowspan="1"><p>2</p></td><td colspan="1" rowspan="1"><p>Header + b:section</p></td><td colspan="1" rowspan="1"><p>Area header gelap, bisa tambah widget nama blog</p></td></tr><tr><td colspan="1" rowspan="1"><p>3</p></td><td colspan="1" rowspan="1"><p>Footer + placeholder tengah</p></td><td colspan="1" rowspan="1"><p>Blog punya kepala &amp; kaki</p></td></tr><tr><td colspan="1" rowspan="1"><p>4</p></td><td colspan="1" rowspan="1"><p>Sidebar + flexbox layout</p></td><td colspan="1" rowspan="1"><p>Dua kolom: konten &amp; sidebar</p></td></tr><tr><td colspan="1" rowspan="1"><p>5</p></td><td colspan="1" rowspan="1"><p>Widget Blog1</p></td><td colspan="1" rowspan="1"><p>Postingan asli tampil lengkap</p></td></tr></tbody></table></div><hr><h2>Langkah Selanjutnya</h2><p>Setelah template berjalan, Anda bisa mengembangkannya lebih lanjut:</p><ul><li><p><strong>Ubah warna</strong> — ganti nilai <code>#2c3e50</code> dan <code>#e74c3c</code> dengan warna pilihan Anda</p></li><li><p><strong>Ganti font</strong> — tambahkan Google Fonts di dalam <code>&lt;head&gt;</code></p></li><li><p><strong>Sempurnakan responsif</strong> — uji di berbagai ukuran layar</p></li><li><p><strong>Tambah SEO</strong> — tambahkan meta description dan Open Graph tags</p></li><li><p><strong>Dark mode</strong> — tambahkan CSS <code>@media (prefers-color-scheme: dark)</code></p></li><li><p><strong>Komentar</strong> — tambahkan widget komentar di bawah <code>&lt;data:post.body/&gt;</code></p></li></ul><hr><p><em>Tutorial ini dibuat untuk pemula yang ingin memahami template Blogger dari dalam. Selamat bereksperimen!</em></p>]]></description><guid isPermaLink="false">5</guid><pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate></item><item><title>Tutorial: Membuat Cover Forum Style IPS untuk XenForo</title><link>https://lalu.pro/articles/platform-web/tutorial-membuat-cover-forum-style-ips-untuk-xenforo-r4/</link><description><![CDATA[
<p><img src="https://lalu.pro/uploads/monthly_2026_07/Membuat_Cover_Forum_Style_IPS_untuk_XenForo.avif.6cad68ff1a2416fdde711d4e309d6a42.avif" /></p>
<p>Panduan lengkap membangun tampilan <strong>cover forum</strong> dengan layout 2 kolom (gambar + info di kiri, daftar thread terbaru di kanan) — mirip gaya Invision Community / IPS, tapi natif di XenForo. Tanpa beli style komersial.</p><p><strong>Estimasi waktu:</strong> 30–45 menit <strong>Tingkat:</strong> Menengah (perlu nyaman edit file PHP via FTP dan template XenForo)</p><p>Konsep tampilan akhir: cover di kiri, daftar thread terbaru di kanan, border atas warna berbeda per kategori.</p><p>Lihat demo: <a rel="external nofollow" href="https://forum.intutekno.com/">https://forum.intutekno.com/</a></p><hr><h2>1. Yang Akan Dibuat</h2><p>Setiap forum di halaman utama akan menampilkan:</p><p><strong>Kolom kiri (30% lebar):</strong></p><ul><li><p>Cover image 3:2 (SVG/WebP/JPG)</p></li><li><p>Judul forum (link ke forum)</p></li><li><p>Deskripsi forum</p></li><li><p>Statistik (jumlah threads &amp; messages)</p></li></ul><p><strong>Kolom kanan (70% lebar):</strong></p><ul><li><p>5 thread terbaru dengan avatar, judul, username, tanggal, jumlah replies</p></li><li><p>Atau ikon + pesan "Belum ada thread" kalau forum masih kosong</p></li></ul><p><strong>Bonus:</strong> Border-top tiap card forum bisa diwarnai berbeda berdasarkan kategori induk-nya.</p><hr><h2>2. Prasyarat</h2><ul><li><p>XenForo 2.3.x (panduan ini diuji di 2.3.10)</p></li><li><p>Akses <strong>AdminCP</strong></p></li><li><p>Akses <strong>FTP/File Manager</strong> ke server (untuk upload PHP files)</p></li><li><p>Akses <strong>cPanel</strong> atau setara untuk backup database (opsional tapi disarankan)</p></li><li><p>Pemahaman dasar HTML/CSS akan membantu</p></li></ul><hr><h2>3. Backup Dulu</h2><p>Sebelum apa pun, lakukan dua backup:</p><p><strong>Backup database:</strong></p><ul><li><p>cPanel → phpMyAdmin → pilih database forum → Export → Quick → Go</p></li></ul><p><strong>Backup file:</strong></p><ul><li><p>File Manager cPanel → kompres folder forum jadi <code>.zip</code> → download</p></li></ul><p>Kalau ada yang salah saat ngoprek, kita bisa balik ke kondisi awal.</p><hr><h2>4. Aktifkan Development Mode</h2><p>Buka <code>/src/config.php</code> lewat File Manager atau FTP. Tambahkan dua baris ini di paling bawah:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>$config['development']['enabled'] = true;
$config['development']['defaultAddOn'] = 'Intutekno/CoverForum';
</code></pre><p>Ganti <code>Intutekno/CoverForum</code> sesuai nama yang Anda inginkan (format: <code>VendorName/AddOnName</code>). Setelah save, refresh AdminCP — menu <strong>Development</strong> dan <strong>Add-ons</strong> akan muncul di sidebar.</p><hr><h2>5. Bikin Add-on PHP</h2><p>Add-on ini bertugas menjalankan satu fungsi: mengambil 5 thread terbaru dari sebuah forum dan menyediakannya untuk dipakai di template.</p><h3>5.1. Buat folder add-on</h3><p>Via FTP, buat struktur folder:</p><pre spellcheck="" class="ipsCode language-plaintext" data-language="Plain Text"><code>/src/addons/Intutekno/CoverForum/
</code></pre><h3>5.2. Buat file <code>addon.json</code></h3><p>Isi:</p><pre spellcheck="" class="ipsCode language-json" data-language="JSON"><code>{
    "title": "Intutekno Cover Forum",
    "version_string": "1.0.0",
    "version_id": 1000070,
    "dev": "Intutekno",
    "supported": true
}
</code></pre><h3>5.3. Buat file <code>Listener.php</code></h3><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;?php
namespace Intutekno\CoverForum;

class Listener
{
    public static function templaterSetup(\XF\Container $container, \XF\Template\Templater &amp;$templater)
    {
        $templater-&gt;addFunction('latest_forum_threads',
            ['Intutekno\CoverForum\TemplateHelper', 'templaterFunction']
        );
    }
}
</code></pre><h3>5.4. Buat file <code>TemplateHelper.php</code></h3><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;?php
namespace Intutekno\CoverForum;

class TemplateHelper
{
    public static function templaterFunction($templater, &amp;$escape, $nodeId, $limit = 5)
    {
        $escape = false;
        return self::getLatestThreads($nodeId, $limit);
    }

    public static function getLatestThreads($nodeId, $limit = 5)
    {
        $finder = \XF::finder('XF:Thread')
            -&gt;where('node_id', $nodeId)
            -&gt;where('discussion_state', 'visible')
            -&gt;where('discussion_type', '&lt;&gt;', 'redirect')
            -&gt;with('User')
            -&gt;with('Prefix')
            -&gt;order('last_post_date', 'DESC')
            -&gt;limit($limit);

        return $finder-&gt;fetch();
    }
}
</code></pre><p><strong>Struktur akhir folder:</strong></p><pre spellcheck="" class="ipsCode language-plaintext" data-language="Plain Text"><code>/src/addons/Intutekno/CoverForum/
├── addon.json
├── Listener.php
└── TemplateHelper.php
</code></pre><hr><h2>6. Install Add-on</h2><p>Di AdminCP:</p><ol><li><p>Klik menu <strong>Add-ons</strong> di sidebar</p></li><li><p>Cari <strong>"Intutekno Cover Forum"</strong> di list</p></li><li><p>Klik tombol <strong>Install</strong> di sampingnya</p></li><li><p>Confirm</p></li></ol><p>Status add-on harus jadi <strong>Active</strong> (toggle hijau).</p><hr><h2>7. Register Code Event Listener</h2><p>Ini langkah yang menghubungkan add-on PHP kita ke template engine XenForo.</p><ol><li><p>AdminCP → <strong>Development</strong> → <strong>Code event listeners</strong></p></li><li><p>Klik <strong>+ Add code event listener</strong> di pojok kanan atas</p></li><li><p>Isi form:</p></li></ol><div class="ipsRichText__table-wrapper"><table style="width: 641px;"><colgroup><col style="width:223px;"><col style="width:418px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Field</p></th><th colspan="1" rowspan="1"><p>Isi</p></th></tr><tr><td colspan="1" rowspan="1"><p><strong>Listen to event</strong></p></td><td colspan="1" rowspan="1"><p><code>templater_setup</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Event hint</strong></p></td><td colspan="1" rowspan="1"><p><em>(kosongkan)</em></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Execute callback: Class</strong></p></td><td colspan="1" rowspan="1"><p><code>Intutekno\CoverForum\Listener</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Execute callback: Method</strong></p></td><td colspan="1" rowspan="1"><p><code>templaterSetup</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Callback execution order</strong></p></td><td colspan="1" rowspan="1"><p><code>10</code> (default)</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Enable callback execution</strong></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span> centang</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Description</strong></p></td><td colspan="1" rowspan="1"><p><code>Register latest_forum_threads function</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Add-on</strong></p></td><td colspan="1" rowspan="1"><p><code>Intutekno Cover Forum 1.0.0</code></p></td></tr></tbody></table></div><ol start="4"><li><p>Klik <strong>Save</strong></p></li></ol><hr><h2>8. Edit Template <code>node_list_forum</code></h2><p>AdminCP → <strong>Appearance → Templates</strong> → cari template <code>node_list_forum</code> → <strong>Edit</strong>.</p><p><strong>Ganti seluruh isi template</strong> dengan kode berikut:</p><pre spellcheck="" class="ipsCode language-plaintext" data-language="Plain Text"><code>&lt;xf:macro id="depth1" arg-node="!" arg-extras="!" arg-children="!" arg-childExtras="!" arg-depth="1"&gt;
	&lt;div class="block"&gt;
		&lt;div class="block-container"&gt;
			&lt;div class="block-body"&gt;
				&lt;xf:macro id="forum"
					arg-node="{$node}"
					arg-extras="{$extras}"
					arg-children="{$children}"
					arg-childExtras="{$childExtras}"
					arg-depth="{$depth}" /&gt;
			&lt;/div&gt;
		&lt;/div&gt;
	&lt;/div&gt;
&lt;/xf:macro&gt;

&lt;xf:macro id="depth2" arg-node="!" arg-extras="!" arg-children="!" arg-childExtras="!" arg-depth="1"&gt;
	&lt;xf:macro id="forum"
		arg-node="{$node}"
		arg-extras="{$extras}"
		arg-children="{$children}"
		arg-childExtras="{$childExtras}"
		arg-depth="{$depth}" /&gt;
&lt;/xf:macro&gt;

&lt;xf:macro id="depthN" arg-node="!" arg-extras="!" arg-children="!" arg-childExtras="!" arg-depth="1"&gt;
	&lt;li&gt;
		&lt;a href="{{ link('forums', $node) }}" class="subNodeLink subNodeLink--forum {{ $extras.hasNew ? 'subNodeLink--unread' : '' }}"&gt;
			&lt;xf:fa icon="{{ $node.Data.TypeHandler.getTypeIconClass() ?: 'fa-comments' }}" class="subNodeLink-icon" /&gt;{$node.title}
		&lt;/a&gt;
		&lt;xf:macro id="forum_list::sub_node_list"
			arg-children="{$children}"
			arg-childExtras="{$childExtras}"
			arg-depth="{{ $depth + 1 }}" /&gt;
	&lt;/li&gt;
&lt;/xf:macro&gt;

&lt;xf:macro id="forum"
	arg-node="!"
	arg-extras="!"
	arg-children="!"
	arg-childExtras="!"
	arg-depth="!"
	arg-chooseName=""
	arg-bonusInfo=""&gt;

	&lt;xf:if is="$depth == 1 OR $depth == 2"&gt;

		&lt;!-- ===== COVER STYLE (forum utama) ===== --&gt;
		&lt;div class="node node--id{$node.node_id} node--depth{$depth} node--forum intuteknoCover intuteknoCover--cat{$node.parent_node_id} {{ $extras.hasNew ? 'node--unread' : 'node--read' }}"&gt;
			&lt;div class="intuteknoCover-body"&gt;

				&lt;div class="intuteknoCover-side"&gt;
					&lt;a href="{{ link('forums', $node) }}" class="intuteknoCover-img"&gt;
						&lt;img src="/data/forum_covers/{$node.node_id}.svg"
							 onerror="this.src='/data/forum_covers/default.svg'"
							 alt="{$node.title}" loading="lazy" /&gt;
					&lt;/a&gt;
					&lt;h3 class="intuteknoCover-title"&gt;
						&lt;a href="{{ link('forums', $node) }}"&gt;{$node.title}&lt;/a&gt;
					&lt;/h3&gt;
					&lt;xf:if is="$node.description"&gt;
						&lt;div class="intuteknoCover-desc"&gt;{$node.description|raw}&lt;/div&gt;
					&lt;/xf:if&gt;
					&lt;div class="intuteknoCover-stats"&gt;
						&lt;span&gt;&lt;b&gt;{{ phrase('threads') }}:&lt;/b&gt; {$extras.discussion_count|number_short(1)}&lt;/span&gt;
						&lt;span&gt;&lt;b&gt;{{ phrase('messages') }}:&lt;/b&gt; {$extras.message_count|number_short(1)}&lt;/span&gt;
					&lt;/div&gt;
				&lt;/div&gt;

				&lt;div class="intuteknoCover-threads"&gt;
					&lt;xf:set var="$latest" value="{{ latest_forum_threads($node.node_id, 5) }}" /&gt;
					&lt;xf:if is="$latest is not empty"&gt;
						&lt;ul class="intuteknoThread-list"&gt;
							&lt;xf:foreach loop="$latest" value="$thread"&gt;
								&lt;li class="intuteknoThread-item"&gt;
									&lt;xf:avatar user="{$thread.User}" size="xxs" defaultname="{$thread.username}" class="intuteknoThread-avatar" /&gt;
									&lt;div class="intuteknoThread-main"&gt;
										&lt;div class="intuteknoThread-title"&gt;
											&lt;xf:if is="$thread.prefix_id"&gt;{{ prefix('thread', $thread) }}&lt;/xf:if&gt;
											&lt;a href="{{ link('threads', $thread) }}"&gt;{$thread.title}&lt;/a&gt;
										&lt;/div&gt;
										&lt;div class="intuteknoThread-meta"&gt;
											&lt;a href="{{ link('members', $thread.User) }}" class="username"&gt;{$thread.username}&lt;/a&gt;
											&lt;span&gt;·&lt;/span&gt;
											&lt;xf:date time="{$thread.last_post_date}" /&gt;
											&lt;span&gt;·&lt;/span&gt;
											&lt;span&gt;&lt;xf:fa icon="fa-comment-alt" /&gt; {$thread.reply_count|number_short(1)}&lt;/span&gt;
										&lt;/div&gt;
									&lt;/div&gt;
								&lt;/li&gt;
							&lt;/xf:foreach&gt;
						&lt;/ul&gt;
					&lt;xf:else /&gt;
						&lt;div class="intuteknoThread-empty"&gt;
							&lt;xf:fa icon="fa-comments" /&gt;
							&lt;span&gt;Belum ada thread di forum ini&lt;/span&gt;
						&lt;/div&gt;
					&lt;/xf:if&gt;
				&lt;/div&gt;

			&lt;/div&gt;
		&lt;/div&gt;

	&lt;xf:else /&gt;

		&lt;!-- ===== ORIGINAL STYLE (sub-forum, depth &gt;= 3) ===== --&gt;
		&lt;div class="node node--id{$node.node_id} node--depth{$depth} node--forum {{ $extras.hasNew ? 'node--unread' : 'node--read' }}"&gt;
			&lt;div class="node-body"&gt;
				&lt;span class="node-icon" aria-hidden="true"&gt;
					&lt;xf:fa icon="{{ $node.Data.TypeHandler.getTypeIconClass() ?: 'fa-comments' }}" /&gt;
				&lt;/span&gt;
				&lt;div class="node-main js-nodeMain"&gt;
					&lt;xf:if is="$chooseName"&gt;
						&lt;xf:checkbox standalone="true"&gt;
							&lt;xf:option labelclass="u-pullRight" class="js-chooseItem" name="{$chooseName}[]" value="{$node.node_id}" /&gt;
						&lt;/xf:checkbox&gt;
					&lt;/xf:if&gt;

					&lt;xf:set var="$descriptionDisplay" value="{{ property('nodeListDescriptionDisplay') }}" /&gt;
					&lt;h3 class="node-title"&gt;
						&lt;a href="{{ link('forums', $node) }}" data-xf-init="{{ $descriptionDisplay == 'tooltip' ? 'element-tooltip' : '' }}" data-shortcut="node-description"&gt;{$node.title}&lt;/a&gt;
					&lt;/h3&gt;
					&lt;xf:if is="$descriptionDisplay != 'none' &amp;&amp; $node.description"&gt;
						&lt;div class="node-description {{ $descriptionDisplay == 'tooltip' ? 'node-description--tooltip js-nodeDescTooltip' : '' }}"&gt;{$node.description|raw}&lt;/div&gt;
					&lt;/xf:if&gt;

					&lt;div class="node-meta"&gt;
						&lt;xf:if is="!{$extras.privateInfo}"&gt;
							&lt;div class="node-statsMeta"&gt;
								&lt;dl class="pairs pairs--inline"&gt;
									&lt;dt&gt;{{ phrase('threads') }}&lt;/dt&gt;
									&lt;dd&gt;{$extras.discussion_count|number_short(1)}&lt;/dd&gt;
								&lt;/dl&gt;
								&lt;dl class="pairs pairs--inline"&gt;
									&lt;dt&gt;{{ phrase('messages') }}&lt;/dt&gt;
									&lt;dd&gt;{$extras.message_count|number_short(1)}&lt;/dd&gt;
								&lt;/dl&gt;
							&lt;/div&gt;
						&lt;/xf:if&gt;

						&lt;xf:if is="$depth == 2 AND property('nodeListSubDisplay') == 'menu'"&gt;
							&lt;xf:macro id="forum_list::sub_nodes_menu"
								arg-children="{$children}"
								arg-childExtras="{$childExtras}"
								arg-depth="{{ $depth + 1 }}" /&gt;
						&lt;/xf:if&gt;
					&lt;/div&gt;

					&lt;xf:if is="$depth == 2 AND property('nodeListSubDisplay') == 'flat'"&gt;
						&lt;xf:macro id="forum_list::sub_nodes_flat"
							arg-children="{$children}"
							arg-childExtras="{$childExtras}"
							arg-depth="{{ $depth + 1 }}" /&gt;
					&lt;/xf:if&gt;

					&lt;xf:if is="$bonusInfo is not empty"&gt;
						&lt;div class="node-bonus"&gt;{$bonusInfo}&lt;/div&gt;
					&lt;/xf:if&gt;
				&lt;/div&gt;

				&lt;xf:if is="!{$extras.privateInfo}"&gt;
					&lt;div class="node-stats"&gt;
						&lt;dl class="pairs pairs--rows"&gt;
							&lt;dt&gt;{{ phrase('threads') }}&lt;/dt&gt;
							&lt;dd&gt;{$extras.discussion_count|number_short(1)}&lt;/dd&gt;
						&lt;/dl&gt;
						&lt;dl class="pairs pairs--rows"&gt;
							&lt;dt&gt;{{ phrase('messages') }}&lt;/dt&gt;
							&lt;dd&gt;{$extras.message_count|number_short(1)}&lt;/dd&gt;
						&lt;/dl&gt;
					&lt;/div&gt;
				&lt;/xf:if&gt;

				&lt;div class="node-extra"&gt;
					&lt;xf:if is="{$extras.privateInfo}"&gt;
						&lt;span class="node-extra-placeholder"&gt;{{ phrase('private') }}&lt;/span&gt;
					&lt;xf:elseif is="{$extras.LastThread}" /&gt;
						&lt;div class="node-extra-icon"&gt;
							&lt;xf:if is="$xf.visitor.isIgnoring($extras.last_post_user_id)"&gt;
								&lt;xf:avatar user="{{ null }}" size="xs" /&gt;
							&lt;xf:else /&gt;
								&lt;xf:avatar user="{$extras.LastPostUser}" defaultname="{$extras.last_post_username}" size="xs" /&gt;
							&lt;/xf:if&gt;
						&lt;/div&gt;
						&lt;div class="node-extra-row"&gt;
							&lt;xf:if is="$extras.LastThread.isUnread()"&gt;
								&lt;a href="{{ link('threads/unread', $extras.LastThread) }}" class="node-extra-title" title="{$extras.LastThread.title}"&gt;{{ prefix('thread', $extras.LastThread) }}{$extras.LastThread.title}&lt;/a&gt;
							&lt;xf:else /&gt;
								&lt;a href="{{ link('threads/post', $extras.LastThread, {'post_id': $extras.last_post_id}) }}" class="node-extra-title" title="{$extras.LastThread.title}"&gt;{{ prefix('thread', $extras.LastThread) }}{$extras.LastThread.title}&lt;/a&gt;
							&lt;/xf:if&gt;
						&lt;/div&gt;
						&lt;div class="node-extra-row"&gt;
							&lt;ul class="listInline listInline--bullet"&gt;
								&lt;li&gt;&lt;xf:date time="{$extras.last_post_date}" class="node-extra-date" /&gt;&lt;/li&gt;
								&lt;xf:if is="$xf.visitor.isIgnoring($extras.last_post_user_id)"&gt;
									&lt;li class="node-extra-user"&gt;{{ phrase('ignored_member') }}&lt;/li&gt;
								&lt;xf:else /&gt;
									&lt;li class="node-extra-user"&gt;&lt;xf:username user="{$extras.LastPostUser}" defaultname="{$extras.last_post_username}" /&gt;&lt;/li&gt;
								&lt;/xf:if&gt;
							&lt;/ul&gt;
						&lt;/div&gt;
					&lt;xf:else /&gt;
						&lt;span class="node-extra-placeholder"&gt;{{ phrase('none') }}&lt;/span&gt;
					&lt;/xf:if&gt;
				&lt;/div&gt;
			&lt;/div&gt;
		&lt;/div&gt;

	&lt;/xf:if&gt;

	&lt;xf:if is="{$depth} == 1"&gt;
		&lt;xf:macro id="forum_list::node_list"
			arg-children="{$children}"
			arg-extras="{$childExtras}"
			arg-depth="{{ $depth + 1 }}" /&gt;
	&lt;/xf:if&gt;
&lt;/xf:macro&gt;
</code></pre><p><strong>Save</strong>.</p><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Penjelasan:</strong> Conditional <code>$depth == 1 OR $depth == 2</code> membuat cover style berlaku untuk forum di root maupun di dalam kategori. Sub-forum (depth ≥ 3) tetap pakai layout original supaya rapi.</p></div></blockquote><hr><h2>9. Tambahkan CSS</h2><p>AdminCP → <strong>Appearance → Style properties → Extra LESS template</strong> → tambahkan blok ini:</p><pre spellcheck="" class="ipsCode language-less" data-language="LESS"><code>/* ============================================
   Intutekno Cover Forum Style
   ============================================ */

.intuteknoCover {
    background: @xf-contentBg;
    border: 1px solid @xf-borderColor;
    border-radius: 8px;
    margin-bottom: 12px;
    overflow: hidden;
    border-top: 5px solid #06b6d4; /* default fallback */
}

.intuteknoCover-body {
    display: flex;
    gap: 0;
}

/* === LEFT: Cover + Info === */
.intuteknoCover-side {
    flex: 0 0 30%;
    max-width: 30%;
    padding: 14px;
    border-right: 1px solid @xf-borderColor;
    background: rgba(0,0,0,0.02);
}

.intuteknoCover-img {
    display: block;
    width: 100%;
    aspect-ratio: 3 / 2;
    overflow: hidden;
    border-radius: 6px;
    margin-bottom: 12px;
}

.intuteknoCover-img img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform .3s ease;
}

.intuteknoCover-img:hover img {
    transform: scale(1.05);
}

.intuteknoCover-title {
    margin: 0 0 8px;
    font-size: 17px;
    font-weight: 700;
}

.intuteknoCover-title a {
    color: @xf-linkColor;
}

.intuteknoCover-desc {
    font-size: 13px;
    color: @xf-textColorMuted;
    line-height: 1.5;
    margin-bottom: 12px;
}

.intuteknoCover-stats {
    display: flex;
    gap: 14px;
    padding-top: 10px;
    border-top: 2px solid #e74c3c;
    font-size: 13px;
}

.intuteknoCover-stats b {
    color: @xf-textColor;
}

/* === RIGHT: Daftar Thread === */
.intuteknoCover-threads {
    flex: 1;
    padding: 8px 14px;
}

.intuteknoThread-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.intuteknoThread-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 12px;
    border-bottom: 1px solid @xf-borderColorLight;
}

.intuteknoThread-item:last-child {
    border-bottom: 0;
}

.intuteknoThread-item .intuteknoThread-avatar {
    flex: 0 0 32px;
    width: 32px;
    height: 32px;
    margin: 0;
}

.intuteknoThread-item .intuteknoThread-avatar.avatar img,
.intuteknoThread-item .intuteknoThread-avatar img {
    width: 32px !important;
    height: 32px !important;
    border-radius: 50%;
}

.intuteknoThread-item .intuteknoThread-avatar.avatar {
    font-size: 14px !important;
    line-height: 32px !important;
}

.intuteknoThread-main {
    flex: 1;
    min-width: 0;
}

.intuteknoThread-title {
    font-size: 14px;
    margin-bottom: 3px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 500;
}

.intuteknoThread-meta {
    font-size: 12px;
    color: @xf-textColorMuted;
    display: flex;
    gap: 6px;
    align-items: center;
    flex-wrap: wrap;
}

.intuteknoThread-meta .username {
    font-weight: 600;
    color: @xf-linkColor;
}

.intuteknoThread-empty {
    padding: 50px 20px;
    text-align: center;
    color: @xf-textColorMuted;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    height: 100%;
    justify-content: center;
}

.intuteknoThread-empty .fa {
    font-size: 36px;
    opacity: 0.25;
}

/* === Responsive Mobile === */
@media (max-width: 768px) {
    .intuteknoCover-body {
        flex-direction: column;
    }
    .intuteknoCover-side,
    .intuteknoCover-threads {
        flex: 1 1 100%;
        max-width: 100%;
        border-right: 0;
        border-bottom: 1px solid @xf-borderColor;
    }
}
</code></pre><p><strong>Save</strong>.</p><hr><h2>10. Setup Cover Image</h2><h3>10.1. Buat folder cover</h3><p>Via File Manager cPanel atau FTP, buat folder:</p><pre spellcheck="" class="ipsCode language-plaintext" data-language="Plain Text"><code>/data/forum_covers/
</code></pre><p>Set permission <code>755</code>.</p><h3>10.2. Cari node_id setiap forum</h3><p>AdminCP → <strong>Forums</strong> → klik nama forum → lihat URL:</p><pre spellcheck="" class="ipsCode language-plaintext" data-language="Plain Text"><code>.../admin.php?nodes/nama-forum.5/edit
                              ↑
                          node_id = 5
</code></pre><h3>10.3. Upload cover image</h3><p>Upload file SVG/WebP/JPG ke <code>/data/forum_covers/</code> dengan nama sesuai node_id:</p><pre spellcheck="" class="ipsCode language-plaintext" data-language="Plain Text"><code>/data/forum_covers/
├── default.svg     ← fallback wajib
├── 5.svg           ← forum dengan node_id 5
├── 6.svg
├── 7.svg
└── ...
</code></pre><p><strong>Spesifikasi cover ideal:</strong></p><ul><li><p>Aspect ratio <strong>3:2</strong></p></li><li><p>Ukuran <strong>600×400 px</strong></p></li><li><p>Format <strong>SVG</strong> (paling ringan dan scalable) atau <strong>WebP</strong></p></li></ul><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Catatan:</strong> Template di langkah 8 sudah pakai ekstensi <code>.svg</code>. Kalau Anda pakai <code>.webp</code> atau <code>.jpg</code>, ubah dua tempat di template dari <code>.svg</code> menjadi ekstensi yang Anda pakai.</p></div></blockquote><hr><h2>11. Customize Border per Kategori</h2><p>Border-top tiap card forum bisa diwarnai berbeda berdasarkan <strong>kategori induk</strong>-nya. Berkat class <code>intuteknoCover--cat{X}</code> yang otomatis disisipkan di langkah 8, kita tinggal target via CSS.</p><h3>11.1. Cari node_id kategori</h3><p>Sama seperti forum: AdminCP → Forums → klik kategori → lihat node_id di URL.</p><h3>11.2. Tambahkan rules CSS</h3><p>Tambahkan ke Extra LESS:</p><pre spellcheck="" class="ipsCode language-less" data-language="LESS"><code>/* === Border Color per Kategori === */
.intuteknoCover--cat3 { border-top-color: #3b82f6; } /* Tutorial - biru */
.intuteknoCover--cat4 { border-top-color: #f59e0b; } /* Info - amber */
.intuteknoCover--cat5 { border-top-color: #10b981; } /* hijau */
.intuteknoCover--cat6 { border-top-color: #ec4899; } /* pink */
/* tambahkan sesuai kategori yang dimiliki */
</code></pre><p>Sesuaikan angka di <code>--cat{X}</code> dengan node_id kategori, dan warna sesuai branding masing-masing.</p><h3>Save dan refresh</h3><p>Setelah semua perubahan CSS:</p><ol><li><p>Save Extra LESS</p></li><li><p><strong>Tools → Rebuild caches</strong> → centang semua → Rebuild</p></li><li><p>Buka homepage forum → <strong>Ctrl+F5</strong></p></li></ol><hr><h2>12. Troubleshooting</h2><h3>Error <code>Function "latest_forum_threads" is not callable</code></h3><p>Listener belum aktif atau salah class path. Cek:</p><ol><li><p><strong>Add-ons</strong> — add-on Status harus Active</p></li><li><p><strong>Code event listeners</strong> — entry untuk <code>templater_setup</code> harus enabled, Class dan Method harus persis sama dengan file PHP</p></li><li><p><strong>Tools → Rebuild caches</strong> → centang semua</p></li></ol><h3>Cover tidak muncul, ada celah putih di kiri</h3><p>Berarti gambar gagal dimuat. Cek:</p><ol><li><p>File ada di <code>/data/forum_covers/</code>?</p></li><li><p>Permission folder 755, file 644?</p></li><li><p>Nama file sesuai node_id?</p></li><li><p>Path di template sesuai ekstensi file?</p></li><li><p>Ada file <code>default.svg</code> sebagai fallback?</p></li></ol><h3>Layout tidak berubah, masih style asli</h3><ol><li><p>Pastikan template <code>node_list_forum</code> ter-save dengan kode lengkap di langkah 8</p></li><li><p><strong>Rebuild templates</strong> wajib</p></li><li><p><strong>Ctrl+F5</strong> untuk bypass cache browser</p></li><li><p>Cek dengan inspect element — class <code>intuteknoCover</code> harus muncul di div</p></li></ol><h3>Sub-forum jadi style cover juga (tidak diinginkan)</h3><p>Cek conditional di template:</p><pre spellcheck="" class="ipsCode language-plaintext" data-language="Plain Text"><code>&lt;xf:if is="$depth == 1 OR $depth == 2"&gt;
</code></pre><p>Sub-forum biasanya depth ≥ 3, jadi seharusnya tidak ter-cover. Kalau forum Anda strukturnya berbeda, sesuaikan condition.</p><h3>Avatar ada celah ke kiri</h3><p>Pastikan template pakai sintaks ini (avatar inline, bukan dibungkus <code>&lt;a&gt;</code>):</p><pre spellcheck="" class="ipsCode language-plaintext" data-language="Plain Text"><code>&lt;xf:avatar user="{$thread.User}" size="xxs" defaultname="{$thread.username}" class="intuteknoThread-avatar" /&gt;
</code></pre><h3>Class <code>intuteknoCover--cat{id}</code> tidak muncul di HTML</h3><p>Berarti template belum ter-update. Edit ulang, save, dan rebuild templates.</p><hr><h2>13. Penutup</h2><p>Setelah semua langkah di atas, halaman utama forum akan menampilkan card cover untuk setiap forum dengan layout 2 kolom dan border atas warna per kategori.</p><h3>Pengembangan Selanjutnya</h3><p>Beberapa peningkatan yang bisa dilakukan kapan-kapan:</p><ul><li><p><strong>Caching</strong> thread terbaru lewat XF registry supaya tidak query 5 thread per forum setiap homepage di-load</p></li><li><p><strong>Custom field</strong> untuk upload cover via AdminCP (tanpa FTP)</p></li><li><p><strong>Hover effect</strong> halus di card dan row thread</p></li><li><p><strong>Lazy load</strong> cover image dengan blur placeholder</p></li></ul><h3>Struktur File Akhir</h3><pre spellcheck="" class="ipsCode language-plaintext" data-language="Plain Text"><code>/src/addons/Intutekno/CoverForum/
├── addon.json
├── Listener.php
└── TemplateHelper.php

/data/forum_covers/
├── default.svg
├── 5.svg
├── 6.svg
└── ... (sesuai jumlah forum)

AdminCP → Templates: node_list_forum (modified)
AdminCP → Style properties → Extra LESS (modified)
AdminCP → Code event listeners: templater_setup → Listener::templaterSetup
</code></pre><hr><p><strong>Tutorial dibuat berdasarkan implementasi nyata di forum.intutekno.com</strong></p><p>Lisensi: silakan adaptasi dan bagikan ulang. Kalau bermanfaat, sebutkan sumber sebagai <em>amal jariah</em> — pengetahuan yang terus bermanfaat.</p>]]></description><guid isPermaLink="false">4</guid><pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate></item><item><title>Membuat Sistem Glossary di Invision Community 5</title><link>https://lalu.pro/articles/platform-web/membuat-sistem-glossary-di-invision-community-5-r3/</link><description><![CDATA[
<p><img src="https://lalu.pro/uploads/monthly_2026_10/membuat-kamus-istilah-di-invision-community-5.webp.8c26a40d00851b35aa4d621f9bed2931.webp" /></p>
<p>Panduan ini menjelaskan cara saya membuat Kamus Istilah di <a rel="external nofollow" href="http://lalu.pro">lalu.pro</a> memakai fitur Database di Invision Community (sering disingkat IPS): membuat database dan kolomnya, kategori, tiga template tampilan, sampai memasukkan istilah. Hasil akhirnya bisa dilihat di <a rel="external nofollow" href="https://claude.ai/kamus-istilah/">Kamus Istilah</a>.</p><p>"Database" di sini bukan database MySQL tempat IPS menyimpan seluruh datanya. Di aplikasi Pages, database adalah kumpulan isi yang bentuknya kamu tentukan sendiri: kamu memilih kolom apa saja yang ada (misalnya Sinonim dan Definisi Singkat), mengelompokkannya dalam kategori, lalu mengatur tampilannya. Kamus istilah cocok jadi contoh karena bentuknya sederhana dan seragam.</p><h2>Yang akan kamu dapatkan</h2><ul><li><p><strong>Halaman depan</strong> (<code>/kamus-istilah/</code>): kotak-kotak kategori beserta jumlah istilahnya.</p></li><li><p><strong>Daftar per kategori</strong>: istilah terurut abjad, lengkap dengan sinonim dan definisi singkatnya.</p></li><li><p><strong>Halaman istilah</strong>: judul, sinonim, definisi singkat yang menonjol, penjelasan lengkap, dan istilah terkait.</p></li></ul><h2>Sebelum mulai</h2><ul><li><p><strong>Versi.</strong> Invision Community 5 dengan aplikasi Pages. Panduan ini mengikuti tampilan versi 5.0.19. Nama tab, sintaks template, dan nama property di versi 5 berbeda dari versi 4. Kalau versimu lain, sebagian langkah bisa bergeser, jadi cek dulu sebelum menyalin kode mentah-mentah.</p></li><li><p><strong>Akses.</strong> Kamu perlu masuk ke AdminCP (panel admin situs).</p></li><li><p><strong>Pengetahuan dasar.</strong> HTML dan CSS dasar. Kode di panduan ini tinggal disalin, tetapi kamu perlu bisa membacanya kalau ada yang ingin disesuaikan.</p></li><li><p><strong>Cadangan.</strong> Sebelum mengubah template, salin isi lamanya ke berkas teks. Kalau ada yang rusak, kamu bisa mengembalikannya.</p></li></ul><h3>Istilah yang dipakai di panduan ini</h3><ul><li><p><strong>AdminCP</strong>: panel admin Invision Community.</p></li><li><p><strong>Record</strong>: satu isi di dalam database. Di sini, satu record adalah satu istilah.</p></li><li><p><strong>Kategori</strong>: pengelompokan record di dalam database.</p></li><li><p><strong>Template</strong>: kerangka HTML yang menentukan tampilan satu jenis halaman.</p></li><li><p><strong>Slug (FURL)</strong>: bagian alamat URL yang berupa kata, seperti <code>seo-analytics</code>.</p></li><li><p><strong>Template group</strong>: kumpulan template milik satu database.</p></li></ul><h2>Isi panduan</h2><ul><li><p>Gambaran: bagaimana semuanya tersambung</p></li><li><p>Fase 1: Menyiapkan database (termasuk izin akses)</p></li><li><p>Fase 2: Kolom tambahan</p></li><li><p>Fase 3: Kategori</p></li><li><p>Fase 4: Template tampilan</p></li><li><p>Fase 5: Mengisi istilah</p></li><li><p>Fase 6: Menyambungkan ke situs</p></li><li><p>Fase 7: Pengujian</p></li><li><p>Fase 8: Perawatan</p></li><li><p>Pemecahan masalah</p></li></ul><h2>Gambaran: bagaimana semuanya tersambung</h2><p>Kamus ini punya tiga jenis halaman, dan masing-masing memakai satu template:</p><pre spellcheck="" class="ipsCode" data-language="text"><code>/kamus-istilah/                          → Landing (grid kategori)
/kamus-istilah/seo-analytics/            → Listing kategori (daftar istilah)
/kamus-istilah/seo-analytics/backlink-rN/ → Detail istilah
</code></pre><ul><li><p>Halaman depan memakai template <strong>Category List</strong>.</p></li><li><p>Daftar per kategori memakai template <strong>Record Listing</strong>.</p></li><li><p>Halaman istilah memakai template <strong>Record</strong>.</p></li></ul><p><strong>Penting:</strong> URL memakai <code>kamus-istilah</code>, mengikuti Page Filename yang dibuat otomatis dari nama database, bukan <code>glossary</code>. Jangan menulis <code>/glossary/</code> di mana pun. Halaman itu tidak ada dan akan menghasilkan error 404 (1S160/2).</p><h3>Susunan data</h3><pre spellcheck="" class="ipsCode" data-language="text"><code>Database: Kamus Istilah (key: glossary)
├── Kategori (5)
│   ├── SEO &amp; Analytics
│   ├── Monetisasi
│   ├── Content &amp; Copywriting
│   ├── Technical &amp; Performance
│   └── Platform &amp; Tools
└── Records (Istilah)
    ├── Title      (field bawaan, dipakai sebagai judul + URL)
    ├── Content    (field bawaan, penjelasan lengkap)
    ├── Aliases    (custom, key: aliases)
    └── Definisi Singkat (custom, key: definisi-singkat)
</code></pre><h2>Fase 1: Menyiapkan database</h2><h3>1.1 Membuat database</h3><p>AdminCP → Pages → Databases → Create New.</p><p>Tab yang ada di IPS 5.0.19: Details, Display, Language, Options, Forums, dan SEO. Tidak ada tab "Page" atau "Permissions" terpisah seperti di IPS 4. Pengaturan halaman menyatu di bawah tab Details.</p><p><strong>Tab Details:</strong></p><div class="ipsRichText__table-wrapper"><table style="width: 536px;"><colgroup><col style="width:154px;"><col style="width:382px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Field</p></th><th colspan="1" rowspan="1"><p>Value</p></th></tr><tr><td colspan="1" rowspan="1"><p>Name</p></td><td colspan="1" rowspan="1"><p>Kamus Istilah</p></td></tr><tr><td colspan="1" rowspan="1"><p>Description</p></td><td colspan="1" rowspan="1"><p>Kamus istilah blogging &amp; webmaster</p></td></tr><tr><td colspan="1" rowspan="1"><p>Database Key</p></td><td colspan="1" rowspan="1"><p>glossary</p></td></tr><tr><td colspan="1" rowspan="1"><p>Use categories</p></td><td colspan="1" rowspan="1"><p>Yes, allow records to be stored in different categories</p></td></tr></tbody></table></div><p><strong>Di bagian bawah tab Details</strong> (bagian Page yang menyatu):</p><div class="ipsRichText__table-wrapper"><table style="width: 420px;"><colgroup><col style="width:154px;"><col style="width:266px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Field</p></th><th colspan="1" rowspan="1"><p>Value</p></th></tr><tr><td colspan="1" rowspan="1"><p>Page title</p></td><td colspan="1" rowspan="1"><p>Use page title → Kamus Istilah</p></td></tr><tr><td colspan="1" rowspan="1"><p>Page Filename</p></td><td colspan="1" rowspan="1"><p>kamus-istilah</p></td></tr><tr><td colspan="1" rowspan="1"><p>Page Folder</p></td><td colspan="1" rowspan="1"><p>No Parent (root)</p></td></tr></tbody></table></div><p>IPS otomatis membuat Page bernama <code>kamus-istilah</code> dan menetapkan database ini ke halaman tersebut. Inilah sumber URL final <code>/kamus-istilah/</code>.</p><h3>1.2 Tab Language</h3><p>Sesuaikan kata benda konten supaya berbahasa Indonesia. IPS memakai kata-kata ini untuk membentuk label dan tombol, misalnya "Add new istilah".</p><div class="ipsRichText__table-wrapper"><table style="width: 438px;"><colgroup><col style="width:194px;"><col style="width:244px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Field</p></th><th colspan="1" rowspan="1"><p>Value</p></th></tr><tr><td colspan="1" rowspan="1"><p>Singular Lowercase Noun</p></td><td colspan="1" rowspan="1"><p>istilah</p></td></tr><tr><td colspan="1" rowspan="1"><p>Plural Lowercase Noun</p></td><td colspan="1" rowspan="1"><p>istilah-istilah</p></td></tr><tr><td colspan="1" rowspan="1"><p>Singular Capitalized Noun</p></td><td colspan="1" rowspan="1"><p>Istilah</p></td></tr><tr><td colspan="1" rowspan="1"><p>Plural Capitalized Noun</p></td><td colspan="1" rowspan="1"><p>Istilah-istilah</p></td></tr><tr><td colspan="1" rowspan="1"><p>Indefinite Article</p></td><td colspan="1" rowspan="1"><p>Biarkan default (a record). Tidak berpengaruh untuk situs berbahasa Indonesia.</p></td></tr></tbody></table></div><h3>1.3 Tab Options</h3><div class="ipsRichText__table-wrapper"><table style="min-width: 631px;"><colgroup><col style="width:227px;"><col style="min-width:20px;"><col style="width:384px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Setting</p></th><th colspan="1" rowspan="1"><p>Value</p></th><th colspan="1" rowspan="1"><p>Lokasi di form</p></th></tr><tr><td colspan="1" rowspan="1"><p>Allow comments</p></td><td colspan="1" rowspan="1"><p>OFF</p></td><td colspan="1" rowspan="1"><p>Bagian Comments and Reviews</p></td></tr><tr><td colspan="1" rowspan="1"><p>Allow reviews</p></td><td colspan="1" rowspan="1"><p>OFF</p></td><td colspan="1" rowspan="1"><p>Bagian Comments and Reviews</p></td></tr><tr><td colspan="1" rowspan="1"><p>Can use tags?</p></td><td colspan="1" rowspan="1"><p>ON</p></td><td colspan="1" rowspan="1"><p>Bagian Tags. Dipakai untuk "Istilah Terkait"</p></td></tr><tr><td colspan="1" rowspan="1"><p>Enable record image upload</p></td><td colspan="1" rowspan="1"><p>OFF</p></td><td colspan="1" rowspan="1"><p>Bagian Record Image Settings. Kamus ini hanya teks</p></td></tr><tr><td colspan="1" rowspan="1"><p>Store revisions</p></td><td colspan="1" rowspan="1"><p>ON (default)</p></td><td colspan="1" rowspan="1"><p>-</p></td></tr><tr><td colspan="1" rowspan="1"><p>Allow searching</p></td><td colspan="1" rowspan="1"><p>ON (default)</p></td><td colspan="1" rowspan="1"><p>-</p></td></tr></tbody></table></div><p>Rating dan polls tidak punya tombol sendiri di sini. Rating diatur per kategori, dan polls adalah fitur forum, jadi keduanya otomatis tidak aktif.</p><h3>1.4 Tab Forums</h3><div class="ipsRichText__table-wrapper"><table style="width: 471px;"><colgroup><col style="width:125px;"><col style="width:346px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Setting</p></th><th colspan="1" rowspan="1"><p>Value</p></th></tr><tr><td colspan="1" rowspan="1"><p>Post topic</p></td><td colspan="1" rowspan="1"><p>OFF. Kamus tidak perlu otomatis diposting ke forum</p></td></tr></tbody></table></div><h3>1.5 Tab SEO</h3><div class="ipsRichText__table-wrapper"><table style="width: 473px;"><colgroup><col style="width:143px;"><col style="width:330px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Setting</p></th><th colspan="1" rowspan="1"><p>Value</p></th></tr><tr><td colspan="1" rowspan="1"><p>Canonical Tag</p></td><td colspan="1" rowspan="1"><p>All pages should be considered the same (default, sudah tepat untuk kamus)</p></td></tr></tbody></table></div><h3>1.6 Tab Display</h3><div class="ipsRichText__table-wrapper"><table style="width: 613px;"><colgroup><col style="width:202px;"><col style="width:155px;"><col style="width:256px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Field</p></th><th colspan="1" rowspan="1"><p>Value</p></th><th colspan="1" rowspan="1"><p>Catatan</p></th></tr><tr><td colspan="1" rowspan="1"><p>Index Page</p></td><td colspan="1" rowspan="1"><p>Category Listing</p></td><td colspan="1" rowspan="1"><p>Halaman depan menampilkan kotak kategori, bukan daftar record</p></td></tr><tr><td colspan="1" rowspan="1"><p>Category Listing Layout</p></td><td colspan="1" rowspan="1"><p>Custom</p></td><td colspan="1" rowspan="1"><p>Wajib "Custom" agar template group kita bisa dipilih nanti</p></td></tr><tr><td colspan="1" rowspan="1"><p>Listing Layout</p></td><td colspan="1" rowspan="1"><p>Custom</p></td><td colspan="1" rowspan="1"><p>Sama, agar template Glossarylisting bisa dipilih</p></td></tr><tr><td colspan="1" rowspan="1"><p>Sort</p></td><td colspan="1" rowspan="1"><p>Title</p></td><td colspan="1" rowspan="1"><p>Urut abjad, bukan Record ID</p></td></tr><tr><td colspan="1" rowspan="1"><p>Order</p></td><td colspan="1" rowspan="1"><p>Ascending</p></td><td colspan="1" rowspan="1"><p>A-Z</p></td></tr><tr><td colspan="1" rowspan="1"><p>Records per page</p></td><td colspan="1" rowspan="1"><p>50</p></td><td colspan="1" rowspan="1"><p>Supaya satu kategori muat dalam satu halaman</p></td></tr></tbody></table></div><p><strong>Catatan:</strong> Layout harus diatur ke "Custom" dulu. Baru setelah itu muncul menu pilihan "Template Group" untuk memilih template buatan kita. Kalau Layout dibiarkan "Grid" atau "Rows", template custom tidak akan pernah terpakai dan IPS tetap memakai tampilan bawaan. Template Group diisi di Fase 4, setelah template-nya dibuat.</p><h3>1.7 Izin akses</h3><p>Kamus sebaiknya bisa dibaca semua orang, tetapi hanya pengurus yang boleh menambah atau mengubah istilah. Izin diatur di tiga tingkat, dari yang paling atas:</p><ul><li><p><strong>Halaman (Page).</strong> Kalau sebuah grup tidak boleh melihat halamannya, database di dalamnya ikut tidak terlihat.</p></li><li><p><strong>Database.</strong> Izin bawaan untuk semua kategori di dalamnya.</p></li><li><p><strong>Kategori.</strong> Bisa menimpa izin database lewat Set Custom Permissions. Di panduan ini opsi itu dimatikan (Fase 3), jadi semua kategori mengikuti izin database.</p></li></ul><p>Menurut dokumentasi Invision, IPS menawarkan pengaturan izin database setelah database disimpan. Atur supaya grup Tamu dan Member biasa hanya bisa melihat dan membaca, sedangkan menambah dan mengubah istilah hanya untuk grup Admin atau staf. Nama dan letak opsinya bisa berbeda antarversi, jadi pastikan hasilnya lewat pengujian di Fase 7.</p><h2>Fase 2: Kolom tambahan</h2><p>AdminCP → Pages → Databases → Kamus Istilah → Fields.</p><p>Ada dua kolom bawaan yang terkunci: Title dan Content (lencana hijau dengan ikon gembok).</p><p><strong>Bug yang ditemukan:</strong> mencoba mengedit kolom bawaan Title atau Content yang terkunci memicu error PHP:</p><pre spellcheck="" class="ipsCode" data-language="text"><code>Attempt to assign property "options" on null
</code></pre><p>Solusinya, jangan edit kolom bawaan. Mengganti label hanya kosmetik dan tidak penting, jadi biarkan Title dan Content apa adanya. Kolom baru (Create New) tidak terkena bug ini.</p><h3>2.1 Kolom Aliases</h3><p>Create New:</p><div class="ipsRichText__table-wrapper"><table style="width: 496px;"><colgroup><col style="width:177px;"><col style="width:319px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Setting</p></th><th colspan="1" rowspan="1"><p>Value</p></th></tr><tr><td colspan="1" rowspan="1"><p>Title</p></td><td colspan="1" rowspan="1"><p>Aliases (atau Sinonim / Akronim)</p></td></tr><tr><td colspan="1" rowspan="1"><p>Description</p></td><td colspan="1" rowspan="1"><p>Pisahkan dengan koma. Contoh: Tautan Balik, Inbound Link</p></td></tr><tr><td colspan="1" rowspan="1"><p>Type</p></td><td colspan="1" rowspan="1"><p>Text</p></td></tr><tr><td colspan="1" rowspan="1"><p>Maximum Length</p></td><td colspan="1" rowspan="1"><p>200</p></td></tr><tr><td colspan="1" rowspan="1"><p>Required</p></td><td colspan="1" rowspan="1"><p>No</p></td></tr></tbody></table></div><p>Setelah dibuat, catat Template Key dari tab Display options. Hasilnya: <code>aliases</code>.</p><h3>2.2 Kolom Definisi Singkat</h3><p>Create New:</p><div class="ipsRichText__table-wrapper"><table style="width: 502px;"><colgroup><col style="width:166px;"><col style="width:336px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Setting</p></th><th colspan="1" rowspan="1"><p>Value</p></th></tr><tr><td colspan="1" rowspan="1"><p>Title</p></td><td colspan="1" rowspan="1"><p>Definisi Singkat</p></td></tr><tr><td colspan="1" rowspan="1"><p>Description</p></td><td colspan="1" rowspan="1"><p>Definisi satu kalimat untuk ringkasan di halaman istilah dan daftar kategori. Maksimal 160 karakter.</p></td></tr><tr><td colspan="1" rowspan="1"><p>Type</p></td><td colspan="1" rowspan="1"><p>Text (Textarea juga boleh)</p></td></tr><tr><td colspan="1" rowspan="1"><p>Maximum Length</p></td><td colspan="1" rowspan="1"><p>160</p></td></tr><tr><td colspan="1" rowspan="1"><p>Required</p></td><td colspan="1" rowspan="1"><p>Yes (disarankan; template tetap jalan kalau kolom ini kosong)</p></td></tr></tbody></table></div><p>Template Key: <code>definisi-singkat</code>.</p><p><strong>Catatan:</strong> jangan mengandalkan kolom ini sebagai meta description. Template di panduan ini tidak membuat meta tag, dan saya belum menemukan opsi resmi untuk memakai kolom kustom sebagai meta description. Kalau itu penting buatmu, buka sumber halaman sebuah istilah (view-source) dan cari <code>&lt;meta name="description"</code> untuk melihat isinya.</p><h3>2.3 Matikan lencana bawaan kedua kolom</h3><p>Aliases dan Definisi Singkat nanti ditampilkan dengan tata letak custom di template. Matikan tampilan lencana otomatisnya supaya tidak muncul dua kali.</p><p>Pada kedua kolom, buka tab Display options:</p><ul><li><p><strong>Show in display template:</strong> OFF.</p></li><li><p><strong>Show in listing template:</strong> biarkan ON. Kolom ini masih dipakai di recordRow custom, tetapi boleh dimatikan kalau kamu tidak mau.</p></li><li><p>Supaya lolos validasi "Display View Format REQUIRED", pilih <strong>Value</strong> saja. Pilihan ini tidak akan terpakai karena tampilan display sudah OFF.</p></li></ul><h2>Fase 3: Kategori</h2><p>AdminCP → Pages → Databases → Kamus Istilah → Categories.</p><p>Saat "Use categories" diaktifkan, IPS otomatis membuat satu kategori bawaan bernama "Records". Ganti nama kategori ini jadi kategori pertama, lalu buat sisanya.</p><p>Buat lima kategori. <strong>FURL Slug wajib diisi manual</strong>; jangan biarkan terisi otomatis seperti <code>N_records</code>:</p><div class="ipsRichText__table-wrapper"><table style="min-width: 616px;"><colgroup><col style="min-width:20px;"><col style="width:212px;"><col style="width:384px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Name</p></th><th colspan="1" rowspan="1"><p>Description</p></th><th colspan="1" rowspan="1"><p>FURL Slug</p></th></tr><tr><td colspan="1" rowspan="1"><p>SEO &amp; Analytics</p></td><td colspan="1" rowspan="1"><p>Istilah SEO, search, dan analytics</p></td><td colspan="1" rowspan="1"><p>seo-analytics</p></td></tr><tr><td colspan="1" rowspan="1"><p>Monetisasi</p></td><td colspan="1" rowspan="1"><p>AdSense, affiliate, sponsorship, CPM/RPM</p></td><td colspan="1" rowspan="1"><p>monetisasi</p></td></tr><tr><td colspan="1" rowspan="1"><p>Content &amp; Copywriting</p></td><td colspan="1" rowspan="1"><p>Cornerstone, pillar, evergreen, copy</p></td><td colspan="1" rowspan="1"><p>content-copywriting</p></td></tr><tr><td colspan="1" rowspan="1"><p>Technical &amp; Performance</p></td><td colspan="1" rowspan="1"><p>Core Web Vitals, hosting, speed, schema</p></td><td colspan="1" rowspan="1"><p>technical-performance</p></td></tr><tr><td colspan="1" rowspan="1"><p>Platform &amp; Tools</p></td><td colspan="1" rowspan="1"><p>WordPress, Blogspot, plugins, software</p></td><td colspan="1" rowspan="1"><p>platform-tools</p></td></tr></tbody></table></div><p><strong>Pengaturan tiap kategori (tab Configuration):</strong></p><div class="ipsRichText__table-wrapper"><table style="width: 446px;"><colgroup><col style="width:197px;"><col style="width:249px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Setting</p></th><th colspan="1" rowspan="1"><p>Value</p></th></tr><tr><td colspan="1" rowspan="1"><p>Parent</p></td><td colspan="1" rowspan="1"><p>No Parent (top-level)</p></td></tr><tr><td colspan="1" rowspan="1"><p>Show Records in Listing?</p></td><td colspan="1" rowspan="1"><p>ON</p></td></tr><tr><td colspan="1" rowspan="1"><p>Enable Star Ratings?</p></td><td colspan="1" rowspan="1"><p>OFF</p></td></tr><tr><td colspan="1" rowspan="1"><p>Set Custom Permissions</p></td><td colspan="1" rowspan="1"><p>OFF (ikut izin database)</p></td></tr><tr><td colspan="1" rowspan="1"><p>Allow anonymous content?</p></td><td colspan="1" rowspan="1"><p>OFF</p></td></tr><tr><td colspan="1" rowspan="1"><p>Listing Layout / Display Template</p></td><td colspan="1" rowspan="1"><p>Use database selected template</p></td></tr></tbody></table></div><p>Tab Meta Tags &amp; Title boleh dikosongkan; IPS memakai Name dan Description kategori. Di tab Forum, Override OFF. Di tab Form Fields, centang "All".</p><h2>Fase 4: Template tampilan</h2><h3>Cara kerja template di IPS 5</h3><p>Di IPS 5, template database dibuat lewat Templates → Add... → Add database template, dengan isian "Assign to database". Kalau kamu memilih database di situ, IPS otomatis membuat template group khusus untuk database itu. Ini mirip folder "Hosting Review" yang mungkin sudah ada di situsmu. Jangan mengedit template record generik yang dipakai bersama oleh banyak database.</p><p><strong>Jenis template yang dipakai:</strong></p><div class="ipsRichText__table-wrapper"><table style="width: 650px;"><colgroup><col style="width:220px;"><col style="width:163px;"><col style="width:267px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Template Type (menu pilihan)</p></th><th colspan="1" rowspan="1"><p>Fungsi</p></th><th colspan="1" rowspan="1"><p>Nama yang dipakai</p></th></tr><tr><td colspan="1" rowspan="1"><p>Record (Displays the record and comments)</p></td><td colspan="1" rowspan="1"><p>Halaman istilah</p></td><td colspan="1" rowspan="1"><p>glossarydisplay</p></td></tr><tr><td colspan="1" rowspan="1"><p>Record Listing (Shown when clicking a category)</p></td><td colspan="1" rowspan="1"><p>Daftar per kategori</p></td><td colspan="1" rowspan="1"><p>glossarylisting</p></td></tr><tr><td colspan="1" rowspan="1"><p>Category List (Shows all available database categories)</p></td><td colspan="1" rowspan="1"><p>Halaman depan berisi kotak kategori</p></td><td colspan="1" rowspan="1"><p>glossarycategorylist</p></td></tr></tbody></table></div><h3>Catatan teknis yang menghemat waktu</h3><p>Hal-hal ini ditemukan lewat coba-coba. Pakai versi yang benar dari awal:</p><ul><li><p>Komentar <code>{{* ... *}}</code> <strong>tidak valid</strong> di IPS 5 dan memicu pesan "syntax of tags or HTML logic is badly formatted". Pakai <code>&lt;!-- --&gt;</code> atau hapus komentarnya.</p></li><li><p><code>content</code> adalah method, bukan property. Tulis <code>{$record-&gt;content()|raw}</code>, bukan <code>{$record-&gt;content}</code>.</p></li><li><p>Jumlah tayangan bernama <code>record_views</code>, bukan <code>views</code>. Tulis <code>{$record-&gt;record_views}</code>.</p></li><li><p>Tautan breadcrumb ke halaman depan sebaiknya ditulis sebagai teks polos <code>/kamus-istilah/</code>. Pemanggilan dinamis seperti <code>{\IPS\cms\Databases::load(...)-&gt;url()}</code> tidak dieksekusi (muncul sebagai teks mentah di URL), sedangkan <code>{$record::database()-&gt;url()}</code> dan <code>{expression=...}</code> memicu error fatal ("widgetArea is throwing an error"). Teks statis paling aman.</p></li><li><p>Berkas CSS terpisah tidak dimuat otomatis. Berkas CSS yang dibuat lewat Add CSS file masuk ke grup "Page CSS" dan tidak ikut di halaman record. Solusinya, taruh CSS langsung di blok <code>&lt;style&gt;</code> di dalam template HTML. Cara ini terbukti bekerja.</p></li><li><p>Nama variabel loop berbeda per template: template record memakai <code>$record</code>, recordRow memakai <code>$rows</code> dan <code>$row</code>, categoryRow memakai <code>$category</code>.</p></li><li><p>Tanda <code>|raw</code> berarti HTML ditampilkan apa adanya, tidak diubah jadi teks. Pakai hanya untuk isi yang ditulis pengurus, seperti isi istilah.</p></li><li><p>Pada <code>var(--theme-..., #warna)</code>, nilai setelah koma adalah warna cadangan yang dipakai kalau temamu tidak punya variabel itu.</p></li><li><p>CSS di bawah memakai font Geist, Instrument Serif, dan Geist Mono. Kalau situsmu tidak memuatnya, browser memakai huruf cadangan (Georgia dan sejenisnya) sehingga tampilan tetap terbaca. Ganti nama font di CSS kalau kamu ingin font lain.</p></li></ul><h3>4.1 Template Record (halaman istilah): glossarydisplay</h3><p>Add database template → Name <code>glossarydisplay</code>, Type Record, Assign to Kamus Istilah. Buka sub-template <code>record</code>, lalu ganti seluruh isinya dengan:</p><pre spellcheck="" class="ipsCode" data-language="html"><code>&lt;style&gt;
.lalu-glossary-detail {
	max-width: 720px;
	margin: 40px auto;
	padding: 0 24px;
	font-family: 'Geist', -apple-system, sans-serif;
}
.lalu-glossary-breadcrumb {
	font-family: 'Geist Mono', monospace;
	font-size: 11px;
	letter-spacing: 0.05em;
	color: var(--theme-accent, #a07855);
	margin-bottom: 24px;
}
.lalu-glossary-breadcrumb a { color: var(--theme-accent, #a07855); text-decoration: none; }
.lalu-glossary-breadcrumb a:hover { color: var(--theme-text_dark, #5c3a21); }
.lalu-glossary-breadcrumb .sep { margin: 0 8px; opacity: 0.5; }
.lalu-glossary-breadcrumb .current { color: var(--theme-text_dark, #5c3a21); }
.lalu-glossary-detail-header { margin-bottom: 32px; }
.lalu-glossary-detail-title {
	font-family: 'Instrument Serif', Georgia, serif;
	font-size: clamp(40px, 7vw, 64px);
	line-height: 0.95;
	letter-spacing: -0.03em;
	font-weight: 400;
	color: var(--theme-text_color, #181818);
	margin: 0 0 12px;
}
.lalu-glossary-detail-aliases {
	font-family: 'Geist Mono', monospace;
	font-size: 12px;
	letter-spacing: 0.05em;
	color: var(--theme-accent, #a07855);
	margin: 0;
}
.lalu-glossary-detail-aliases .label { font-weight: 500; margin-right: 8px; text-transform: uppercase; }
.lalu-glossary-pullquote {
	background: var(--theme-area_background, #f5efea);
	border-left: 3px solid var(--theme-accent, #a07855);
	padding: 20px 24px;
	margin: 24px 0 32px;
	border-radius: 0 6px 6px 0;
	font-family: 'Instrument Serif', Georgia, serif;
	font-size: 20px;
	line-height: 1.4;
	color: var(--theme-text_dark, #5c3a21);
	font-style: italic;
}
.lalu-glossary-detail-content { font-size: 16px; line-height: 1.7; color: var(--theme-text_color, #181818); }
.lalu-glossary-detail-content p { margin: 0 0 16px; }
.lalu-glossary-related {
	margin-top: 40px;
	padding-top: 24px;
	border-top: 1px dashed var(--theme-border_color, #e2dcd5);
}
.lalu-glossary-related .label {
	font-family: 'Geist Mono', monospace;
	font-size: 11px;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--theme-accent, #a07855);
	margin: 0 0 12px;
}
.lalu-glossary-tags { display: flex; flex-wrap: wrap; gap: 8px; }
.lalu-glossary-tag {
	font-family: 'Instrument Serif', Georgia, serif;
	font-style: italic;
	font-size: 14px;
	padding: 6px 14px;
	background: var(--theme-area_background, #f5efea);
	color: var(--theme-text_dark, #5c3a21);
	border-radius: 20px;
	border: 1px solid var(--theme-border_color, #d4c9be);
}
.lalu-glossary-detail-meta {
	margin-top: 32px;
	padding-top: 16px;
	border-top: 1px solid var(--theme-border_color, #e2dcd5);
}
.lalu-glossary-detail-meta p {
	font-family: 'Geist Mono', monospace;
	font-size: 11px;
	letter-spacing: 0.05em;
	color: var(--theme-accent, #a07855);
	margin: 0;
}
&lt;/style&gt;

{{$container = $record-&gt;container();}}
{{$aliases = $record-&gt;customFieldDisplayByKey('aliases', 'raw');}}
{{$shortDef = $record-&gt;customFieldDisplayByKey('definisi-singkat', 'raw');}}
{{$tags = $record-&gt;tags();}}

&lt;div class="lalu-glossary-detail"&gt;

	&lt;div class="lalu-glossary-breadcrumb"&gt;
		&lt;a href="/kamus-istilah/"&gt;Kamus Istilah&lt;/a&gt;
		&lt;span class="sep"&gt;/&lt;/span&gt;
		{{if $container}}
			&lt;a href="{$container-&gt;url()}"&gt;{$container-&gt;_title}&lt;/a&gt;
			&lt;span class="sep"&gt;/&lt;/span&gt;
		{{endif}}
		&lt;span class="current"&gt;{$record-&gt;_title}&lt;/span&gt;
	&lt;/div&gt;

	&lt;header class="lalu-glossary-detail-header"&gt;
		&lt;h1 class="lalu-glossary-detail-title"&gt;{$record-&gt;_title}&lt;/h1&gt;
		{{if $aliases}}
			&lt;p class="lalu-glossary-detail-aliases"&gt;
				&lt;span class="label"&gt;Sinonim:&lt;/span&gt;
				{$aliases}
			&lt;/p&gt;
		{{endif}}
	&lt;/header&gt;

	{{if $shortDef}}
		&lt;blockquote class="lalu-glossary-pullquote"&gt;
			{$shortDef}
		&lt;/blockquote&gt;
	{{endif}}

	&lt;div class="lalu-glossary-detail-content ipsType_richText"&gt;
		{$record-&gt;content()|raw}
	&lt;/div&gt;

	{{if $tags and \count($tags)}}
		&lt;div class="lalu-glossary-related"&gt;
			&lt;p class="label"&gt;Istilah Terkait&lt;/p&gt;
			&lt;div class="lalu-glossary-tags"&gt;
				{{foreach $tags as $tag}}
					&lt;span class="lalu-glossary-tag"&gt;{$tag}&lt;/span&gt;
				{{endforeach}}
			&lt;/div&gt;
		&lt;/div&gt;
	{{endif}}

	&lt;footer class="lalu-glossary-detail-meta"&gt;
		&lt;p&gt;{lang="num_views_with_number" pluralize="$record-&gt;record_views"}&lt;/p&gt;
	&lt;/footer&gt;

&lt;/div&gt;
</code></pre><p>Cara membacanya: blok <code>&lt;style&gt;</code> berisi tampilan; empat baris <code>{{$...}}</code> mengambil data (kategori, sinonim, definisi singkat, tag); sisanya HTML halaman: breadcrumb, judul dan sinonim, definisi singkat dalam kutipan, isi lengkap, istilah terkait, lalu jumlah tayangan. Tag tampil sebagai label biasa dan belum bisa diklik.</p><h3>4.2 Template Record Listing (daftar per kategori): glossarylisting</h3><p>Add database template → Name <code>glossarylisting</code>, Type Record Listing, Assign to Kamus Istilah.</p><p><strong>Keputusan desain:</strong> navigasi huruf A–Z yang menempel di layar (sticky) sengaja tidak dibuat. Alasannya: template pembungkus <code>categoryTable</code> di IPS 5 memakai abstraksi class <code>$table</code> penuh (sorting, filtering, pagination, moderasi) dan menyerahkan render baris ke <code>recordRow</code> secara dinamis. Tidak ada akses mudah ke array records untuk dikelompokkan per huruf tanpa risiko merusak fitur bawaan. Karena Sort sudah Title (Fase 1.6), daftar tetap terurut abjad, dan itu cukup untuk sebuah kamus.</p><p>Edit sub-template <code>recordRow</code>, lalu ganti seluruh isinya dengan:</p><pre spellcheck="" class="ipsCode" data-language="html"><code>&lt;style&gt;
.lalu-glossary-list {
	display: flex;
	flex-direction: column;
}
.lalu-glossary-record {
	display: block;
	padding: 20px 0;
	border-top: 1px solid var(--theme-border_color, #e2dcd5);
	text-decoration: none !important;
	color: inherit;
	transition: background 0.2s;
}
.lalu-glossary-record:hover {
	background: rgba(160, 120, 85, 0.04);
}
.lalu-glossary-record-head {
	display: flex;
	align-items: baseline;
	gap: 12px;
	flex-wrap: wrap;
	margin-bottom: 6px;
}
.lalu-glossary-record-title {
	font-family: 'Instrument Serif', Georgia, serif;
	font-size: 22px;
	font-weight: 400;
	color: var(--theme-text_color, #181818);
	margin: 0;
}
.lalu-glossary-record:hover .lalu-glossary-record-title {
	color: var(--theme-accent, #a07855);
}
.lalu-glossary-record-alias {
	font-family: 'Geist Mono', monospace;
	font-size: 11px;
	color: var(--theme-accent, #a07855);
	background: rgba(160, 120, 85, 0.1);
	padding: 2px 8px;
	border-radius: 10px;
}
.lalu-glossary-record-def {
	font-size: 14px;
	line-height: 1.5;
	color: var(--theme-text_dark, #5c3a21);
	margin: 0;
}
&lt;/style&gt;
&lt;div class="lalu-glossary-list"&gt;
	{{foreach $rows as $row}}
		{{$rowAlias = $row-&gt;customFieldDisplayByKey('aliases', 'raw');}}
		{{$rowDef = $row-&gt;customFieldDisplayByKey('definisi-singkat', 'raw');}}
		&lt;a href="{$row-&gt;url()}" class="lalu-glossary-record"&gt;
			&lt;div class="lalu-glossary-record-head"&gt;
				&lt;h3 class="lalu-glossary-record-title"&gt;{$row-&gt;_title}&lt;/h3&gt;
				{{if $rowAlias}}
					&lt;span class="lalu-glossary-record-alias"&gt;{$rowAlias}&lt;/span&gt;
				{{endif}}
			&lt;/div&gt;
			{{if $rowDef}}
				&lt;p class="lalu-glossary-record-def"&gt;{$rowDef}&lt;/p&gt;
			{{endif}}
		&lt;/a&gt;
	{{endforeach}}
&lt;/div&gt;
</code></pre><h3>4.3 Template Category List (halaman depan): glossarycategorylist</h3><p>Add database template → Name <code>glossarycategorylist</code>, Type Category List, Assign to Kamus Istilah. Template group ini punya dua sub-template: <code>index</code> dan <code>categoryRow</code>.</p><p><strong>Sub-template </strong><code>index</code><strong>:</strong></p><pre spellcheck="" class="ipsCode" data-language="html"><code>&lt;style&gt;
.lalu-glossary-hero {
	text-align: center;
	padding: 60px 0 40px;
	border-bottom: 1px solid var(--theme-border_color, #e2dcd5);
	margin-bottom: 40px;
}
.lalu-glossary-title {
	font-family: 'Instrument Serif', Georgia, serif;
	font-size: clamp(48px, 8vw, 96px);
	line-height: 0.95;
	letter-spacing: -0.03em;
	font-weight: 400;
	color: var(--theme-text_color, #181818);
	margin: 0 0 16px;
}
.lalu-glossary-desc {
	font-size: 16px;
	line-height: 1.5;
	color: var(--theme-text_dark, #5c3a21);
	max-width: 480px;
	margin: 0 auto;
}
.lalu-glossary-categories {
	display: grid;
	grid-template-columns: repeat(2, 1fr);
	gap: 16px;
	max-width: 900px;
	margin: 0 auto;
}
@media (max-width: 700px) {
	.lalu-glossary-categories { grid-template-columns: 1fr; }
}
&lt;/style&gt;

&lt;div class="lalu-glossary-hero"&gt;
	&lt;h1 class="lalu-glossary-title"&gt;{$database-&gt;_title}&lt;/h1&gt;
	{{if $database-&gt;_description}}
		&lt;p class="lalu-glossary-desc"&gt;{$database-&gt;_description}&lt;/p&gt;
	{{endif}}
&lt;/div&gt;

&lt;div class="lalu-glossary-categories"&gt;
	{{foreach $categories as $category}}
		{template="categoryRow" app="cms" location="database" group="glossarycategorylist" params="$category"}
	{{endforeach}}
&lt;/div&gt;
</code></pre><p><strong>Sub-template </strong><code>categoryRow</code><strong>:</strong></p><pre spellcheck="" class="ipsCode" data-language="html"><code>&lt;style&gt;
.lalu-glossary-cat-card {
	background: var(--theme-area_background, #fdfbfa);
	border: 1px solid var(--theme-border_color, #d4c9be);
	border-radius: 6px;
	padding: 24px;
	text-decoration: none !important;
	color: inherit;
	transition: all 0.3s;
	display: block;
}
.lalu-glossary-cat-card:hover {
	background: var(--theme-area_background_alt, #f5efea);
	border-color: var(--theme-accent, #a07855);
}
.lalu-glossary-cat-header {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	margin-bottom: 8px;
}
.lalu-glossary-cat-name {
	font-family: 'Instrument Serif', Georgia, serif;
	font-size: 24px;
	font-weight: 400;
	color: var(--theme-text_color, #181818);
	margin: 0;
}
.lalu-glossary-cat-count {
	font-family: 'Geist Mono', monospace;
	font-size: 12px;
	color: var(--theme-accent, #a07855);
}
.lalu-glossary-cat-desc {
	font-size: 13px;
	line-height: 1.5;
	color: var(--theme-text_dark, #5c3a21);
	margin: 0;
}
&lt;/style&gt;

{{$RecordsClass = $category::$contentItemClass;}}
{{$count = $RecordsClass::contentCount( $category, FALSE );}}

&lt;a href="{$category-&gt;url()}" class="lalu-glossary-cat-card"&gt;
	&lt;div class="lalu-glossary-cat-header"&gt;
		&lt;h2 class="lalu-glossary-cat-name"&gt;{$category-&gt;_title}&lt;/h2&gt;
		&lt;span class="lalu-glossary-cat-count"&gt;{$count}&lt;/span&gt;
	&lt;/div&gt;
	{{if $category-&gt;_description}}
		&lt;p class="lalu-glossary-cat-desc"&gt;{$category-&gt;_description|raw}&lt;/p&gt;
	{{endif}}
&lt;/a&gt;
</code></pre><h3>4.4 Pasang template group ke database</h3><p>Kembali ke Databases → Kamus Istilah → Display. Sekarang template sudah ada, jadi pilih di menu:</p><div class="ipsRichText__table-wrapper"><table style="width: 593px;"><colgroup><col style="width:197px;"><col style="width:396px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Field</p></th><th colspan="1" rowspan="1"><p>Value</p></th></tr><tr><td colspan="1" rowspan="1"><p>Category Listing Layout</p></td><td colspan="1" rowspan="1"><p>Custom → Category Template Group: Glossarycategorylist</p></td></tr><tr><td colspan="1" rowspan="1"><p>Listing Layout</p></td><td colspan="1" rowspan="1"><p>Custom → Listing Template Group: Glossarylisting</p></td></tr><tr><td colspan="1" rowspan="1"><p>Display Template Group</p></td><td colspan="1" rowspan="1"><p>Glossarydisplay</p></td></tr></tbody></table></div><p>Simpan. Halaman depan <code>/kamus-istilah/</code> sekarang menampilkan kotak kategori custom, klik kategori menampilkan daftar istilah custom, dan klik istilah menampilkan halaman detail custom.</p><h2>Fase 5: Mengisi istilah</h2><h3>5.1 Saran pembagian awal: 40 istilah</h3><p>Ini target, bukan syarat. Kamu boleh mulai dari 10 sampai 15 istilah yang paling sering kamu pakai, lalu menambah sambil jalan. Kamus yang rapi dengan 15 istilah lebih berguna daripada 40 istilah yang setengah jadi.</p><div class="ipsRichText__table-wrapper"><table style="min-width: 546px;"><colgroup><col style="width:191px;"><col style="min-width:20px;"><col style="width:335px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Kategori</p></th><th colspan="1" rowspan="1"><p>Target</p></th><th colspan="1" rowspan="1"><p>Contoh</p></th></tr><tr><td colspan="1" rowspan="1"><p>SEO &amp; Analytics</p></td><td colspan="1" rowspan="1"><p>12</p></td><td colspan="1" rowspan="1"><p>Backlink, SERP, CTR, Bounce Rate, DA, Anchor Text, Meta Description, Schema, Canonical, Sitemap, robots.txt, Indexing</p></td></tr><tr><td colspan="1" rowspan="1"><p>Monetisasi</p></td><td colspan="1" rowspan="1"><p>8</p></td><td colspan="1" rowspan="1"><p>AdSense, CPM, RPM, eCPM, CPC, Affiliate, Sponsorship, Display Ad</p></td></tr><tr><td colspan="1" rowspan="1"><p>Content &amp; Copywriting</p></td><td colspan="1" rowspan="1"><p>7</p></td><td colspan="1" rowspan="1"><p>Cornerstone, Pillar Page, Evergreen, Listicle, How-to, CTA, Hook</p></td></tr><tr><td colspan="1" rowspan="1"><p>Technical &amp; Performance</p></td><td colspan="1" rowspan="1"><p>6</p></td><td colspan="1" rowspan="1"><p>Core Web Vitals, LCP, CLS, INP, TTFB, Caching</p></td></tr><tr><td colspan="1" rowspan="1"><p>Platform &amp; Tools</p></td><td colspan="1" rowspan="1"><p>7</p></td><td colspan="1" rowspan="1"><p>WordPress, Blogspot, CMS, Plugin, Theme, Hosting, CDN</p></td></tr></tbody></table></div><h3>5.2 Cara menambah istilah</h3><p>Lewat halaman situs: buka <code>/kamus-istilah/</code>, klik kategori, lalu klik "Add new istilah". Tombol ini hanya muncul untuk akun yang punya izin menambah (lihat 1.7). Lewat AdminCP: Pages → Databases → Kamus Istilah → Records → Create New.</p><p>Isi per record:</p><div class="ipsRichText__table-wrapper"><table style="width: 465px;"><colgroup><col style="width:155px;"><col style="width:310px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Field</p></th><th colspan="1" rowspan="1"><p>Praktik terbaik</p></th></tr><tr><td colspan="1" rowspan="1"><p>Title</p></td><td colspan="1" rowspan="1"><p>Tulis istilahnya persis (Backlink, bukan "Apa itu Backlink")</p></td></tr><tr><td colspan="1" rowspan="1"><p>Aliases</p></td><td colspan="1" rowspan="1"><p>Sinonim atau akronim, dipisah koma (Tautan Balik, Inbound Link)</p></td></tr><tr><td colspan="1" rowspan="1"><p>Definisi Singkat</p></td><td colspan="1" rowspan="1"><p>Satu kalimat, maksimal 160 karakter</p></td></tr><tr><td colspan="1" rowspan="1"><p>Content</p></td><td colspan="1" rowspan="1"><p>Penjelasan lengkap (lihat struktur di bawah)</p></td></tr><tr><td colspan="1" rowspan="1"><p>Tags</p></td><td colspan="1" rowspan="1"><p>3 sampai 5 tag. Tampil sebagai "Istilah Terkait"</p></td></tr><tr><td colspan="1" rowspan="1"><p>Category</p></td><td colspan="1" rowspan="1"><p>Pilih yang paling sesuai</p></td></tr></tbody></table></div><h3>5.3 Susunan isi per istilah</h3><pre spellcheck="" class="ipsCode" data-language="html"><code>&lt;p&gt;{Definisi pembuka — 2-3 kalimat jelas}&lt;/p&gt;

&lt;h3&gt;Jenis / Variasi&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Tipe 1&lt;/strong&gt; — penjelasan&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Tipe 2&lt;/strong&gt; — penjelasan&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Contoh Praktis&lt;/h3&gt;
&lt;p&gt;{Contoh dari konteks Indonesia}&lt;/p&gt;

&lt;h3&gt;Mengapa Penting&lt;/h3&gt;
&lt;p&gt;{Relevansi untuk blogger}&lt;/p&gt;
</code></pre><p>Lewati bagian "Jenis / Variasi" kalau istilahnya memang tidak punya variasi. Lebih baik pendek dan jelas daripada panjang karena memaksakan bagian.</p><h3>5.4 Bantuan AI untuk draf awal</h3><p>Prompt yang bisa dipakai (Claude, ChatGPT, atau yang lain):</p><pre spellcheck="" class="ipsCode" data-language="text"><code>Tulis definisi istilah blogging berikut dalam bahasa Indonesia untuk
komunitas blogger. Format:
- Definisi singkat (max 160 karakter)
- Penjelasan lengkap (3-5 paragraf)
- Sertakan: jenis/variasi, contoh praktis, mengapa penting
- Bahasa: informal-profesional, pemula s/d menengah
- Hindari jargon yang tidak dijelaskan

Istilah: {ISTILAH}
Kategori: {KATEGORI}
</code></pre><p>Selalu periksa dulu sebelum menerbitkan:</p><ul><li><p>Akurasi teknis.</p></li><li><p>Relevansi untuk pembaca Indonesia.</p></li><li><p>Tidak ada angka atau fakta karangan.</p></li><li><p>Nada tulisan sama dengan bagian lain di situsmu.</p></li></ul><h3>5.5 Hapus record percobaan</h3><p>Sebelum kamus dibuka untuk umum, hapus record percobaan (misalnya yang isinya teks acak) atau ganti dengan konten asli.</p><h2>Fase 6: Menyambungkan ke situs</h2><h3>6.1 Tambahkan ke menu utama</h3><p>AdminCP → System → Settings → Menu Manager. Nama dan letak menu pengaturan ini bisa berbeda di versi lain; carilah pengelola navigasi di versimu. Tambahkan item:</p><div class="ipsRichText__table-wrapper"><table style="width: 460px;"><colgroup><col style="width:204px;"><col style="width:256px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Field</p></th><th colspan="1" rowspan="1"><p>Value</p></th></tr><tr><td colspan="1" rowspan="1"><p>Type</p></td><td colspan="1" rowspan="1"><p>Link (atau langsung item Pages kalau tersedia)</p></td></tr><tr><td colspan="1" rowspan="1"><p>Title</p></td><td colspan="1" rowspan="1"><p>Kamus Istilah</p></td></tr><tr><td colspan="1" rowspan="1"><p>URL</p></td><td colspan="1" rowspan="1"><p>/kamus-istilah/</p></td></tr><tr><td colspan="1" rowspan="1"><p>Display in main navigation</p></td><td colspan="1" rowspan="1"><p>Yes</p></td></tr></tbody></table></div><h3>6.2 Blok "Istilah Acak" di beranda (opsional)</h3><p>Bagian ini opsional dan belum disertai kode contoh. AdminCP → Pages → Blocks → Add → Plugin → Record Feed:</p><div class="ipsRichText__table-wrapper"><table style="width: 327px;"><colgroup><col style="width:151px;"><col style="width:176px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Field</p></th><th colspan="1" rowspan="1"><p>Value</p></th></tr><tr><td colspan="1" rowspan="1"><p>Database</p></td><td colspan="1" rowspan="1"><p>Kamus Istilah</p></td></tr><tr><td colspan="1" rowspan="1"><p>Number to show</p></td><td colspan="1" rowspan="1"><p>5</p></td></tr><tr><td colspan="1" rowspan="1"><p>Sort by</p></td><td colspan="1" rowspan="1"><p>Random</p></td></tr></tbody></table></div><p>Untuk mengatur tampilannya, buat template blok custom yang me-loop <code>$records</code> dan menampilkan Title serta <code>customFieldDisplayByKey('definisi-singkat', 'raw')</code> untuk tiap item. Pasang bloknya di beranda lewat editor Page.</p><h3>6.3 Tautan dari artikel</h3><p>Saat menulis artikel, tautkan manual ke istilah di kamus:</p><pre spellcheck="" class="ipsCode" data-language="html"><code>Gunakan &lt;a href="/kamus-istilah/seo-analytics/backlink-r1/"&gt;backlink&lt;/a&gt;
berkualitas untuk meningkatkan authority.
</code></pre><p>Angka di ujung alamat (<code>r1</code>) adalah nomor record. Salin alamat lengkap dari browser, jangan mengetiknya sendiri.</p><h2>Fase 7: Pengujian</h2><p>Sebelum menganggap kamus selesai, periksa satu per satu:</p><ul><li><p><strong>Halaman depan</strong> (<code>/kamus-istilah/</code>): kategori tampil sebagai kotak dan jumlah istilahnya benar.</p></li><li><p><strong>Daftar kategori</strong>: istilah terurut abjad, sinonim dan definisi singkat tampil.</p></li><li><p><strong>Halaman istilah</strong>: judul, sinonim, definisi singkat, isi, istilah terkait, dan jumlah tayangan tampil. Tautan breadcrumb bisa diklik.</p></li><li><p><strong>Sebagai tamu</strong>: buka di jendela privat tanpa masuk akun. Semua halaman terbuka, dan tidak ada tombol untuk menambah atau mengubah istilah.</p></li><li><p><strong>Di ponsel</strong>: buka di ponsel atau perkecil jendela browser sampai di bawah 700 piksel. Kotak kategori harus menjadi satu kolom.</p></li><li><p><strong>Pencarian</strong>: cari satu istilah lewat kolom pencarian situs. Istilah itu harus muncul di hasil.</p></li><li><p><strong>Menu</strong>: item Kamus Istilah mengarah ke <code>/kamus-istilah/</code>.</p></li><li><p><strong>Alamat salah</strong>: <code>/glossary/</code> menghasilkan 404. Itu normal; pastikan tidak ada tautan yang mengarah ke sana.</p></li><li><p><strong>Record percobaan</strong>: tidak ada lagi record dengan teks acak.</p></li></ul><h2>Fase 8: Perawatan</h2><ul><li><p><strong>Menambah istilah.</strong> Ikuti 5.2. Kalau kategori hampir penuh atau terasa campur aduk, tambahkan kategori baru lewat Fase 3 dan isi slug-nya manual.</p></li><li><p><strong>Mengubah template.</strong> Salin dulu isi lama ke berkas teks. Setelah menyimpan, kalau tampilan tidak berubah, kosongkan cache situs (misalnya Redis, kalau dipakai) dan muat ulang halaman tanpa cache (Ctrl+F5).</p></li><li><p><strong>Jangan mengubah Page Filename atau slug kategori</strong> setelah ada tautan yang masuk, termasuk tautan dari artikelmu sendiri. Alamat lama akan menghasilkan 404.</p></li><li><p><strong>Setelah memperbarui IPS.</strong> Buka kembali tiga jenis halaman dan ulangi Fase 7. Template custom bisa terpengaruh perubahan di versi baru.</p></li><li><p><strong>Cadangan.</strong> Simpan salinan ketiga template (glossarydisplay, glossarylisting, glossarycategorylist) di tempat yang aman.</p></li></ul><h2>Pemecahan masalah</h2><div class="ipsRichText__table-wrapper"><table style="width: 860px;"><colgroup><col style="width:187px;"><col style="width:284px;"><col style="width:389px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Masalah</p></th><th colspan="1" rowspan="1"><p>Penyebab</p></th><th colspan="1" rowspan="1"><p>Solusi</p></th></tr><tr><td colspan="1" rowspan="1"><p>"widgetArea is throwing an error"</p></td><td colspan="1" rowspan="1"><p>Sintaks PHP dinamis di template (misalnya <code>{$record::database()-&gt;url()}</code> atau <code>{expression=...}</code>) gagal dirender</p></td><td colspan="1" rowspan="1"><p>Ganti dengan teks statis, sederhanakan kode, dan uji bagian demi bagian</p></td></tr><tr><td colspan="1" rowspan="1"><p>"syntax of tags or HTML logic is badly formatted"</p></td><td colspan="1" rowspan="1"><p>Komentar <code>{{* *}}</code> tidak valid</p></td><td colspan="1" rowspan="1"><p>Pakai <code>&lt;!-- --&gt;</code> atau hapus</p></td></tr><tr><td colspan="1" rowspan="1"><p>Kolom kosong padahal ada data</p></td><td colspan="1" rowspan="1"><p>Nama property salah</p></td><td colspan="1" rowspan="1"><p>Content adalah <code>content()</code> (method); jumlah tayangan adalah <code>record_views</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>URL breadcrumb berisi kode mentah</p></td><td colspan="1" rowspan="1"><p>Pemanggilan class tidak dieksekusi sebagai PHP</p></td><td colspan="1" rowspan="1"><p>Tulis <code>/kamus-istilah/</code> sebagai teks polos</p></td></tr><tr><td colspan="1" rowspan="1"><p>Halaman <code>/glossary/</code> error 404</p></td><td colspan="1" rowspan="1"><p>Page bernama <code>kamus-istilah</code>, bukan <code>glossary</code></p></td><td colspan="1" rowspan="1"><p>Pakai URL atau Page Filename yang benar</p></td></tr><tr><td colspan="1" rowspan="1"><p>CSS tidak diterapkan</p></td><td colspan="1" rowspan="1"><p>Berkas CSS terpisah tidak dimuat otomatis</p></td><td colspan="1" rowspan="1"><p>Taruh CSS di blok <code>&lt;style&gt;</code> di dalam template HTML</p></td></tr><tr><td colspan="1" rowspan="1"><p>"Attempt to assign property options on null"</p></td><td colspan="1" rowspan="1"><p>Mengedit kolom bawaan yang terkunci (Title atau Content)</p></td><td colspan="1" rowspan="1"><p>Jangan edit kolom bawaan</p></td></tr><tr><td colspan="1" rowspan="1"><p>Halaman depan menampilkan record, bukan kategori</p></td><td colspan="1" rowspan="1"><p>Index Page masih "All Content"</p></td><td colspan="1" rowspan="1"><p>Atur Index Page ke "Category Listing"</p></td></tr><tr><td colspan="1" rowspan="1"><p>Template custom tidak terpakai</p></td><td colspan="1" rowspan="1"><p>Layout masih "Grid" atau "Rows"</p></td><td colspan="1" rowspan="1"><p>Atur Layout ke "Custom", lalu pilih Template Group</p></td></tr><tr><td colspan="1" rowspan="1"><p>Error required saat mematikan tampilan kolom</p></td><td colspan="1" rowspan="1"><p>Display View Format wajib diisi</p></td><td colspan="1" rowspan="1"><p>Pilih "Value" walau tidak dipakai</p></td></tr><tr><td colspan="1" rowspan="1"><p>Tombol "Add new istilah" tidak muncul</p></td><td colspan="1" rowspan="1"><p>Akun yang dipakai belum punya izin menambah record</p></td><td colspan="1" rowspan="1"><p>Periksa izin database (1.7) dan izin kategori (Fase 3), lalu masuk dengan akun admin</p></td></tr><tr><td colspan="1" rowspan="1"><p>Huruf tidak sama dengan contoh</p></td><td colspan="1" rowspan="1"><p>Font Geist, Instrument Serif, atau Geist Mono tidak dimuat oleh temamu</p></td><td colspan="1" rowspan="1"><p>Muat font itu lewat tema, atau ganti nama font di CSS</p></td></tr></tbody></table></div><h2>Bacaan lanjutan</h2><ul><li><p><a rel="external nofollow" href="https://invisioncommunity.com/4guides/pages/core-concepts/databases-part-ii-r154/">Invision Community: Databases, Part II</a>. Membahas izin di tingkat halaman, database, dan kategori. Ditulis untuk versi 4, jadi sebagian langkah bisa berbeda di versi 5.</p></li><li><p><a rel="external nofollow" href="https://invisioncommunity.com/docs/pages/case-study-building-a-recipe-section-r236/">Invision Community: Case study, building a recipe section</a>. Contoh lain membuat database dengan kolom dan kategori.</p></li></ul><h2>Catatan teknis</h2><p>Kode template di Fase 4 sudah dijalankan di Invision Community 5.0.19 pada situs ini. Langkah di Fase 6 bisa berbeda di versi lain, dan bagian izin (1.7) mengikuti dokumentasi Invision, jadi pastikan lewat Fase 7. Dibuat untuk <a rel="external nofollow" href="http://lalu.pro">lalu.pro</a>.</p>]]></description><guid isPermaLink="false">3</guid><pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate></item><item><title>Cara Membuat PageSpeed Tester Sendiri untuk Blog</title><link>https://lalu.pro/articles/platform-web/cara-membuat-pagespeed-tester-sendiri-untuk-blog-r2/</link><description><![CDATA[
<p><img src="https://lalu.pro/uploads/monthly_2026_05/pagespeed_tester_blogger.avif.543ff463e184ac645f0e7c56a4e47223.avif" /></p>
<p>Tutorial lengkap membuat tool PageSpeed Tester yang aman dan gratis untuk blog Blogger Anda. Tool ini menggunakan <strong>Google PageSpeed Insights API</strong> dengan <strong>Cloudflare Worker</strong> sebagai proxy untuk menyembunyikan API key.</p><h2>Apa yang Akan Anda Buat</h2><p>Tool yang bisa:</p><ul><li><p>Cek skor Performance, Accessibility, Best Practices, dan SEO halaman manapun</p></li><li><p>Tampilkan Core Web Vitals (FCP, LCP, TBT, CLS, dll)</p></li><li><p>Berikan rekomendasi perbaikan diurutkan berdasarkan impact</p></li><li><p>Cache hasil 1 jam untuk hemat quota</p></li><li><p>100% gratis (Google API quota gratis cukup untuk blog normal)</p></li></ul><h2>Mengapa Butuh Cloudflare Worker?</h2><p>API Google butuh <strong>API key</strong> untuk akses unlimited. Kalau key disimpan langsung di JavaScript blog Anda:</p><ul><li><p>Key terlihat di <strong>view-source</strong> dan <strong>Network tab</strong> browser</p></li><li><p>Siapa saja bisa copy key Anda</p></li><li><p>Pakai untuk panggil API mereka sendiri</p></li><li><p>Quota Anda habis, atau worse — kena billing kalau key Anda attach ke service berbayar</p></li></ul><p><strong>Solusi:</strong> Simpan key di <strong>Cloudflare Worker</strong> (server). Tool browser hanya panggil Worker, Worker yang panggil Google API.</p><pre spellcheck="" class="ipsCode language-plaintext" data-language="Plain Text"><code>Browser (tool Anda) → Cloudflare Worker (key di sini) → Google PageSpeed API
</code></pre><p>Key tidak pernah keluar dari server. Aman.</p><h2>Prasyarat</h2><p>Sebelum mulai, siapkan:</p><ol><li><p><strong>Akun Google</strong> (untuk Google Cloud Console)</p></li><li><p><strong>Akun Cloudflare</strong> (gratis, daftar di <a rel="external nofollow" href="https://cloudflare.com/">cloudflare.com</a>)</p></li><li><p><strong>Blog Blogger</strong> Anda</p></li></ol><p>Tidak perlu credit card untuk setup ini.</p><hr><h2>STEP 1: Dapatkan Google API Key</h2><h3>1.1 Aktifkan PageSpeed Insights API</h3><ol><li><p>Buka <a rel="external nofollow" href="https://console.cloud.google.com/">Google Cloud Console</a></p></li><li><p>Buat project baru (atau pilih yang sudah ada)</p></li><li><p>Di sidebar kiri: <strong>APIs &amp; Services</strong> → <strong>Library</strong></p></li><li><p>Search: <strong>"PageSpeed Insights API"</strong></p></li><li><p>Klik hasil, lalu <strong>Enable</strong></p></li></ol><h3>1.2 Buat API Key</h3><ol><li><p><strong>APIs &amp; Services</strong> → <strong>Credentials</strong></p></li><li><p><strong>Create Credentials</strong> → <strong>API key</strong></p></li><li><p>Key baru muncul. <strong>Copy key tersebut</strong> dan simpan sementara di Notepad/text editor</p></li><li><p>Klik <strong>Edit API key</strong> untuk setup restriction</p></li></ol><h3>1.3 Set Restriction (Sangat Penting!)</h3><p>Tanpa restriction, key bisa dipakai siapa saja yang punya key tersebut.</p><p><strong>Application restrictions:</strong></p><ul><li><p>Pilih <strong>Websites</strong></p></li><li><p>Klik <strong>Add an item</strong></p></li><li><p>Isi domain Anda:</p><ul><li><p><code>*.laluabdrahman.com*</code> (ganti dengan domain Anda)</p></li><li><p><code>https://laluabdrahman.com/*</code></p></li><li><p><code>https://www.laluabdrahman.com/*</code></p></li></ul></li></ul><p><strong>API restrictions:</strong></p><ul><li><p>Pilih <strong>Restrict key</strong></p></li><li><p>Centang <strong>hanya</strong> "PageSpeed Insights API"</p></li><li><p>Klik <strong>Save</strong></p></li></ul><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Tips keamanan:</strong> Restriction ini berarti key Anda tidak bisa dipakai dari domain lain ATAU untuk API Google lain (Maps, Translate, dll yang berbayar).</p></div></blockquote><hr><h2>STEP 2: Deploy Cloudflare Worker</h2><h3>2.1 Buat Worker Baru</h3><ol><li><p>Login ke <a rel="external nofollow" href="https://dash.cloudflare.com/">Cloudflare dashboard</a></p></li><li><p>Sidebar kiri: <strong>Workers &amp; Pages</strong></p></li><li><p>Klik <strong>Create application</strong> → <strong>Create Worker</strong></p></li><li><p>Beri nama, misal: <code>pagespeed-proxy</code></p></li><li><p>Klik <strong>Deploy</strong> (dengan default code)</p></li><li><p>Setelah deploy, klik <strong>Edit code</strong></p></li></ol><h3>2.2 Paste Kode Worker</h3><p>Hapus semua default code, paste kode berikut:</p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>export default {
  async fetch(request, env, ctx) {
    // CORS preflight
    if (request.method === 'OPTIONS') {
      return new Response(null, { status: 204, headers: corsHeaders() });
    }
    if (request.method !== 'GET') {
      return errorResponse('Only GET allowed', 405);
    }

    const url = new URL(request.url);
    const targetUrl = url.searchParams.get('url');
    const strategy = (url.searchParams.get('strategy') || 'mobile').toLowerCase();

    if (!targetUrl) {
      return errorResponse('Missing url parameter', 400);
    }
    if (!['mobile', 'desktop'].includes(strategy)) {
      return errorResponse('strategy must be mobile or desktop', 400);
    }

    // Validate target URL
    let target;
    try {
      target = new URL(targetUrl);
    } catch (e) {
      return errorResponse('Invalid URL format', 400);
    }
    if (!['http:', 'https:'].includes(target.protocol)) {
      return errorResponse('Only http(s) protocol allowed', 403);
    }

    // SSRF prevention
    const hostname = target.hostname.toLowerCase();
    if (['localhost', '127.0.0.1', '0.0.0.0', '::1'].includes(hostname) ||
        /^10\./.test(hostname) ||
        /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) ||
        /^192\.168\./.test(hostname) ||
        /^169\.254\./.test(hostname)) {
      return errorResponse('Hostname not allowed', 403);
    }

    if (!env.GOOGLE_API_KEY) {
      return errorResponse('Worker not configured: missing GOOGLE_API_KEY secret', 500);
    }

    // Cache check
    const cacheKey = new Request(
      `https://psi-cache.internal/${strategy}/${encodeURIComponent(target.href)}`,
      { method: 'GET' }
    );
    const cache = caches.default;
    let cached = await cache.match(cacheKey);
    if (cached) {
      const newHeaders = new Headers(cached.headers);
      newHeaders.set('X-PSI-Cache', 'HIT');
      return new Response(cached.body, { status: cached.status, headers: newHeaders });
    }

    // Call Google PageSpeed API
    const apiUrl = new URL('https://www.googleapis.com/pagespeedonline/v5/runPagespeed');
    apiUrl.searchParams.set('url', target.href);
    apiUrl.searchParams.set('strategy', strategy);
    ['performance', 'accessibility', 'best-practices', 'seo'].forEach(cat =&gt; {
      apiUrl.searchParams.append('category', cat);
    });
    apiUrl.searchParams.set('key', env.GOOGLE_API_KEY);

    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(() =&gt; controller.abort(), 60000);

      // GANTI Referer di bawah dengan domain blog Anda
      const apiRes = await fetch(apiUrl.toString(), {
        headers: { 'Referer': 'https://www.laluabdrahman.com/' },
        signal: controller.signal
      });
      clearTimeout(timeoutId);

      if (!apiRes.ok) {
        let errMsg = `Google API returned HTTP ${apiRes.status}`;
        try {
          const errBody = await apiRes.json();
          if (errBody.error?.message) errMsg = errBody.error.message;
        } catch (e) {}
        return errorResponse(errMsg, apiRes.status === 429 ? 429 : 502);
      }

      const data = await apiRes.json();
      const response = new Response(JSON.stringify(data), {
        status: 200,
        headers: {
          ...corsHeaders(),
          'Content-Type': 'application/json',
          'Cache-Control': 'public, max-age=3600',
          'X-PSI-Cache': 'MISS'
        }
      });

      ctx.waitUntil(cache.put(cacheKey, response.clone()));
      return response;
    } catch (err) {
      if (err.name === 'AbortError') {
        return errorResponse('PageSpeed API timeout (&gt;60s)', 504);
      }
      return errorResponse('Fetch failed: ' + err.message, 502);
    }
  }
};

function corsHeaders() {
  return {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type'
  };
}

function errorResponse(message, status) {
  return new Response(JSON.stringify({ error: message }), {
    status,
    headers: { ...corsHeaders(), 'Content-Type': 'application/json' }
  });
}
</code></pre><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>PENTING:</strong> Cari baris <code>'Referer': 'https://www.laluabdrahman.com/'</code> dan ganti dengan domain Anda. Ini harus match dengan restriction key di Google Cloud Console.</p></div></blockquote><p>Klik <strong>Save and deploy</strong>.</p><h3>2.3 Tambah API Key sebagai Secret</h3><p>Sekarang masukkan API key Anda ke Worker sebagai secret (encrypted):</p><ol><li><p>Di Worker → tab <strong>Settings</strong></p></li><li><p>Scroll ke section <strong>Variables and Secrets</strong></p></li><li><p>Klik <strong>+ Add</strong></p></li><li><p>Isi:</p><ul><li><p><strong>Type</strong>: <code>Secret</code> (penting! bukan Text)</p></li><li><p><strong>Variable name</strong>: <code>GOOGLE_API_KEY</code> (case-sensitive, persis seperti ini)</p></li><li><p><strong>Value</strong>: paste API key dari Step 1.2</p></li></ul></li><li><p>Klik <strong>Save</strong></p></li></ol><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Catatan:</strong> Setelah secret ditambahkan, Worker mungkin perlu di-redeploy. Buka tab <strong>Deployments</strong>, kalau tidak ada deployment baru otomatis, edit kode dan save lagi (tambah satu spasi lalu hapus juga cukup).</p></div></blockquote><h3>2.4 Test Worker</h3><p>Buka di browser:</p><pre spellcheck="" class="ipsCode language-plaintext" data-language="Plain Text"><code>https://pagespeed-proxy.YOUR-NAME.workers.dev/?url=https://example.com&amp;strategy=mobile
</code></pre><p>Ganti <code>YOUR-NAME</code> dengan subdomain Worker Anda.</p><p><strong>Expected:</strong></p><ul><li><p>Tunggu 15-40 detik</p></li><li><p>Lihat JSON besar muncul dengan field <code>lighthouseResult</code></p></li><li><p>Jika ada error, baca pesan dan cek troubleshooting di bawah</p></li></ul><p>Catat URL Worker Anda — akan dipakai di Step 3.</p><hr><h2>STEP 3: Setup Tool HTML di Blogger</h2><h3>3.1 Edit Konfigurasi Tool</h3><p>Buka file <code>pagespeed-tester.html</code> Anda. Cari baris ini (sekitar baris 510):</p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>const WORKER_URL = '';
</code></pre><p>Ganti dengan URL Worker Anda:</p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>const WORKER_URL = 'https://pagespeed-proxy.YOUR-NAME.workers.dev/';
</code></pre><h3>3.2 Paste ke Blogger</h3><ol><li><p>Blogger dashboard → <strong>Pages</strong> → <strong>New Page</strong></p></li><li><p>Klik mode <strong>HTML view</strong> (bukan Compose)</p></li><li><p>Paste seluruh isi file <code>pagespeed-tester.html</code></p></li><li><p>Beri title halaman: "PageSpeed Tester"</p></li><li><p><strong>Publish</strong></p></li></ol><h3>3.3 Test Tool</h3><p>Buka halaman yang baru dipublish, lalu:</p><ol><li><p>Input URL, contoh: <code>https://example.com</code></p></li><li><p>Pilih Mobile atau Desktop</p></li><li><p>Klik <strong>Mulai Tes</strong></p></li><li><p>Tunggu 15-40 detik</p></li><li><p>Hasil muncul dengan score rings, Core Web Vitals, dan rekomendasi</p></li></ol><p><strong>Cache test:</strong></p><ul><li><p>Klik <strong>Mulai Tes</strong> lagi dengan URL dan strategy sama</p></li><li><p>Harusnya muncul <strong>&lt;1 detik</strong> dari cache</p></li><li><p>Footer berubah jadi: "✓ dari cache"</p></li></ul><hr><h2>Troubleshooting</h2><h3>Error: "Worker not configured: missing GOOGLE_API_KEY secret"</h3><p>Secret belum tersimpan atau Worker belum redeploy. Solusi:</p><ol><li><p>Cek di <strong>Settings → Variables and Secrets</strong>, pastikan ada <code>GOOGLE_API_KEY</code> (case-sensitive)</p></li><li><p>Pastikan <strong>Type-nya Secret</strong>, bukan Text</p></li><li><p>Edit kode Worker, save lagi untuk trigger redeploy</p></li></ol><h3>Error: "Requests from referer &lt;empty&gt; are blocked"</h3><p>API key Anda di-restrict ke domain, tapi Worker tidak kirim Referer header.</p><p><strong>Solusi:</strong> Pastikan di kode Worker ada baris ini di dalam <code>fetch()</code> ke Google API:</p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>headers: { 'Referer': 'https://www.YOUR-DOMAIN.com/' }
</code></pre><p>Domain di sini harus match dengan salah satu restriction di Google Cloud Console.</p><h3>Tombol "Mulai Tes" tidak respon</h3><p>Buka Developer Console (F12 → Console), lihat error apa yang muncul.</p><p>Cek juga:</p><ul><li><p><code>WORKER_URL</code> sudah diisi dengan URL Worker Anda</p></li><li><p>URL Worker bisa diakses langsung di browser</p></li></ul><h3>Error: "HTTP 429" (Too Many Requests)</h3><p>Rate limit dari Google. Tunggu 1-2 menit, lalu coba lagi. Atau:</p><ul><li><p>Pastikan API key sudah terkonfigurasi dengan benar</p></li><li><p>Caching seharusnya mengurangi request rate</p></li></ul><h3>Hasil PageSpeed berbeda dari pagespeed.web.dev</h3><p>Normal. Score PageSpeed bisa fluktuasi 5-10 point antar test karena:</p><ul><li><p>Variasi network condition di server Google</p></li><li><p>Random sampling dari data CrUX (Chrome User Experience Report)</p></li><li><p>Cache TTL yang berbeda</p></li></ul><p>Untuk benchmark akurat, jalankan test 3-5 kali dan ambil median.</p><hr><h2>Quota dan Biaya</h2><h3>Google PageSpeed Insights API</h3><ul><li><p><strong>25,000 query per hari</strong> gratis</p></li><li><p><strong>Cukup untuk blog normal</strong> — bahkan dengan caching off</p></li><li><p>Tidak perlu billing setup</p></li></ul><h3>Cloudflare Workers</h3><ul><li><p><strong>100,000 request per hari</strong> gratis (free tier)</p></li><li><p><strong>10ms CPU time per request</strong> (kita pakai jauh lebih sedikit)</p></li><li><p>Cache pakai Cloudflare Cache API (built-in, gratis)</p></li></ul><h3>Estimasi penggunaan blog dengan 1,000 visitor/hari</h3><p>Asumsi: 1% visitor pakai tool = 10 test/hari</p><div class="ipsRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Resource</p></th><th colspan="1" rowspan="1"><p>Limit</p></th><th colspan="1" rowspan="1"><p>Penggunaan</p></th></tr><tr><td colspan="1" rowspan="1"><p>Cloudflare Workers</p></td><td colspan="1" rowspan="1"><p>100k/hari</p></td><td colspan="1" rowspan="1"><p>10 req</p></td></tr><tr><td colspan="1" rowspan="1"><p>Google API</p></td><td colspan="1" rowspan="1"><p>25k/hari</p></td><td colspan="1" rowspan="1"><p>10 req</p></td></tr><tr><td colspan="1" rowspan="1"><p>Cache hit rate</p></td><td colspan="1" rowspan="1"><p>-</p></td><td colspan="1" rowspan="1"><p>~30% (URL repeat)</p></td></tr></tbody></table></div><p>Penggunaan masih jauh di bawah free tier. Aman untuk blog dengan trafik wajar.</p><hr><h2>Cara Kerja Detail</h2><h3>Arsitektur Defense in Depth</h3><p>Tool ini punya <strong>5 layer keamanan</strong>:</p><ol><li><p><strong>Key disembunyikan di Worker secret</strong> — encrypted di Cloudflare, tidak visible di dashboard setelah disimpan</p></li><li><p><strong>API restriction di Google</strong> — key hanya bisa pakai PageSpeed Insights API, bukan Maps/Translate yang berbayar</p></li><li><p><strong>Website restriction di Google</strong> — referer harus match domain Anda</p></li><li><p><strong>Worker forward referer</strong> — memungkinkan website restriction bekerja meski request datang dari Cloudflare IP</p></li><li><p><strong>SSRF prevention di Worker</strong> — block private IP dan localhost di parameter URL</p></li></ol><p>Kalau salah satu layer compromised, layer lain masih protect:</p><ul><li><p>Key bocor → masih di-restrict ke PageSpeed Insights API only</p></li><li><p>Restriction lemah → key tetap tidak visible di browser</p></li></ul><h3>Cache Strategy</h3><p>Worker pakai <strong>Cloudflare Cache API</strong> (built-in, no setup):</p><ul><li><p><strong>Cache key:</strong> <code>URL + strategy</code></p></li><li><p><strong>TTL:</strong> 1 jam (3600 detik)</p></li><li><p><strong>Storage:</strong> Cloudflare edge, automatic eviction</p></li></ul><p>Mobile dan Desktop terhitung cache terpisah. Cache di-share antar visitor — jadi kalau 100 orang test URL sama, hanya 1 yang fresh fetch, sisanya dari cache.</p><h3>Kenapa Hasil Pertama Lambat?</h3><p>Google PageSpeed API actually menjalankan Lighthouse audit di server mereka — load page, render, simulate user interaction, analyze. Itu butuh 15-40 detik. Tidak bisa diakselerasi.</p><p>Setelah cache, hasil instan karena tidak perlu re-audit.</p><hr><h2>Kesimpulan</h2><p>Anda sekarang punya tool PageSpeed Tester sendiri yang:</p><ul><li><p><strong>Gratis</strong> sampai 25k test/hari (jauh di atas kebutuhan blog normal)</p></li><li><p><strong>Aman</strong> — API key tidak pernah terlihat publik</p></li><li><p><strong>Cepat</strong> untuk repeat test (cache 1 jam)</p></li><li><p><strong>Customizable</strong> — bisa modify UI, tambah fitur, dll</p></li></ul><p>Tool ini juga jadi foundation pattern untuk tool lain yang butuh API key. Misalnya kalau Anda mau buat:</p><ul><li><p><strong>Image Alt Text Generator</strong> (pakai OpenAI/Claude API)</p></li><li><p><strong>SERP Checker</strong> (pakai SerpApi)</p></li><li><p><strong>Backlink Checker</strong> (pakai Ahrefs API)</p></li></ul><p>Polanya sama: Worker proxy + secret env variable.</p><p>Selamat mencoba!</p>]]></description><guid isPermaLink="false">2</guid><pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate></item><item><title>Tutorial Komprehensif Membuat Homepage Kustom di Invision Community 5</title><link>https://lalu.pro/articles/platform-web/tutorial-komprehensif-membuat-homepage-kustom-di-invision-community-5-r1/</link><description><![CDATA[<p>Panduan lengkap berdasarkan pengalaman membangun <strong>lalu.pro</strong> — dengan Invision Community 5.0.18.</p><p><strong>Target:</strong> Pengguna IPS 5 yang ingin membangun homepage editorial dengan data live dari berbagai aplikasi IPS (Forum, Pages, Downloads).  </p><p><strong>Tingkat:</strong> Menengah (butuh dasar HTML/CSS, tidak butuh PHP advance).</p><h2>Prasyarat</h2><p>Sebelum mulai, pastikan Anda punya:</p><p>Sebelum mulai, pastikan Anda punya:</p><p>Teknis</p><ul><li><p><strong>Invision Community 5.0.x</strong> ter-install (panduan ini di-test di 5.0.18)</p></li><li><p><strong>Aplikasi Pages</strong> aktif (wajib — ini yang memungkinkan custom homepage)</p></li><li><p><strong>AdminCP access</strong> sebagai administrator</p></li><li><p><strong>Akses cPanel atau file manager</strong> (untuk backup tema, opsional tapi sangat direkomendasikan)</p></li></ul><p>Pengetahuan</p><ul><li><p>HTML dasar (tag, attribute, nested element)</p></li><li><p>CSS dasar (selector, properties, flexbox, grid)</p></li><li><p>Konsep template tag (mirip Twig, Liquid, atau Blade — tidak perlu PHP)</p></li><li><p>Mengerti perbedaan client-side (HTML/CSS) vs server-side (PHP) rendering</p></li></ul><p>Aplikasi IPS yang Direkomendasikan untuk Live Data</p><ul><li><p><strong>Forum</strong> (untuk fetch topik diskusi)</p></li><li><p><strong>Pages</strong> dengan custom database (untuk artikel)</p></li><li><p><strong>Downloads</strong> (untuk file/resources)</p></li><li><p><strong>Calendar</strong> (untuk events — opsional)</p></li></ul><p>Persiapan Konten</p><ul><li><p>Minimal <strong>3-5 topik forum</strong> sudah dipost</p></li><li><p>Minimal <strong>3-5 artikel</strong> sudah di-publish di Pages database</p></li><li><p>Minimal <strong>2-3 file</strong> sudah di-upload di Downloads</p></li></ul><p>Tanpa konten ini, section live data akan tampak kosong saat testing.</p><h2>Konsep IPS 5 yang Wajib Dipahami</h2><p>Sebelum nyentuh kode, pahami dulu konsep-konsep ini. Banyak frustrasi muncul karena pengguna tidak tahu <strong>vocabulary</strong> IPS 5.</p><h3>1. Pages — Aplikasi Pembuat Halaman</h3><p><strong>Pages </strong>adalah aplikasi IPS yang memungkinkan Anda membuat halaman custom di luar struktur forum standar. Homepage custom Anda akan jadi sebuah Page.</p><p><strong>Path:</strong> <em>AdminCP → Pages</em></p><h3>2. Page Types</h3><p>Saat membuat Page baru, ada dua tipe utama:</p><div class="ipsRichText__table-wrapper"><table style="width: 600px;"><colgroup><col style="width:148px;"><col style="width:297px;"><col style="width:155px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Tipe</p></th><th colspan="1" rowspan="1"><p>Kapan Dipakai</p></th><th colspan="1" rowspan="1"><p>Tutorial Ini Pakai?</p></th></tr><tr><td colspan="1" rowspan="1"><p>Page Builder</p></td><td colspan="1" rowspan="1"><p>Visual drag-and-drop dengan widget</p></td><td colspan="1" rowspan="1"><p>Tidak</p></td></tr><tr><td colspan="1" rowspan="1"><p>Manual</p></td><td colspan="1" rowspan="1"><p>HTML/template tag custom penuh</p></td><td colspan="1" rowspan="1"><p>Ya</p></td></tr></tbody></table></div><p><strong>Manual</strong> memberi Anda kontrol penuh atas HTML, sementara Page Builder cocok untuk yang tidak mau repot <em>ngoding</em>.</p><h3>3. Use Suite HTML Wrapper — Setting Paling Krusial</h3><p>Ini setting yang sering bikin bingung. Ada dua mode:</p><h4>Mode A: Suite Wrapper ON (Direkomendasikan untuk Production)</h4><p><span class="ipsEmoji" title="">✅</span> Konten Anda di-wrap dalam template global IPS (header, footer, sidebar, scripts)<br><span class="ipsEmoji" title="">✅</span> Security IPS aktif (CSRF, session, auth)<br><span class="ipsEmoji" title="">✅</span> JavaScript dan PHP IPS jalan native<br><span class="ipsEmoji" title="">✅</span> Konsisten dengan halaman lain (forum, artikel detail)<br><span class="ipsEmoji" title="">❌</span> Tidak bisa custom &lt;head&gt;, &lt;body&gt;, atau full HTML</p><p><strong>Kapan pakai:</strong> Saat Anda mau pakai header &amp; footer bawaan IPS, dan hanya custom area konten tengah.</p><h4>Mode B: Suite Wrapper OFF (Full Custom)</h4><p><span class="ipsEmoji" title="">✅</span> Full kontrol dari &lt;!DOCTYPE html&gt; sampai &lt;/html&gt;<br><span class="ipsEmoji" title="">✅</span> Bisa pakai font, CSS, JS dari mana saja<br><span class="ipsEmoji" title="">❌</span> Harus handle security sendiri<br><span class="ipsEmoji" title="">❌</span> Tidak otomatis dapat update template IPS<br><span class="ipsEmoji" title="">❌</span> Header/footer/user dropdown IPS hilang<br><span class="ipsEmoji" title="">❌</span> Login/logout/notifications bawaan IPS tidak otomatis ada</p><p><strong>Kapan pakai:</strong> Saat homepage benar-benar berbeda total dari sisa situs (misal, landing page marketing).</p><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Rekomendasi panduan ini:</strong> Pakai <strong>Suite Wrapper ON</strong>. Lebih aman, maintenance-friendly, dan transisi antar halaman seamless.</p></div></blockquote><h3>4. Blocks — Mesin Live Data</h3><p>Blocks adalah komponen reusable yang bisa menampilkan data dari aplikasi IPS. Ada <strong>4 tipe block</strong> di IPS 5:</p><div class="ipsRichText__table-wrapper"><table style="width: 636px;"><colgroup><col style="width:119px;"><col style="width:271px;"><col style="width:246px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Tipe</p></th><th colspan="1" rowspan="1"><p>Fungsi</p></th><th colspan="1" rowspan="1"><p>Contoh Penggunaan</p></th></tr><tr><td colspan="1" rowspan="1"><p>Plugin</p></td><td colspan="1" rowspan="1"><p>Feed otomatis dari aplikasi IPS</p></td><td colspan="1" rowspan="1"><p>Topic Feed, Record Feed, File Feed</p></td></tr><tr><td colspan="1" rowspan="1"><p>Manual HTML</p></td><td colspan="1" rowspan="1"><p>HTML + expression tag + PHP inline</p></td><td colspan="1" rowspan="1"><p>Counter, custom widget</p></td></tr><tr><td colspan="1" rowspan="1"><p>Editor</p></td><td colspan="1" rowspan="1"><p>Konten statis pakai WYSIWYG editor</p></td><td colspan="1" rowspan="1"><p>Welcome message, announcement</p></td></tr><tr><td colspan="1" rowspan="1"><p>Template</p></td><td colspan="1" rowspan="1"><p>Reusable dengan input fields</p></td><td colspan="1" rowspan="1"><p>Multi-purpose container</p></td></tr></tbody></table></div><p>Tutorial ini akan beberapa pakai <strong>Plugin</strong> (untuk feed) dan <strong>Manual HTML</strong> (untuk stats counter).</p><h3>5. Block Templates</h3><p>Tampilan default block IPS sering tidak match dengan desain Anda. Solusinya: <strong>Block Template</strong> — custom template yang bisa Anda terapkan ke block tertentu.</p><p><strong>Path:</strong> <em>AdminCP → Pages → Templates → Add → block template</em></p><p>Block Template menggunakan <strong>template tag IPS</strong> (<code>{{foreach}}, {$variable}</code>, dll) — bukan PHP murni.</p><h3>6. Template Tags vs Expression</h3><p>Dua jenis sintaks yang akan sering Anda lihat:</p><p><strong>Template Tag (logic flow):</strong></p><pre spellcheck="" class="ipsCode language-php-template" data-language="PHP Template"><code>{{if $count &gt; 1000}}
{{foreach $items as $item}}
...
{{endforeach}}
{{endif}}</code></pre><p><strong>Expression (eksekusi PHP):</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>{expression="number_format($count)"}
{expression="$record-&gt;_title"}</code></pre><p>Aturan: gunakan <code>{{...}}</code> untuk logic, <code>{expression="..."}</code> untuk output PHP.</p><h3>7. Default Page Setting</h3><p>Setelah membuat homepage, Anda harus set sebagai default agar muncul saat user buka root domain (<code>https://situs.com/</code>).</p><p><strong>Path:</strong> <em>AdminCP → System → Settings → Site Features → Default Page</em></p><hr><h2>Fase 1 — Setup Halaman Homepage</h2><h3>Step 1.1: Backup Tema Anda (Wajib!)</h3><p>Sebelum modifikasi apapun, backup tema aktif:</p><p>AdminCP → Customization → Appearance → Themes<br>└─ Hover icon download di samping tema → Save .xml file</p><p>Simpan file <code>.xml</code> ini di komputer lokal. Kalau ada yang rusak, Anda bisa <em>restore</em>.</p><h3>Step 1.2: Buat Page Baru</h3><p><strong>Path:</strong> <em>AdminCP → Pages → Add Page</em></p><p>Pilih Content Editor-nya Manual HTML</p><p><img class="ipsImage ipsRichText__align--block" data-fileid="1" src="https://lalu.pro/uploads/monthly_2026_05/Add_Page_Manual_HTML.webp.ae3e9a92f2e7ae6699c8272ed8a0ec16.webp" alt="Add_Page_Manual_HTML.webp" title="Add_Page_Manual_HTML.webp" loading="lazy"></p><p>Lengkapi Page Details dengan isian seperti dalam tabel di bawah.</p><p><img class="ipsImage ipsImage_thumbnailed ipsRichText__align--block" data-fileid="2" src="https://lalu.pro/uploads/monthly_2026_05/Add_Page_Details.thumb.webp.6d5c6d80f73ba377249a5456d9daac67.webp" alt="Add_Page_Details.webp" title="Add_Page_Details.webp" data-full-image="https://lalu.pro/uploads/monthly_2026_05/Add_Page_Details.webp.83b61743c308e52875748f318e9494e3.webp" loading="lazy"></p><p>Isian mencakup:</p><div class="ipsRichText__table-wrapper"><table style="width: 674px;"><colgroup><col style="width:195px;"><col style="width:208px;"><col style="width:271px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Field</p></th><th colspan="1" rowspan="1"><p>Isi dengan</p></th><th colspan="1" rowspan="1"><p>Catatan</p></th></tr><tr><td colspan="1" rowspan="1"><p>Page Name</p></td><td colspan="1" rowspan="1"><p>Homepage</p></td><td colspan="1" rowspan="1"><p>Display name internal</p></td></tr><tr><td colspan="1" rowspan="1"><p>Page Filename</p></td><td colspan="1" rowspan="1"><p>home</p></td><td colspan="1" rowspan="1"><p>URL slug (tanpa ekstensi), jika dikosongkan otomatis namanya mengikuti isian Page Name</p></td></tr><tr><td colspan="1" rowspan="1"><p>Page Folder</p></td><td colspan="1" rowspan="1"><p>Select</p></td><td colspan="1" rowspan="1"><p>Biarkan kosong untuk root, centang No Parent</p></td></tr><tr><td colspan="1" rowspan="1"><p>Use suite HTML wrapper</p></td><td colspan="1" rowspan="1"><p>ON (centang)</p></td><td colspan="1" rowspan="1"><p>Critical setting</p></td></tr><tr><td colspan="1" rowspan="1"><p>Page Title</p></td><td colspan="1" rowspan="1"><p>Komunitas Blogger Indonesia</p></td><td colspan="1" rowspan="1"><p>Untuk SEO <code>&lt;title&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Content</p></td><td colspan="1" rowspan="1"><p>Kode HTML</p></td><td colspan="1" rowspan="1"><p>Lihat step 1.3</p></td></tr><tr><td colspan="1" rowspan="1"><p>Meta Description</p></td><td colspan="1" rowspan="1"><p>(deskripsi singkat)</p></td><td colspan="1" rowspan="1"><p>Untuk SEO</p></td></tr></tbody></table></div><p>Klik <strong>Save &amp; Edit</strong>.</p><h3>Step 1.3: Struktur HTML Dasar</h3><p>Karena Suite Wrapper ON, <strong>TIDAK</strong> butuh <code>&lt;!DOCTYPE&gt;</code>, <code>&lt;html&gt;</code>, <code>&lt;head&gt;</code>, atau <code>&lt;body&gt;</code>. IPS akan menambahkannya otomatis. Anda cukup tulis konten yang ada di dalam <code>&lt;body&gt;</code> saja.</p><p>Template dasar yang bisa Anda pakai sebagai starting point:</p><pre spellcheck="" class="ipsCode language-xml" data-language="HTML/XML"><code>&lt;link rel="preconnect" href="https://fonts.googleapis.com"&gt;
&lt;link rel="preconnect" href="https://fonts.gstatic.com" crossorigin&gt;
&lt;link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;family=Geist:wght@300;400;500;600;700&amp;family=Geist+Mono:wght@400;500;600&amp;display=swap" rel="stylesheet"&gt;

&lt;style&gt;
/* Reset &amp; base */
* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
--bg: #ebe7e0;
--ink: #181818;
--accent: #ff5c2b;
/* ... variabel lainnya ... */
}

/* Style section di sini */
&lt;/style&gt;

&lt;!-- HERO SECTION --&gt;
&lt;section class="hero"&gt;
&lt;h1&gt;Selamat datang di komunitas kami&lt;/h1&gt;
&lt;p&gt;Deskripsi singkat tentang komunitas Anda.&lt;/p&gt;
&lt;/section&gt;

&lt;!-- SECTION LAIN --&gt;
&lt;section class="features"&gt;
&lt;!-- ... --&gt;
&lt;/section&gt;</code></pre><p>Itu saja kerangka dasarnya.</p><h3>Step 1.4: Penting — Cara Paste Kode di Editor IPS</h3><p>Editor IPS Pages punya mode Invision HTML Template dan Plain Text.</p><ol><li><p>Klik tab <strong>Content</strong>, secara default terpilih editor Invision HTML Template.</p></li><li><p>Salin kerangka HTML pada langkah 1.3 di atas.</p></li><li><p>Paste di kolom yang tersedia.</p></li><li><p><strong>Save</strong>.</p></li></ol><p>Setelah Save tentukan <strong>View Page</strong>, centang saja semua agar homepage dapat dilihat oleh semua pengunjung.</p><h3>Step 1.5: Set Homepage Sebagai Default</h3><p>Setelah halaman terbuat, jadikan default, langsung saja klik ikon bintang, lalu geser tombol <strong>Set as default</strong> page ke kanan (ON). Klik Save. Sekarang akses <code>https://situs-anda.com/</code> — homepage Anda muncul.</p><p>Catatan: Jangan lupa atur aplikasi Pages menjadi default juga.</p><h3>Step 1.6: Verifikasi</h3><p>Buka homepage di browser baru (incognito) untuk pastikan tampil benar:</p><ul><li><p>Tampil tanpa error</p></li><li><p>Header &amp; footer IPS muncul (karena Suite Wrapper ON)</p></li><li><p>Konten custom Anda di tengah</p></li><li><p>Font Google Fonts loaded (cek dengan Inspector → Network)</p></li></ul><p>Sampai di sini Anda sudah berhasil membuat homepage. Tapi masih sangat sederhana, hanya berisi teks selamat datang. Selanjutnya kita akan menambahkan beberapa block live.</p><h2>Fase 2 — Live Data Integration</h2><p>Bagian ini yang membuat homepage Anda <strong>hidup</strong> — menampilkan data real dari forum, artikel, dan files.</p><h3>2.1 Strategi Konversi Bertahap</h3><p>Urutan pekerjaan saya saat membuat homepage <a rel="external nofollow" href="https://lalu.pro">lalu.pro</a> adalah per section.<br></p><div class="ipsRichText__table-wrapper"><table style="min-width: 649px;"><colgroup><col style="min-width:20px;"><col style="width:227px;"><col style="width:178px;"><col style="width:224px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Urutan</p></th><th colspan="1" rowspan="1"><p>Section</p></th><th colspan="1" rowspan="1"><p>Tipe Block</p></th><th colspan="1" rowspan="1"><p>Tingkat Kesulitan</p></th></tr><tr><td colspan="1" rowspan="1"><p>1</p></td><td colspan="1" rowspan="1"><p>Stats counter (member, topic, artikel)</p></td><td colspan="1" rowspan="1"><p>Manual HTML</p></td><td colspan="1" rowspan="1"><p>Mudah</p></td></tr><tr><td colspan="1" rowspan="1"><p>2</p></td><td colspan="1" rowspan="1"><p>Latest articles</p></td><td colspan="1" rowspan="1"><p>Plugin (Record Feed)</p></td><td colspan="1" rowspan="1"><p>Sedang</p></td></tr><tr><td colspan="1" rowspan="1"><p>3</p></td><td colspan="1" rowspan="1"><p>Hot topics</p></td><td colspan="1" rowspan="1"><p>Plugin (Topic Feed)</p></td><td colspan="1" rowspan="1"><p>Sedang</p></td></tr><tr><td colspan="1" rowspan="1"><p>4</p></td><td colspan="1" rowspan="1"><p>Latest files (Download)</p></td><td colspan="1" rowspan="1"><p>Plugin (File Feed)</p></td><td colspan="1" rowspan="1"><p>Sedang</p></td></tr></tbody></table></div><p>Tapi saya hanya akan memandu Anda membuat <em>stat counter</em> dulu agar Anda melihat tampilannya dan memahmi alur kerjanya.</p><h3>2.2 Stats Counter — Member Count</h3><p>Kita perlu membuat sebuah block.</p><p><strong>Path:</strong> <em>AdminCP → Pages → Blocks → Create New Block → pilih "Manual HTML</em></p><p><img class="ipsImage ipsRichText__align--block" data-fileid="3" src="https://lalu.pro/uploads/monthly_2026_05/Create_New_Block.webp.4b1923dbbf4c0330c732fb574b626e27.webp" alt="Create_New_Block.webp" title="" loading="lazy"></p><p>Setelah klik tombol Next, isi form <strong>Details</strong> dan <strong>Content</strong>.</p><p><img class="ipsImage ipsRichText__align--block" data-fileid="4" src="https://lalu.pro/uploads/monthly_2026_05/Details_block.webp.01fd7cc122aaaa5a58076cd6c0995afe.webp" alt="Details_block.webp" title="" width="764" height="442" loading="lazy"></p><p><img class="ipsImage ipsRichText__align--block" data-fileid="6" src="https://lalu.pro/uploads/monthly_2026_05/Block_content.webp.5f27c0c45edc6af20a854ffaac5c6b3e.webp" alt="Block_content.webp" title="" loading="lazy"></p><p><strong>Title:</strong> Stat Members Count<br><strong>Template Key:</strong> <em>lalu_stat_members</em>, (tanpa spasi)<br><strong>Description:</strong> (boleh kosong)<br><strong>Content (paste persis):</strong></p><pre spellcheck="" class="ipsCode language-xml" data-language="HTML/XML"><code>{expression="number_format( \IPS\Db::i()-&gt;select( 'COUNT(*)', 'core_members', array( 'completed=?', 1 ) )-&gt;first() )"}</code></pre><p>Penjelasan kode:</p><ul><li><p><code>\IPS\Db::i()</code> — akses database IPS</p></li><li><p><code>select('COUNT(*)', 'core_members'...)</code> — hitung jumlah member</p></li><li><p><code>'completed=?', 1</code> — hanya yang sudah verifikasi email</p></li><li><p><code>number_format(...)</code> — format dengan koma (2,847)</p></li></ul><p>Klik <strong>Save</strong>. Klik Save sekali lagi (set view page).</p><h3>2.3 Stats Counter — Topic Count</h3><p>Sekarang buat block baru lagi, langkah-langkahnya seperti block member counter.</p><p>Isiannya:</p><p><strong>Title:</strong> Stat Topics Count<br><strong>Template Key:</strong> <em>lalu_stat_topics</em><br><strong>Content:</strong></p><pre spellcheck="" class="ipsCode language-xml" data-language="HTML/XML"><code>{{$count = \IPS\Db::i()-&gt;select( 'COUNT(*)', 'forums_topics', array( 'approved=?', 1 ) )-&gt;first();}}
{{if $count &gt;= 1000}}
{expression="round( $count / 1000, 1 )"}&lt;em&gt;k&lt;/em&gt;
{{else}}
{expression="number_format( $count )"}
{{endif}}</code></pre><p>Penjelasan:</p><ul><li><p>Hitung topic yang approved (approved=1)</p></li><li><p>Kalau ≥1000, tampilkan format 14k, 1.5k</p></li><li><p>Kalau &lt;1000, tampilkan angka penuh</p></li></ul><h3>2.4 Stats Counter — Article Count</h3><p>Cek dulu Database ID Articles:</p><p>AdminCP → Pages → Databases<br>└─ Cari "Articles" → catat angka ID di kolom paling kiri</p><p>Misal ID = <code>1</code>, maka tabel-nya adalah <code>cms_custom_database_1</code>.</p><p>Buat block dengan isian berikut:</p><p><strong>Title:</strong> Stat Articles Count<br><strong>Template Key:</strong> lalu_stat_articles<br><strong>Content (ganti angka 1 dengan ID Anda):</strong></p><pre spellcheck="" class="ipsCode language-xml" data-language="HTML/XML"><code>{expression="number_format( \IPS\Db::i()-&gt;select( 'COUNT(*)', 'cms_custom_database_1', array( 'record_approved=?', 1 ) )-&gt;first() )"}</code></pre><h3>2.5 Pasang Stats Block di Homepage</h3><p>Di Page Content homepage, di tempat yang sesuai:</p><pre spellcheck="" class="ipsCode language-xml" data-language="HTML/XML"><code>&lt;div class="hero-stats"&gt;
&lt;div class="stat"&gt;
&lt;div class="stat-num"&gt;{block="lalu_stat_members"}&lt;/div&gt;
&lt;div class="stat-label"&gt;Member aktif&lt;/div&gt;
&lt;/div&gt;
&lt;div class="stat"&gt;
&lt;div class="stat-num"&gt;{block="lalu_stat_topics"}&lt;/div&gt;
&lt;div class="stat-label"&gt;Diskusi&lt;/div&gt;
&lt;/div&gt;
&lt;div class="stat"&gt;
&lt;div class="stat-num"&gt;{block="lalu_stat_articles"}&lt;/div&gt;
&lt;div class="stat-label"&gt;Tutorial&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</code></pre><p>Anda bisa mencoba kode yang siap pakai berikut:</p><pre spellcheck="" class="ipsCode language-xml" data-language="HTML/XML"><code>&lt;style&gt;
/* Reset &amp; base */
* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
--bg: #ebe7e0;
--ink: #181818;
--accent: #ff5c2b;
--muted: #6b6b6b;
--card: #ffffff;
--radius: 12px;
--gap: 1.5rem;
}

body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.6;
padding: 2rem 1rem;
}

/* HERO */
.hero {
max-width: 960px;
margin: 0 auto 3rem;
text-align: center;
padding: 3rem 1.5rem;
}

.hero h1 {
font-size: clamp(1.75rem, 4vw, 2.75rem);
font-weight: 700;
line-height: 1.2;
margin-bottom: 1rem;
}

.hero h1::after {
content: "";
display: block;
width: 60px;
height: 4px;
background: var(--accent);
margin: 1rem auto 0;
border-radius: 2px;
}

.hero p {
font-size: 1.1rem;
color: var(--muted);
max-width: 600px;
margin: 0 auto 2.5rem;
}

/* Hero stats */
.hero-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--gap);
max-width: 700px;
margin: 0 auto;
}

.stat {
background: var(--card);
padding: 1.5rem 1rem;
border-radius: var(--radius);
border: 1px solid rgba(0, 0, 0, 0.06);
transition: transform 0.2s ease;
}

.stat:hover {
transform: translateY(-3px);
}

.stat-num {
font-size: 2rem;
font-weight: 700;
color: var(--accent);
line-height: 1;
margin-bottom: 0.5rem;
}

.stat-label {
font-size: 0.9rem;
color: var(--muted);
}

/* Features section */
.features {
max-width: 960px;
margin: 0 auto;
padding: 2rem 1rem;
}

/* Responsive */
@media (max-width: 600px) {
.hero-stats {
grid-template-columns: 1fr;
}

.hero {
padding: 2rem 1rem;
}
}
&lt;/style&gt;

&lt;!-- HERO SECTION --&gt;
&lt;section class="hero"&gt;
&lt;h1&gt;Selamat datang di komunitas kami&lt;/h1&gt;
&lt;p&gt;Deskripsi singkat tentang komunitas Anda.&lt;/p&gt;

&lt;div class="hero-stats"&gt;
&lt;div class="stat"&gt;
&lt;div class="stat-num"&gt;{block="lalu_stat_members"}&lt;/div&gt;
&lt;div class="stat-label"&gt;Member aktif&lt;/div&gt;
&lt;/div&gt;
&lt;div class="stat"&gt;
&lt;div class="stat-num"&gt;{block="lalu_stat_topics"}&lt;/div&gt;
&lt;div class="stat-label"&gt;Diskusi&lt;/div&gt;
&lt;/div&gt;
&lt;div class="stat"&gt;
&lt;div class="stat-num"&gt;{block="lalu_stat_articles"}&lt;/div&gt;
&lt;div class="stat-label"&gt;Tutorial&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/section&gt;</code></pre><p>Save → Clear cache → Refresh.</p><p>Lihat hasilnya, harusnya seperti berikut:</p><p><img class="ipsImage ipsRichText__align--block" data-fileid="5" src="https://lalu.pro/uploads/monthly_2026_05/Membuat_homepage_kustom_ips_v5.webp.9479a1a1d9456c28cc881e25425d0a04.webp" alt="Membuat_homepage_kustom_ips_v5.webp" title="" loading="lazy"></p><p>Saya harap Anda memahami kerjanya dan bisa berkreasi.</p>]]></description><guid isPermaLink="false">1</guid><pubDate>Thu, 01 Jan 1970 00:00:00 +0000</pubDate></item></channel></rss>
