Skip to content
Blume
Esc
↑↓navigate↵open⌘Jpreview
Guides

Writing

Create architecture documentation with Mermaid diagrams

An architecture page with a component flowchart and a request sequence diagram, kept as Mermaid source in Git, plus text that explains the system wherever diagrams don't render.

By 11 min read

To keep architecture explanations and diagrams together in Git, write the architecture page as an MDX file in the same repository as the code, and draw each diagram in Mermaid, a text syntax for diagrams. The diagram source lives in a mermaid code fence on the page, or in a .mmd file the page includes. A change to the system then changes the prose and the diagram in one pull request, reviewers read both as line diffs, and Blume draws the diagrams in the reader's browser, in the site's light or dark theme.

This guide builds that page for Acme Messages, a fictional service that sends email and SMS: a flowchart of its components, a sequence diagram of one request, and text that states every relationship the diagrams show. That text isn't optional. Blume renders diagrams on the client, so without JavaScript, in an EPUB export, and in the default search index, the diagram isn't there and the text is.

If your architecture notes only need to live in the repository, you may not need a docs site: GitHub renders Mermaid fences in Markdown files, issues, and pull requests. Blume adds a published site with navigation and search, and a Markdown copy of every page for agents. If a diagram must appear without JavaScript, render it to an SVG with Mermaid CLI, commit it under public/, and embed it as an image with alt text. You then keep the source and the image in sync by hand.

Lay out the files

Put the page and its diagram sources in the docs folder, side by side:

docs/
  architecture/
    _diagrams/
      components.mmd
      send-message.mmd
    index.mdx
  index.mdx
blume.config.ts

docs/architecture/index.mdx becomes the page at /architecture. Blume leaves any file or folder whose name starts with an underscore out of routing, navigation, search, and the sitemap, so _diagrams/ holds sources without turning them into pages.

The page must be .mdx. Diagrams are an MDX-only feature: in a .md page, a mermaid fence renders as a plain code block. Blume loads the Mermaid library only on pages that contain a diagram, so the rest of the site never downloads it.

Separate .mmd files are optional. The page pulls each one in with an include, which turns it into the same mermaid fence you could write inline. As files, diagrams paste straight into Mermaid's live editor, one diagram can appear on several pages, and Mermaid CLI can check them for syntax errors, which the build doesn't. For a one-off diagram, a fence written on the page works the same.

Draw the components as a flowchart

Start with the parts of the system and how they connect. This flowchart runs top to bottom, which fits a docs column better than a wide left-to-right layout:

flowchart TB
  accTitle: Acme Messages components
  accDescr: A client app calls the Messages API, which saves the message in Postgres and adds a send job to the queue. The delivery worker takes each job, sends it through the email or SMS provider, records the result in Postgres, and posts a delivery event to the customer's webhook endpoint.

  %% Nodes: a short ID, then the label readers see.
  client([Client app])
  api[Messages API]
  db[(Postgres)]
  queue[Send queue]
  worker[Delivery worker]
  email[Email provider]
  sms[SMS provider]
  hook([Customer webhook endpoint])

  %% Edges: one relationship per line.
  %% The API writes the row before it enqueues, so the worker always finds it.
  client -->|"POST /messages"| api
  api -->|insert, status queued| db
  api -->|enqueue send job| queue
  queue -->|deliver job| worker
  worker -->|send email| email
  worker -->|send SMS| sms
  worker -->|update status| db
  worker -->|delivery event| hook

  %% Dashed outlines mark systems Acme doesn't run. No colors, so both themes work.
  classDef external stroke-dasharray: 5 5
  class client,email,sms,hook external

A few choices here make the source easier to maintain:

  • IDs and labels. Each node has a short ID (api) and the label readers see (Messages API). Edges use the IDs, so renaming a component changes one line.
  • One edge per line. Mermaid can chain edges, but a line per relationship means a diff shows exactly which connection changed.
  • Comments. Mermaid ignores lines that start with %%. Use them for the reason behind an edge, where the next person to edit the diagram will see it.
  • Style without color. The external class dashes the outline of systems Acme doesn't run. It sets no colors, so it reads the same in Blume's light and dark themes.
  • An accessible name. Mermaid turns accTitle and accDescr into the SVG's <title> and <desc>, linked with aria-labelledby and aria-describedby, so assistive technology can name and describe the diagram. Keep the description to a summary, since the page text carries the detail.

Draw one request as a sequence diagram

A flowchart shows what connects to what. A sequence diagram shows the order things happen in, which is where most architecture questions are: what the caller waits for, and what happens on failure. This one follows a single email:

