Di 23 Templates kita kenal semua tipe template dan strukturnya. Sekarang kita bedah isinya satu-satu: object apa yang dipakai, form apa yang wajib, dan jebakan di tiap halaman.

Satu prinsip yang berlaku buat semua template di bab ini: kalau template-nya JSON, semua HTML dan Liquid harus ada di dalam section yang dirujuk template. Jadi waktu bab ini bilang “template produk harus punya form produk”, maksudnya section utama template produk (biasanya main-product.liquid) yang punya form-nya.

Tiap tipe punya contoh implementasi di theme Dawn yang bisa kamu buka di repository Dawn: template-nya di templates/, section utamanya di sections/main-*.liquid.

index

Halaman depan di URL root /. Ini kesan pertama pelanggan, jadi template-nya harus ngasih opsi yang luwes buat merchant nyeritain tokonya: banner, produk unggulan, koleksi, testimoni, dan sebagainya.

Bawaannya hampir gak ada isi “wajib” di sini, karena semua tergantung section yang dipilih merchant. Yang penting: pakai index.json, bukan index.liquid. Dua-duanya bikin merchant bisa ngatur section, tapi JSON lebih luwes (bisa app section, bisa diurutin bebas) dan nyimpen datanya sendiri sehingga theme editor lebih cepat.

Kalau kamu nemuin index.liquid di theme lama, isinya pasti ada {{ content_for_index }} yang dibahas di 13 Objects Global > 3. content_for_index. Di index.json, object itu gak dibutuhin.

product

Halaman produk: media, konten, dan form buat milih varian dan nambah ke keranjang. Ini template paling penting di toko, karena di sinilah keputusan beli terjadi.

Yang harus ada di template (atau section utamanya):

  1. Object product dari 14 Objects Produk dan Koleksi > product buat nampilin detailnya.
  2. Form produk dengan komponen: pemilih varian, input kuantitas, tombol accelerated checkout, dan input properti baris (opsional).
  3. Opsional: rekomendasi produk.

1. Form Produk

Cara utama pelanggan nambah varian ke keranjang. Pakai tag form bertipe product dari 12 Tags Khusus Theme > 1. product:

{% form 'product' %}
  <!-- form content -->
  <input type="submit" value="Add to cart" />
{% endform %}

Kalau butuh atribut kustom di <form>-nya (class, ID), tambahin sebagai parameter tag.

2. Pemilih Varian

Biasanya berupa satu atau lebih input nilai opsi (dropdown Size, tombol Color) yang nentuin varian mana yang dibeli. Yang dikirim ke server tetap <input name="id"> berisi ID varian, jadi pemilih opsi itu cuma antarmuka yang nyari varian yang cocok terus ngisi input id-nya. Implementasi lengkapnya di 39 Produk Variants dan High-variant Products.

3. Input Kuantitas

Wajib punya atribut name="quantity" dengan nilai bilangan bulat minimal 1:

<input type="text" name="quantity" min="1" value="1" />

4. Tombol Accelerated Checkout

Biar pelanggan bisa langsung beli tanpa lewat keranjang (Buy it now, Shop Pay, PayPal). Pakai filter payment_button dari 20 Filters Money, Metafield, Cart, Customer, dan Localization > 1. payment_button, di dalam form:

{% form 'product' %}
  <!-- form content -->
  <input type="submit" value="Add to cart" />
  {{ form | payment_button }}
{% endform %}

Bahasan gaya dan kustomisasinya di 43 Accelerated Checkout.

5. Properti Baris

Buat ngasih pelanggan opsi nambahin informasi tambahan ke item yang dibeli: teks ukiran, monogram, atau upload file. Ditangkap lewat input name="properties[nama-properti]", wajib di dalam form produk:

{% form 'product' %}
  <!-- form content -->
  <input type="text" name="properties[Monogram]" />
  <input type="submit" value="Add to cart" />
{% endform %}

Cara nampilinnya di keranjang ada di bagian cart di bawah.

❌ salah, input properti di luar form, nilainya gak ikut terkirim

<input type="text" name="properties[Monogram]" />
{% form 'product' %}
  <input type="submit" value="Add to cart" />
{% endform %}

✅ benar

