Mulai gratis dengan batas paket Free — upgrade ke Plus atau Custom kapan saja saat situsmu tumbuh.
Fitur Cara Kerja Harga Docs Donate
Masuk Daftar Gratis

Dokumentasi

Panduan CMS, logo guide, cara membuat template kustom, dan referensi API.

Memulai

Nalar CMS membantu kamu mengelola situs berita dari satu dasbor — mulai dari menulis artikel sampai mengatur tampilan situs.

Langkah pertama

  1. Lengkapi identitas situs di Pengaturan → Identitas Situs (nama, logo, tagline).
  2. Pilih template dasar di Template & Desain.
  3. Buat kategori awal di Kategori & Tag supaya artikel punya struktur.
  4. Tulis artikel pertamamu dari Tulis Artikel.

Menulis Artikel

Editor mendukung format teks tebal, miring, tautan, gambar, dan daftar tanpa plugin tambahan. Setiap artikel punya slug otomatis dari judul, ringkasan untuk pratinjau share, dan kolom SEO terpisah.

Status artikel bisa Draft, Terbit sekarang, atau Jadwalkan untuk tayang otomatis di waktu tertentu.

Flag tampilan

  • Highlight — artikel masuk slider / hero di beranda (misalnya di template Oto Jurnal).
  • Pilihan Editor — artikel masuk section “Pilihan Editor” di template yang mendukungnya.

Aktifkan keduanya dari panel kanan saat menulis atau mengedit artikel.

Kategori & Tag

Kategori bisa disusun berjenjang (induk & anak) untuk navigasi situs, sementara tag membantu mengelompokkan artikel lintas kategori. Kelola keduanya dari halaman Kategori & Tag.

Template & Desain

Mode Mudah mengatur warna, font, dan lebar konten tanpa kode. Mode Lanjutan memberi akses CSS per komponen untuk penyesuaian lebih dalam — CSS divalidasi otomatis sebelum disimpan.

Ingin membuat template baru dari nol? Lihat tab Membuat Template.

Mengelola Tim

Undang rekan sebagai Penulis (kelola artikel sendiri) atau Editor (kelola semua konten) dari Pengaturan → Pengguna.

Tutorial: buat template baru dari nol

Panduan ini menuntun kamu membuat template PHP baru yang bisa dipilih di Template & Desain dan dipakai situs publik. Contoh di bawah memakai kode sport — ganti jadi nama template kamu sendiri (huruf kecil, tanpa spasi).

Hasil akhir: folder templates/sport/ dengan home.php + article.php, baris di tabel templates, kartu di galeri CMS, lalu bisa di-Activate.

Yang kamu butuhkan

  • Akses ke source code proyek (folder templates/, public/assets/js/).
  • Akses database MySQL (atau bisa menjalankan script seed).
  • Editor teks + browser untuk uji pratinjau.

Kalau baru belajar, salin dulu templates/classic/ lalu ubah markupnya — lebih cepat daripada mulai kosong.

1 Pilih kode template

Kode = nama folder = nilai templates.code di database. Harus unik.

  • Huruf kecil saja: sport, kuliner, techblog
  • Boleh angka dan strip: news-24
  • Jangan pakai spasi, huruf besar, atau karakter spesial
  • Jangan bentrok dengan yang sudah ada: classic, modern, minimal, oto
Di tutorial ini: kode = sport, nama tampilan = Sport Daily.

2 Buat folder template

Dari root proyek, buat folder baru:

terminal
mkdir -p templates/sport

Struktur target setelah langkah 4–5:

templates/
  _layout_start.php   ← jangan diubah (shared)
  _layout_end.php     ← jangan diubah (shared)
  sport/
    home.php          ← wajib
    article.php       ← wajib
    styles.inc.php    ← opsional
Penting: nama folder harus persis sama dengan kode DB. Kalau folder sport tapi DB-nya sports, engine akan fallback ke Classic.

3 Tulis home.php (beranda)

Buat file templates/sport/home.php. Salin seluruh isi di bawah ini:

templates/sport/home.php
<?php
// Meta default (boleh di-override dari luar)
$metaTitle = $metaTitle ?? ($site['meta_title'] ?: $site['name']);
$metaDesc  = $metaDesc ?? ($site['meta_description'] ?? '');

// Selalu mulai & akhiri dengan layout shared
include TEMPLATES_PATH . '/_layout_start.php';
?>
<header class="site-header">
  <div class="container" style="padding:0">
    <h1>
      <a href="/" style="color:inherit;text-decoration:none">
        <?= e($site['name']) ?>
      </a>
    </h1>
    <?php if (!empty($tagline)): ?>
      <p><?= e($tagline) ?></p>
    <?php endif; ?>
  </div>