sequenceDiagram
  accTitle: Sending an email
  accDescr: The API saves and queues the message and answers 202 right away. The worker then sends it. On success it marks the message sent and posts a delivery event; on a provider error it requeues the job with a delay.

  participant C as Client app
  participant A as Messages API
  participant D as Postgres
  participant Q as Send queue
  participant W as Delivery worker
  participant P as Email provider
  participant H as Customer webhook

  C->>A: POST /messages
  A->>D: Insert message, status queued
  A->>Q: Enqueue send job
  A-->>C: 202 Accepted with message ID
  Q->>W: Deliver job
  W->>P: Send email
  alt Provider accepts
    P-->>W: Provider message ID
    W->>D: Set status sent
    W->>H: POST delivery event
  else Provider returns an error
    P-->>W: Error
    W->>Q: Requeue with a delay
  end

Solid arrows (->>) are calls here, and dotted ones (-->>) are responses. The alt and else blocks draw the two outcomes as labeled sections, and the one-letter aliases keep each message line short while the diagram shows full names.

Write the page around the diagrams

Now the page itself. Each diagram is followed by text that says the same thing in words: a table for the components and a numbered list for the request.

---
title: Architecture
description: How Acme Messages accepts, queues, and delivers email and SMS, and where each part keeps its state.
---

Acme Messages has one write path: the Messages API saves and queues each message, and the delivery worker sends it later. This page covers that path and every part it touches.

## Components

<include lang="mermaid">./_diagrams/components.mmd</include>

| Component | What it does | Talks to |
| --- | --- | --- |
| Messages API | Validates `POST /messages`, saves the message, and queues a send job | Postgres, send queue |
| Postgres | Stores each message and its status: `queued`, `sent`, or `failed` | Messages API, delivery worker |
| Send queue | Holds send jobs until a worker takes them | Messages API, delivery worker |
| Delivery worker | Sends each job through a provider and records the result | Send queue, Postgres, providers, customer webhooks |
| Email and SMS providers | Deliver the message. Acme doesn't run them. | Delivery worker |
| Customer webhook endpoint | Receives a delivery event per message. The customer runs it. | Delivery worker |

In the diagram, a dashed outline marks a system Acme doesn't run.

## Sending a message

<include lang="mermaid">./_diagrams/send-message.mmd</include>

1. The client app calls `POST /messages`.
2. The Messages API inserts the message into Postgres with status `queued`, then adds a send job to the queue. It writes first, so a worker never takes a job for a message that isn't saved.
3. The API answers `202 Accepted` with the message ID, without waiting for delivery.
4. The delivery worker takes the job and sends the message through the email or SMS provider.
5. If the provider accepts it, the worker sets the status to `sent` and posts a delivery event to the customer's webhook endpoint.
6. If the provider returns an error, the worker puts the job back on the queue with a delay. After the last retry, it sets the status to `failed`.

<include lang="mermaid"> splices the file in as a mermaid fence. The lang attribute is required, because Blume takes an included code file's language from its extension, and mmd isn't mermaid. Each include statement must sit on a line of its own.

The text under each diagram covers every place the diagram doesn't reach:

  • Without JavaScript. The prerendered HTML holds only the diagram's source, in an attribute. Mermaid draws the SVG in the browser.
  • In search. Blume's default search index leaves out code fences, diagrams included, so a search for "delivery worker" finds this page through the table, not the flowchart.
  • In exports. If you turn on export, the EPUB strips every SVG, so it has the text and no diagrams. PDF export prints the diagrams as they're drawn on screen, in the current theme. The PDF and EPUB guide covers what else changes in each format.
  • Read aloud. Narration skips diagrams and tables, so the numbered steps are what a listener hears.
  • For agents. The page's Markdown copy at /architecture.md has the includes spliced in, so an agent reads the Mermaid source and the prose together.

A quick test: cover each diagram and read the page. If a reader could still say what calls what, and in what order, the text is complete.

Review diagram changes in Git

Because the diagram is text, a pull request that changes the architecture shows the change line by line. Here Acme adds a dead-letter queue for jobs that fail every retry. The diagram gains a node and an edge, and the same pull request updates the table and step 6:

diff --git a/docs/architecture/_diagrams/components.mmd b/docs/architecture/_diagrams/components.mmd
--- a/docs/architecture/_diagrams/components.mmd
+++ b/docs/architecture/_diagrams/components.mmd
@@ -13,2 +13,3 @@ flowchart TB
   hook([Customer webhook endpoint])
+  dlq[Dead-letter queue]

@@ -24,2 +25,3 @@ flowchart TB
   worker -->|delivery event| hook
+  worker -->|after the last retry| dlq

diff --git a/docs/architecture/index.mdx b/docs/architecture/index.mdx
--- a/docs/architecture/index.mdx
+++ b/docs/architecture/index.mdx
@@ -16,3 +16,4 @@
 | Send queue | Holds send jobs until a worker takes them | Messages API, delivery worker |
