Building themes
Snippets
Reusable pieces of Plang: your own snippets, the render tag, the platform snippets (heading, button, image, stars) and how to override them.
A snippet is a small template you write once and use in many places. Snippets live in snippets/<name>.plang and are used with the render tag. The platform also ships four snippets of its own (heading, button, image and stars) so your sections match the rest of the website.
Writing a snippet
A snippet file is plain Plang, with no {% schema %} and no {% style %} (style the markup from the section or from assets/*.css).
{%- comment -%} snippets/promo-chip.plang {%- endcomment -%}
<span class="chip">{{ label | default: 'Promo' }}{% if percent %} {{ percent }}%{% endif %}</span>Names are lowercase letters, digits, - and _ (at most 61 characters): promo-chip, product_card. A theme can have 120 snippets.
Use it from a section:
{% render 'promo-chip', label: 'Diskon', percent: 20 %}The snippet name in render must be a quoted literal, not a variable. If it doesn't exist the file can't be saved (The snippet “promo-chip” doesn’t exist. Create snippets/promo-chip.plang.).
Parameters and scope
Everything after the name is a list of name: value parameters. Values can use filters:
{% render 'product-card', title: p.title, price: p.price, link: p.url | default: '#' %}A snippet runs in its own scope:
- It sees the parameters you pass, as plain variables.
- It also sees the platform globals:
section,settings,theme,site,page,entryandediting. So a snippet can readsection.settingsorsite.namewithout being passed them. - It does not see variables the caller made with
assign,captureorfor. Pass what it needs. - Its own
assignandcapturestay inside the snippet. - A
{% shell %}inside a snippet sets the wrapper of the section that rendered it.
{% assign accent = 'red' %}
{% render 'promo-chip' %}The snippet above does not see accent. To give it the value: {% render 'promo-chip', accent: accent %}.
with and for
with passes one value, and for renders the snippet once per item. Both name the value with as; without as the variable is named after the snippet (hyphens turned into underscores).
{% render 'promo-chip' with s.badge as label %}
{% render 'product-card' for s.products.items as p, show_price: true %}In the for form the snippet also gets a forloop with index, index0, first, last and length. A value that isn't a list renders nothing. A render ... for costs one step per item, and all steps count towards the same limit as the section.
Nesting
A snippet may render other snippets, up to 8 levels deep. A snippet that renders itself (directly or through others) stops with Snippets are nested more than 8 deep (does “x” render itself?).
Platform snippets
These work in every theme without creating a file. They are written in Plang too, with the same markup the built-in sections use, so buttons follow the theme's button style and headings follow its heading style. Their class parameters take Tailwind-style classes that the platform already uses (see the caveat on the styling page), or your own class names that you style yourself.
heading
A section heading block: a small label, a title and an intro text.
{% render 'heading', eyebrow: 'Menu', heading: 'Kopi pilihan kami', text: s.text, align: 'center' %}| Parameter | Meaning |
|---|---|
heading | The title, written as an <h2> with the theme's heading style |
text | An intro paragraph. Line breaks are kept |
eyebrow | A small uppercase label above the title |
align | center centres the block. Anything else, or nothing, aligns left |
class | Extra classes for the block, for example mb-4! |
If heading, text and eyebrow are all blank, nothing is written. The block is at most 42rem wide and has a bottom margin.
button
A link that looks like a theme button.
{% render 'button', label: 'Pesan lewat WhatsApp', link: s.button_link, size: 'lg' %}
{% render 'button', label: 'Lihat menu', link: 'page:menu', variant: 'outline' %}| Parameter | Meaning |
|---|---|
label | The button text. Nothing is written when it is blank |
link | A link setting, a link object, or a stored link text such as wa:Halo or page:menu. Plain text is converted the same way as the link filter. Without a usable link the button goes to # |
variant | primary (the default: filled with the brand colour, following the theme's button style), outline, or ghost (text with an underline on hover) |
size | sm, md (the default) or lg |
class | Extra classes, for example w-full |
External links (web addresses and WhatsApp chats) open in a new tab with rel="noopener noreferrer". WhatsApp links also get the WhatsApp logo before the label. The shape comes from the theme's button shape (rounded-btn).
image
A photo, or a placeholder when there is none.
{% render 'image', src: p.image, alt: p.title, class: 'aspect-[4/3] w-full rounded-site' %}| Parameter | Meaning |
|---|---|
src | The image address. It passes through image_url, so only uploaded images (/uploads/...) and https:// addresses are used |
alt | The text for screen readers and search engines |
class | Classes for the image or the placeholder. Set a size here, because the image fills what you give it (object-cover is always added) |
eager | true loads the image at once with high priority (use it for the first big photo on a page). Default is lazy loading |
When src is empty or not allowed, a grey box with an image icon is written instead, with role="img" and the alt text, so layouts don't collapse before photos are added.
stars
A row of five stars for ratings.
{% render 'stars', rating: r.rating, class: 'size-5' %}| Parameter | Meaning |
|---|---|
rating | A number. It is rounded to a whole number and kept between 0 and 5. Missing or empty means 5. 0 writes nothing |
class | Classes for each star. Default size-4 |
The stars are gold, the unfilled ones are faded, and the row has the label "N dari 5 bintang" for screen readers.
Overriding a platform snippet
Create a snippet with the same name and yours is used instead, everywhere in the theme: in your own sections, in the platform snippets that call it, and in built-in sections that your theme rewrites in Plang. Keep the same parameter names so every caller keeps working.
{%- comment -%} snippets/button.plang: a flat button with an arrow {%- endcomment -%}
{%- if label != blank -%}
{%- assign to = link | link -%}
<a href="{{ to | default: '#' }}"{% if to.external %} target="_blank" rel="noopener noreferrer"{% endif %} class="btn btn-{{ variant | default: 'primary' }} {{ class }}">
{{ label }} {{ 'ArrowRight' | icon: class: 'btn-arrow' }}
</a>
{%- endif -%}.btn { display: inline-flex; align-items: center; gap: .5rem; padding: .75rem 1.25rem; font-weight: 700; }
.btn-primary { background: var(--s-btn); color: var(--s-btn-fg); }
.btn-outline { border: 2px solid currentColor; }
.btn-arrow { width: 1.1em; height: 1.1em; }Put that CSS in an assets/*.css file (snippets can't contain {% style %}). The link | link line works for a link object or for stored text alike, because the link filter leaves a link object as it is.
The shell tag
Every section is wrapped in a <section> that carries the background, the padding and the data-code="<handle>" attribute. Two things about the wrapper can be set from the template with {% shell %}:
{% shell padded: false, class: 'my-band' %}| Argument | Meaning |
|---|---|
padded | false removes the wrapper's top and bottom padding (useful for banners and fixed bars). Without the tag the schema decides: "padding": false in the schema turns padding off by default |
class | Extra classes for the wrapper, at most 300 characters. Characters outside letters, digits and : @ / . [ ] % # ( ) ! , - are removed |
tag | Accepted, but has no effect at the moment |
shell works in the section file and in the snippets it renders, and has no effect on the header and footer, which have no wrapper.