</header>

<main class="container">
  <?php if (empty($articles)): ?>
    <p>Belum ada artikel.</p>
  <?php else: ?>
    <?php foreach ($articles as $a): ?>
      <article class="article-card">
        <div class="article-meta">
          <?= e($a['category_name'] ?? 'Umum') ?>
          ·
          <?= e(article_date_label($a)) ?>
        </div>
        <h2>
          <a href="/artikel/<?= e($a['slug']) ?>">
            <?= e($a['title']) ?>
          </a>
        </h2>
        <?php if (!empty($a['excerpt'])): ?>
          <p class="excerpt"><?= e($a['excerpt']) ?></p>
        <?php endif; ?>
      </article>
    <?php endforeach; ?>
  <?php endif; ?>
</main>

<?php include TEMPLATES_PATH . '/_layout_end.php'; ?>

Yang harus diingat di langkah ini

  • Selalu include _layout_start.php di awal dan _layout_end.php di akhir.
  • Semua teks dari database di-escape dengan e().
  • Link detail artikel: /artikel/{slug}.
  • Variabel $articles, $site, $opts, $tagline sudah diisi engine — tidak perlu query DB sendiri.

4 Tulis article.php (halaman detail)

Buat file templates/sport/article.php:

templates/sport/article.php
<?php include TEMPLATES_PATH . '/_layout_start.php'; ?>
<header class="site-header">
  <div class="container" style="padding:0">
    <h1>
      <a href="/" style="color:inherit;text-decoration:none">
        <?= e($site['name']) ?>
      </a>
    </h1>
  </div>
</header>

<main class="container">
  <article>
    <div class="article-meta">
      <?= e($article['category_name'] ?? '') ?>
      ·
      <?= e($article['author_name'] ?? '') ?>
      ·
      <?= e(article_date_label($article)) ?>
    </div>
    <h1 style="font-size:32px;margin:8px 0 16px">
      <?= e($article['title']) ?>
    </h1>
    <?php if (!empty($article['excerpt'])): ?>
      <p style="font-size:18px;opacity:.75;margin-bottom:24px">
        <?= e($article['excerpt']) ?>
      </p>
    <?php endif; ?>
    <div class="article-body">
      <?= nl2br(e($article['content'] ?? '')) ?>
    </div>
  </article>

  <?php if (!empty($related)): ?>
  <section style="margin-top:40px;border-top:1px solid rgba(0,0,0,.08);padding-top:24px">
    <h3 style="margin:0 0 16px">Artikel Lainnya</h3>
    <?php foreach ($related as $r): ?>
      <div style="padding:8px 0">
        <a href="/artikel/<?= e($r['slug']) ?>"><?= e($r['title']) ?></a>
      </div>
    <?php endforeach; ?>
  </section>
  <?php endif; ?>
</main>
<?php include TEMPLATES_PATH . '/_layout_end.php'; ?>

Di halaman ini engine mengirim $article (satu berita) dan $related (artikel terkait).

5 (Opsional) CSS khas template

Warna & font dasar sudah diurus _layout_start.php lewat CSS variables dari Mode Mudah CMS. Kalau butuh gaya ekstra (hero, grid, dsb.), buat file:

templates/sport/styles.inc.php
.sport-hero {
  background: var(--color-primary);
  color: #fff;
  padding: 28px 20px;
  border-radius: var(--radius);
  margin-bottom: 24px;
}
.sport-hero h2 { margin: 0; font-family: var(--font-heading); }

Supaya CSS ini ikut ter-load, kamu perlu menambahkannya di TemplateRenderer (lihat langkah 8). Untuk template minimal, langkah 5 & 8 bisa dilewati dulu — layout shared sudah cukup.

CSS variables yang bisa dipakai

--color-primary   --color-accent   --color-bg
--color-header    --color-text
--font-heading    --font-body
--scale           --lh             --content-w
--radius          --gap

Nilai diisi dari opsi CMS: primary, accent, bg, text, header, fontHeading, fontBody, scale, lh, width, radius, gap, sidebar, shadow, ticker, thumbs.

6 Daftarkan ke database

Tanpa baris di tabel templates, tombol Activate di CMS akan gagal. Jalankan SQL berikut (ganti nilai sesuai template kamu):

MySQL
INSERT INTO templates
  (code, name, category, description, author, version,
   base_html, base_css, default_opts, is_active, created_at, updated_at)
