@dr-ishaan/remake-blocks
v3.0.2
Published
Astro 6 + remark plugin — 42 callout types, 4 themes, CSS layers, design tokens, type allow/blocklists, default collapse, multi-syntax, icon sprite, minimal theme CSS, dev warnings, config validation, CLASSES constants, per-type ARIA roles, sr-only icon t
Maintainers
Keywords
Readme
@dr-ishaan/remake-blocks
Astro 6 + remark plugin for rendering GitHub-style callouts (admonitions), enhanced blockquotes, disclosure widgets, pull quotes, and epigraphs in Markdown content — 27 first-class types + 4 themes + inline callouts + directive syntax + safe-by-default HTML escaping + Lucide/emoji icon sets.
Each directive maps 1:1 to its own unique visual identity, SVG icon, color palette, and CSS class. No alias resolution.
Features
Callout types & syntax
- 27 built-in callout types — GFM standard (5) + Obsidian core (10) + promoted aliases (12)
- GitHub admonition syntax —
> [!NOTE],> [!TIP],> [!WARNING], etc. - Directive syntax —
:::note[Title]...:::(Starlight/Docusaurus compat, opt-in) - MkDocs admonition syntax —
!!! note,??? note,???+ note(MkDocs Material compat, opt-in) - Custom titles —
> [!WARNING] Deprecated API - Collapsible callouts —
> [!FAQ]-(collapsed) and> [!TIP]+(expanded) - Disclosure widgets —
> [!] Titlecreates a plain collapsible block - Pull quotes —
> [!PULL]creates a large, centered, italic display quote with auto-detected<cite>attribution - Epigraphs —
> [!EPIGRAPH]creates a small, centered, bold italic quote with<cite>attribution - Attribution auto-detection —
— Authoror-- Authorat end of body is auto-detected and rendered as<cite> - Accordion grouping — consecutive
[!]blocks auto-group into an accordion - Tree view — nested
[!]blocks get depth indicators - Custom callout types — define your own with icon, color, and CSS class
- Configurable aliases —
aliases: { note: ["n"] }for short names
Themes & styling
- 4 swappable CSS themes — github, obsidian, vitepress, docusaurus
- Inline callouts —
{inline}floats left,{inline-end}floats right (responsive) - Appearance variants —
{appearance=minimal},{appearance=hidden}, etc. - Icon sets — octicon (default), lucide, emoji, none
- Per-callout named icon —
{icon="rocket"}uses a specific Lucide icon - Enhanced blockquotes — rounded left accent line, darker text, auto-styled
- i18n label customization —
labels: { note: "注意" }for localized titles - Dark mode — automatic via
prefers-color-scheme+ manual viadata-themeattribute - Auto CSS injection — styles injected into every page by default
- CSS custom properties — full
var()system for easy customization
Accessibility (WCAG-compliant)
role="note"for ALL callout types (norole="alert"on static content — WCAG fix)aria-labelledbylinking container → title idaria-hidden="true"on all decorative iconsdir="auto"on container + title (RTL/Unicode support)data-collapsibleattribute on all callout containers- Semantic HTML —
<aside>for non-collapsible,<details>/<summary>for collapsible - Zero JavaScript for callouts — native HTML5 (collapsibles use
<details>/<summary>) - axe-core verified — 0 critical/serious violations across all 27 types
Security (safe-by-default)
- HTML escaping — raw HTML in markdown body is escaped by default
allowDangerousHtmlopt-in — settrueonly if you trust your markdown sourceclassName/colorescaping — prevents attribute breakout from config values- Custom callout config validation — missing fields use defaults, no crashes
- rehype-sanitize schema — shipped as
sanitize-schema.jsonfor strict pipelines - Directive attribute security — event handlers (
on*) andstyleblocked
Power-user features
tagsoption — override container/title/body element namespropsoption — per-type static or dynamic HTML attributes (function form)build()escape hatch — full custom render function for total output control- Directive attributes —
:::note[Title]{#id .class key="value"}(remark-directive compat) - Exported helpers —
escapeHtml,escapeAttribute,sanitizeColorforbuild()authors - Print-optimized — clean print styles
- Single dependency — only
unist-util-visit
Install
npm install @dr-ishaan/remake-blocksQuick Start (Astro)
// astro.config.mjs
import { defineConfig } from "astro/config";
import remakeBlocks from "@dr-ishaan/remake-blocks/astro";
export default defineConfig({
integrations: [remakeBlocks()],
});Then use callouts in any Markdown or MDX file:
> [!NOTE]
> This is a note callout with useful information.
> [!TIP] Keyboard Shortcut
> Press `Ctrl+Shift+P` to open the command palette.
> [!WARNING]-
> This collapsible warning starts collapsed.Quick Start (Remark only)
import { unified } from "unified";
import remarkParse from "remark-parse";
import { remarkRemakeBlocks } from "@dr-ishaan/remake-blocks";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
const processor = unified()
.use(remarkParse)
.use(remarkRemakeBlocks)
.use(remarkRehype, { allowDangerousHtml: true }) // pass through callout HTML
.use(rehypeStringify, { allowDangerousHtml: true });All 27 Callout Types
GFM Standard (5)
| Directive | Default Title | CSS Class |
|-----------|--------------|-----------|
| [!NOTE] | Note | callout-note |
| [!TIP] | Tip | callout-tip |
| [!IMPORTANT] | Important | callout-important |
| [!WARNING] | Warning | callout-warning |
| [!CAUTION] | Caution | callout-caution |
Obsidian Core (10)
| Directive | Default Title | CSS Class |
|-----------|--------------|-----------|
| [!ABSTRACT] | Abstract | callout-abstract |
| [!INFO] | Info | callout-info |
| [!SUCCESS] | Success | callout-success |
| [!QUESTION] | Question | callout-question |
| [!FAILURE] | Failure | callout-failure |
| [!DANGER] | Danger | callout-danger |
| [!QUOTE] | Quote | callout-quote |
| [!BUG] | Bug | callout-bug |
| [!EXAMPLE] | Example | callout-example |
| [!TODO] | Todo | callout-todo |
Promoted Aliases — First-Class Types (12)
Each of these is its own distinct type with unique colors, not an alias:
| Directive | Default Title | CSS Class |
|-----------|--------------|-----------|
| [!SUMMARY] | Summary | callout-summary |
| [!TLDR] | TL;DR | callout-tldr |
| [!HINT] | Hint | callout-hint |
| [!CHECK] | Check | callout-check |
| [!DONE] | Done | callout-done |
| [!HELP] | Help | callout-help |
| [!FAQ] | FAQ | callout-faq |
| [!ATTENTION] | Attention | callout-attention |
| [!FAIL] | Fail | callout-fail |
| [!MISSING] | Missing | callout-missing |
| [!ERROR] | Error | callout-error |
| [!CITE] | Cite | callout-cite |
Disclosure Widgets
| Directive | Description |
|-----------|-------------|
| [!] | Plain collapsible block (no color, no icon) — uses native <details>/<summary> |
Consecutive [!] blocks are automatically grouped into an accordion. Nested [!] blocks get tree-view styling.
Pull Quotes & Epigraphs
| Directive | Description | CSS Class |
|-----------|-------------|-----------|
| [!PULL] | Large, centered, italic display quote. Auto-detects — Author → <cite>. Supports {inline} for floating. | .pull-quote |
| [!EPIGRAPH] | Small, centered, bold italic quote. Auto-detects — Author → <cite>. Max-width 70%. | .epigraph |
Attribution auto-detection: If the last line of the body starts with — (em dash) or -- (double hyphen), it's automatically extracted and rendered as a <cite> element. If no attribution is found, the entire body is the quote text.
Syntax
Blockquote callouts (primary syntax)
> [!NOTE]
> Content goes here.
> [!WARNING] Breaking Change
> The v2 API has been deprecated.
> [!FAQ]-
> Click to reveal the answer.
> [!TIP]+
> This starts expanded but can be collapsed.Directive syntax (Starlight/Docusaurus compat)
Opt-in via enableDirectiveSyntax: true:
:::note
Basic callout.
:::
:::note[Custom Title]
With title.
:::
:::warning{fold=-}
Collapsed callout.
:::
:::note[Title]{#my-id .custom-class data-type="example"}
With full directive attributes.
:::MkDocs admonition syntax (MkDocs Material compat)
Opt-in via enableMkDocsSyntax: true:
!!! note
Non-collapsible callout (4-space indented body).
??? note
Collapsed callout.
???+ note
Expanded collapsible callout.
!!! note "Custom Title"
With custom title.Per-callout overrides
Use {key=value} syntax after the directive:
> [!NOTE]{icon=false} — hide icon for this callout
> [!NOTE]{icon="rocket"} — use a specific Lucide icon
> [!NOTE]{appearance=minimal} — minimal appearance variant
> [!NOTE]{appearance=hidden} — no title, no icon
> [!NOTE]{inline} — float left (responsive)
> [!NOTE]{inline-end} — float right (responsive)
> [!NOTE]{icon=false inline appearance=minimal} — multiple overridesDisclosure widgets
> [!] Section Title
> Content hidden by default.
> [!] Another Section
> Content hidden by default.
> [!]+ Expanded Section
> Content visible by default.Pull quotes
> [!PULL]
> The best way to predict the future is to invent it.
> — Alan Kay
> [!PULL]{inline}
> A floated pull quote with text wrapping around it.
> — Someone
> [!PULL]
> A pull quote with no attribution.Epigraphs
> [!EPIGRAPH]
> The fog comes on little cat feet.
> — Carl Sandburg
> [!EPIGRAPH]
> An epigraph with no attribution.Configuration
// astro.config.mjs
import { defineConfig } from "astro/config";
import remakeBlocks from "@dr-ishaan/remake-blocks/astro";
export default defineConfig({
integrations: [
remakeBlocks({
// ── Core ──────────────────────────────────────────────
injectStyles: true, // auto-inject CSS (default: true)
enhanceBlockquotes: true, // style regular blockquotes (default: true)
dataCalloutType: true, // add data-callout-type attribute (default: true)
allowDangerousHtml: false, // ⚠️ safe-by-default HTML escaping (default: false)
// ── Callout classes ──────────────────────────────────
calloutClass: "callout",
calloutTitleClass: "callout-title",
calloutBodyClass: "callout-body",
// ── Disclosure widgets ───────────────────────────────
enableDisclosures: true, // enable [!] syntax (default: true)
enableAccordion: true, // auto-group consecutive [!] (default: true)
enableTreeView: true, // nested [!] tree view (default: true)
// ── Alternative syntaxes (opt-in) ────────────────────
enableDirectiveSyntax: false, // :::note[Title] syntax (default: false)
enableMkDocsSyntax: false, // !!! note / ??? note syntax (default: false)
// ── Visual options ───────────────────────────────────
showIndicator: true, // show icons globally (default: true)
iconSet: "octicon", // "octicon" | "lucide" | "emoji" | "none"
appearance: "default", // "default" | "minimal" | "simple" | "hidden"
// ── i18n labels ──────────────────────────────────────
labels: {
note: "注意",
tip: "ヒント",
warning: "警告",
},
// ── Aliases ──────────────────────────────────────────
aliases: {
note: ["n"],
tip: ["t"],
},
// ── Power-user options ───────────────────────────────
tags: { // override HTML element names
container: "aside",
title: "div",
body: "div",
},
props: { // per-type HTML attributes (static or function)
note: { "data-category": "info" },
warning: (parsed) => ({ "data-severity": parsed.collapsible ? "collapsible" : "static" }),
},
build: (parsed, config, bodyHtml, options) => { // full custom render
return `<div class="my-callout">${bodyHtml}</div>`;
},
// ── Custom callout types ─────────────────────────────
customCallouts: [
{
type: "machine-learning",
icon: "🧠",
className: "callout-ml",
defaultTitle: "Machine Learning",
color: "#7c3aed",
backgroundColor: "#f5f3ff",
},
],
}),
],
});Themes
4 swappable CSS themes are available. Import the one you want:
// Default (github) — auto-injected when injectStyles: true
import "@dr-ishaan/remake-blocks/styles.css";
// Or pick a specific theme:
import "@dr-ishaan/remake-blocks/theme/github.css"; // GitHub style
import "@dr-ishaan/remake-blocks/theme/obsidian.css"; // Obsidian style
import "@dr-ishaan/remake-blocks/theme/vitepress.css"; // VitePress style
import "@dr-ishaan/remake-blocks/theme/docusaurus.css"; // Docusaurus styleEach theme includes:
- Full light + dark mode (
prefers-color-scheme+data-themeattribute) - All 27 callout types with per-type colors
- Enhanced blockquote styling (rounded left accent line)
- Disclosure widget styling (card with rounded left accent line)
- Accordion + tree view styling
- Inline callout CSS (responsive)
- Print-optimized styles
Security: Safe-by-Default
By default, raw HTML in markdown is HTML-escaped before being placed in callout bodies. This prevents XSS attacks from untrusted markdown content.
> [!NOTE]
> <img src=x onerror=alert(1)>Default behavior (allowDangerousHtml: false):
<aside class="callout callout-note" ...>
...
<div class="callout-body">
<p><img src=x onerror=alert(1)></p>
</div>
</aside>Opt-in to raw HTML (allowDangerousHtml: true):
<aside class="callout callout-note" ...>
...
<div class="callout-body">
<p><img src=x onerror=alert(1)></p> <!-- ⚠️ XSS risk! -->
</div>
</aside>Only set allowDangerousHtml: true if:
- You fully trust your markdown source, AND
- You have sanitized the markdown with a tool like
rehype-sanitize
rehype-sanitize schema
A compatible sanitize schema is shipped for strict security pipelines:
import schema from "@dr-ishaan/remake-blocks/sanitize-schema.json";
import rehypeSanitize from "rehype-sanitize";
// In your pipeline:
const processor = unified()
.use(remarkParse)
.use(remarkRemakeBlocks)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeSanitize, schema)
.use(rehypeStringify);Custom Callout Config Validation
Custom callout configurations are validated and normalized:
- Missing fields are filled with safe defaults (no crash)
nullor non-object entries are skippedclassNameis HTML-escaped (prevents attribute breakout)coloris validated against a safe CSS color pattern; invalid values fall back to gray
Custom Callout Types
Define your own callout types with full control over appearance:
remakeBlocks({
customCallouts: [
{
type: "machine-learning", // [!MACHINE-LEARNING]
icon: "🧠", // Emoji or SVG string
className: "callout-ml", // CSS class
defaultTitle: "Machine Learning",
color: "#7c3aed", // Border & accent color
backgroundColor: "#f5f3ff", // Background color
iconColor: "#7c3aed", // Icon color (optional, defaults to color)
},
],
})If a custom callout's type matches a built-in, the built-in is overridden.
i18n Label Customization
Override default titles for any callout type — useful for non-English documentation:
remakeBlocks({
labels: {
note: "注意",
tip: "ヒント",
warning: "警告",
caution: "危険",
important: "重要",
},
})Works with all 27 builtin types, custom callout types, and all 3 syntaxes (blockquote, directive, MkDocs).
Dark Mode
Dark mode is supported automatically via prefers-color-scheme: dark and manually via the data-theme attribute:
<html data-theme="dark">
<!-- callouts switch to dark palette -->
</html>Supported data-theme values: "dark", "dim".
CSS Custom Properties
Override any design token at the :root level:
:root {
--callout-radius: 8px;
--callout-padding-y: 12px;
--callout-padding-x: 16px;
--callout-icon-size: 24px;
--callout-title-font-size: 1.1em;
/* Per-type overrides */
--callout-note-border: #3b82f6;
--callout-note-bg: #eff6ff;
}See styles.css for the complete list of CSS custom properties.
HTML Output
Each callout renders as:
<aside class="callout callout-note" data-callout-type="note" data-collapsible="false"
role="note" aria-labelledby="callout-note-1" dir="auto">
<div class="callout-title" style="color:#0969da" id="callout-note-1" dir="auto">
<span class="callout-icon" style="color:#0969da" aria-hidden="true">
<!-- SVG icon -->
</span>
<span class="callout-title-text">Note</span>
</div>
<div class="callout-body">
<p>Content goes here.</p>
</div>
</aside>Collapsible callouts use <details>/<summary>:
<details class="callout callout-faq collapsible" data-callout-type="faq" data-collapsible="true"
role="note" aria-labelledby="callout-faq-1" dir="auto">
<summary class="callout-title" style="color:#b45309" id="callout-faq-1" dir="auto">
<span class="callout-icon" style="color:#b45309" aria-hidden="true"><!-- SVG --></span>
<span class="callout-title-text">FAQ</span>
</summary>
<div class="callout-body">
<p>Content revealed on click.</p>
</div>
</details>Pull quotes render as:
<aside class="pull-quote" dir="auto">
<p class="pull-quote-text">The best way to predict the future is to invent it.</p>
<p class="pull-quote-attribution">— <cite>Alan Kay</cite></p>
</aside>Epigraphs render as:
<aside class="epigraph" dir="auto">
<p class="epigraph-text">The fog comes on little cat feet.</p>
<p class="epigraph-attribution">— <cite>Carl Sandburg</cite></p>
</aside>Exported Helpers
For users who supply a custom build() render function:
import { escapeHtml, escapeAttribute, sanitizeColor } from "@dr-ishaan/remake-blocks";| Helper | Description |
|--------|-------------|
| escapeHtml(str) | Escapes & < > " ' to HTML entities |
| escapeAttribute(str) | Same as escapeHtml, for attribute values |
| sanitizeColor(value, fallback) | Validates CSS color; returns fallback if dangerous |
Caveats
Strikethrough (~~text~~) requires remark-gfm
The plugin's body serializer handles <del> nodes correctly, but remark-parse alone does not enable GFM strikethrough. To use ~~strikethrough~~ syntax in callout bodies, add remark-gfm:
import remarkGfm from "remark-gfm";
export default defineConfig({
markdown: { remarkPlugins: [remarkGfm] },
integrations: [remakeBlocks()],
});Horizontal rule after directive becomes a setext H2
In CommonMark, > [!NOTE]\n> --- is interpreted as a setext H2 heading, not as a callout with a thematic break body. Add a blank line before ---:
> [!NOTE]
> Above
>
> ---
>
> BelowTables and other GFM features require remark-gfm
GFM-only features like tables, autolinks, and task list items require the remark-gfm plugin.
How It Works: Parsing Pipeline
The plugin processes markdown through 4 passes. Each syntax is detected at a different stage:
Pass overview
| Pass | Name | When it runs | What it detects | Output |
|------|------|-------------|-----------------|--------|
| 0a | Directive syntax transformer | enableDirectiveSyntax: true | :::type[Title]{attrs} ... ::: paragraphs | Callout HTML node |
| 0b | MkDocs syntax transformer | enableMkDocsSyntax: true | !!! type, ??? type, ???+ type paragraphs | Callout HTML node |
| 1 | Blockquote callout transformer | Always runs | > [!TYPE] in blockquote nodes | Callout HTML node or enhanced blockquote |
| 2 | Accordion grouping | enableAccordion: true | Consecutive disclosure <details> nodes | <div class="disclosure-accordion"> wrapper |
Syntax detection methods
| Syntax | How remark-parse sees it | How the plugin detects it | Key parsing logic |
|--------|------------------------|--------------------------|-------------------|
| Blockquote (> [!NOTE]) | blockquote → paragraph → text starting with [!TYPE] | Regex on first text child: ^\[!(NOTE\|TIP\|...)\]([+-]?)(title)?({overrides})? | Dynamically built regex from registered types + aliases + empty string for [!] disclosures |
| Directive (:::note) | paragraph → text containing entire :::type...\n::: block | text.startsWith(":::") → count colons → extract type → parse [Title] → parse {attrs} → find closing ::: | String methods (not regex ^ — avoids Node.js anchor issue); body is everything between opening and closing lines |
| MkDocs (!!! note) | paragraph → text containing entire !!! type\n body block | text.startsWith("!!!") or text.startsWith("???") → extract marker → extract type → parse "Title" → body is 4-space indented | String methods; strips 4-space indent from each body line |
Blockquote callout regex capture groups
| Group | Content | Example | Used for |
|-------|---------|---------|----------|
| [1] | Type name (or empty for [!]) | note, warning, "" | Look up callout config or identify as disclosure |
| [2] | Fold marker | +, -, or "" | + = expanded collapsible, - = collapsed, "" = not collapsible |
| [3] | Custom title text | Breaking Change | Override default title |
| [4] | Overrides block | {icon=false appearance=minimal} | Parsed by parseOverrides() + parseDirectiveAttrs() |
Disclosure, Pull Quote & Epigraph detection
The [!], [!PULL], and [!EPIGRAPH] syntaxes are all detected in the same blockquote pass as regular callouts. The key difference is what's added to the regex alternatives:
[!]→ empty string""added to regex (whenenableDisclosures: true)[!PULL]→"PULL"always added to regex[!EPIGRAPH]→"EPIGRAPH"always added to regex
| Aspect | Regular callout (> [!NOTE]) | Disclosure (> [!]) |
|--------|------------------------------|---------------------|
| Type name | note, warning, etc. | disclosure (synthetic) |
| Config lookup | configMap.get(type) → icon, color, className | No config — plain |
| Icon | SVG icon from iconSet | None |
| Color | Per-type border/bg colors | None (gray left line only) |
| CSS class | callout callout-note | disclosure |
| Container | <aside> or <details> | Always <details> |
| Collapsible | Only if +/- marker present | Always collapsible |
| Default title | From config (e.g., "Note") | "Details" |
| data-callout-type | note, warning, etc. | Not set |
| role | role="note" | Not set (uses native <details>) |
| Left accent line | Colored (matches callout type) | Gray (#d0d7de) |
| Fold markers | +/- = collapsible, none = not collapsible | +/none = expanded, - = collapsed |
{overrides} block parsing
Two parsers run on the {...} block (used by all 3 syntaxes):
| Parser | Input pattern | Output keys | Purpose |
|--------|--------------|-------------|---------|
| parseOverrides() | icon=false, appearance=minimal, inline, icon="rocket" | overrides.icon, overrides.appearance, overrides.inline, overrides.iconName | Plugin-specific per-callout visual overrides |
| parseDirectiveAttrs() | #my-id, .custom-class, data-x="y" | directiveAttrs.id, directiveAttrs.classes, directiveAttrs.attrs | remark-directive compatible HTML attributes |
Override keys reference
| Key | Values | Effect | Example |
|-----|--------|--------|---------|
| icon | false, true, "name" | Hide icon / show icon / use named Lucide icon | {icon=false}, {icon="rocket"} |
| appearance | default, minimal, simple, hidden | Visual density variant | {appearance=minimal} |
| inline | inline, inline-end (or bare keywords) | Float left / float right (responsive) | {inline}, {inline-end} |
| fold | +, - | Collapsible expanded / collapsed (directive syntax only) | {fold=-} |
| #id | Any valid HTML id | Sets element id | {#my-section} |
| .class | Any valid CSS class name | Adds CSS class | {.highlight} |
| key="value" | Any key/value pair | Adds HTML attribute (event handlers and style blocked) | {data-type="example"} |
Processing order
| Step | What happens | Why this order |
|------|-------------|----------------|
| 1. remark-parse | Raw markdown → mdast tree | Must happen before plugin (external) |
| 2. Pass 0a: Directive | :::type paragraphs → callout HTML | Must run before blockquote pass (directive paragraphs are not blockquotes) |
| 3. Pass 0b: MkDocs | !!! type paragraphs → callout HTML | Same reason — paragraph-level, not blockquote-level |
| 4. Pass 1: Blockquote | > [!TYPE] blockquotes → callout HTML | Processes deepest blockquotes first (inside-out) so nested callouts are resolved before parents |
| 5. Pass 2: Accordion | Groups consecutive disclosure <details> | Must run after all disclosures are rendered |
ParsedCallout object (internal)
All 3 syntaxes produce the same ParsedCallout object, which is passed to buildCalloutHtml():
| Field | Type | Source | Description |
|-------|------|--------|-------------|
| type | string | Regex group [1] | Callout type (lowercased), "disclosure", "pull", or "epigraph" |
| customTitle | string \| undefined | Regex group [3] or [Title] or "Title" | User-provided title (overrides default) |
| collapsible | boolean | Fold marker or ???/???+ | Whether callout is collapsible |
| collapsibleOpen | boolean | + marker or ???+ or no marker (disclosures) | Whether collapsible starts expanded |
| isDisclosure | boolean | Empty type match | True for [!] syntax |
| isPullQuote | boolean | Type = "pull" | True for [!PULL] syntax |
| isEpigraph | boolean | Type = "epigraph" | True for [!EPIGRAPH] syntax |
| overrides | object \| undefined | {overrides} block via parseOverrides() | Per-callout icon/appearance/inline overrides |
| directiveAttrs | object \| undefined | {attrs} block via parseDirectiveAttrs() | Per-callout id/class/HTML attributes |
Advanced: Using the Remark Plugin Directly
The remark plugin can be used independently of the Astro integration:
import { unified } from "unified";
import remarkParse from "remark-parse";
import { remarkRemakeBlocks } from "@dr-ishaan/remake-blocks";
const processor = unified()
.use(remarkParse)
.use(remarkRemakeBlocks, { enhanceBlockquotes: true });A backward-compatible alias remarkCalloutBlocks is also exported:
import { remarkCalloutBlocks } from "@dr-ishaan/remake-blocks";License
MIT
Plugin Order (v2.2.0+)
When using remake-blocks with other remark/rehype plugins, the correct order is:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import remakeBlocks from '@dr-ishaan/remake-blocks/astro';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import schema from '@dr-ishaan/remake-blocks/sanitize-schema.json';
export default defineConfig({
integrations: [remakeBlocks()],
markdown: {
remarkPlugins: [
// remake-blocks must run BEFORE remark-rehype (it transforms mdast)
// No explicit registration needed — the Astro integration handles it.
],
rehypePlugins: [
// 1. rehype-raw — must come FIRST to parse raw HTML emitted by callouts
rehypeRaw,
// 2. rehype-sanitize — must come AFTER rehype-raw, uses the bundled schema
[rehypeSanitize, schema],
// 3. Other rehype plugins (syntax highlighters, etc.) run last
],
},
});Key ordering rules:
remarkRemakeBlocksruns as a remark plugin (before remark-rehype) — it transforms the mdast tree.rehype-rawmust come beforerehype-sanitizeso raw HTML from callouts is parsed into hast nodes before sanitization.- The bundled
sanitize-schema.jsonallows all callout elements + attributes while stripping<script>,on*=handlers, andjavascript:URLs. - For syntax highlighters (e.g.
rehype-pretty-code), place them AFTERrehype-rawso they can process code blocks inside callout bodies.
MDX Compatibility (v2.2.0+)
The plugin works with .mdx files. Since remake-blocks is a standard remark plugin, it runs during MDX's markdown parsing phase — before MDX component processing. Callout directives in MDX files are transformed the same way as in .md files.
// astro.config.mjs
import mdx from '@astrojs/mdx';
import remakeBlocks from '@dr-ishaan/remake-blocks/astro';
export default defineConfig({
integrations: [
remakeBlocks(),
mdx(),
],
});MDX-specific notes:
- Callouts inside MDX components: supported (the remark plugin runs before component rendering).
- MDX components inside callouts: supported (callout bodies accept any markdown content, including MDX expressions).
- JSX inside callout bodies: requires
allowDangerousHtml: trueor proper MDX evaluation.
Content Collections Compatibility (v2.2.0+)
The plugin works with Astro Content Collections (both legacy and Content Layer API). Since the remark plugin is registered via updateConfig({ markdown: { remarkPlugins: [...] } }), it runs during markdown rendering for all content — including collection entries loaded via glob(), file(), or custom loaders.
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
date: z.date(),
}),
});
export const collections = { blog };No special configuration needed — callout directives in collection markdown files are transformed automatically.
View Transitions Compatibility (v2.2.0+)
The accordion.js runtime script handles Astro View Transitions:
- On initial page load:
DOMContentLoadedevent initializes accordion behavior. - After View Transitions:
astro:page-loadevent re-initializes (idempotent — usesdata-enhancedflag to skip already-processed accordions). - Collapsible
<details>state: each page transition starts fresh (collapsed/expanded state does NOT persist across pages — this is correct behavior).
// astro.config.mjs
import { defineConfig } from 'astro/config';
import remakeBlocks from '@dr-ishaan/remake-blocks/astro';
export default defineConfig({
integrations: [remakeBlocks()],
});View Transitions work out-of-the-box — no additional configuration needed.
