Quick start (unified pipeline)
Get your first Rho rendering up within 5 minutes after installing
@rho/md.
Complete minimal integration (~30 lines)
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Rho demo</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div id="root" class="markdown-body"></div>
<script type="module" src="./main.js"></script>
</body>
</html>
main.js:
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import { registerAllInnerPlugins, remarkPlugins, hydrateAll } from '@rho/md';
import '@rho/md/css';
// 1. Register inner plugins (required for nested DSL)
registerAllInnerPlugins();
// 2. Build unified processor
const processor = unified()
.use(remarkParse)
.use(remarkGfm);
// 3. Register Rho's 7 remark plugins
remarkPlugins.forEach((p) => processor.use(p));
// 4. To HTML AST + stringify
processor
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeStringify, { allowDangerousHtml: true });
// 5. Your markdown source
const markdown = `
# Hello Rho
> [!INFO]
> This is a callout
\`\`\`interact
slider x 0 10 5 1
template stl:
[Selected] -> [{x}]
\`\`\`
`;
// 6. process → inject into DOM
const html = String(await processor.process(markdown));
const container = document.getElementById('root');
container.innerHTML = html;
// 7. Hydrate interactive blocks (slider / tabs / etc.)
hydrateAll(container);
Open index.html → you see:
- Heading "Hello Rho"
- Blue callout
- Draggable slider + live output "Selected = 5"
Step-by-step
Step 1: registerAllInnerPlugins()
registerAllInnerPlugins();
What it does: tells Rho's inner processor "when nesting DSLs, apply all 7 remark plugins." Why required: nested callout / layout / interact need the inner processor to know which DSL plugins exist. See Inner processor.
Common bug: forgetting this line → callouts inside layouts show as literals.
Step 2-4: build unified processor
const processor = unified()
.use(remarkParse)
.use(remarkGfm);
remarkPlugins.forEach((p) => processor.use(p));
processor
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeStringify, { allowDangerousHtml: true });
| Step | Role |
|---|---|
remarkParse |
markdown string → markdown AST |
remarkGfm |
GFM extensions (task lists / tables / etc.) |
remarkPlugins (7) |
Rho's callout / layout / tabs / stepper / timeline / annotate / modal transformations |
remarkRehype |
markdown AST → HTML AST |
rehypeStringify |
HTML AST → HTML string |
allowDangerousHtml: trueis required — Rho DSL transformations produce HTML with marker classes / data attributes that rehype considers "dangerous" and must allow. Rho's SVG sanitizer handles safety internally.
Step 5: your markdown
Any legal markdown + Rho DSL works:
const markdown = `
# Title
A paragraph.
> [!ZEN]
> Rho's soul
\`\`\`layout grid cols=2
:::card accent=blue
Left card content
:::
:::card accent=red
Right card content
:::
\`\`\`
\`\`\`interact
slider rate 0 0.15 0.05 0.01
template stl: [Rate] -> [{(rate*100):.1f}%]
\`\`\`
`;
Step 6: process → inject into DOM
const html = String(await processor.process(markdown));
container.innerHTML = html;
String(...) is required: process() returns a VFile; String() extracts .value.
Step 7: hydrate
hydrateAll(container);
What it does: scans the container for all Rho marker classes (.rho-interact / .rho-tabs / .rho-stepper / .rho-modal); turns static HTML into interactive components.
Must run in browser — for SSR, put hydrate in your client-side bundle.
Complete file structure
my-rho-demo/
├── package.json
├── index.html
├── main.js
└── style.css ← optional, extends Rho default styles
package.json:
{
"name": "my-rho-demo",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@rho/md": "^0.1.0",
"unified": "^11.0.5",
"remark-parse": "^11.0.0",
"remark-gfm": "^4.0.1",
"remark-rehype": "^11.0.0",
"rehype-stringify": "^10.0.0",
"vega-embed": "^7.0.0"
}
}
Run:
npm install
npx vite # or any dev server supporting ES modules
Framework integration
React
import { useEffect, useRef } from 'react';
import { /* ... */ } from '@rho/md';
function RhoMarkdown({ source }) {
const ref = useRef(null);
useEffect(() => {
const html = renderRho(source); // your unified pipeline
ref.current.innerHTML = html;
hydrateAll(ref.current);
}, [source]);
return <div className="markdown-body" ref={ref} />;
}
Vue 3
<template>
<div class="markdown-body" ref="root"></div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import { hydrateAll } from '@rho/md';
const props = defineProps(['source']);
const root = ref(null);
const update = async () => {
const html = await renderRho(props.source);
root.value.innerHTML = html;
hydrateAll(root.value);
};
onMounted(update);
watch(() => props.source, update);
</script>
Astro / Next.js / other static sites
Run unified pipeline at build time to generate HTML; call hydrateAll from client-side script at runtime.
SSR + hydration
Server side:
// SSR (Node)
const html = String(await processor.process(markdown));
return `<html>...<div id="root">${html}</div>...<script src="/client.js">...`;
Client side (client.js):
import { hydrateAll } from '@rho/md';
import '@rho/md/css';
hydrateAll(document.getElementById('root'));
Common issues
Nested callout in layout doesn't render
→ Forgot registerAllInnerPlugins().
Chart doesn't render but everything else works
→ Forgot to install vega-embed.
Slider isn't draggable
→ Forgot hydrateAll(container), or hydrate ran on SSR side instead of client.
TypeScript complains about types
→ Check tsconfig.json for module: "esnext" and moduleResolution: "bundler".
What's next
→ API reference — full API surface → Hydration utilities — partial hydrate / timing control → CSS theming — restyling → Inner processor — nested DSL details