Building themes
Data
Show products, menu items and articles from your data collections in theme code: the collection and entry objects, with a product grid and a detail page.
Plangku websites keep their products, menu items, articles and other lists in data collections (the Data tab). Theme code can show them in two ways:
- A section with a
collectionsetting lists the items of the collection the owner picks: a product grid, a menu, a list of articles. - On a detail page (the page shown for each single item) the
entryobject holds that item.
Both are read-only. Theme code can't change data, and it can't build forms (see security).
The collection setting
Declare a setting of type collection in the section's schema:
{ "type": "collection", "id": "products", "label": "Products", "info": "Choose the collection to list." }In the template section.settings.products is a collection object, or nil until the owner has picked one. A section can have several collection settings. Theme settings (config/settings_schema.json) and list items can't contain collections.
| Property | Value |
|---|---|
id | The collection's id |
name | The collection's name, such as Produk |
slug | Its address part, such as produk |
url | The address of the collection's list page, such as /produk (with the language prefix on translated versions) |
items | The items, as described below |
Rules for items:
- At most 48 items. To show more, send visitors to the list page (
collection.url). - Only visible items. Hidden items (drafts) are not included on the live site.
- Items are in the order set in the Data tab.
Items
Every item in items has these properties:
| Property | Value |
|---|---|
id | The item's id |
title | The item's name |
url | The address of its detail page, or nil when the collection has no detail pages. Use {{ item.url | default: '#' }} |
image | The first photo's address, or ''. Pass it through the image snippet, which shows a placeholder when there is none |
price | The price as text, already formatted: Rp 25.000, or '' |
old_price | A second price, such as the price before a discount, formatted the same way, or '' |
badge | A short label such as Terlaris, or '' |
subtitle | A short line under the title (a one-line text or date field), or '' |
summary | A short description: the long-text field, or the start of the article text (about 160 characters), or '' |
fields | Every plain field of the item by its field key: texts, numbers, yes/no values, and lists of texts (for galleries) |
Which field plays which part is decided by the field types and their order in the collection, the same as on the built-in product cards: price is the first Price field, old_price the second, badge is a Short text field whose key is badge, image the first Photo or Photo gallery field, subtitle the first other Short text or Date field (or a Choice field), and summary the first Long text field. Reordering the fields in the Data tab changes what the template shows.
fields is for everything else. If a Products collection has a number field with the key stok and a yes/no field halal, a template can use {{ item.fields.stok }} and {% if item.fields.halal %}. Raw prices are numbers there (item.fields.harga is 25000), so {{ item.fields.harga | money }} works too. Rich text, photos and dates come as stored text, so use the item's own properties for display.
entry of a detail page. See translations.A product grid
{%- assign s = section.settings -%}
<div class="site-wrap">
{% render 'heading', heading: s.heading, align: 'center' %}
{%- if s.products.items == blank %}
{% if editing %}<p>Pilih koleksi produk di pengaturan, lalu tambahkan produk di tab Data.</p>{% endif %}
{%- else %}
<ul class="cards">
{%- for p in s.products.items limit: s.max %}
<li class="card">
<a href="{{ p.url | default: '#' }}">
{% render 'image', src: p.image, alt: p.title, class: 'card-img' %}
{%- if p.badge != blank %}<span class="card-badge">{{ p.badge }}</span>{% endif %}
</a>
<h3>{{ p.title }}</h3>
<p>
<strong>{{ p.price }}</strong>
{%- if p.old_price != blank %} <s>{{ p.old_price }}</s>{% endif %}
</p>
{%- assign order = 'wa:Halo, saya mau pesan ' | append: p.title -%}
{% render 'button', label: 'Pesan lewat WhatsApp', link: order, size: 'sm' %}
</li>
{%- endfor %}
</ul>
{%- endif %}
</div>Things to note:
s.products.items == blankis true both when the owner hasn't picked a collection (s.productsisnil) and when the collection has no visible items. Comparing with0would not catchnil.limit: s.maxpairs with arangesetting namedmax. Don't use a number setting that can be0, sincelimit: 0shows nothing.- The WhatsApp message is built with
appendand turned into a link by thebuttonsnippet. Thewa:prefix tells Plangku it is a WhatsApp message.
The complete section, with CSS and schema, is in examples.
The entry object
On a detail page entry is the item being shown. On other pages it is nil. The properties:
| Property | Value |
|---|---|
title | The item's name |
url | The address of this page |
collection | The collection it belongs to: name, slug and url (the list page) |
images | Every photo of the item (all Photo and Photo gallery fields, the main photo first, without duplicates) |
image | The first photo, or '' |
price, old_price | Formatted prices (Rp 75.000), or '' |
badge | The badge text, or '' |
summary | The short description (Long text field), or '' |
excerpt | The first 160 characters of the article text as plain text, or '' |
body | The article text (the Article text field) as safe HTML. It is written without escaping, because it comes from Markdown that was escaped as it was converted |
facts | A list of { label, value, href }: every other field as a label and a ready-to-show value. Empty ones are left out. href is only set for Link fields. Yes/no fields show Ya or Tidak, and dates look like 4 November 2026 |
fields | The plain field values by key, as for items |
facts leaves out the fields already shown by price, old_price, badge, summary and body, and leaves out photos and long texts. A product with a weight, a size and a website field gives three facts.
Test for entry before using it, since the same section could be placed on a normal page:
{% if entry %}
<h1>{{ entry.title }}</h1>
{% elsif editing %}
<p>Letakkan section ini di halaman detail produk.</p>
{% endif %}In the editor, a detail page shows a sample item (for example "Contoh ..." texts and a price of Rp 150.000), so you can design it before real items exist.
A detail page
{%- if entry -%}
<div class="site-wrap detail">
<p><a href="{{ entry.collection.url }}">{{ entry.collection.name }}</a> / {{ entry.title }}</p>
<div class="detail-grid">
<div>
{% render 'image', src: entry.image, alt: entry.title, class: 'main-photo', eager: true %}
{%- if entry.images.size > 1 %}
<div class="thumbs">
{%- for src in entry.images limit: 5 %}
{% render 'image', src: src, alt: entry.title, class: 'thumb' %}
{%- endfor %}
</div>
{%- endif %}
</div>
<div>
{%- if entry.badge != blank %}<span class="badge">{{ entry.badge }}</span>{% endif %}
<h1 class="site-heading">{{ entry.title }}</h1>
<p class="price">
<strong>{{ entry.price }}</strong>
{%- if entry.old_price != blank %} <s>{{ entry.old_price }}</s>{% endif %}
</p>
{%- if entry.summary != blank %}<p>{{ entry.summary }}</p>{% endif %}
{%- assign order = 'wa:Halo, saya mau pesan ' | append: entry.title | append: ' (' | append: entry.price | append: ')' -%}
{% render 'button', label: 'Pesan lewat WhatsApp', link: order, size: 'lg' %}
{%- if entry.facts.size > 0 %}
<dl class="facts">
{%- for fact in entry.facts %}
<dt>{{ fact.label }}</dt>
<dd>{% if fact.href %}<a href="{{ fact.href }}">{{ fact.value }}</a>{% else %}{{ fact.value }}{% endif %}</dd>
{%- endfor %}
</dl>
{%- endif %}
</div>
</div>
{%- if entry.body != blank %}
<div class="body">{{ entry.body }}</div>
{%- endif %}
</div>
{%- endif -%}{{ entry.body }} is the one place where output is not escaped, because body is already safe HTML made from Markdown (headings, lists, links and photos). Like all theme output it is still cleaned by the security allowlist.
The finished file, with CSS and schema, is in examples.
Where next
- Objects for the other globals.
- Settings for how to declare the
collectionsetting. - Translations for what is translated automatically.