Ditulis oleh Tim Unloyd

Membuat Callout Keren di Astro dengan Satteri

7 menit baca

Panduan santai membuat custom callout di Astro pakai Satteri. Tukar blockquote biasa jadi kotak info yang cantik dengan satu baris kode.

Visual cover ilustrasi cara membuat Callout kustom MDX Components

Pernah nggak sih kamu baca dokumentasi terus nemuin kotak-kotak warna-warni kayak gini?

Ini catatan penting.

Ini tips berguna.

Keren, kan? Itu namanya callout. Fungsinya buat nyorot informasi penting biar pembaca nggak kelewat. Dan kabar baiknya: kita bisa bikin sendiri di Astro!

Di artikel ini, aku bakal tunjukin cara bikin callout custom pakai Satteri — sebuah pustaka kecil tapi sakti yang bisa ngubah Markdown jadi apapun yang kita mau. Hasilnya? Kamu tinggal nulis > [!TIP] di konten, dan voila! callout cantik muncul otomatis.


Kenapa Satteri?

Sebenernya ada dua cara bikin callout di Astro:

  1. Pakai komponen MDX — kamu harus import komponen di setiap file. Ribet.
  2. Pakai plugin Markdown — kamu tulis > [!TIP], plugin ubah jadi HTML. Otomatis dan bersih.

Aku milih cara kedua. Dan Satteri adalah alat yang tepat karena:

  • Ringan — nggak nambah berat bundel.
  • Fleksibel — bisa ubah AST Markdown sesuka hati.
  • Mudah — cuma butuh beberapa baris kode.

