# Referensi template freeundangan

Sumber: https://freeundangan.id/bantuan/template

Template adalah satu file HTML dengan placeholder Mustache. Daftar di bawah dibuat langsung dari kode validator, jadi selalu sama dengan yang diperiksa saat diunggah.

## Mulai cepat

1. Mulai dari kerangka di bawah, atau dari starter template yang memakai semua blok dan kedelapan partial: https://freeundangan.id/bantuan/template/starter.html
2. Pakai field dari daftar "Field baku"; setiap field disertai arti dan kapan nilainya kosong.
3. Cek dengan validator (bagian "Cek otomatis") sampai tidak ada yang ditolak.
4. Unggah di https://freeundangan.id/templates/new, lalu periksa pratinjaunya dengan data contoh lengkap dan minim.

Kerangka ini adalah template terkecil yang lolos validator tanpa peringatan: layar pembuka, nama mempelai, sesi akad dan resepsi, amplop digital, dan konfirmasi kehadiran.

```html
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{couple_short}}</title>
<style>
  body { margin: 0; font-family: Georgia, serif; text-align: center; }
  section { padding: 3rem 1.25rem; max-width: 32rem; margin: 0 auto; }
  .cover { min-height: 100svh; display: grid; place-content: center; gap: 1rem; }
  html.fu-opened .cover { display: none; }
  .fu-rsvp[data-state="sending"] .fu-rsvp__submit { opacity: .6; }
  .fu-copy[data-state="copied"] { color: seagreen; }
</style>
</head>
<body>
<section class="cover">
  <p>Kepada Yth. {{> guest_name}}</p>
  <h1>{{couple_short}}</h1>
  {{> open_button}}
</section>
<section>
  <p>{{opening_text}}</p>
  <h2>{{first.full_name}}</h2><p>{{first.parents_line}}</p>
  <h2>{{second.full_name}}</h2><p>{{second.parents_line}}</p>
</section>
{{#akad}}<section>
  <h2>{{title}}</h2>
  <p>{{date_long}}, {{time_range}}</p>
  <p>{{venue_name}}</p>
  <a href="{{calendar_url}}">Simpan ke kalender</a>
</section>{{/akad}}
{{#resepsi}}<section>
  <h2>{{title}}</h2>
  <p>{{date_long}}, {{time_range}}</p>
  <p>{{venue_name}}</p>
  <a href="{{calendar_url}}">Simpan ke kalender</a>
</section>{{/resepsi}}
{{#has_gifts}}<section>
  <h2>Amplop digital</h2>
  {{#gifts}}<p>{{bank_name}} {{account_number}} a.n. {{account_holder}} {{> copy_button}}</p>{{/gifts}}
</section>{{/has_gifts}}
<section><h2>Konfirmasi kehadiran</h2>{{> rsvp_form}}</section>
<section><p>{{closing_text}}</p></section>
</body>
</html>

```

## Dasar Mustache

- `{{bride.full_name}}` mencetak teks. Semua teks di-escape otomatis.
- `{{#akad}}...{{/akad}}` hanya dirender bila sesi akad ada. Di dalamnya, field sesi bisa ditulis langsung: `{{date_long}}`.
- `{{#gifts}}...{{/gifts}}` mengulang isi untuk setiap item list.
- `{{^has_gallery}}...{{/has_gallery}}` dirender bila nilainya kosong atau tidak.
- Section hanya boleh pada field berjenis objek, list, atau ya/tidak. Section pada field teks ditolak.
- Template wajib berisi `<html` dan `</body>`, berupa UTF-8, maksimal 2 MB. Inline `<script>` dan `<style>` diizinkan.

## Field baku

Field di dalam list ditulis dengan nama list-nya, misalnya `gifts.bank_name`; di template tulis `{{bank_name}}` di dalam `{{#gifts}}`. Contoh nilai berasal dari data contoh yang dipakai pratinjau. Field yang "bisa kosong" juga harus tampil rapi saat kosong.

