<?xml version="1.0"?>
<rss version="2.0"><channel><title>Articles</title><link>https://lalu.pro/articles/</link><description>Baca artikel kami</description><language>en</language><item><title>Membuat Template Blogger Mulai dari Nol</title><link>https://lalu.pro/articles/tutorial/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>Sun, 21 Jun 2026 00:45:49 +0000</pubDate></item><item><title>Tutorial: Membuat Cover Forum Style IPS untuk XenForo</title><link>https://lalu.pro/articles/forum-software/tutorial-membuat-cover-forum-style-ips-untuk-xenforo-r4/</link><description><![CDATA[<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>Sun, 17 May 2026 08:30:50 +0000</pubDate></item><item><title>Cara Membuat PageSpeed Tester Sendiri untuk Blog</title><link>https://lalu.pro/articles/tutorial/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>Sun, 17 May 2026 06:25:00 +0000</pubDate></item><item><title>Tutorial Komprehensif Membuat Homepage Kustom di Invision Community 5</title><link>https://lalu.pro/articles/forum-software/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>Fri, 15 May 2026 23:37:35 +0000</pubDate></item></channel></rss>
