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: true is 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