{% form 'product' %}
  <input type="text" name="properties[Monogram]" />
  <input type="submit" value="Add to cart" />
{% endform %}

6. Cart AJAX API dan Rekomendasi

Dua hal yang bikin halaman produk modern:

  1. Cart API (bagian dari Ajax API) buat nambah varian ke keranjang tanpa pindah ke halaman keranjang. Bahasannya di 51 Ajax API dan Section Rendering API.
  2. Product Recommendations API buat nampilin produk terkait dan pelengkap. Bahasannya di 41 Produk Recommendations, Gift Card, dan Bundles.

7. Template Seller Product

Ada satu alternate template produk khusus: product.remote.seller.json, buat nampilin produk dari toko lain (fitur remote products). Di theme editor namanya Seller product.

Aturannya:

  1. Semua produk remote otomatis pakai template ini, dan toko gak bisa ngasih template ini ke produknya sendiri.
  2. Awalan remote. itu dilindungi, gak boleh dipakai buat tujuan lain.
  3. Kalau theme pakai Liquid template, namanya product.remote.seller.liquid.
  4. Template ini bisa disediain theme dari awal, atau toko diminta bikin waktu pasang app yang butuh. Kalau toko gak ikut fitur remote products, template ini gak muncul di theme editor walaupun ada di kode.

Isinya harus cuma info inti produk: nama, harga, media, deskripsi, pemilih varian, kuantitas, tombol beli. Yang jangan dimasukin: nilai brand (“Berkelanjutan”, “Fair Trade”), info pengiriman dan refund, opsi pembayaran khusus (Klarna, Afterpay), bundle atau diskon, dan apapun yang bergantung pada metafield, karena gak ada jaminan produk dari toko lain punya metafield yang sama. Object remote_product-nya dibahas di 14 Objects Produk dan Koleksi > Object Remote.

collection

Halaman koleksi yang ngelist semua produk di dalamnya. Yang harus ada: object collection dari 14 Objects Produk dan Koleksi > collection.

Tiga hal yang harus dikuasai di template ini:

1. Filter

Pakai storefront filtering buat nyaring koleksi jadi subset yang lebih kecil. Object filter-nya di 17 Objects Lainnya > Object Filtering, implementasinya di 37 Filtering.

2. Urutan

Urutan produk diatur lewat parameter URL sort_by:

https://my-store.myshopify.com/collections/frontpage?sort_by=price-descending

Dari object collection kamu dapat sort_options (semua pilihan), sort_by (yang aktif), dan default_sort_by (bawaan). Polanya: tampilin di <select>, terus pakai JavaScript buat nambahin parameter URL dan refresh:

<select id="sort-by">
  {% assign sort_by = collection.sort_by | default: collection.default_sort_by %}
  {% for option in collection.sort_options %}
    <option value="{{ option.value }}" {% if option.value == sort_by %}selected="selected"{% endif %}>
      {{ option.name }}
    </option>
  {% endfor %}
</select>
<script>
  Shopify.queryParams = {};
  // Preserve existing query parameters
  if (location.search.length) {
    var params = location.search.substr(1).split('&');
    for (var i = 0; i < params.length; i++) {
      var keyValue = params[i].split('=');
      if (keyValue.length) {
        Shopify.queryParams[decodeURIComponent(keyValue[0])] = decodeURIComponent(keyValue[1]);
      }
    }
  }
  // Update sort_by query parameter on select change
  document.querySelector('#sort-by').addEventListener('change', function(e) {
    var value = e.target.value;
    Shopify.queryParams.sort_by = value;
    location.search = new URLSearchParams(Shopify.queryParams).toString();
  });
</script>

Perhatiin Shopify.queryParams yang nyimpen parameter yang udah ada (misalnya filter yang aktif) sebelum nambahin sort_by. Kalau langsung location.search = '?sort_by=...', filter yang aktif bakal hilang.

3. Paginasi

collection.products maksimal 50 per halaman, jadi wajib di-paginate biar semua produk bisa diakses:

{% paginate collection.products by 20 %}
  {% for product in collection.products %}
    <!-- product info -->
  {% endfor %}
  {{ paginate | default_pagination }}
{% endpaginate %}

Aturannya udah dibahas di 11 Tags Perulangan > paginate.