VALUES (
  'sport',
  'Sport Daily',
  'berita',
  'Portal olahraga sederhana — daftar berita + halaman detail.',
  'Tim Kamu',
  '1.0.0',
  '<!-- sport -->',
  '/* sport */',
  '{"primary":"#0b3d91","accent":"#f59e0b","bg":"#ffffff","text":"#111827","header":"#e8eef8","fontHeading":"Inter","fontBody":"Inter","scale":1,"lh":1.65,"width":1180,"radius":8,"gap":1,"sidebar":"right","shadow":false,"ticker":false,"thumbs":true}',
  1,
  NOW(),
  NOW()
);

Cek hasilnya:

SELECT id, code, name, is_active FROM templates WHERE code = 'sport';
Alternatif: tambahkan array template di scripts/seed.php lalu jalankan php scripts/seed.php (hati-hati: seed juga menyentuh data lain).

7 Tampilkan di galeri CMS

Galeri di halaman Template & Desain membaca array TEMPLATES di file JS — bukan langsung dari DB. Buka:

public/assets/js/template-engine.js

Tambahkan objek baru di dalam array TEMPLATES (setelah entry oto misalnya):

public/assets/js/template-engine.js
{
  id: 'sport',
  dbCode: 'sport',
  name: 'Sport Daily',
  category: 'berita',
  catLabel: 'Olahraga',
  desc: 'Portal olahraga sederhana dengan daftar berita dan halaman detail.',
  author: 'Tim Kamu',
  version: '1.0.0',
  php: true,
  opts: {
    primary: '#0b3d91', accent: '#f59e0b', bg: '#ffffff',
    text: '#111827', header: '#e8eef8',
    fontHeading: 'Inter', fontBody: 'Inter',
    scale: 1, lh: 1.65, width: 1180, radius: 8, gap: 1,
    sidebar: 'right', shadow: false, ticker: false, thumbs: true
  },
  css: ''
},

Arti field penting

  • id — ID di sisi JS (biasanya sama dengan kode).
  • dbCode — harus sama dengan templates.code di DB. Saat Activate, nilai ini yang dikirim ke server.
  • php: true — pratinjau memakai iframe PHP asli, bukan mock HTML.
  • opts — default Mode Mudah; samakan dengan default_opts di DB.

Lalu buka juga resources/views/cms/template-customize.php dan tambahkan mapping di ENGINE_IDS:

var ENGINE_IDS = {
  classic: 'classic',
  modern: 'magazine',
  minimal: 'minimal',
  oto: 'oto',
  sport: 'sport'   // ← tambahkan
};
Hard refresh browser (Cmd/Ctrl + Shift + R) setelah mengubah JS supaya cache tidak menyimpan versi lama.

8 (Opsional) Data ekstra di TemplateRenderer

Langkah ini hanya perlu jika beranda butuh data di luar daftar artikel biasa — misalnya slider highlight, pilihan editor, atau daftar kategori.

Buka app/Services/TemplateRenderer.php, method homeData(). Contoh pola (mirip Oto):

app/Services/TemplateRenderer.php → homeData()
if ($code === 'sport') {
    $data['latest'] = $articles;
    $data['heroSlides'] = Article::highlightsForSite($siteId, 3);
    $data['editorPicks'] = Article::editorPicksForSite($siteId, 4);
    $data['categories'] = Category::forSite($siteId);

    $styles = TEMPLATES_PATH . '/sport/styles.inc.php';
    if (is_file($styles)) {
        ob_start();
        include $styles;
        $data['customCss'] = $customCss . "\n" . (string) ob_get_clean();
    }
}

Lalu di home.php kamu bisa pakai $heroSlides, $editorPicks, dst.

Flag artikel dari CMS

  • Highlight (is_highlight) → Article::highlightsForSite() → cocok untuk slider/hero.
  • Pilihan Editor (is_editor_pick) → Article::editorPicksForSite() → cocok untuk section khusus.

Aktifkan flag-nya dari form artikel di CMS (panel Publikasikan).

Halaman opsional

Kalau ingin layout sendiri untuk kategori / pencarian / kontak, tambahkan file di folder template:

  • category.php → URL /kategori/{slug}
  • search.php → URL /cari?q=...
  • contact.php → URL /kontak

Lihat contoh lengkap di templates/oto/.