-| Delivery worker | Sends each job through a provider and records the result | Send queue, Postgres, providers, customer webhooks |
+| Dead-letter queue | Keeps jobs that failed every retry, for inspection | Delivery worker |
+| Delivery worker | Sends each job through a provider and records the result | Send queue, Postgres, providers, customer webhooks, dead-letter queue |
 | Email and SMS providers | Deliver the message. Acme doesn't run them. | Delivery worker |
@@ -31,2 +32,2 @@
 5. If the provider accepts it, the worker sets the status to `sent` and posts a delivery event to the customer's webhook endpoint.
-6. If the provider returns an error, the worker puts the job back on the queue with a delay. After the last retry, it sets the status to `failed`.
+6. If the provider returns an error, the worker puts the job back on the queue with a delay. After the last retry, it sets the status to `failed` and moves the job to the dead-letter queue.

Three habits keep these reviews honest:

  • Change the diagram and its text together. A pull request that touches a .mmd file and not the page is the sign that the prose is now out of date.
  • Look at the drawing, not only the diff. Paste the changed file into the Mermaid Live Editor, or open your host's preview deployment if it builds pull requests.
  • Check the syntax before merging. blume build and blume validate don't parse Mermaid, because the browser does. A broken diagram only shows up on the page.

Mermaid CLI catches syntax errors from the command line. This loop renders each source to a throwaway SVG and names any file that fails:

for f in docs/architecture/_diagrams/*.mmd; do
  npx -y -p @mermaid-js/mermaid-cli@12.0.0 mmdc -q -i "$f" -o /tmp/diagram-check.svg \
    || echo "Broken: $f"
done

The first run downloads the CLI and the headless browser it renders with. Pin the CLI to the same major version as the mermaid package Blume installs, which npm ls mermaid shows. Treat it as a syntax check, not a preview: the CLI renders with Mermaid's defaults rather than Blume's settings, so its drawing can differ from the site's. If Chrome won't start on a Linux CI runner, Mermaid CLI documents a sandbox workaround.

Check the published page

Start the dev server and open http://localhost:4321/architecture:

npx blume dev

Both themes. Switch between light and dark with the toggle in the header. Each diagram redraws in Mermaid's matching theme, and the dashed outlines stay visible in both.

Text alternatives. Run this in the browser console. It prints each diagram's title and description, the text assistive technology gets:

for (const svg of document.querySelectorAll("blume-mermaid svg")) {
  console.log(svg.querySelector("title")?.textContent, "/", svg.querySelector("desc")?.textContent);
}

Without JavaScript. Turn off JavaScript in your browser's developer tools and reload. The diagrams are gone, and the table and steps should still explain the system on their own.

The Markdown copy. Fetch what agents read. Both diagrams appear as mermaid fences between the text:

curl http://localhost:4321/architecture.md

Then stop the dev server, build, and check the links:

npx blume build
npx blume validate

Troubleshooting

The diagram shows up as a code block

The page is a .md file. Blume turns mermaid fences into diagrams only in MDX, and an included file is parsed in the including page's format. Rename the page to .mdx.

The page says "Could not render this diagram."

Mermaid couldn't parse the source, and Blume shows this message in place of the diagram. Blume doesn't log Mermaid's parser error, so the browser console won't help: run the source through the mmdc loop above or the live editor to see the error. Two common causes: a lowercase end as a flowchart node ID, which Mermaid reads as a keyword (write End or pick another ID), and a label with parentheses or other special characters, which needs quotes, as in api["Messages API (v1)"].

The build fails with BLUME_INCLUDE_NOT_FOUND

A .mmd file was renamed or moved, and an include still points at the old path. The error names the path it looked for. Update the include, and search the docs folder for other pages that include the same file.

A diagram doesn't follow dark mode

A theme set in the diagram's own front matter (a config: block at the top of the fence) or in an %%{init}%% directive outranks the theme Blume picks, so the diagram stays in that theme. Remove it. Colors set with style or classDef also stay fixed in both themes, so a light fill can leave light text unreadable in dark mode. Style with outlines and dashes instead, as the external class does.

Links and HTML in a diagram don't work

Blume runs Mermaid at its strict security level, which disables click interactions and encodes HTML tags in labels as text. Put links in the prose next to the diagram instead.

A large diagram is too small to read

By default, Mermaid scales a diagram down to fit the column, so a wide one shrinks. Split it into smaller diagrams, one per concern, or let it keep its natural width and scroll sideways by adding front matter at the top of the source:

---
config:
  flowchart:
    useMaxWidth: false
---
flowchart TB

For a sequence diagram, use sequence: in place of flowchart:. A diagram can also ask for the ELK layout with layout: elk under config:; Blume loads that engine only for diagrams that request it.

Next step

Record why the architecture looks this way

Keep decision records beside the architecture page, so each change to a diagram can point to the decision behind it.

Read the engineering handbook guide

A step here not working for you? Report a broken step.

Keep going.More guides.

Upgrade your docs with Blume.

Install today and ship a production-grade docs site in minutes. Free and open source, forever.

npx blume init