Bayangin: kamu lagi nulis artikel, pengen kasih tips. Daripada buka komponen, import, dan nulis JSX, kamu cukup ketik > [!TIP] dan lanjut nulis. Gampang, kan?


  1. Langkah 1: Siapin Bahan

    Pertama, install Satteri di proyek Astro-mu:

    Plugin Satteri ini bekerja untuk file .md dan .mdx karena MDX juga menggunakan remark di balik layar.

  2. Langkah 2: Buat Plugin Satteri

    Buat file src/lib/mdx/satteri-callout.ts. Ini adalah “otak” dari callout kita.

    Kode ini mungkin keliatan panjang, tapi sebenarnya simpel: kita tangkap blockquote yang diawali > [!TYPE], lalu ubah jadi <blockquote data-callout="type">.

    import { defineMdastPlugin } from "satteri";// Daftar tipe callout yang didukungconst CALLOUT_TYPES = {  NOTE: "note",  TIP: "tip",  IMPORTANT: "important",  WARNING: "warning",  CAUTION: "caution",  DANGER: "danger"} as const;// Pola regex buat nangkep [!TYPE]const CALLOUT_PATTERN = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION|DANGER)\]\s*/i;export const satteriCallout = defineMdastPlugin({  name: "satteri-callout",  blockquote(node, ctx) {      // Ambil paragraf pertama dalam blockquote      const firstChild = node.children[0];      if (!firstChild || firstChild.type !== "paragraph") return;      const firstText = firstChild.children[0];      if (!firstText || firstText.type !== "text") return;      // Cek apakah teksnya diawali pola callout      const match = firstText.value.match(CALLOUT_PATTERN);      if (!match) return;      // Ambil tipe callout dari hasil regex      const rawType = match[1].toUpperCase() as keyof typeof CALLOUT_TYPES;      const type = CALLOUT_TYPES[rawType];      // Hapus teks "[!TYPE]" dari awal      const remainingText = firstText.value.slice(match[0].length);      // Update paragraf pertama (tanpa prefix)      const newChildren = [...firstChild.children];      if (remainingText) {      newChildren[0] = {           ...firstText,           value: remainingText       };      } else {          newChildren.shift();      }      const newParagraph = {           ...firstChild,           children: newChildren       };      // Ganti blockquote dengan versi baru yang punya atribut data-callout      const newNode = {          ...node,          children: [newParagraph, ...node.children.slice(1)],          data: {              ...node.data,              hProperties: {                  ...node.data?.hProperties,                  "data-callout": type,              },          },      };      ctx.replaceNode(node, newNode);  },});

    Intinya plugin ini ngubah Markdown > [!TIP] jadi HTML <blockquote data-callout="tip">. Sisanya urusan CSS.

  3. Langkah 3: Daftarin Plugin ke Astro

    Di astro.config.mjs, kita daftarin plugin ke remarkPlugins. Karena Satteri adalah plugin mdast (bukan remark), kita perlu bungkus dikit:

    // astro.config.mjsimport { defineConfig } from "astro/config";import { satteri } from "@astrojs/markdown-satteri";import { satteriCallout } from "./src/lib/mdx/satteri-callout";export default defineConfig({  markdown: {      processor: satteri({      // ... fitur lainnya      mdastPlugins: [          // ... plugin lain          satteriCallout, // 🔥 ini dia!      ],      }),      // ...  },  });

    Sekarang semua file .md dan .mdx akan diproses oleh plugin callout. Kamu nggak perlu mikirin lagi.

  4. Langkah 4: Kasih Baju (CSS)

    Biar callout-nya cantik, kita perlu styling. Buat file src/styles/prose/callout.css dan isi dengan kode di bawah ini:

    /* src/styles/prose/callout.css */:root {  --callout-icon-note: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJjdXJyZW50Q29sb3IiIGNsYXNzPSJpY29uIGljb24tdGFibGVyIGljb25zLXRhYmxlci1maWxsZWQgaWNvbi10YWJsZXItaW5mby1jaXJjbGUiPjxwYXRoIHN0cm9rZT0ibm9uZSIgZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIgLz48cGF0aCBkPSJNMTIgMmM1LjUyMyAwIDEwIDQuNDc3IDEwIDEwYTEwIDEwIDAgMCAxIC0xOS45OTUgLjMyNGwtLjAwNSAtLjMyNGwuMDA0IC0uMjhjLjE0OCAtNS4zOTMgNC41NjYgLTkuNzIgOS45OTYgLTkuNzJ6bTAgOWgtMWwtLjExNyAuMDA3YTEgMSAwIDAgMCAwIDEuOTg2bC4xMTcgLjAwN3YzbC4wMDcgLjExN2ExIDEgMCAwIDAgLjg3NiAuODc2bC4xMTcgLjAwN2gxbC4xMTcgLS4wMDdhMSAxIDAgMCAwIC44NzYgLS44NzZsLjAwNyAtLjExN2wtLjAwNyAtLjExN2ExIDEgMCAwIDAgLS43NjQgLS44NTdsLS4xMTIgLS4wMmwtLjExNyAtLjAwNnYtM2wtLjAwNyAtLjExN2ExIDEgMCAwIDAgLS44NzYgLS44NzZsLS4xMTcgLS4wMDd6bS4wMSAtM2wtLjEyNyAuMDA3YTEgMSAwIDAgMCAwIDEuOTg2bC4xMTcgLjAwN2wuMTI3IC0uMDA3YTEgMSAwIDAgMCAwIC0xLjk4NmwtLjExNyAtLjAwN3oiIC8+PC9zdmc+");  --callout-icon-tip: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJjdXJyZW50Q29sb3IiIGNsYXNzPSJpY29uIGljb24tdGFibGVyIGljb25zLXRhYmxlci1maWxsZWQgaWNvbi10YWJsZXItZmxhbWUiPjxwYXRoIHN0cm9rZT0ibm9uZSIgZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIgLz48cGF0aCBkPSJNMTAgMmMwIC0uODggMS4wNTYgLTEuMzMxIDEuNjkyIC0uNzIyYzEuOTU4IDEuODc2IDMuMDk2IDUuOTk1IDEuNzUgOS4xMmwtLjA4IC4xNzRsLjAxMiAuMDAzYy42MjUgLjEzMyAxLjIwMyAtLjQzIDIuMzAzIC0yLjE3M2wuMTQgLS4yMjRhMSAxIDAgMCAxIDEuNTgyIC0uMTUzYzEuMzM0IDEuNDM1IDIuNjAxIDQuMzc3IDIuNjAxIDYuMjdjMCA0LjI2NSAtMy41OTEgNy43MDUgLTggNy43MDVzLTggLTMuNDQgLTggLTcuNzA2YzAgLTIuMjUyIDEuMDIyIC00LjcxNiAyLjYzMiAtNi4zMDFsLjYwNSAtLjU4OWMuMjQxIC0uMjM2IC40MzQgLS40MyAuNjE4IC0uNjI0YzEuNDMgLTEuNTEyIDIuMTQ1IC0yLjkyNCAyLjE0NSAtNC43OCIgLz48L3N2Zz4=");  --callout-icon-important: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJjdXJyZW50Q29sb3IiIGNsYXNzPSJpY29uIGljb24tdGFibGVyIGljb25zLXRhYmxlci1maWxsZWQgaWNvbi10YWJsZXItbGFiZWwtaW1wb3J0YW50Ij48cGF0aCBzdHJva2U9Im5vbmUiIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiIC8+PHBhdGggZD0iTTE2LjUyIDZhMiAyIDAgMCAxIDEuNTYxIC43NWwzLjcgNC42MjVhMSAxIDAgMCAxIDAgMS4yNWwtMy43IDQuNjI0YTIgMiAwIDAgMSAtMS41NjEgLjc1MWgtMTIuNTJhMSAxIDAgMCAxIC0uNzggLTEuNjI1bDMuNSAtNC4zNzVsLTMuNSAtNC4zNzVhMSAxIDAgMCAxIC42NjggLTEuNjJsLjExMiAtLjAwNXoiIC8+PC9zdmc+");  --callout-icon-warning: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJjdXJyZW50Q29sb3IiIGNsYXNzPSJpY29uIGljb24tdGFibGVyIGljb25zLXRhYmxlci1maWxsZWQgaWNvbi10YWJsZXItYWxlcnQtdHJpYW5nbGUiPjxwYXRoIHN0cm9rZT0ibm9uZSIgZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIgLz48cGF0aCBkPSJNMTIgMS42N2MuOTU1IDAgMS44NDUgLjQ2NyAyLjM5IDEuMjQ3bC4xMDUgLjE2bDguMTE0IDEzLjU0OGEyLjkxNCAyLjkxNCAwIDAgMSAtMi4zMDcgNC4zNjNsLS4xOTUgLjAwOGgtMTYuMjI1YTIuOTE0IDIuOTE0IDAgMCAxIC0yLjU4MiAtNC4ybC4wOTkgLS4xODVsOC4xMSAtMTMuNTM4YTIuOTE0IDIuOTE0IDAgMCAxIDIuNDkxIC0xLjQwM3ptLjAxIDEzLjMzbC0uMTI3IC4wMDdhMSAxIDAgMCAwIDAgMS45ODZsLjExNyAuMDA3bC4xMjcgLS4wMDdhMSAxIDAgMCAwIDAgLTEuOTg2bC0uMTE3IC0uMDA3em0tLjAxIC03YTEgMSAwIDAgMCAtLjk5MyAuODgzbC0uMDA3IC4xMTd2NGwuMDA3IC4xMTdhMSAxIDAgMCAwIDEuOTg2IDBsLjAwNyAtLjExN3YtNGwtLjAwNyAtLjExN2ExIDEgMCAwIDAgLS45OTMgLS44ODN6IiAvPjwvc3ZnPg==");  --callout-icon-caution: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJjdXJyZW50Q29sb3IiIGNsYXNzPSJpY29uIGljb24tdGFibGVyIGljb25zLXRhYmxlci1maWxsZWQgaWNvbi10YWJsZXItZmxhZyI+PHBhdGggc3Ryb2tlPSJub25lIiBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIiAvPjxwYXRoIGQ9Ik00IDVhMSAxIDAgMCAxIC4zIC0uNzE0YTYgNiAwIDAgMSA4LjIxMyAtLjE3NmwuMzUxIC4zMjhhNCA0IDAgMCAwIDUuMjcyIDBsLjI0OSAtLjIyN2MuNjEgLS40ODMgMS41MjcgLS4wOTcgMS42MSAuNjc2bC4wMDUgLjExM3Y5YTEgMSAwIDAgMSAtLjMgLjcxNGE2IDYgMCAwIDEgLTguMjEzIC4xNzZsLS4zNTEgLS4zMjhhNCA0IDAgMCAwIC01LjEzNiAtLjExNHY2LjU1MmExIDEgMCAwIDEgLTEuOTkzIC4xMTdsLS4wMDcgLS4xMTd2LTE2eiIgLz48L3N2Zz4=");  --callout-icon-danger: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJjdXJyZW50Q29sb3IiIGNsYXNzPSJpY29uIGljb24tdGFibGVyIGljb25zLXRhYmxlci1maWxsZWQgaWNvbi10YWJsZXItYWxlcnQtY2lyY2xlIj48cGF0aCBzdHJva2U9Im5vbmUiIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiIC8+PHBhdGggZD0iTTEyIDJjNS41MjMgMCAxMCA0LjQ3NyAxMCAxMGExMCAxMCAwIDAgMSAtMTkuOTk1IC4zMjRsLS4wMDUgLS4zMjRsLjAwNCAtLjI4Yy4xNDggLTUuMzkzIDQuNTY2IC05LjcyIDkuOTk2IC05Ljcyem0uMDEgMTNsLS4xMjcgLjAwN2ExIDEgMCAwIDAgMCAxLjk4NmwuMTE3IC4wMDdsLjEyNyAtLjAwN2ExIDEgMCAwIDAgMCAtMS45ODZsLS4xMTcgLS4wMDd6bS0uMDEgLThhMSAxIDAgMCAwIC0uOTkzIC44ODNsLS4wMDcgLjExN3Y0bC4wMDcgLjExN2ExIDEgMCAwIDAgMS45ODYgMGwuMDA3IC0uMTE3di00bC0uMDA3IC0uMTE3YTEgMSAwIDAgMCAtLjk5MyAtLjg4M3oiIC8+PC9zdmc+");}.prose blockquote[data-callout] {  @apply text-body-14;  --callout-color: var(--fg-muted);  display: block;  box-sizing: border-box;  position: relative;  margin: 1.75rem 0;  padding: 0.5rem 1rem 0.5rem 3.5rem;  background: var(--bg-subtle);  border: 1px solid rgb(from var(--border) r g b / 0.4);  border-radius: var(--radius-md);  color: var(--fg);}.prose blockquote[data-callout]::before {  content: "";  position: absolute;  inset-block: 0.5rem;  inset-inline-start: 0.5rem;  width: 3px;  background-color: var(--callout-color) !important;  border-radius: var(--radius-full);}.prose blockquote[data-callout]::after {  content: "";  position: absolute;  inset-block-start: 0.55rem;  inset-inline-start: 1.5rem;  width: 1.25rem;  height: 1.25rem;  background-color: var(--callout-color) !important;  mask: var(--callout-icon) center / contain no-repeat;  -webkit-mask: var(--callout-icon) center / contain no-repeat;  pointer-events: none;}.prose blockquote[data-callout="note"] {  --callout-color: var(--fg-muted);  --callout-icon: var(--callout-icon-note);}.prose blockquote[data-callout="tip"] {  --callout-color: var(--accent);  --callout-icon: var(--callout-icon-tip);}.prose blockquote[data-callout="important"] {  --callout-color: #8b5cf6;  --callout-icon: var(--callout-icon-important);}.prose blockquote[data-callout="warning"] {  --callout-color: var(--color-warning);  --callout-icon: var(--callout-icon-warning);}.prose blockquote[data-callout="caution"] {  --callout-color: #f97316;  --callout-icon: var(--callout-icon-caution);}.prose blockquote[data-callout="danger"] {  --callout-color: var(--color-error);  --callout-icon: var(--callout-icon-danger);}.prose blockquote[data-callout] *:first-child {  margin-top: 0 !important;}.prose blockquote[data-callout] *:last-child {  margin-bottom: 0 !important;}.prose blockquote[data-callout] > * + * {  margin-block-start: 0.75rem;}.prose blockquote[data-callout] ul,.prose blockquote[data-callout] ol {  padding-inline-start: 1.5rem;  margin-block: 0.5rem 0; }.prose blockquote[data-callout] li {  margin-block-start: 0.375rem;}.prose blockquote[data-callout] li::marker {  color: var(--callout-color);}.prose blockquote[data-callout] a {  color: var(--fg-strong);  font-weight: 500;  text-decoration: underline;  text-decoration-color: var(--callout-color);  text-underline-offset: 0.2em;  text-decoration-thickness: 1.5px;}.prose blockquote[data-callout] :not(pre) > code {  font-size: 0.875em;  color: var(--fg-strong);  background: color-mix(in srgb, var(--callout-color) 10%, var(--bg-subtle));  border: 1px solid color-mix(in srgb, var(--callout-color) 15%, transparent);  padding: 0.15em 0.4em;  border-radius: 0.25rem;  font-weight: 500;}

    Setelah file CSS dibuat, jangan lupa import di layout utama:

    /* src/styles/global.css */@import './prose/callout.css';

    Atau langsung di layout:

    ---// src/layouts/Layout.astroimport '../styles/prose/callout.css';---

    CSS ini akan ngasih:

    • Garis vertikal 3px di kiri (kayak tongkat)
    • Ikon sesuai tipe (note, tip, warning, dll)
    • Background soft yang nyaman di mata
    • Border dan radius yang elegan

    Hasilnya? Callout yang kelihatan profesional tanpa ribet.

  5. Langkah 5: Tulis Konten dengan Callout

    Sekarang bagian paling seru: nulis konten. Kamu tinggal pakai format ini:

    > [!NOTE]> Ini adalah catatan penting buat pembaca.> [!TIP]> Tips: Pakai keyboard shortcut `Ctrl+S` buat save.> [!IMPORTANT]> Jangan lupa backup data sebelum update!> [!WARNING]> Hati-hati, fitur ini masih eksperimental.> [!CAUTION]> Perubahan ini akan memengaruhi semua pengguna.> [!DANGER]> Ini berbahaya! Jangan coba di production.

    Plugin akan mengubahnya jadi:

    <blockquote data-callout="tip">  <p>Tips: Pakai keyboard shortcut `Ctrl+S` buat save.</p></blockquote>

    Dan CSS akan mengubahnya jadi kotak cantik dengan ikon dan warna sesuai tipe.

  6. Langkah 6: Test

    Buat file src/pages/test-callout.md:

    ---title: "Test Callout"---> [!NOTE]> Catatan: Jangan lupa backup data.> [!TIP]> Tips: Pakai keyboard shortcut `Ctrl+S` buat save.

    Jalankan npm run dev, buka http://localhost:4321/test-callout, dan lihat hasilnya. Kalau muncul kotak-kotak cantik, berarti berhasil! 🎉

Alternatif: Pakai Komponen MDX (Manual)

Kalau kamu pengen lebih fleksibel, bisa juga bikin komponen Astro dan pake di file .mdx:

---// src/components/Callout.astroconst { type = "note" } = Astro.props;---<blockquote data-callout={type}>  <slot /></blockquote>

Trus di file .mdx:

import Callout from '../components/Callout.astro';<Callout type="warning">Ini peringatan dari komponen.</Callout>

Tapi jujur, cara plugin lebih praktis. Nggak perlu import tiap kali, nggak perlu mikir JSX, tinggal tulis > [!TIP] dan lanjut nulis.

Penutup

Dengan sedikit kode dan konfigurasi, kita berhasil bikin callout custom di Astro. Hasilnya: pengalaman nulis konten jadi lebih enak, pembaca lebih paham, dan website jadi lebih profesional.

Ini adalah contoh kecil bagaimana ngerti AST (Abstract Syntax Tree) bisa membuka banyak kemungkinan. Tapi yang paling penting: kita nggak perlu jadi ahli dulu buat mulai. Cukup ikuti langkah-langkah di atas, dan callout-mu siap dipakai.