---
title: "Code and diagrams"
description: "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."
canonical: "https://duvlify.dev/guides/diagrams-and-code"
updated: "2026-08-10"
---

# Code and diagrams

## 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`](https://github.com/DuvInc/duvlify/blob/main/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:

```ts title="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.

> **Warning: CodeBlock is not the same thing**
>
> A `<CodeBlock>` component exists for layouts a fence cannot express. Content
> passed to it is **not** highlighted. If you reach for it only to get a title
> or a copy button, use a fence instead. A fence already has both.

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>
````

```bash title="npm"
npm install
```

```bash title="pnpm"
pnpm install
```

```bash title="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 build downloads the library only on pages that contain a
diagram. The drawing re-renders when the visitor changes colour mode.

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

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

> **Warning: Quote your node labels**
>
> Unquoted Mermaid labels break on parentheses and commas. `A["Evaluate (server)"]`
> is fine. `A[Evaluate (server)]` causes a parse error in the diagram, not in
> the build, so it fails in front of a reader rather than in CI.

For diagrams with `actions={true}`, interactive controls appear on hover or
keyboard focus. These controls give directional movement, zoom, reset, and a
fullscreen viewer with drag, mouse-wheel zoom, and keyboard navigation.
Position the inline controls with `placement="top-left"`, `top-right`,
`bottom-left`, or `bottom-right`. Set `actions={false}` for a diagram that
should stay static.

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>
```

## Icons

Every icon comes from [Lucide](https://lucide.dev) 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`](https://github.com/DuvInc/duvlify/blob/main/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.