4. Produk Remote

Kalau toko ikut fitur remote products, produk dari toko lain otomatis muncul di koleksi tanpa perubahan theme, dengan lencana khusus di gambarnya. Syaratnya theme harus pakai collection.products dari Liquid, bukan API.

list-collections

Halaman daftar semua koleksi di /collections. Yang harus ada: object collections dari 13 Objects Global > 2. collections.

1. Urutan Koleksi

Loop biasa ngasih urutan abjad:

{% for collection in collections %}
  <!-- collection info -->
{% endfor %}

Kalau merchant mau urutan sendiri, caranya: bikin menu berisi koleksi-koleksi sesuai urutan, sediain setting bertipe link_list biar merchant milih menunya, terus loop item menu yang tipenya collection_link dan ambil koleksinya dari link.object:

{% for link in settings.collection_list_menu.links %}
{% if link.type == 'collection_link' %}
  {% assign collection = link.object %}
  <!-- collection info -->
{% endif %}
{% endfor %}

Ini contoh bagus pemakaian link.object dan link.type dari 15 Objects Konten > 2. link.

2. Gambar Cadangan

Koleksi gak selalu punya gambar. Sediain cadangan, misalnya gambar produk pertama di koleksi:

{% if collection.image %}
{{ collection.image | image_url: width: 450, height: 450 | image_tag: collection.image.alt }}
{% else %}
{% assign alt = collection.title | escape %}
{{ collection.products.first.image | image_url: width: 450, height: 450 | image_tag: alt }}
{% endif %}

cart

Halaman /cart dengan ringkasan isi keranjang, biasanya tabel dengan satu baris per line item. Yang harus ada: object cart dari 16 Objects Cart, Checkout, Order, dan Customer > cart.

Ingat, satu line item = satu varian. Kalau pelanggan nambah kaos ukuran M dan L, itu dua baris.

1. Loop Item

{% for item in cart.items %}
  <!-- line item info -->
{% endfor %}

2. Lanjut ke Checkout

Item-item keranjang harus di dalam <form> dengan action="{{ routes.cart_url }}" dan method="post", plus input submit bernama checkout yang jadi pemicu ke halaman checkout:

<form action="{{ routes.cart_url }}" method="post">
  {% for item in cart.items %}
    <!-- line item info -->
  {% endfor %}
  <input type="submit" name="checkout" value="Checkout" />
</form>

Atau pakai tag form bertipe cart dari 12 Tags Khusus Theme > 2. cart yang ngasilin form yang sama.

❌ salah, tombolnya gak bernama checkout, cuma reload halaman

<input type="submit" value="Checkout" />

✅ benar

<input type="submit" name="checkout" value="Checkout" />

3. Hapus Item

Sediain link dengan href ke item.url_to_remove:

{% for item in cart.items %}
  <!-- line item info -->
  <a href="{{ item.url_to_remove }}">Remove</a>
{% endfor %}

Buat versi tanpa reload halaman, pakai endpoint /cart/change dari Cart API.

4. Update Kuantitas

Input dengan name="updates[]" dan nilai kuantitas sekarang, per item:

{% for item in cart.items %}
  <!-- line item info -->
  <input type="text" name="updates[]" value="{{ item.quantity }}" />
{% endfor %}

Terus tombol submit biasa di dalam form keranjang buat nerapin:

<input type="submit" value="Update cart" />

Submit form ini reload halaman dengan kuantitas baru. Buat tanpa reload, pakai endpoint /cart/update.

5. Diskon

Diskon bisa kena seluruh keranjang atau per item, jadi tampilin di dua tempat: di ringkasan total dan di tiap baris. Object-nya di 16 Objects Cart, Checkout, Order, dan Customer > Object Diskon, implementasinya di 42 Harga Diskon, Currency, Unit Pricing, Installments, B2B.

6. Catatan dan Atribut Keranjang

Buat ngasih pelanggan opsi info tambahan buat seluruh order. Perhatiin atribut form="cart" di input-nya: ini atribut HTML yang bikin input tetap terhubung ke form keranjang walaupun ditaruh di luar <form>-nya, misalnya di section footer keranjang yang terpisah dari section item.

