File konten tidak membutuhkan component tree hanya untuk menyatakan bahwa satu region adalah hero, callout, card, atau gallery. Ekstensi kecil pada Markdown dapat membawa maksud tersebut, sementara heading, paragraph, link, dan list tetap ditulis sebagai Markdown biasa.
Dengan markdown-it-container, sintaks yang dilihat penulis dapat dibuat sesingkat:
:::hero
# Ship the next release
A short description stays normal Markdown.
:::Struktur ini sudah cukup untuk membentuk DSL UI ringan: hero memiliki arti yang ditentukan aplikasi, sedangkan konten di dalamnya tetap diproses oleh parser Markdown. Batas yang berguna justru sempit. Ketika format mulai membuka row, column, nilai padding, class CSS, event handler, dan component nesting yang dalam, file konten tidak lagi terasa seperti konten. Ia berubah menjadi source code dengan notasi lain.
Container menyatakan maksud, bukan implementasi
Bandingkan dua format berikut:
:::hero
# Hello
:::dan:
:::column gap=16 padding=24 align=center
:::text size=48 weight=700
Hello
:::
:::Format pertama menyatakan region tersebut apa. Format kedua menjelaskan bagaimana layout engine harus menyusunnya.
Keduanya merupakan DSL, tetapi ditujukan untuk jenis penulis yang berbeda. Developer mungkin nyaman dengan layout primitive yang bertingkat. Penulis atau editor CMS cenderung lebih konsisten bekerja dengan kosakata pendek seperti hero, note, quote, dan gallery.
Perbedaannya bersifat arsitektural, bukan sekadar kosmetik. Semantic container membiarkan responsive layout, spacing, accessibility, dan keputusan tema berada di renderer. Layout container memindahkan keputusan tersebut ke setiap dokumen.
Karena itu, semantic container dapat bertahan melewati redesign:
:::hero
|
+-- renderer 2026 -> centered heading + image
|
+-- renderer 2027 -> split layout + gradient surfaceMarkdown tidak perlu berubah karena maknanya memang tidak berubah.
markdown-it-container menyediakan batas block
markdown-it mem-parsing Markdown menjadi token lalu meneruskan token tersebut ke renderer. markdown-it-container menambahkan block rule untuk fenced custom container. Container yang terdaftar menghasilkan opening dan closing container token yang mengapit Markdown di dalamnya.
Registrasi minimal dapat ditulis seperti ini:
import MarkdownIt from 'markdown-it';
import container from 'markdown-it-container';
const md = new MarkdownIt();
md.use(container, 'hero', {
render(tokens, idx) {
if (tokens[idx].nesting === 1) {
return '<section class="hero">\n';
}
return '</section>\n';
}
});
const html = md.render(`
:::hero
# Hello
:::
`);Default validator pada plugin container mengenali nama container dari teks setelah fence. Render hook menerima token yang dihasilkan sehingga aplikasi dapat mengganti output container generik dengan HTML semantik.
HTML akhirnya dapat tetap sederhana:
<section class="hero">
<h1>Hello</h1>
</section>Sintaks untuk penulis tetap kecil, sedangkan renderer memegang struktur DOM sebenarnya.
Daftarkan kosakata, bukan nama komponen sembarang
Jika dokumen dapat meminta nama komponen apa pun, format konten akan terikat pada detail implementasi. Registry membuat kosakata yang didukung menjadi eksplisit.
const blocks = {
hero: {
tag: 'section',
className: 'hero'
},
note: {
tag: 'aside',
className: 'note'
},
panel: {
tag: 'section',
className: 'panel'
}
};
for (const [name, definition] of Object.entries(blocks)) {
md.use(container, name, {
render(tokens, idx) {
if (tokens[idx].nesting === 1) {
return `<${definition.tag} class="${definition.className}">\n`;
}
return `</${definition.tag}>\n`;
}
});
}Dokumen sekarang dapat memilih semantic role yang dikenal, tetapi tidak dapat membuat komponen JavaScript secara sembarang.
Pembatasan tersebut berguna karena renderer memperoleh kontrak yang stabil:
penulis Markdown
-> nama block yang diizinkan
-> renderer terdaftar
-> HTML terkontrol
-> CSS/design systemRegistry juga menyediakan satu tempat untuk melakukan deprecation, menambah alias, atau memigrasikan block tanpa mencari component import di seluruh file konten.
Buat atribut tetap sederhana
Sebuah block pada akhirnya memerlukan variasi kecil. Hero mungkin membutuhkan bentuk compact; note mungkin memiliki varian info dan warning. Atribut dapat menangani kebutuhan itu, tetapi menerima input key=value sembarang lalu menyalinnya langsung ke HTML menghasilkan boundary yang buruk.
Lebih aman memakai grammar tetap dan allowlist:
:::hero variant=compact
# Status page
:::Nilainya kemudian divalidasi sebelum dirender:
const heroVariants = new Set(['default', 'compact']);
function parseHeroInfo(info) {
const match = info.trim().match(
/^hero(?:\s+variant=(default|compact))?$/
);
if (!match) {
return { variant: 'default' };
}
return {
variant: heroVariants.has(match[1])
? match[1]
: 'default'
};
}
md.use(container, 'hero', {
validate(params) {
return /^hero(?:\s+variant=(default|compact))?$/.test(
params.trim()
);
},
render(tokens, idx) {
if (tokens[idx].nesting === 1) {
const { variant } = parseHeroInfo(tokens[idx].info);
return `<section class="hero hero--${variant}">\n`;
}
return '</section>\n';
}
});Penulis dapat memilih varian yang memang didukung, tetapi tidak dapat menyisipkan style, onclick, class sembarang, atau URL yang tidak diharapkan ke elemen hasil render.
Untuk DSL yang berorientasi pada konten, pembatasan seperti ini justru bernilai. Format sebaiknya membuka pilihan yang memang didukung design system, bukan seluruh kemampuan browser.
Raw HTML dan custom container adalah dua keputusan trust yang berbeda
Renderer container yang terkontrol tidak otomatis membuat seluruh input Markdown dapat dipercaya. markdown-it dapat dikonfigurasi untuk menerima raw HTML, dan aplikasi mungkin memasang plugin lain yang memperkenalkan URL atau perilaku yang menghasilkan HTML.
Jika penulis tidak sepenuhnya trusted, buat trust boundary secara eksplisit:
const md = new MarkdownIt({
html: false,
linkify: true
});Nilai container tetap perlu divalidasi dan di-escape setiap kali teks yang dikontrol pengguna masuk ke atribut HTML atau raw HTML string. Menonaktifkan raw HTML menutup satu jalur injection langsung; tindakan tersebut tidak menggantikan review terhadap kode custom renderer.
Model mental yang aman adalah:
source Markdown
-> parser rules
-> metadata container tervalidasi
-> token
-> renderer terkontrol
-> HTMLJangan menganggap source file aman hanya karena sintaksnya terlihat lebih sederhana daripada HTML.
Nesting yang dalam mengubah target pengguna
Satu semantic wrapper mudah dipindai:
:::hero
# A clearer deployment status
Current incidents and maintenance windows.
:::Component tree yang ditulis dengan fence adalah persoalan lain:
:::column
:::row
:::card
:::text
Current incidents
:::
:::
:::card
:::text
Maintenance
:::
:::
:::
:::Format kedua meminta penulis melacak opening dan closing boundary, hierarchy, serta semantic layout. Pada titik tersebut, visual block editor atau component language sungguhan sering menjadi authoring surface yang lebih tepat.
Ada batas praktis yang dapat dipakai:
- gunakan Markdown untuk prose;
- gunakan container untuk sedikit semantic region;
- simpan visual layout di CSS dan renderer component;
- pindahkan komposisi kompleks ke structured editor atau component system.
Format tetap mudah dipakai karena penulis hanya perlu mengenali beberapa named fence, bukan menjalankan layout tree di kepala.
Renderer dapat menargetkan komponen tanpa mengeksposnya
Sintaks Markdown tidak harus dipetakan langsung ke HTML akhir. Sintaks dapat dipetakan ke token atau intermediate representation yang kemudian dikonsumsi framework.
Sebagai contoh, parsing layer dapat menormalkan container menjadi:
{
"type": "hero",
"variant": "compact",
"content": [
{
"type": "heading",
"level": 1,
"text": "Status page"
}
]
}React, Vue, Svelte, server-side template, atau static renderer kemudian dapat menentukan bagaimana hero menjadi UI.
Pemisahan ini penting ketika konten yang sama muncul di beberapa konteks. Website dapat merender hero sebagai section besar, pipeline RSS dapat meratakannya menjadi teks biasa, sedangkan search index dapat mengabaikan visual wrapper sepenuhnya.
DSL mendeskripsikan semantik konten. Consumer menentukan presentasinya.
Versioning berlaku pada makna, bukan setiap perubahan visual
Konten dapat hidup jauh lebih lama daripada implementasi CSS. Setelah custom container muncul di ratusan file, namanya menjadi bagian dari content schema.
Mengubah .hero dari flexbox menjadi grid bukan schema change. Mengganti nama hero menjadi masthead, menghapus atribut, atau mengubah arti variant=compact dapat menjadi schema change.
Perlakukan nama container seperti public API kecil:
stable:
hero
note
gallery
supported hero variants:
default
compactKetika sebuah nama harus dihentikan, migration dapat menulis ulang dokumen lama atau registry dapat mendukung kedua nama untuk sementara. Proses ini jauh lebih mudah jika kosakata hanya berisi sepuluh semantic block daripada puluhan low-level layout primitive.
DSL kecil sering lebih kuat
Nilai utama Markdown UI DSL bukan kemampuannya meniru Jetpack Compose. Nilainya justru muncul ketika format tahu kapan harus berhenti.
Untuk halaman yang dominan konten, bentuk ini:
:::hero
# Hello
:::bisa sudah cukup. Penulis menandai sebuah region sebagai hero. Markdown menangani struktur teks. Renderer memilih HTML yang accessible. CSS menangani responsive layout dan visual design. JavaScript ditambahkan hanya ketika block memang membutuhkan behavior.
Pembagian tersebut menjaga dokumen tetap terbaca tanpa renderer, memisahkan keputusan presentasi dari prose, dan membebaskan aplikasi untuk merombak UI di kemudian hari. Sintaks container menjadi tahan lama ketika hanya membawa informasi semantik yang tidak dapat diekspresikan dengan bersih oleh Markdown biasa.