- `akad` (objek): Sesi akad. Null bila tidak ada, jadi {{#akad}}...{{/akad}} tidak dirender.
- `akad.address` (teks): Alamat lengkap. Bisa kosong. Contoh: `Jl. Tamansari No. 73, Bandung`
- `akad.calendar_url` (teks): Tautan "Simpan ke Google Calendar" yang sudah jadi. Contoh: `https://calendar.google.com/calendar/render?action=TEMPLATE…`
- `akad.date` (teks): Tanggal mentah YYYY-MM-DD. Untuk tampilan pakai date_long. Contoh: `2026-12-28`
- `akad.date_iso` (teks): Tanggal YYYY-MM-DD, sama dengan date. Contoh: `2026-12-28`
- `akad.date_long` (teks): Tanggal untuk ditampilkan, lengkap dengan nama hari dan bulan dalam bahasa Indonesia. Contoh: `Senin, 28 Desember 2026`
- `akad.datetime_iso` (teks): Waktu mulai ISO 8601 dengan zona, untuk atribut datetime atau skrip. Contoh: `2026-12-28T08:00:00+07:00`
- `akad.dress_code` (teks): Aturan busana. Bisa kosong. Contoh: `Batik atau kebaya`
- `akad.lat` (teks): Garis lintang lokasi. Bisa kosong. Contoh: `-6.8869`
- `akad.lng` (teks): Garis bujur lokasi. Bisa kosong. Contoh: `107.6105`
- `akad.maps_url` (teks): Tautan Google Maps lokasi. Bisa kosong. Contoh: `https://maps.app.goo.gl/contoh`
- `akad.note` (teks): Catatan tambahan sesi. Bisa kosong.
- `akad.time_end` (teks): Jam selesai mentah HH:MM:SS. Kosong berarti "selesai". Contoh: `10:00:00`
- `akad.time_range` (teks): Jam untuk ditampilkan: "08.00 - 10.00 WIB", atau "11.00 WIB - selesai" bila tanpa jam selesai. Contoh: `08.00 - 10.00 WIB`
- `akad.time_start` (teks): Jam mulai mentah HH:MM:SS. Untuk tampilan pakai time_range. Contoh: `08:00:00`
- `akad.timezone` (teks): Zona waktu: WIB, WITA, atau WIT. Contoh: `WIB`
- `akad.title` (teks): Nama sesi, misalnya "Akad Nikah". Wajib. Contoh: `Akad Nikah`
- `akad.venue_name` (teks): Nama tempat. Wajib. Contoh: `Masjid Salman ITB`
- `bride` (objek): Mempelai wanita.
- `bride.child_order` (teks): Urutan anak, misalnya "Putri pertama". Bisa kosong. Contoh: `Putri pertama`
- `bride.father_deceased` (ya/tidak): true bila ayah sudah wafat. Contoh: `false`
- `bride.father_name` (teks): Nama ayah. Bisa kosong. Contoh: `Ahmad Suryadi`
- `bride.full_name` (teks): Nama lengkap. Wajib. Contoh: `Rani Puspitasari`
- `bride.guardian` (teks): Nama wali. Bisa kosong.
- `bride.instagram` (teks): Username Instagram tanpa @. Bisa kosong. Contoh: `rani`
- `bride.mother_deceased` (ya/tidak): true bila ibu sudah wafat. Contoh: `false`
- `bride.mother_name` (teks): Nama ibu. Bisa kosong. Contoh: `Siti Aminah`
- `bride.nickname` (teks): Nama panggilan. Wajib. Contoh: `Rani`
- `bride.parents_line` (teks): Baris orang tua yang sudah jadi: "Bapak X & Ibu Y", dengan (Alm.)/(Almh.) bila wafat. Kosong bila nama orang tua kosong. Contoh: `Bapak Ahmad Suryadi & Ibu Siti Aminah`
- `bride.photo_url` (teks): Foto mempelai (https). Null bila tidak ada; sediakan tampilan tanpa foto. Contoh: `https://freeundangan.id/static/demo/mempelai-wanita.webp`
- `closing_text` (teks): Kalimat penutup undangan. Selalu terisi (ada teks bawaan). Contoh: `Merupakan suatu kehormatan dan kebahagiaan bagi kami apabil…`
- `countdown_label` (teks): Tanggal panjang dari sesi yang sama dengan countdown_target. Contoh: `Senin, 28 Desember 2026`
- `countdown_target` (teks): Waktu mulai resepsi (atau akad bila tanpa resepsi), ISO 8601 dengan zona. Dipakai {{> countdown}}. Contoh: `2026-12-28T11:00:00+07:00`
- `couple_order` (teks): Urutan sebutan mempelai: bride_first atau groom_first. Biasanya cukup pakai first/second. Contoh: `bride_first`
- `couple_short` (teks): Nama panggilan berdua sesuai urutan, misalnya "Rani & Budi". Contoh: `Rani & Budi`
- `cover_image_url` (teks): Foto sampul (https). Bisa kosong; sediakan latar cadangan. Contoh: `https://freeundangan.id/static/demo/sampul.webp`
- `custom` (objek): Field custom yang dideklarasikan template. Isinya juga tersedia di root: {{motto}} sama dengan {{custom.motto}}.
- `extra_sessions` (list): Sesi tambahan (ngunduh mantu, syukuran, dll.), urut seperti diisi pasangan. Bisa kosong. Contoh: `1 item`
- `extra_sessions.address` (teks): Alamat lengkap. Bisa kosong. Contoh: `Jl. Tamansari No. 73, Bandung`
- `extra_sessions.calendar_url` (teks): Tautan "Simpan ke Google Calendar" yang sudah jadi. Contoh: `https://calendar.google.com/calendar/render?action=TEMPLATE…`
- `extra_sessions.date` (teks): Tanggal mentah YYYY-MM-DD. Untuk tampilan pakai date_long. Contoh: `2027-01-04`
- `extra_sessions.date_iso` (teks): Tanggal YYYY-MM-DD, sama dengan date. Contoh: `2027-01-04`
- `extra_sessions.date_long` (teks): Tanggal untuk ditampilkan, lengkap dengan nama hari dan bulan dalam bahasa Indonesia. Contoh: `Senin, 4 Januari 2027`
- `extra_sessions.datetime_iso` (teks): Waktu mulai ISO 8601 dengan zona, untuk atribut datetime atau skrip. Contoh: `2027-01-04T10:00:00+07:00`
- `extra_sessions.dress_code` (teks): Aturan busana. Bisa kosong. Contoh: `Batik atau kebaya`
- `extra_sessions.lat` (teks): Garis lintang lokasi. Bisa kosong. Contoh: `-6.8869`
- `extra_sessions.lng` (teks): Garis bujur lokasi. Bisa kosong. Contoh: `107.6105`
- `extra_sessions.maps_url` (teks): Tautan Google Maps lokasi. Bisa kosong. Contoh: `https://maps.app.goo.gl/contoh`
- `extra_sessions.note` (teks): Catatan tambahan sesi. Bisa kosong.
- `extra_sessions.time_end` (teks): Jam selesai mentah HH:MM:SS. Kosong berarti "selesai". Contoh: `13:00:00`
- `extra_sessions.time_range` (teks): Jam untuk ditampilkan: "08.00 - 10.00 WIB", atau "11.00 WIB - selesai" bila tanpa jam selesai. Contoh: `10.00 - 13.00 WIB`
- `extra_sessions.time_start` (teks): Jam mulai mentah HH:MM:SS. Untuk tampilan pakai time_range. Contoh: `10:00:00`
- `extra_sessions.timezone` (teks): Zona waktu: WIB, WITA, atau WIT. Contoh: `WIB`
- `extra_sessions.title` (teks): Nama sesi, misalnya "Akad Nikah". Wajib. Contoh: `Ngunduh Mantu`
- `extra_sessions.venue_name` (teks): Nama tempat. Wajib. Contoh: `Kediaman Keluarga Santoso`
- `first` (objek): Mempelai yang disebut pertama sesuai couple_order (bride atau groom). Pakai first/second agar urutan mengikuti pilihan pasangan.
- `first.child_order` (teks): Urutan anak, misalnya "Putri pertama". Bisa kosong. Contoh: `Putri pertama`
- `first.father_deceased` (ya/tidak): true bila ayah sudah wafat. Contoh: `false`
- `first.father_name` (teks): Nama ayah. Bisa kosong. Contoh: `Ahmad Suryadi`
- `first.full_name` (teks): Nama lengkap. Wajib. Contoh: `Rani Puspitasari`
- `first.guardian` (teks): Nama wali. Bisa kosong.
- `first.instagram` (teks): Username Instagram tanpa @. Bisa kosong. Contoh: `rani`
- `first.mother_deceased` (ya/tidak): true bila ibu sudah wafat. Contoh: `false`
- `first.mother_name` (teks): Nama ibu. Bisa kosong. Contoh: `Siti Aminah`
- `first.nickname` (teks): Nama panggilan. Wajib. Contoh: `Rani`
- `first.parents_line` (teks): Baris orang tua yang sudah jadi: "Bapak X & Ibu Y", dengan (Alm.)/(Almh.) bila wafat. Kosong bila nama orang tua kosong. Contoh: `Bapak Ahmad Suryadi & Ibu Siti Aminah`
- `first.photo_url` (teks): Foto mempelai (https). Null bila tidak ada; sediakan tampilan tanpa foto. Contoh: `https://freeundangan.id/static/demo/mempelai-wanita.webp`
- `gallery` (list): Foto galeri, urut seperti diisi pasangan. Bisa kosong; cek has_gallery. Contoh: `4 item`
- `gallery.url` (teks): URL foto (https), lebar maksimal 1600 px. Contoh: `https://freeundangan.id/static/demo/galeri-1.webp`
- `gift_address` (teks): Alamat kirim kado fisik. Bisa kosong. Contoh: `Jl. Cisitu Indah No. 12, Bandung 40135`
- `gifts` (list): Rekening amplop digital. Bisa kosong; cek has_gifts. Contoh: `2 item`
- `gifts.account_holder` (teks): Nama pemilik rekening. Contoh: `Rani Puspitasari`
- `gifts.account_number` (teks): Nomor rekening, pakai dengan {{> copy_button}} agar mudah disalin. Contoh: `1234567890`
- `gifts.bank_name` (teks): Nama bank atau dompet digital. Contoh: `BCA`
- `groom` (objek): Mempelai pria.
- `groom.child_order` (teks): Urutan anak, misalnya "Putri pertama". Bisa kosong. Contoh: `Putra kedua`
- `groom.father_deceased` (ya/tidak): true bila ayah sudah wafat. Contoh: `false`
- `groom.father_name` (teks): Nama ayah. Bisa kosong. Contoh: `Hadi Santoso`
- `groom.full_name` (teks): Nama lengkap. Wajib. Contoh: `Budi Santoso`
- `groom.guardian` (teks): Nama wali. Bisa kosong.
- `groom.instagram` (teks): Username Instagram tanpa @. Bisa kosong. Contoh: `budi`
- `groom.mother_deceased` (ya/tidak): true bila ibu sudah wafat. Contoh: `false`
- `groom.mother_name` (teks): Nama ibu. Bisa kosong. Contoh: `Ratna Dewi`
- `groom.nickname` (teks): Nama panggilan. Wajib. Contoh: `Budi`
- `groom.parents_line` (teks): Baris orang tua yang sudah jadi: "Bapak X & Ibu Y", dengan (Alm.)/(Almh.) bila wafat. Kosong bila nama orang tua kosong. Contoh: `Bapak Hadi Santoso & Ibu Ratna Dewi`
- `groom.photo_url` (teks): Foto mempelai (https). Null bila tidak ada; sediakan tampilan tanpa foto. Contoh: `https://freeundangan.id/static/demo/mempelai-pria.webp`
- `guest_fallback_text` (teks): Sapaan bila tamu membuka tanpa tautan pribadi. {{> guest_name}} sudah memakainya. Contoh: `Tamu Undangan`
- `has_akad` (ya/tidak): true bila sesi akad ada. Contoh: `true`
- `has_extra_sessions` (ya/tidak): true bila ada sesi tambahan. Contoh: `true`
- `has_gallery` (ya/tidak): true bila galeri berisi foto. Contoh: `true`
- `has_gifts` (ya/tidak): true bila ada rekening amplop digital. Contoh: `true`
- `has_love_story` (ya/tidak): true bila kisah cinta diisi. Contoh: `true`
- `has_music` (ya/tidak): true bila ada musik. Contoh: `true`
- `has_resepsi` (ya/tidak): true bila sesi resepsi ada. Contoh: `true`
- `has_video` (ya/tidak): true bila video_url diisi. Contoh: `true`
- `hashtag` (teks): Hashtag pernikahan, sudah termasuk #. Bisa kosong. Contoh: `#RaniBudiBerbagiCerita`
- `love_story` (list): Kisah cinta, urut seperti diisi pasangan. Bisa kosong; cek has_love_story. Contoh: `3 item`
- `love_story.date` (teks): Waktu momen sebagai teks bebas, misalnya "Agustus 2019" (bukan tanggal ISO). Contoh: `Agustus 2019`
- `love_story.text` (teks): Cerita momen, bisa beberapa kalimat. Contoh: `Kami bertemu di sebuah kegiatan kampus dan ternyata satu an…`
- `love_story.title` (teks): Judul momen, misalnya "Pertama bertemu". Contoh: `Pertama bertemu`
- `music_id` (teks): ID lagu pilihan pasangan. Untuk template, pakai music_url atau {{> music_player}}. Contoh: `contoh`
- `music_url` (teks): URL berkas musik. Null bila pasangan tidak memilih musik; cek has_music. Contoh: `https://files.example.com/music/contoh.mp3`
- `opening_text` (teks): Kalimat pembuka undangan. Selalu terisi (ada teks bawaan). Contoh: `Dengan memohon rahmat Tuhan Yang Maha Esa, kami bermaksud m…`
- `quote` (teks): Kutipan atau ayat. Bisa kosong. Contoh: `Dan di antara tanda-tanda kebesaran-Nya ialah Dia menciptak…`
- `quote_source` (teks): Sumber kutipan, misalnya "QS Ar-Rum: 21". Bisa kosong. Contoh: `QS Ar-Rum: 21`
- `resepsi` (objek): Sesi resepsi. Null bila tidak ada, jadi {{#resepsi}}...{{/resepsi}} tidak dirender.
- `resepsi.address` (teks): Alamat lengkap. Bisa kosong. Contoh: `Jl. Tamansari No. 73, Bandung`
- `resepsi.calendar_url` (teks): Tautan "Simpan ke Google Calendar" yang sudah jadi. Contoh: `https://calendar.google.com/calendar/render?action=TEMPLATE…`
- `resepsi.date` (teks): Tanggal mentah YYYY-MM-DD. Untuk tampilan pakai date_long. Contoh: `2026-12-28`
- `resepsi.date_iso` (teks): Tanggal YYYY-MM-DD, sama dengan date. Contoh: `2026-12-28`
- `resepsi.date_long` (teks): Tanggal untuk ditampilkan, lengkap dengan nama hari dan bulan dalam bahasa Indonesia. Contoh: `Senin, 28 Desember 2026`
- `resepsi.datetime_iso` (teks): Waktu mulai ISO 8601 dengan zona, untuk atribut datetime atau skrip. Contoh: `2026-12-28T11:00:00+07:00`
- `resepsi.dress_code` (teks): Aturan busana. Bisa kosong. Contoh: `Batik atau kebaya`
- `resepsi.lat` (teks): Garis lintang lokasi. Bisa kosong. Contoh: `-6.8869`
- `resepsi.lng` (teks): Garis bujur lokasi. Bisa kosong. Contoh: `107.6105`
- `resepsi.maps_url` (teks): Tautan Google Maps lokasi. Bisa kosong. Contoh: `https://maps.app.goo.gl/contoh`
- `resepsi.note` (teks): Catatan tambahan sesi. Bisa kosong.
- `resepsi.time_end` (teks): Jam selesai mentah HH:MM:SS. Kosong berarti "selesai".
- `resepsi.time_range` (teks): Jam untuk ditampilkan: "08.00 - 10.00 WIB", atau "11.00 WIB - selesai" bila tanpa jam selesai. Contoh: `11.00 WIB - selesai`
- `resepsi.time_start` (teks): Jam mulai mentah HH:MM:SS. Untuk tampilan pakai time_range. Contoh: `11:00:00`
- `resepsi.timezone` (teks): Zona waktu: WIB, WITA, atau WIT. Contoh: `WIB`
- `resepsi.title` (teks): Nama sesi, misalnya "Akad Nikah". Wajib. Contoh: `Resepsi`
- `resepsi.venue_name` (teks): Nama tempat. Wajib. Contoh: `Gedung Sabuga`
- `second` (objek): Mempelai yang disebut kedua sesuai couple_order.
- `second.child_order` (teks): Urutan anak, misalnya "Putri pertama". Bisa kosong. Contoh: `Putra kedua`
- `second.father_deceased` (ya/tidak): true bila ayah sudah wafat. Contoh: `false`
- `second.father_name` (teks): Nama ayah. Bisa kosong. Contoh: `Hadi Santoso`
- `second.full_name` (teks): Nama lengkap. Wajib. Contoh: `Budi Santoso`
- `second.guardian` (teks): Nama wali. Bisa kosong.
- `second.instagram` (teks): Username Instagram tanpa @. Bisa kosong. Contoh: `budi`
- `second.mother_deceased` (ya/tidak): true bila ibu sudah wafat. Contoh: `false`
- `second.mother_name` (teks): Nama ibu. Bisa kosong. Contoh: `Ratna Dewi`
- `second.nickname` (teks): Nama panggilan. Wajib. Contoh: `Budi`
- `second.parents_line` (teks): Baris orang tua yang sudah jadi: "Bapak X & Ibu Y", dengan (Alm.)/(Almh.) bila wafat. Kosong bila nama orang tua kosong. Contoh: `Bapak Hadi Santoso & Ibu Ratna Dewi`
- `second.photo_url` (teks): Foto mempelai (https). Null bila tidak ada; sediakan tampilan tanpa foto. Contoh: `https://freeundangan.id/static/demo/mempelai-pria.webp`
- `slug` (teks): Alamat undangan, bagian akhir tautan (rani-budi). Contoh: `contoh`
- `video_embed_url` (teks): URL embed YouTube (https://www.youtube.com/embed/...) untuk <iframe>. Null bila video bukan YouTube. Contoh: `https://www.youtube.com/embed/dQw4w9WgXcQ`
- `video_url` (teks): Tautan video asli dari pasangan. Bisa kosong; cek has_video. Contoh: `https://www.youtube.com/watch?v=dQw4w9WgXcQ`

## Partial SDK

Partial menyisipkan markup interaktif yang dihidupkan SDK. Partial tidak membawa CSS; semua tampilan diatur template-mu. Partial lain selain delapan ini ditolak, dan setiap partial boleh dipakai lebih dari sekali.

### `{{> open_button}}`

Tanpa `data-state`. Setelah diklik, `<html>` mendapat class `fu-opened`.

```html
<button type="button" class="fu-open">Buka Undangan</button>
```

### `{{> guest_name}}`

`data-state`: `pending`, `resolved`, `fallback`

```html
<span class="fu-guest-name" data-state="pending" aria-busy="true">{{guest_fallback_text}}</span>
```

### `{{> rsvp_form}}`

`data-state`: `no-token`, `closed`, `idle`, `sending`, `done`, `error`

```html
<form class="fu-rsvp" data-state="idle" data-closes-at="{{countdown_target}}">
  <p class="fu-rsvp__no-token" hidden>Konfirmasi kehadiran hanya untuk tamu undangan.</p>
  <p class="fu-rsvp__closed" hidden>Konfirmasi kehadiran sudah ditutup.</p>
  <fieldset class="fu-rsvp__attending">
    <legend>Kehadiran</legend>
    <label><input type="radio" name="attending" value="true" required> Hadir</label>
    <label><input type="radio" name="attending" value="false"> Tidak hadir</label>
  </fieldset>
  <label class="fu-rsvp__party-size">Jumlah orang <input type="number" name="party_size" min="1" max="1" value="1" inputmode="numeric"></label>
  <label class="fu-rsvp__message">Ucapan (opsional) <textarea name="message" maxlength="500"></textarea></label>
  <button type="submit" class="fu-rsvp__submit">Kirim</button>
  <p class="fu-rsvp__done" role="status" hidden>Terima kasih, konfirmasi tersimpan.</p>
  <p class="fu-rsvp__error" role="alert" hidden>Gagal mengirim, coba lagi.</p>
</form>
```

### `{{> guestbook}}`

`data-state`: `loading`, `ready`, `empty`, `error`

```html
<section class="fu-guestbook" data-state="loading" aria-busy="true">
  <form class="fu-guestbook__form">
    <label class="fu-guestbook__name">Nama <input name="name" maxlength="60" required autocomplete="name"></label>
    <label class="fu-guestbook__message">Ucapan <textarea name="message" maxlength="500" required></textarea></label>
    <button type="submit" class="fu-guestbook__submit">Kirim</button>
    <p class="fu-guestbook__sent" role="status" hidden>Ucapan terkirim.</p>
    <p class="fu-guestbook__error" role="alert" hidden>Gagal mengirim, coba lagi.</p>
  </form>
  <ul class="fu-guestbook__list" aria-label="Ucapan"></ul>
  <template class="fu-guestbook__item-template">
    <li class="fu-guestbook__item">
      <strong class="fu-guestbook__item-name"></strong>
      <time class="fu-guestbook__item-time"></time>
      <p class="fu-guestbook__item-message"></p>
    </li>
  </template>
  <button type="button" class="fu-guestbook__more" hidden>Muat lagi</button>
  <p class="fu-guestbook__empty" hidden>Belum ada ucapan. Jadilah yang pertama.</p>
</section>
```

### `{{> countdown}}`

`data-state`: `running`, `past`

```html
{{#countdown_target}}<div class="fu-countdown" data-target="{{countdown_target}}" data-state="running" role="timer" aria-label="Menuju {{countdown_label}}">
  <span class="fu-countdown__unit"><span class="fu-countdown__days">0</span> <span class="fu-countdown__label">Hari</span></span>
  <span class="fu-countdown__unit"><span class="fu-countdown__hours">0</span> <span class="fu-countdown__label">Jam</span></span>
  <span class="fu-countdown__unit"><span class="fu-countdown__minutes">0</span> <span class="fu-countdown__label">Menit</span></span>
  <span class="fu-countdown__unit"><span class="fu-countdown__seconds">0</span> <span class="fu-countdown__label">Detik</span></span>
</div>{{/countdown_target}}
```

### `{{> music_player}}`

`data-state`: `paused`, `playing`

```html
{{#has_music}}<button type="button" class="fu-music" data-state="paused" aria-label="Musik latar" aria-pressed="false">
  <span class="fu-music__play" aria-hidden="true"></span>
  <span class="fu-music__pause" aria-hidden="true"></span>
  <audio class="fu-music__audio" src="{{music_url}}" loop preload="none"></audio>
</button>{{/has_music}}
```

### `{{> copy_button}}`

`data-state`: `idle`, `copied`

```html
<button type="button" class="fu-copy" data-copy="{{account_number}}" data-state="idle">
  Salin <span class="fu-copy__done" role="status" hidden>Tersalin</span>
</button>
```

### `{{> arrival_qr}}`

`data-state`: `hidden`, `ready`

```html
<div class="fu-arrival" data-state="hidden" hidden>
  <p class="fu-arrival__title">Kode Kedatangan</p>
  <div class="fu-arrival__qr" role="img" aria-label="QR Kode Kedatangan"></div>
  <p class="fu-arrival__name"></p>
  <p class="fu-arrival__hint">Tunjukkan QR ini ke penerima tamu saat datang.</p>
</div>
```

## Menata status

Status partial ada di atribut `data-state` pada elemen root. Pesan per status disembunyikan SDK dengan atribut `hidden`, sehingga template yang belum menata status tetap tampil benar. Template yang memakai partial tanpa selector `.fu-` atau `[data-state` di CSS mendapat peringatan saat diunggah.

```css
/* Layar pembuka hilang setelah tombol buka diklik */
html.fu-opened .cover { display: none; }

/* Status di elemen root */
.fu-rsvp[data-state="sending"] .fu-rsvp__submit { opacity: .6; }
.fu-copy[data-state="copied"] { background: seagreen; color: white; }
.fu-music[data-state="playing"] .fu-music__play { display: none; }

/* Pesan yang disembunyikan SDK */
[hidden] { display: none !important; }

/* Atau beri animasi masuk dengan menimpa [hidden] */
.fu-rsvp__done { transition: opacity .3s; }
.fu-rsvp__done[hidden] { display: block; opacity: 0; }
```

Pratinjau template bisa memaksa status `no-token`, `error`, `empty`, dan `closed` agar setiap tampilan bisa dicek tanpa data sungguhan.

## Undangan Private

Pasangan bisa menjadikan undangannya Undangan Private. Template tidak perlu diubah. Bedanya hanya pada cara halaman sampai ke tamu:

- Orang yang bukan tamu, atau membuka dari perangkat lain, melihat layar kunci platform yang berisi nama Mempelai. Isi template tidak tampil sama sekali.
- Layar kunci meminjam tampilan template: tautan Google Fonts, semua variabel di `:root`, serta `background`, `color`, dan `font` dari aturan `html` dan `body`. Judulnya memakai `--display`, atau `--script` kalau `--display` tidak ada. Jadi tulis warna dan huruf utama di aturan itu, bukan hanya di elemen sampul. Deklarasi yang memakai `url(` tidak ikut dipinjam.
- Di perangkat tamu, layar kunci diganti seluruhnya oleh HTML template dengan `document.write`. Skrip template tetap berjalan seperti biasa, dan layar pembuka beserta tombol `.fu-open` tetap tampil.
- SDK otomatis mengirim tanda perangkat tamu ke API, jadi partial RSVP, Buku Tamu, dan nama tamu tetap bekerja tanpa kode tambahan.
- Muat data hanya lewat field dan partial. Permintaan ke API platform yang dibuat sendiri, di luar SDK, akan ditolak pada Undangan Private.

## Field custom

Butuh isian yang tidak ada di daftar field baku? Tulis nama satu kata yang belum dipakai, misalnya `{{motto}}` atau `{{custom.motto}}`. Saat diunggah, validator mencatatnya sebagai field custom dan kamu diminta mendeklarasikannya: label yang dilihat pasangan, jenis isian (`text`, `textarea`, `image`, atau `url`), dan nilai bawaan.

- Nama yang sama dengan field baku ditolak.
- Nama yang mirip field baku, misalnya `hastag`, mendapat peringatan salah ketik.
- Path bertitik yang tidak dikenal, misalnya `bride.nam`, ditolak; bila mirip nama yang benar, pesan galatnya menyertakan saran.
- Pasangan mengisi field custom di bagian Custom pada editor. Selama kosong, nilai bawaan yang dipakai.

## Gambar dan aset

Atribut `src`, `href`, `srcset`, dan `poster` pada `img`, `source`, `video`, `audio`, `link`, `script`, dan `iframe`, juga `action` pada `form`, hanya boleh mengarah ke alamat berikut:

- `https://mengundang.digital`
- `https://fonts.googleapis.com`
- `https://fonts.gstatic.com`
- `https://cdnjs.cloudflare.com`
- `https://cdn.jsdelivr.net`
- `https://unpkg.com`
- `https://www.youtube.com`
- `https://www.google.com/maps`

- **Gambar dekorasi** (ornamen, tekstur, bingkai): unggah di halaman template-mu, bagian "Gambar untuk template", lalu tempel URL hasilnya. JPG, PNG, atau WebP, maksimal 8 MB; gambar lebih lebar dari 1600 px diperkecil otomatis. URL-nya berada di alamat pertama di atas.
- **Gambar kecil** hingga 100 KB boleh ditanam sebagai URI `data:`, tetapi hanya di `<img>`.
- **Foto pasangan, sampul, galeri, dan musik** tidak perlu diunggah: semuanya diisi dari data undangan, misalnya `src="{{bride.photo_url}}"`. Nilai yang berisi placeholder dilewati saat diunggah.
- **Huruf dan library**: Google Fonts, cdnjs, jsDelivr, atau unpkg.
- Alamat relatif seperti `ornamen.png` selalu ditolak. `url()` di dalam CSS tidak diperiksa validator, tetapi tetap pakai alamat di atas.

## Panduan desain

- **Rancang untuk HP lebih dulu.** Hampir semua tamu membuka undangan dari WhatsApp di HP. Pastikan rapi di lebar 360 px, lalu lebarkan untuk layar besar.
- **Viewport.** `<meta name="viewport">` ditambahkan otomatis bila tidak ada, tetapi tulis sendiri agar file yang dibuka langsung di browser tampil sama.
- **Layar pembuka.** Pakai `{{> open_button}}`. Setelah diklik, `<html>` mendapat class `fu-opened`; sembunyikan atau animasikan layar pembuka dengan CSS.
- **Data bisa kosong.** Bungkus bagian opsional dengan section-nya (`{{#akad}}`, `{{#has_gallery}}`) dan sediakan tampilan tanpa foto. Cek pratinjau dengan data contoh lengkap dan minim (tambahkan `?data=minimal` pada alamat pratinjau).
- **Teks bisa panjang.** Nama lengkap, alamat, dan ucapan panjangnya beragam. Hindari tinggi tetap pada kotak teks.
- **Ringan.** Tamu sering memakai data seluler. Pakai WebP, ukuran gambar secukupnya, dan sedikit mungkin skrip.
- **Gerak secukupnya.** Hormati `prefers-reduced-motion` untuk animasi.
- **Tanpa merek platform.** Jangan menulis kredit, logo, atau tautan freeundangan di template; undangan sepenuhnya milik pasangan.

## Ditolak atau peringatan

**Ditolak**, unggahan gagal sampai diperbaiki:

- Tidak berisi `<html` dan `</body>`, bukan UTF-8, atau lebih dari 2 MB.
- Tag Mustache tidak seimbang, misalnya section yang tidak ditutup.
- Path bertitik yang tidak dikenal, misalnya `{{bride.nam}}`.
- Section pada field teks, atau list/objek yang dicetak langsung.
- `{{.}}` di luar section list berisi teks.
- Partial selain delapan partial di atas.
- Field custom yang namanya sama dengan field baku.
- Aset dengan alamat relatif, dari domain di luar daftar, `data:` selain di `<img>`, atau `data:` lebih dari 100 KB. `<base href>` dan `<meta http-equiv="refresh">` ikut diperiksa.

**Peringatan**, tetap bisa diunggah:

- Nama field custom yang mirip field baku, mungkin salah ketik.
- Partial dipakai, tetapi CSS tidak punya selector `.fu-` atau `[data-state`.

Nama satu kata yang tidak dikenal tidak ditolak: nama itu dicatat sebagai field custom dan diminta dideklarasikan saat diunggah.

## Cek otomatis

Validator yang sama dengan saat unggah bisa dipanggil tanpa masuk akun. Cocok untuk asisten AI yang membuatkan template: kirim isi file HTML, terima hasil dalam JSON.

```sh
curl -s -X POST -H 'Content-Type: text/html' \
  --data-binary @template.html \
  https://freeundangan.id/api/v1/templates/validate
```

```json
{
  "ok": false,
  "violations": [{ "line": 12, "message": "Baris 12: field {{bride.nam}} tidak dikenal." }],
  "warnings": [],
  "custom_fields": [],
  "partials_used": []
}
```

- `ok: false` berarti unggahan akan ditolak; perbaiki semua `violations`.
- `warnings` tidak menghalangi unggahan.
- `custom_fields` adalah nama yang nanti diminta dideklarasikan.
- Maksimal 2 MB per permintaan dan 30 permintaan per menit per alamat IP.