Skip to content
View in the app

A better way to browse. Learn more.

Lalu Pro

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

Tutorial: Membuat Cover Forum Style IPS untuk XenForo

Panduan lengkap membangun tampilan cover forum 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.

Estimasi waktu: 30–45 menit Tingkat: Menengah (perlu nyaman edit file PHP via FTP dan template XenForo)

Konsep tampilan akhir: cover di kiri, daftar thread terbaru di kanan, border atas warna berbeda per kategori.

Lihat demo: https://forum.intutekno.com/


1. Yang Akan Dibuat

Setiap forum di halaman utama akan menampilkan:

Kolom kiri (30% lebar):

  • Cover image 3:2 (SVG/WebP/JPG)

  • Judul forum (link ke forum)

  • Deskripsi forum

  • Statistik (jumlah threads & messages)

Kolom kanan (70% lebar):

  • 5 thread terbaru dengan avatar, judul, username, tanggal, jumlah replies

  • Atau ikon + pesan "Belum ada thread" kalau forum masih kosong

Bonus: Border-top tiap card forum bisa diwarnai berbeda berdasarkan kategori induk-nya.


2. Prasyarat

  • XenForo 2.3.x (panduan ini diuji di 2.3.10)

  • Akses AdminCP

  • Akses FTP/File Manager ke server (untuk upload PHP files)

  • Akses cPanel atau setara untuk backup database (opsional tapi disarankan)

  • Pemahaman dasar HTML/CSS akan membantu


3. Backup Dulu

Sebelum apa pun, lakukan dua backup:

Backup database:

  • cPanel → phpMyAdmin → pilih database forum → Export → Quick → Go

Backup file:

  • File Manager cPanel → kompres folder forum jadi .zip → download

Kalau ada yang salah saat ngoprek, kita bisa balik ke kondisi awal.


4. Aktifkan Development Mode

Buka /src/config.php lewat File Manager atau FTP. Tambahkan dua baris ini di paling bawah:

$config['development']['enabled'] = true;
$config['development']['defaultAddOn'] = 'Intutekno/CoverForum';

Ganti Intutekno/CoverForum sesuai nama yang Anda inginkan (format: VendorName/AddOnName). Setelah save, refresh AdminCP — menu Development dan Add-ons akan muncul di sidebar.


5. Bikin Add-on PHP

Add-on ini bertugas menjalankan satu fungsi: mengambil 5 thread terbaru dari sebuah forum dan menyediakannya untuk dipakai di template.

5.1. Buat folder add-on

Via FTP, buat struktur folder:

/src/addons/Intutekno/CoverForum/

5.2. Buat file addon.json

Isi:

{
    "title": "Intutekno Cover Forum",
    "version_string": "1.0.0",
    "version_id": 1000070,
    "dev": "Intutekno",
    "supported": true
}

5.3. Buat file Listener.php

<?php
namespace Intutekno\CoverForum;

class Listener
{
    public static function templaterSetup(\XF\Container $container, \XF\Template\Templater &$templater)
    {
        $templater->addFunction('latest_forum_threads',
            ['Intutekno\CoverForum\TemplateHelper', 'templaterFunction']
        );
    }
}

5.4. Buat file TemplateHelper.php

<?php
namespace Intutekno\CoverForum;

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

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

        return $finder->fetch();
    }
}

Struktur akhir folder:

/src/addons/Intutekno/CoverForum/
├── addon.json
├── Listener.php
└── TemplateHelper.php

6. Install Add-on

Di AdminCP:

  1. Klik menu Add-ons di sidebar

  2. Cari "Intutekno Cover Forum" di list

  3. Klik tombol Install di sampingnya

  4. Confirm

Status add-on harus jadi Active (toggle hijau).


7. Register Code Event Listener

Ini langkah yang menghubungkan add-on PHP kita ke template engine XenForo.

  1. AdminCP → DevelopmentCode event listeners

  2. Klik + Add code event listener di pojok kanan atas

  3. Isi form:

Field

Isi

Listen to event

templater_setup

Event hint

(kosongkan)

Execute callback: Class

Intutekno\CoverForum\Listener

Execute callback: Method

templaterSetup

Callback execution order

10 (default)

Enable callback execution

centang

Description

Register latest_forum_threads function

Add-on

Intutekno Cover Forum 1.0.0

  1. Klik Save


8. Edit Template node_list_forum

AdminCP → Appearance → Templates → cari template node_list_forumEdit.

Ganti seluruh isi template dengan kode berikut:

<xf:macro id="depth1" arg-node="!" arg-extras="!" arg-children="!" arg-childExtras="!" arg-depth="1">
	<div class="block">
		<div class="block-container">
			<div class="block-body">
				<xf:macro id="forum"
					arg-node="{$node}"
					arg-extras="{$extras}"
					arg-children="{$children}"
					arg-childExtras="{$childExtras}"
					arg-depth="{$depth}" />
			</div>
		</div>
	</div>
</xf:macro>

<xf:macro id="depth2" arg-node="!" arg-extras="!" arg-children="!" arg-childExtras="!" arg-depth="1">
	<xf:macro id="forum"
		arg-node="{$node}"
		arg-extras="{$extras}"
		arg-children="{$children}"
		arg-childExtras="{$childExtras}"
		arg-depth="{$depth}" />
</xf:macro>

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

