remark-dgmo
v0.14.2
Published
Remark plugin to render DGMO diagrams from fenced code blocks at build time. Framework-agnostic core shared by astro-dgmo, docusaurus-plugin-dgmo, and any unified pipeline.
Maintainers
Readme
remark-dgmo
Framework-agnostic remark plugin that renders DGMO diagrams from ```dgmo fenced code blocks at build time. Powered by @diagrammo/dgmo. Zero client JavaScript by default.
📖 Setup guides for Astro, Docusaurus & Fumadocs: diagrammo.app/embed
sequence
Client -POST /login-> API
API -validate-> Auth
Auth -JWT-> API
API -200 OK-> ClientDrop a fenced block with the language dgmo into any markdown or MDX file processed by a unified-style pipeline — Astro, Docusaurus, Starlight, Vitepress, eleventy-with-remark, or your own custom toolchain — and it becomes an inline <svg> at build time.
By default, every diagram is rendered twice (once with the palette's light mode, once with its dark mode) and wrapped in <div class="dgmo-light"> / <div class="dgmo-dark">. A tiny shipped stylesheet hides the wrong one based on [data-theme="dark"] (the convention used by Docusaurus, Starlight, and most other docs frameworks). The result: your diagrams follow the host page's color-mode toggle without any client-side rendering.
Install
pnpm add remark-dgmo @diagrammo/dgmo
# or
npm install remark-dgmo @diagrammo/dgmo@diagrammo/dgmo is a peer dependency.
ESM-only. Your config file must be .mjs, .ts, or .mts — or your package.json must have "type": "module".
Use — three integration patterns
Pattern 1: Astro
Use astro-dgmo — it wraps this plugin and handles the integration plumbing.
pnpm add astro-dgmo @diagrammo/dgmo// astro.config.mjs
import { defineConfig } from 'astro/config';
import dgmo from 'astro-dgmo';
export default defineConfig({
integrations: [dgmo()],
});You'll also need to import the color-mode stylesheet in your global layout:
---
// src/layouts/Base.astro
import 'remark-dgmo/client.css';
---Pattern 2: Docusaurus
Use docusaurus-plugin-dgmo — it handles getClientModules() registration for the CSS + client script.
pnpm add docusaurus-plugin-dgmo @diagrammo/dgmo// docusaurus.config.ts
import type { Config } from '@docusaurus/types';
const config: Config = {
// …
plugins: ['docusaurus-plugin-dgmo'],
presets: [
[
'classic',
{
docs: {
remarkPlugins: [
(await import('docusaurus-plugin-dgmo/remark')).default,
],
},
blog: {
remarkPlugins: [
(await import('docusaurus-plugin-dgmo/remark')).default,
],
},
pages: {
remarkPlugins: [
(await import('docusaurus-plugin-dgmo/remark')).default,
],
},
},
],
],
};
export default config;The plugin registers client.css + client.js via getClientModules(). You still wire remarkPlugins into each preset slot manually — Docusaurus's plugin API has no hook to auto-inject into a sibling preset.
Pattern 3: Fumadocs (Next.js app router)
Use fumadocs-dgmo — it wraps mdxOptions for fumadocs-mdx, ships a .dark-rewritten stylesheet (Fumadocs UI's next-themes default), and provides a Client Component that re-binds on every soft navigation.
pnpm add fumadocs-dgmo @diagrammo/dgmo// source.config.ts
import { defineConfig } from 'fumadocs-mdx/config';
import { withDgmo } from 'fumadocs-dgmo/config';
export default defineConfig({
mdxOptions: withDgmo(),
});/* app/global.css */
@import 'fumadocs-ui/css/preset.css';
@import 'fumadocs-dgmo/client.css';// app/layout.tsx — add <DgmoClient /> inside <RootProvider>
import { DgmoClient } from 'fumadocs-dgmo/client';
// …
<RootProvider>
{children}
<DgmoClient />
</RootProvider>;Pattern 4: Vanilla unified pipeline
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import remarkDgmo from 'remark-dgmo';
const out = await unified()
.use(remarkParse)
.use(remarkDgmo, { mode: 'showcase', palette: 'catppuccin' })
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeStringify, { allowDangerousHtml: true })
.process(source);In your output HTML's <head>, add the shipped stylesheet (or inline its three rules):
<link
rel="stylesheet"
href="/path/to/node_modules/remark-dgmo/dist/client.css"
/>
<script
type="module"
src="/path/to/node_modules/remark-dgmo/dist/client.js"
></script>The client script is optional — it tightens each diagram's viewBox to its content bounds and wires up showcase-mode copy buttons. Without it, diagrams still render but may have extra whitespace and copy buttons won't function.
Options
remarkDgmo({
// Output mode for `dgmo` blocks. 'diagram' (default) = SVG only.
// 'showcase' = syntax-highlighted source + diagram + copy + open-in-editor.
mode: 'diagram',
// Default palette name (any registered @diagrammo/dgmo palette).
palette: 'nord',
// Color-mode strategy. 'auto' (default) renders both light and dark and
// toggles via CSS. 'light' or 'dark' single-renders with the matching theme.
colorMode: 'auto',
// Default theme when colorMode is 'light' or 'dark' (single-render). Ignored under 'auto'.
theme: 'dark',
// Showcase chrome — each toolbar button toggles independently. Enabled
// automatically in showcase mode; set any to false to hide just that button.
showSource: undefined, // boolean; default = (mode === 'showcase')
showCopy: undefined, // boolean; default = (mode === 'showcase')
showExpand: undefined, // boolean; default = (mode === 'showcase')
showOpenInEditor: undefined, // boolean; default = (mode === 'showcase')
// Where the "Open in editor" link points.
editorBaseUrl: 'https://online.diagrammo.app',
// Outer wrapper element + class hook.
wrapper: 'figure',
className: 'dgmo',
// Append additional class names to every emitted wrapper. Used by
// astro-dgmo v0.3.0 to keep the legacy `astro-dgmo*` class names for one
// minor cycle of backward compat.
legacyClassNames: [],
// Emit MDX-compatible output. Default: false (raw `html` mdast node).
// Set to true when the host pipeline routes files through @mdx-js/mdx —
// Docusaurus with `markdown.format: 'mdx'`, Astro `.mdx`, Fumadocs, etc.
// The plugin then emits an `mdxJsxFlowElement` instead of an `html` node,
// so MDX accepts the output without "Cannot handle unknown node `raw`".
mdx: false,
});Per-block overrides
Append options to the fence info string. Tokens are space-separated; values may be quoted.
```dgmo showcase title="Login flow" palette=catppuccin theme=light
sequence
A -> B
```| Token | Effect |
| ------------------------------------------------------- | ----------------------------------- |
| diagram / showcase | Set mode for this block |
| palette=<name> | Override palette |
| theme=light / theme=dark / theme=transparent | Override theme (single-render only) |
| colorMode=auto / colorMode=light / colorMode=dark | Override color-mode strategy |
| title="…" | Add a caption (<figcaption>) |
| source / noSource | Force source view + toggle on/off |
| copy / noCopy | Force copy button on/off |
| expand / noExpand | Force expand (full-screen) on/off |
| openInEditor / noOpenInEditor | Force editor link on/off |
Each toolbar button is independent — e.g. ```dgmo showcase noSource noExpand
keeps just the copy + open-in-editor buttons, and ```dgmo copy adds only a
copy button to an otherwise bare diagram.
Live links (on by default)
A fence can name a diagram living in Diagrammo Cloud instead of carrying its own source:
```dgmo
live-link dgm_01HQ3RSTUV
```The build fetches that diagram's source, renders it exactly like a pasted one
(fence-meta and per-block overrides all still apply), and writes what it fetched
into .dgmo/references/<id>.json. Commit that directory. Three spellings are
accepted — live-link <id> in a fence, ![[live-link:<id>]] in a note, or the
plain share URL.
// on by default — this turns it OFF
remarkDgmo({ liveLink: { enabled: false } });Switched off, a live-link fence renders a small card naming the diagram and linking through to it, plus a hover-revealed "Show this diagram here" link to the guide, and the build warns naming the file and line. Nothing is fetched. See the live links guide.
Only published diagrams can be referenced. A private diagram is not fetchable at all — there are no tokens, no signed links, and no origin allowlists to configure, which is also why every referenced byte is cacheable.
Why the cache is committed
So that our availability is never your build's problem. A clean CI checkout renders from the committed copy if we are unreachable, and a diagram changing shows up in your pull request as a source diff you can read.
| What happened | Your build | Your page | | -------------------------------- | --------------------------------- | ------------------------- | | all well | writes the cache | the current diagram | | we're unreachable, cache present | succeeds, with a warning | last known good | | we're unreachable, no cache | fails | — | | unknown id, never cached | fails (it can only be a typo) | — | | id gone, cache present | succeeds, with a warning | last known good | | the author unshared it | succeeds, with a warning | a "no longer shared" card |
Set liveLink: { offline: true } to skip the network entirely and build from
the cache alone.
⚠️ Content-Security-Policy
If your site sets a CSP, it must allow connect-src https://api.diagrammo.app.
Without it the diagram still renders — it was baked at build time — but it will
never refresh, and nothing on the page can tell you so, because the report
would be blocked too. This is the one thing to get right before shipping
live links.
How the refresh works
remark-dgmo/client.js checks, once the page is idle, whether any referenced
diagram has moved since the build. Almost always it hasn't, and the check costs
one edge-cached request.
When one has, the default is to say so — a small link to the live diagram —
rather than to re-render it. That default is about your bundle, not about
laziness: re-rendering needs the dgmo renderer in the browser, and a bundler
that can see the import ships it whether or not it is ever used. On this repo's
own Astro fixture the difference is 1 chunk / 8.9 KB gzipped versus 88 chunks
/ 634 KB. Lazy for your readers; not free for your dist/.
If you want the swap anyway — a docs site that publishes far more often than it rebuilds is the case that wants it — opt in:
import 'remark-dgmo/client.js';
import 'remark-dgmo/client-render.js'; // adds the renderer to your bundleThat second line registers a renderer by running and exports nothing, so it
needs a remark-dgmo newer than 0.14.1 to survive your build: up to and
including that version the package declared itself free of side effects, which
licensed bundlers to delete the import outright — silently, leaving you the link and no renderer. If
you are wiring this from application code rather than a side-effect import, a
dynamic import('remark-dgmo/client-render.js') works on any version and is
what the framework wrappers do.
Using a wrapper? Each one has its own way in, and setting refresh: 'render'
without it now tells you so at build time: astro-dgmo and
docusaurus-plugin-dgmo inject the runtime for you, while fumadocs-dgmo and
nextra-dgmo want <DgmoRenderClient /> mounted and vitepress-dgmo wants
setupDgmoRender() called in your theme.
Even then, the client refuses a swap it cannot make safely: a renderer version that disagrees with the one that baked the page, or a new diagram that would reflow your layout. Those fall back to the same small link, and your diagram is left exactly as it was.
Working reference site
For an end-to-end example of remark-dgmo running inside a real
framework, see docusaurus-plugin-dgmo's tests/fixture/
— a minimal Docusaurus 3 site that wires this plugin into every preset
slot and exercises plain, tagged, showcase, and per-block-override
blocks. The astro-dgmo repo has an equivalent Astro 6 fixture at
tests/fixture/;
the fumadocs-dgmo repo has a Next.js app-router fixture at
tests/fixture/.
All three fixtures pin to link:../.. against the wrapper plugin's
source, so they're the canonical reference for the smallest correct
config — including the non-obvious gotchas (Docusaurus's async-function
default export + markdown: { format: 'md' }, Astro's manual import
'remark-dgmo/client.css', Fumadocs's mdx: true requirement and
html.dark selector mapping).
Custom color-mode selector
The shipped client.css keys on [data-theme="dark"] — the convention used by Docusaurus and Starlight. For Tailwind-style sites that signal dark mode via a .dark class on <html> (which is also what Fumadocs UI's next-themes default produces), don't import client.css directly — fumadocs-dgmo/client.css ships a build-time rewrite for that case. For any other custom selector, inline these three rules in your own CSS instead, swapping the selector:
.dgmo-dark {
display: none;
}
html.dark .dgmo-light {
display: none;
}
html.dark .dgmo-dark {
display: block;
}For data-color-scheme="dark", :root[data-mode="dark"], etc. — same three rules, swap the selector to match what your toggle sets.
How it works
- The remark transformer walks the mdast, finding
codenodes withlang === 'dgmo'. - For each block,
renderDgmoBlock()callsrender()from@diagrammo/dgmo— twice ifcolorMode: 'auto'(one light, one dark), once otherwise. - Each SVG is normalized: width/height stripped,
viewBoxadded, inline background removed. - The original
codenode is replaced with anhtmlnode carrying the rendered wrapper(s). - The optional client script (
dist/client.js) tightens viewBoxes and binds showcase-mode copy buttons.
Rendering happens at build time. The browser sees only the inline SVG and the small color-mode CSS.
License
MIT