Catatan keranjang, input bernama note:

<textarea name="note" form="cart"></textarea>

Atribut keranjang, input bernama attributes[nama]:

<p>
  <label>Please let us know your favorite color</label>
  <input type="text" name="attributes[Favorite color]" form="cart" value="{{ cart.attributes['Favorite color'] }}" />
</p>

Nilainya dibaca lewat cart.note dan cart.attributes, udah dibahas di 16 Objects Cart, Checkout, Order, dan Customer > 4. Catatan dan Atribut.

7. Tampilin Properti Baris

Properti yang ditangkap di form produk ditampilin dengan loop item.properties. Ada trik buat file yang di-upload: nilainya berisi /uploads/, jadi tampilin sebagai link:

{% for item in cart.items %}
  <!-- line item info -->
  {% unless item.properties == empty %}
    {% for property in item.properties %}
      {{ property.first }}:
      {% if property.last contains '/uploads/' %}
        <a href="{{ property.last }}">{{ property.last | split: '/' | last }}</a>
      {% else %}
        {{ property.last }}
      {% endif %}
    {% endfor %}
  {% endunless %}
{% endfor %}

Dan catatan: dua item yang sama tapi propertinya beda dipisah jadi dua baris. Kaos M dengan ukiran “A” dan kaos M dengan ukiran “B” itu dua line item.

8. Produk Remote

Produk dari toko lain bisa ada di keranjang tanpa perubahan theme. Judulnya bakal berisi nama toko asalnya, dan bisa dikenali lewat product.remote_details. Theme harus pakai cart.items atau Cart Ajax API, bukan Storefront API.

page

Halaman statis kaya About us dan Contact us. Yang harus ada: object page dari 15 Objects Konten > page, terutama page.title dan page.content.

Ini template paling sederhana. Yang biasanya ditambahin: alternate template page.contact.json yang punya section form kontak, dibahas di 46 Customer Sign-in, Account Component, Contact Form, Email Consent.

blog

Halaman blog yang ngelist semua artikelnya. Yang harus ada: section yang pakai object blog dari 15 Objects Konten > blog, dengan blog.articles yang di-paginate.

Filter Artikel per Tag

Pakai tag artikel buat nyaring blog, dengan nambahin /tagged/[handle-tag] ke URL blog:

https://my-store.myshopify.com/blogs/main/tagged/news

Beberapa tag digabung pakai +:

https://my-store.myshopify.com/blogs/main/tagged/news+breaking

Jangan bikin URL ini manual, pakai filter link_to_tag, link_to_add_tag, dan link_to_remove_tag dari 19 Filters HTML, URL, Media, Color, dan Font > 3. Filter Tag Filtering.

article

Halaman satu artikel dengan isi lengkap dan komentar opsional. Yang harus ada: object article dari 15 Objects Konten > article dan form komentar.

1. Form Komentar

Pakai tag form bertipe new_comment dengan object article sebagai parameter. Di dalamnya wajib tiga input dengan nama tertentu:

  1. Nama: type="text", name="comment[author]".
  2. Email: type="email", name="comment[email]".
  3. Komentar: textarea, name="comment[body]".
{% form 'new_comment', article %}
  {{ form.errors | default_errors }}
  <div class="name">
    <label for="name">Name</label>
    <input type="text" name="comment[author]" value="{{ form.author }}" />
  </div>
  <div class="email">
    <label for="email">Email</label>
    <input type="email" name="comment[email]" value="{{ form.email }}" />
  </div>
  <div class="comment">
    <label for="comment">Comment</label>
    <textarea name="comment[body]">{{ form.body }}</textarea>
  </div>
  <div class="submit">
    <input type="submit" value="Post" />
  </div>
{% endform %}

Perhatiin value="{{ form.author }}" dan seterusnya: itu isi ulang dari object form di 17 Objects Lainnya > form kalau submit gagal. Dan kasih umpan balik ke pelanggan: berhasil, atau errornya apa, pakai form.posted_successfully? dan form.errors.

2. Paginasi Komentar

article.comments maksimal 50 per halaman, jadi di-paginate:

{% paginate article.comments by 20 %}
  {% for comment in article.comments %}
    <!-- comment info -->
  {% endfor %}
  {{ paginate | default_pagination }}
{% endpaginate %}