<xf:macro id="forum"
	arg-node="!"
	arg-extras="!"
	arg-children="!"
	arg-childExtras="!"
	arg-depth="!"
	arg-chooseName=""
	arg-bonusInfo="">

	<xf:if is="$depth == 1 OR $depth == 2">

		<!-- ===== COVER STYLE (forum utama) ===== -->
		<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' }}">
			<div class="intuteknoCover-body">

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

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

			</div>
		</div>

	<xf:else />

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

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

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

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

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

					<xf:if is="$bonusInfo is not empty">
						<div class="node-bonus">{$bonusInfo}</div>
					</xf:if>
				</div>

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

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

	</xf:if>

	<xf:if is="{$depth} == 1">
		<xf:macro id="forum_list::node_list"
			arg-children="{$children}"
			arg-extras="{$childExtras}"
			arg-depth="{{ $depth + 1 }}" />
	</xf:if>
</xf:macro>

Save.

Penjelasan: Conditional $depth == 1 OR $depth == 2 membuat cover style berlaku untuk forum di root maupun di dalam kategori. Sub-forum (depth ≥ 3) tetap pakai layout original supaya rapi.


9. Tambahkan CSS

AdminCP → Appearance → Style properties → Extra LESS template → tambahkan blok ini:

/* ============================================
   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;
    }
}

Save.


10. Setup Cover Image

10.1. Buat folder cover

Via File Manager cPanel atau FTP, buat folder:

/data/forum_covers/

Set permission 755.

10.2. Cari node_id setiap forum

AdminCP → Forums → klik nama forum → lihat URL:

.../admin.php?nodes/nama-forum.5/edit
                              ↑
                          node_id = 5

10.3. Upload cover image

Upload file SVG/WebP/JPG ke /data/forum_covers/ dengan nama sesuai node_id:

/data/forum_covers/
├── default.svg     ← fallback wajib
├── 5.svg           ← forum dengan node_id 5
├── 6.svg
├── 7.svg
└── ...

Spesifikasi cover ideal:

  • Aspect ratio 3:2

  • Ukuran 600×400 px

  • Format SVG (paling ringan dan scalable) atau WebP

Catatan: Template di langkah 8 sudah pakai ekstensi .svg. Kalau Anda pakai .webp atau .jpg, ubah dua tempat di template dari .svg menjadi ekstensi yang Anda pakai.


11. Customize Border per Kategori

Border-top tiap card forum bisa diwarnai berbeda berdasarkan kategori induk-nya. Berkat class intuteknoCover--cat{X} yang otomatis disisipkan di langkah 8, kita tinggal target via CSS.

11.1. Cari node_id kategori

Sama seperti forum: AdminCP → Forums → klik kategori → lihat node_id di URL.

11.2. Tambahkan rules CSS

Tambahkan ke Extra LESS:

/* === 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 */

Sesuaikan angka di --cat{X} dengan node_id kategori, dan warna sesuai branding masing-masing.

Save dan refresh

Setelah semua perubahan CSS:

  1. Save Extra LESS

  2. Tools → Rebuild caches → centang semua → Rebuild

  3. Buka homepage forum → Ctrl+F5


12. Troubleshooting

Error Function "latest_forum_threads" is not callable

Listener belum aktif atau salah class path. Cek:

  1. Add-ons — add-on Status harus Active

  2. Code event listeners — entry untuk templater_setup harus enabled, Class dan Method harus persis sama dengan file PHP

  3. Tools → Rebuild caches → centang semua

Cover tidak muncul, ada celah putih di kiri

Berarti gambar gagal dimuat. Cek:

  1. File ada di /data/forum_covers/?

  2. Permission folder 755, file 644?

  3. Nama file sesuai node_id?

  4. Path di template sesuai ekstensi file?

  5. Ada file default.svg sebagai fallback?

Layout tidak berubah, masih style asli

  1. Pastikan template node_list_forum ter-save dengan kode lengkap di langkah 8

  2. Rebuild templates wajib

  3. Ctrl+F5 untuk bypass cache browser

  4. Cek dengan inspect element — class intuteknoCover harus muncul di div

Sub-forum jadi style cover juga (tidak diinginkan)

Cek conditional di template:

<xf:if is="$depth == 1 OR $depth == 2">

Sub-forum biasanya depth ≥ 3, jadi seharusnya tidak ter-cover. Kalau forum Anda strukturnya berbeda, sesuaikan condition.

Avatar ada celah ke kiri

Pastikan template pakai sintaks ini (avatar inline, bukan dibungkus <a>):

<xf:avatar user="{$thread.User}" size="xxs" defaultname="{$thread.username}" class="intuteknoThread-avatar" />

Class intuteknoCover--cat{id} tidak muncul di HTML

Berarti template belum ter-update. Edit ulang, save, dan rebuild templates.


13. Penutup

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.

Pengembangan Selanjutnya

Beberapa peningkatan yang bisa dilakukan kapan-kapan:

  • Caching thread terbaru lewat XF registry supaya tidak query 5 thread per forum setiap homepage di-load

  • Custom field untuk upload cover via AdminCP (tanpa FTP)

  • Hover effect halus di card dan row thread

  • Lazy load cover image dengan blur placeholder

Struktur File Akhir

/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

Tutorial dibuat berdasarkan implementasi nyata di forum.intutekno.com

Lisensi: silakan adaptasi dan bagikan ulang. Kalau bermanfaat, sebutkan sumber sebagai amal jariah — pengetahuan yang terus bermanfaat.


Panduan lengkap membuat cover forum XenForo yang tampialannya mirip dengan IPS. Konsep tampilan akhir: cover di kiri, daftar thread terbaru di kanan, border atas warna berbeda per kategori. Mau coba?

Bagaimana pendapat Anda mengenai topik ini? Mari bagikan perspektif Anda di bawah!

User Feedback

Recommended Comments

There are no comments to display.

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.