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 collection setting 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 entry object 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:

json
{ "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.

PropertyValue
idThe collection's id
nameThe collection's name, such as Produk
slugIts address part, such as produk
urlThe address of the collection's list page, such as /produk (with the language prefix on translated versions)
itemsThe 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:

PropertyValue
idThe item's id
titleThe item's name
urlThe address of its detail page, or nil when the collection has no detail pages. Use {{ item.url | default: '#' }}
imageThe first photo's address, or ''. Pass it through the image snippet, which shows a placeholder when there is none
priceThe price as text, already formatted: Rp 25.000, or ''
old_priceA second price, such as the price before a discount, formatted the same way, or ''
badgeA short label such as Terlaris, or ''
subtitleA short line under the title (a one-line text or date field), or ''
summaryA short description: the long-text field, or the start of the article text (about 160 characters), or ''
fieldsEvery 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.

Note: on a page in another language, the collection's name and its items' text fields come translated, like the built-in lists and the entry of a detail page. See translations.

A product grid

plang
{%- 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 == blank is true both when the owner hasn't picked a collection (s.products is nil) and when the collection has no visible items. Comparing with 0 would not catch nil.
  • limit: s.max pairs with a range setting named max. Don't use a number setting that can be 0, since limit: 0 shows nothing.
  • The WhatsApp message is built with append and turned into a link by the button snippet. The wa: 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:

PropertyValue
titleThe item's name
urlThe address of this page
collectionThe collection it belongs to: name, slug and url (the list page)
imagesEvery photo of the item (all Photo and Photo gallery fields, the main photo first, without duplicates)
imageThe first photo, or ''
price, old_priceFormatted prices (Rp 75.000), or ''
badgeThe badge text, or ''
summaryThe short description (Long text field), or ''
excerptThe first 160 characters of the article text as plain text, or ''
bodyThe 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
factsA 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
fieldsThe 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:

plang
{% 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

plang
{%- 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