Duvlify
GitHubGet started
ReferenceCode and diagrams

Code and diagrams

Fenced code blocks get syntax highlighting, a filename header and a copy button. Mermaid fences render real diagrams that follow the reader's colour mode.

Code blocks

Write a fenced code block. Do not use a component. Shiki highlights a fence, and the fence also gets the filename header and copy button. These come from src/lib/rehype-code-chrome.ts, which adds them.

MDX
```ts title="src/client.ts"
export const client = createClient({ token: process.env.API_TOKEN });
```

Which renders as:

src/client.ts
export const client = createClient({ token: process.env.API_TOKEN });

title="…" sets the filename in the header. Without it, the build uses the language name.

Both Shiki themes ship as CSS variables rather than inlined colours. Switching colour mode therefore needs no re-render, and a light theme’s inline styles never override dark mode.

Code groups

Group alternatives in a <CodeGroup>. Each fence’s title becomes its tab label. The group gets a single copy button that follows the selected tab.

MDX
<CodeGroup>
```bash title="npm"
npm install
```
```bash title="pnpm"
pnpm install
```
</CodeGroup>
npm
npm install
pnpm
pnpm install
yarn
yarn install

Every group that offers the same labels in the same order moves together. The build remembers the reader’s choice for the next page. Add dropdown when a horizontal language strip would be crowded. Add sync={false} to keep one group local.

Diagrams

A ```mermaid fence renders a real Mermaid diagram, with title="…" as its caption. The drawing re-renders when the visitor changes colour mode.

Diagrams are drawn in the reader’s browser, and Mermaid is a large library. It is fetched only when a diagram comes within 600px of the viewport, so a page without diagrams costs nothing and a reader who never scrolls to one pays nothing. But a reader who does downloads about 180 KB of JavaScript (28 chunks, compressed) to draw it. Each diagram is then laid out on its own as it is reached, rather than the whole page at once.

MDX
```mermaid title="Publishing a change"
flowchart LR
  A["Author edits MDX"] --> B["Preview build"]
  B --> C["Production"]
```
flowchart LR A["Author edits MDX"] --> B["Preview build"] B --> C["Production"]
Publishing a change

Interactive controls appear on hover or keyboard focus: directional movement, zoom, reset, and a fullscreen viewer with drag, mouse-wheel zoom and keyboard navigation.

They are added by height, not by default on everything. A diagram taller than 120px gets them; a short one does not, because panning a shape that already fits on screen is a control with nothing to do. The flowchart above is three nodes wide and one row tall, which is why it has none. actions={true} forces them on regardless of size, actions={false} forces them off, and leaving the option out asks for the height rule.

Position the inline controls with placement="top-left", top-right, bottom-left, or bottom-right.

Any diagram type Mermaid supports works in the same fence. This one is a sequence diagram, and it is tall enough to cross the threshold, so it carries the controls the flowchart above does not. Hover it.

sequenceDiagram participant Agent participant MCP as "/mcp" participant Index as "Retrieval" Agent->>MCP: search("rate limits") MCP->>Index: query the corpus Index-->>MCP: passages, with page and line offsets MCP-->>Agent: results grouped by page Agent->>MCP: fetch("/agents/mcp") MCP-->>Agent: the page as clean Markdown Note over Agent: Answers by quoting, not from memory
An agent answering from your docs

A second diagram on a page that already has one costs no extra JavaScript: the 180 KB is per reader, not per diagram.

A <Mermaid> component also exists, for one-liners and for compatibility with content authored elsewhere. A fence is the better default choice.

Images

The build handles content images for you. It automatically adds loading="lazy" and decoding="async". It also adds intrinsic width and height, read from the file in public/, which is the file that matters. Without these values, the browser reserves no space, and the page jumps as each image arrives.

Wrap an image in <Frame> to give it a border and a caption:

MDX
<Frame caption="The rollout screen after a guard metric halts a stage.">
  ![A halted rollout](/screenshots/rollout-halted.png)
</Frame>

How wide a screenshot should be

Export at 1600px wide, and let the build do the rest. That is the widest size anything here displays: the reading column is 712px, which a 1392px image already covers at 2× for a retina screen, and the lightbox a reader opens by clicking a screenshot asks for around 1650px at its largest on a laptop.

A screenshot taken on a retina display arrives at 2400px or more, and the extra pixels are invisible. The browser scales them away, so nothing looks wrong while every reader downloads detail no screen shows. One real site was serving 1.3 MB of screenshots to deliver about 500 KB of visible detail.

Terminal
npm run images:optimize

That resizes anything in public/ wider than the cap, re-encoding WebP at a quality chosen for small text rather than for photographs. It is idempotent, and npm run images:check reports without writing. The test suite runs the same check, so an oversized screenshot fails the build rather than quietly costing bandwidth.

Icons

Every icon comes from Lucide through <Icon name="…" />. This applies in the navigation, in buttons, and in any icon slot on a Card or Tile. Never use a raw emoji or Unicode glyph (✦, ↗, ⌕). These render without error, but they look inconsistent beside Lucide’s icons, with a different weight, baseline, and style.

MDX
<Card title="Deploy everywhere">
  <Icon slot="icon" name="rocket" size={22} />
  …
</Card>

name is a short alias, not the Lucide icon name directly. See the icons map in src/components/Icon.astro. If the icon you need is not there yet, import it from @lucide/astro and add one line to that map. Do not use a glyph as a shortcut instead.

Video embeds

The build rewrites a raw <iframe> in content, at build time, into the same bordered, edge-to-edge card that <Frame> produces. So an embed pasted from YouTube or Loom gets an intrinsic aspect ratio instead of the browser’s old default box. This runs as a remark plugin, before the HTML stage, so it also handles JSX in content authored for other platforms.

Use these docs with your AI tools

An AI agent can read this documentation directly. You do not need an account or an API key. Everything here is public and read-only.

Query these docs via MCP

Recommended

Add this server to Claude, Claude Code, Cursor, Mistral, or any tool that supports MCP. Your agent can then search Duvlify documentation and read it in full, instead of answering from memory.

https://duvlify.dev/mcp
  • searchFind the passages that answer a question.
  • fetchRead one page in full, as Markdown.
  • list_pagesSee every page in this documentation.

Query these docs over HTTP

The same tools also work as plain web requests. Use this for scripts, or for any tool that does not support MCP. There is one endpoint per tool. Arguments go in the query string, and the answer comes back as JSON.

https://duvlify.dev/api/docs/search?query=custom+domain

Read the OpenAPI description. It is built from the same definitions as the tools, so it always matches what the endpoints do.

Read these docs as Markdown

Add .md to any page URL to get its Markdown source. You can also send the headerAccept: text/markdown to the page URL itself.

To read the whole documentation in one file, open llms-full.txt. For a short index of every page, open llms.txt.