Halaman /search dengan hasil pencarian. Yang harus ada: object search dari 17 Objects Lainnya > search, form pencarian, dan hasil pencarian.

1. Form Pencarian

<form> dengan action="{{ routes.search_url }}" dan input type="text" bernama q. Isi value-nya dengan search.terms biar kata yang dicari tetap ada di kolom setelah hasil muncul:

<form action="{{ routes.search_url }}">
  <input type="text"
    placeholder="Search"
    name="q"
    value="{{ search.terms | escape }}"
  />
  <input type="submit" value="Search" />
</form>

Perhatiin | escape di value: kata pencarian itu input pengguna, wajib di-escape.

2. Hasil

Loop search.results, bedain tipenya pakai object_type:

{% for item in search.results %}
  <!-- item details -->
{% endfor %}

3. Sorot Kata Pencarian

Pakai filter highlight buat nebelin kata yang dicari di dalam konten hasil:

{% for item in search.results %}
  <!-- item details -->
  {{ item.content | highlight: search.terms }}
{% endfor %}

Hasilnya <strong class="highlight"> yang bisa kamu gayain. Bahasan lengkap pencarian di 38 Search dan Predictive Search.

404

Halaman kalau pelanggan buka URL yang gak ada. Gak ada object yang disarankan, tapi template harus jelas bilang halamannya gak ketemu dan ngasih jalan keluar: link ke halaman populer, kolom pencarian, atau link ke beranda atau koleksi buat lanjut belanja:

<p>
  404
</p>
<h1 class="title">
  Page not found
</h1>
<a href="{{ routes.all_products_collection_url }}" class="button">
  Continue shopping
</a>

Perhatiin routes.all_products_collection_url, bukan /collections/all manual, sesuai aturan 13 Objects Global > routes.

password

Halaman /password waktu toko dikunci. Isinya pesan yang bisa diedit merchant dan form password, plus opsional form daftar email.

1. Pesan Password

Dari shop.password_message:

{% unless shop.password_message == blank %}
  {{ shop.password_message }}
{% endunless %}

2. Form Password

Tag form bertipe storefront_password dengan input type="password" bernama password:

{% form 'storefront_password' %}
  {{ form.errors | default_errors }}
  <div class="password">
    <label for="password">Password</label>
    <input type="password" name="password" />
  </div>
  <div class="submit">
    <input type="submit" value="Sign in" />
  </div>
{% endform %}

3. Form Daftar Email

Buat ngumpulin email calon pelanggan sebelum toko buka. Tag form bertipe customer dengan input tersembunyi contact[tags] berisi prospect, password page dan input email contact[email]:

{% form 'customer' %}
  {{ form.errors | default_errors }}
  <div class="tags">
    <input type="hidden" name="contact[tags]" value="prospect, password page" />
  </div>
  <div class="email">
    <label for="email">Email</label>
    <input type="email" name="contact[email]" />
  </div>
  <div class="submit">
    <input type="submit" value="Sign in" />
  </div>
{% endform %}

Tag prospect dan password page itu yang Shopify pakai buat bantu merchant marketing ke pelanggan ini nanti, tapi kamu bisa pakai tag sendiri.

Catatan dari 04 Dev Store, Akun Kolaborator, dan Theme Access > 3. Bagiin Dev Store: di dev store, halaman password kustom ini gak dipakai buat ngontrol akses. Yang tampil selalu halaman password khusus dev store. Buat lihat versi kustomnya, login dulu terus buka /password.

gift_card.liquid

Halaman gift card yang dilihat pelanggan setelah beli. Wajib Liquid, gak bisa JSON. Dan beda dari halaman lain, ini di-host di domain checkout.shopify.com:

https://checkout.shopify.com/gift_cards/[store_id]/[gift_card_token]

Yang harus ada: object gift_card dari 16 Objects Cart, Checkout, Order, dan Customer > gift_card.

1. QR Code

Tambahin skrip QR code bawaan Shopify di <head>:

{{ 'vendor/qrcode.js' | shopify_asset_url | script_tag }}

Terus di <body>, bikin QR yang isinya bisa diatur lewat text:

