Membuat Callout Keren di Astro dengan Satteri
Panduan santai bikin callout custom di Astro pakai Satteri. Ubah blockquote biasa jadi kotak info cantik tanpa ribet.
Pernah nggak kamu baca dokumentasi atau blog terus nemu kotak warna-warni kayak gini?
Itu namanya callout. Fungsinya buat nyorot informasi penting biar pembaca nggak kelewat. Dan kabar baiknya: kita bisa bikin sendiri di Astro!
Di artikel ini, kita bakal bikin callout custom pakai Satteri — sebuah pustaka kecil tapi powerful yang bisa ngubah Markdown jadi HTML sesuai keinginan kita. Hasilnya? Kamu tinggal nulis > [!TIP] di konten, dan voila! callout cantik muncul otomatis.
Tenang, kita bahas dari nol, perlahan. Yuk mulai!
Kenapa Harus Satteri?
Sebenernya ada dua cara umum bikin callout di Astro:
- Pakai komponen MDX — kamu harus import komponen di setiap file
.mdx. Ribet dan banyak kode. - Pakai plugin Markdown — kamu tulis
> [!TIP], plugin ubah jadi HTML. Otomatis, bersih, dan nggak repot.
Kita pilih cara kedua. Dan Satteri adalah alat yang tepat karena:
- Ringan — nggak nambah berat bundel website.
- Fleksibel — bisa ubah Markdown jadi apapun.
- Mudah — cuma butuh beberapa baris kode.
Bayangin: lagi asyik nulis artikel, pengen kasih tips. Daripada buka file baru, import komponen, dan nulis JSX, kamu cukup ketik > [!TIP] dan lanjut nulis. Gampang banget, kan?
-
Langkah 1: Pasang Satteri
Pertama, install Satteri di proyek Astro-mu. Pilih salah satu yang sesuai dengan package manager favoritmu:
npm install @astrojs/markdown-satteripnpm add @astrojs/markdown-satteriyarn add @astrojs/markdown-satteribun add @astrojs/markdown-satteri -
Langkah 2: Buat Plugin Callout
Buat file
src/lib/mdx/satteri-callout.ts. File ini bakal jadi “otak” dari callout kita. Jangan takut sama kodenya—kita bedah pelan-pelan.import { defineMdastPlugin } from "satteri";const CALLOUT_TYPES = { NOTE: "note", TIP: "tip", IMPORTANT: "important", WARNING: "warning", CAUTION: "caution", DANGER: "danger"} as const;const CALLOUT_PATTERN = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION|DANGER)\]\s*/i;export const satteriCallout = defineMdastPlugin({ name: "satteri-callout", blockquote(node, ctx) { const firstChild = node.children[0]; if (!firstChild || firstChild.type !== "paragraph") return; const firstText = firstChild.children[0]; if (!firstText || firstText.type !== "text") return; const match = firstText.value.match(CALLOUT_PATTERN); if (!match) return; const rawType = match[1].toUpperCase() as keyof typeof CALLOUT_TYPES; const type = CALLOUT_TYPES[rawType]; const remainingText = firstText.value.slice(match[0].length); const newChildren = [...firstChild.children]; if (remainingText) { newChildren[0] = { ...firstText, value: remainingText }; } else { newChildren.shift(); } const newParagraph = { ...firstChild, children: newChildren }; ctx.setProperty(node, 'children', [ newParagraph, ...node.children.slice(1), ]); const baseData = node.data || {}; ctx.setProperty(node, 'data', { ...baseData, hName: 'aside', hProperties: { ...(baseData.hProperties || {}), className: ['callout'], 'data-callout': type, }, }); },});Apa yang terjadi di sini? Singkatnya:
- Plugin ini “mengintai” setiap blockquote (
>). - Kalau blockquote-nya dimulai dengan
[!NOTE],[!TIP], atau sejenisnya, dia kenali sebagai callout. - Lalu teks
[!NOTE]dibuang, dan blockquote diubah menjadi<aside>dengan atribut data-callout sesuai jenisnya. - Hasil akhirnya:
<aside class="callout" data-callout="note">.
Sisanya tinggal urusan CSS biar cantik.
- Plugin ini “mengintai” setiap blockquote (
-
Langkah 3: Daftarkan Plugin ke Astro
Di
astro.config.mjs, Di sana, kita tambahkan plugin callout ke daftarmdastPlugins:// 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, ], }), // ... }, });Dengan begitu, semua file
.mddan.mdxdi proyekmu bakal diproses sama plugin ini. Kamu nggak perlu mikirin lagi. -
Langkah 4: Kasih Styling Biar Cantik
Callout yang sudah jadi aside perlu dibikinin CSS. Buat file
src/styles/prose/callout.cssdan 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 aside[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 aside[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 aside[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 aside[data-callout="note"] { --callout-color: var(--fg-muted); --callout-icon: var(--callout-icon-note);}.prose aside[data-callout="tip"] { --callout-color: var(--accent); --callout-icon: var(--callout-icon-tip);}.prose aside[data-callout="important"] { --callout-color: #8b5cf6; --callout-icon: var(--callout-icon-important);}.prose aside[data-callout="warning"] { --callout-color: var(--color-warning); --callout-icon: var(--callout-icon-warning);}.prose aside[data-callout="caution"] { --callout-color: #f97316; --callout-icon: var(--callout-icon-caution);}.prose aside[data-callout="danger"] { --callout-color: var(--color-error); --callout-icon: var(--callout-icon-danger);}.prose aside[data-callout] *:first-child { margin-top: 0 !important;}.prose aside[data-callout] *:last-child { margin-bottom: 0 !important;}.prose aside[data-callout] > * + * { margin-block-start: 0.75rem;}.prose aside[data-callout] ul,.prose aside[data-callout] ol { padding-inline-start: 1.5rem; margin-block: 0.5rem 0;}.prose aside[data-callout] li { margin-block-start: 0.375rem;}.prose aside[data-callout] li::marker { color: var(--callout-color);}.prose aside[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 aside[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 itu, impor file CSS tersebut di layout utama atau di file
global.css:/* src/styles/global.css */@import './prose/callout.css';Atau langsung di layout
.astro:---// src/layouts/Layout.astroimport '../styles/prose/callout.css'; ---Penjelasan singkat CSS-nya:
- Garis vertikal 3px di kiri sebagai aksen.
- Ikon yang berubah sesuai tipe callout.
- Background soft dan border tipis biar keliatan elegan.
- Warna icon, teks, dan marker menyesuaikan tipe.
-
Langkah 5: Pakai Callout di Markdown
Sekarang bagian paling seru: menulis 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 HTML seperti ini:
<aside class="callout" data-callout="tip"> <p>Tips: Pakai keyboard shortcut `Ctrl+S` buat save.</p></aside>Dan CSS akan mengubahnya jadi kotak cantik dengan ikon serta warna sesuai tipe.
-
Langkah 6: Coba Langsung
Buat file
src/pages/test-callout.md:---title: "Test Callout"---> [!NOTE]> Catatan: Jangan lupa backup data.> [!TIP]> Tips: Pakai shortcut `Ctrl+S` buat save.Jalankan
npm run dev, lalu bukahttp://localhost:4321/test-callout. Kalau muncul kotak-kotak cantik, berarti berhasil! 🎉
Alternatif Pakai Komponen MDX (Manual)
Kalau kamu mau fleksibilitas lebih tinggi, kamu juga bisa bikin callout sebagai komponen Astro dan pakai di file .mdx:
---// src/components/Callout.astroconst { type = "note" } = Astro.props;---<aside class="callout" data-callout={type}> <slot /></aside>
Kemudian di file .mdx:
import Callout from '../components/Callout.astro';<Callout type="warning">Ini peringatan dari komponen.</Callout>
Tapi jujur, cara plugin lebih praktis. Kamu 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:
Penulisan konten lebih cepat — cukup pakai blockquote biasa.
Tampilan lebih profesional — kotak info dengan ikon dan warna.
Kode tetap rapi — plugin terpisah dari konten.
Ini adalah contoh kecil bagaimana pemahaman tentang AST (Abstract Syntax Tree) bisa membuka banyak kemungkinan di Astro. Tapi yang paling penting: kita nggak perlu jadi ahli dulu buat mulai. Cukup ikuti langkah-langkah di atas, dan callout-mu siap dipakai.
Selamat mencoba! 😊