9 Aktifkan & uji

  1. Login CMS → buka Template & Desain.
  2. Hard refresh halaman (Cmd/Ctrl + Shift + R).
  3. Cari kartu Sport Daily di daftar template.
  4. Klik Pratinjau — harus muncul layout home.php kamu (bukan Classic).
  5. Klik Activate → konfirmasi.
  6. Buka situs publik (domain/subdomain situs) — beranda harus memakai template baru.
  7. Klik satu artikel → harus masuk article.php.
  8. Opsional: buka Kustomisasi, ubah warna primary, simpan, lalu refresh situs publik — warna harus ikut berubah (karena CSS variables).
Tips uji cepat: flag 2–3 artikel sebagai Highlight + Pilihan Editor, lalu pastikan section di beranda terisi sesuai markup kamu.

10 Checklist & troubleshooting

  • Folder templates/sport/ ada, berisi home.php + article.php
  • Baris DB: SELECT code FROM templates WHERE code='sport' AND is_active=1 mengembalikan 1 baris
  • Entry di template-engine.js dengan php: true dan dbCode: 'sport'
  • ENGINE_IDS di template-customize memuat sport
  • Hard refresh CMS sudah dilakukan
  • Activate berhasil tanpa error JSON
  • Situs publik menampilkan markup baru (bukan Classic)

Kalau bermasalah

  • Template tidak muncul di galeri — cek JS entry + hard refresh; pastikan tidak ada error syntax di template-engine.js (buka Console browser).
  • Activate gagal / “template tidak ditemukan” — cek baris di tabel templates dan pastikan dbCode JS = code DB.
  • Pratinjau / situs masih Classic — nama folder ≠ kode, atau home.php belum ada / path salah.
  • Halaman putih / error PHP — cek log PHP; pastikan semua output memakai e() dan tidak ada typo variabel.
  • Warna kustomisasi tidak berubah — pakai var(--color-primary) dll., jangan hardcode hex di CSS template.

Referensi: variabel & helper

Semua halaman

  • $site — name, tagline, meta_title, meta_description, tracking, …
  • $opts — opsi desain Mode Mudah
  • $customCss — CSS kustom situs (+ CSS template jika di-inject)
  • $metaTitle, $metaDesc
  • $isPreview — true saat pratinjau CMS (jangan muat GA/iklan)
  • $tagline — sudah di-extract di layout start dari $site['tagline']

Beranda

  • $articles — daftar artikel terbit
  • $latest, $heroSlides, $editorPicks, $popular, $categories — jika kamu inject di langkah 8

Detail artikel

  • $article — satu artikel (title, slug, content, excerpt, category_name, author_name, …)
  • $related — artikel terkait

Helper

  • e($str) — escape HTML (wajib)
  • article_image_url($article) — URL gambar utama
  • article_date_label($article) — tanggal lokal siap tampil
  • TEMPLATES_PATH — path absolut ke folder templates/

Referensi: URL publik

  • / — beranda → home.php
  • /artikel/{slug} — detail → article.php
  • /kategori/{slug} — kategori → category.php
  • /cari?q=... — pencarian → search.php
  • /kontak — kontak → contact.php

Referensi: aturan wajib

  • Escape semua output teks dengan e().
  • Jangan query database langsung dari template; pakai variabel yang sudah diberikan / inject di TemplateRenderer.
  • Jangan taruh <script> di custom HTML situs; tracking lewat Pengaturan situs.
  • Hormati $isPreview — jangan muat GA/GTM/iklan saat pratinjau.
  • Hindari @import di CSS kustom (diblokir validator).
  • Kode folder = kode DB = dbCode di JS — harus sama persis.
  • Untuk desain kompleks, pelajari templates/oto/ sebagai referensi lengkap.

Autentikasi

Semua permintaan API memerlukan header Authorization. Ambil kunci API kamu dari Pengaturan → API & Integrasi.

# contoh permintaan
curl http://nalar.link/api/v1/articles \
  -H "Authorization: Bearer SASSLY_API_KEY"

Artikel

GET/api/v1/articles

Mengambil daftar artikel. Mendukung parameter status, category, dan page.

GET/api/v1/articles/{id}

Mengambil satu artikel berdasarkan ID.

// contoh respons
{
  "id": "1",
  "title": "Harga Emas Antam Naik...",
  "status": "published",
  "category": "ekonomi",
  "is_highlight": true,
  "is_editor_pick": false
}

Kategori & Tag

GET/api/v1/categories

Mengambil daftar kategori beserta jumlah artikel di dalamnya.

Kode Error

401 kunci API tidak valid atau kedaluwarsa. 429 permintaan melebihi limit rate. 500 galat di sisi server.