<div id="qr-code"></div>
<script>
  new QRCode(document.getElementById('qr-code'), {
    text: '{{ shop.url }}',
    width: 120,
    height: 120
  });
</script>

Buat QR yang isinya kode gift card, pakai gift_card.qr_identifier.

2. Apple Wallet

{% if gift_card.pass_url %}
<a href="{{ gift_card.pass_url }}" >
  <img id="apple-wallet-badge"
    src="{{ 'gift-card/add-to-apple-wallet.svg' | shopify_asset_url }}"
    width="120"
    height="40"
    alt="Add To Apple Wallet" />
  </a>
{% endif %}

3. Tanpa Layout

Karena halaman ini biasanya buat dicetak atau disimpan, seringnya gak butuh header dan footer. Pakai layout none atau layout kustom, sesuai 22 Layouts > Layout Alternatif:

{% layout none %}
<!-- template content -->

Tapi kalau layout none, kamu harus nulis <html>, <head>, dan content_for_header sendiri di template. Kalau template gift card gak muncul di menu theme editor, kemungkinan content_for_header-nya lupa.

4. Preview

Di theme editor, buka dropdown di atas, Templates > Others > Gift card.

metaobject

Template buat halaman metaobject (artis, penulis, toko fisik). Syaratnya definisi metaobject-nya punya capability onlineStore (biar bisa dirender sebagai halaman) dan sebaiknya renderable (biar ada pengaturan SEO).

Lokasinya di sub-folder dengan nama tipenya:

└── theme
  ├── templates
  |   |── metaobject
  |   |   |── {type}.json

Aturannya:

  1. Template pertama yang dibikin buat sebuah tipe jadi template default, otomatis dipakai semua entri aktif tipe itu. Setelah itu boleh bikin alternate.
  2. Template-nya kosong waktu dibikin di theme editor, merchant harus nambahin section sendiri.
  3. Semua section yang tersedia buat template atau section group manapun otomatis tersedia di template metaobject.
  4. Field-nya diakses lewat object metaobject dari 15 Objects Konten > 4. Di Template Metaobject.

Ringkasan

  1. Template JSON gak bisa berisi kode, semua HTML dan Liquid ada di section utamanya (main-*.liquid).
  2. index.json buat halaman depan, content_for_index cuma buat index.liquid lama.
  3. Template produk wajib punya form produk dengan input quantity, pemilih varian yang ngisi <input name="id">, dan payment_button di dalam form. Properti baris lewat properties[nama] di dalam form.
  4. product.remote.seller.json buat produk toko lain, isinya cuma info inti, tanpa metafield, dan awalan remote. dilindungi.
  5. Template koleksi butuh paginasi (maksimal 50 per halaman), urutan lewat ?sort_by=, dan JavaScript pengurut harus mempertahankan parameter yang udah ada.
  6. list-collections urutannya abjad, buat urutan kustom pakai menu dan link.object. Sediain gambar cadangan buat koleksi tanpa gambar.
  7. Form keranjang butuh action="{{ routes.cart_url }}", method="post", dan submit bernama checkout. Hapus item lewat url_to_remove, update kuantitas lewat updates[].
  8. Atribut form="cart" bikin input catatan dan atribut keranjang bisa ditaruh di section lain tapi tetap ikut terkirim.
  9. Properti baris yang berisi /uploads/ itu file, tampilin sebagai link. Item sama dengan properti beda jadi baris terpisah.
  10. Form komentar butuh comment[author], comment[email], comment[body], dan komentar di-paginate.
  11. Form pencarian butuh action="{{ routes.search_url }}" dan input bernama q yang value-nya search.terms | escape.
  12. 404 harus ngasih jalan keluar, password punya form storefront_password dan form customer dengan tag prospect, password page.
  13. gift_card.liquid wajib Liquid, di-host di checkout.shopify.com, biasanya layout none tapi jangan lupa content_for_header.
  14. Template metaobject di templates/metaobject/{type}.json, butuh capability onlineStore, dan template pertama jadi default.

Bab selanjutnya kita bahas empat template yang outputnya bukan halaman HTML tapi file teks buat mesin: robots.txt buat mesin pencari, dan agents.md, llms.txt, llms-full.txt buat AI agent.