Back

Loading…

Documentation

πŸ“ Living Architecture Docs: Markdown + Mermaid in Your App

Date: 2026-05-25 Tags: documentation Β· mermaid Β· markdown Β· symfony Β· architecture


πŸ€” Why bother documenting architecture while you build?

Here is an honest scenario: you open a project you have not touched in three months. The code is there, the tests pass, the CI is green. But you cannot remember why the event ticketing module talks to the mailer service before the reservation is confirmed, or what the difference is between Reservation and ReservationType. You end up doing a full archaeological dig through git blame and controller actions β€” work you already did once.

Architecture documentation written while the code is fresh costs almost nothing. Architecture documentation written six months later costs an afternoon and is still wrong about two things.

The trap most developers fall into is treating docs as a final deliverable, like packaging. In reality they are a development tool β€” a forcing function that makes you articulate decisions you made instinctively, and a compass that keeps future-you (and teammates) oriented when the codebase has grown to hundreds of files.

graph LR
    A[🧠 Developer has context] -->|writes docs now| B[πŸ“„ Accurate living docs]
    A -->|skips docs| C[😰 Developer forgets context]
    C -->|three months later| D[πŸ•³οΈ Archaeology session]
    D --> E[πŸ“„ Docs that are 70% right]
    B -->|future developer opens project| F[βœ… Oriented in 5 minutes]
    E -->|future developer opens project| G[⚠️ Partially oriented, partially misled]

Writing a diagram forces you to think about your system as a whole β€” edges that feel awkward to draw are often a sign of coupling you have not noticed yet. A Mermaid diagram is not just pretty: it is a lightweight architectural review you do on yourself.


πŸ—ΊοΈ What we built: a Markdown-based internal blog

In SeriousDesign (SD-Sy7) we have an internal documentation and blog system built on top of Markdown files. The controller reads .md files from docs/ subdirectories, parses them with league/commonmark, and renders the HTML result inside a Twig template.

The pipeline looks like this:

sequenceDiagram
    participant Dev as πŸ‘©β€πŸ’» Developer
    participant FS as πŸ“‚ docs/ filesystem
    participant Ctrl as MarkdownReaderController
    participant Svc as MarkdownParserService
    participant CM as league/commonmark
    participant Twig as Twig template
    participant Browser as 🌐 Browser

    Dev->>FS: writes .md file
    Browser->>Ctrl: GET /docs/blog/2026-05-25/my-post
    Ctrl->>FS: file_get_contents(path.md)
    FS-->>Ctrl: raw Markdown string
    Ctrl->>Svc: parse(markdown, section)
    Svc->>Svc: processImages() β€” rewrites local img paths
    Svc->>CM: convert(markdown)
    CM-->>Svc: HTML string
    Svc-->>Ctrl: HTML string
    Ctrl->>Twig: render page.html.twig with content
    Twig-->>Browser: full HTML page

This covers headings, tables, task lists, code blocks, and images perfectly. The only thing it does not handle out of the box is Mermaid diagrams.


🧜 The Mermaid gap

Mermaid is a JavaScript diagramming library. You write diagram definitions in a text DSL and it renders them as interactive SVGs in the browser. It looks like this in Markdown:

```mermaid
graph TD
    A[Start] --> B{Decision}
    B -->|yes| C[Do the thing]
    B -->|no| D[Skip it]
```

The problem: league/commonmark β€” like every standard Markdown parser β€” has no idea what Mermaid is. It sees a fenced code block with language mermaid and faithfully outputs:

<pre><code class="language-mermaid">
graph TD
    A[Start] --> B{Decision}
    ...
</code></pre>

The diagram source is preserved as literal text inside a <code> element. Nothing renders. The browser just shows the raw DSL.

To get from that to an SVG diagram you need two things:

  1. Transform the DOM β€” turn <pre><code class="language-mermaid"> into a <pre class="mermaid"> element containing only the raw diagram text.
  2. Run Mermaid.js β€” the library scans for .mermaid elements and replaces them with SVGs.

πŸ”§ The Symfony implementation

In this project (Symfony 8.0, AssetMapper, no Webpack), all we needed to change was a single Twig template: templates/markdown_reader/page.html.twig.

Step 1 β€” prevent CSS fighting the diagram

The template already had a rule that gives every pre a grey background, a border, and padding. A Mermaid diagram is rendered as an SVG inside that pre β€” it would look boxed-in and wrong. We add a targeted override:

.markdown-content pre.mermaid {
    background-color: transparent;
    border: none;
    padding: 0;
    overflow-x: visible;
    text-align: center;
}

Step 2 β€” load Mermaid and rewire the DOM

We add a <script type="module"> block at the bottom of the template:

import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: false, theme: 'default' });

document.querySelectorAll('pre > code.language-mermaid').forEach(el => {
    const pre = el.parentElement;
    const diagram = document.createElement('pre');
    diagram.classList.add('mermaid');
    diagram.textContent = el.textContent;
    pre.replaceWith(diagram);
});

await mermaid.run();

That is the entire backend-to-diagram pipeline:

graph LR
    A["<pre><code class=language-mermaid>"] -->|querySelectorAll| B[JS finds it]
    B -->|creates| C["<pre class=mermaid>"]
    C -->|mermaid.run| D[πŸ–ΌοΈ SVG diagram]

Why type="module"? The await mermaid.run() call at the top level requires a module context. It also gives us native ESM imports without a bundler.

Why startOnLoad: false? Mermaid's default behaviour is to scan the DOM automatically on load. We want explicit control β€” first we rewrite the DOM, then we call run(). Setting it to false prevents a race condition where Mermaid fires before the code block transformation runs.


🌐 Vanilla implementation (any project, any stack)

You do not need Symfony, CommonMark, or AssetMapper. Here is the minimum you need in any HTML page:

The HTML page

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Docs</title>
    <style>
        pre { background: #f5f5f5; padding: 1rem; border-radius: 4px; }
        pre.mermaid { background: transparent; border: none; padding: 0; text-align: center; }
    </style>
</head>
<body>

    <!-- What a Markdown parser outputs for a mermaid block -->
    <pre><code class="language-mermaid">
graph TD
    A[User] --> B[Your App]
    B --> C[(Database)]
    </code></pre>

    <script type="module">
        import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
        mermaid.initialize({ startOnLoad: false, theme: 'default' });

        document.querySelectorAll('pre > code.language-mermaid').forEach(el => {
            const pre = el.parentElement;
            const diagram = document.createElement('pre');
            diagram.classList.add('mermaid');
            diagram.textContent = el.textContent;
            pre.replaceWith(diagram);
        });

        await mermaid.run();
    </script>

</body>
</html>

That is it. Save it, open it in a browser, and you get a rendered diagram.

Integration by stack

Stack Where to put the script block Notes
Plain HTML Before </body> Works as shown above
Jekyll / Hugo In your _layouts/post.html or layouts/_default/single.html Wrap in a {{ if .Params.mermaid }} guard if you want opt-in
Docusaurus Use the official @docusaurus/theme-mermaid plugin Zero config needed
MkDocs Add mermaid2 plugin to mkdocs.yml pip install mkdocs-mermaid2-plugin
VitePress Set markdown.config.mermaid: true in config Built-in support
Next.js / Astro Create a <MermaidDiagram> client component Pass diagram source as prop
WordPress Enqueue the script in functions.php + add the DOM transform Or use a plugin
Symfony (AssetMapper) <script type="module"> in the Twig template As shown in this post
Symfony (Webpack Encore) import mermaid from 'mermaid' in your entry JS Add to importmap or yarn

The DOM transform explained

graph TB
    subgraph Before ["Before mermaid.run()"]
        P["&lt;pre&gt;"]
        C["&lt;code class='language-mermaid'&gt;"]
        T["graph TD\n  A --> B"]
        P --> C --> T
    end

    subgraph After ["After transform + mermaid.run()"]
        PM["&lt;pre class='mermaid'&gt;"]
        SVG["πŸ–ΌοΈ &lt;svg&gt; diagram"]
        PM --> SVG
    end

    Before -->|"JS rewrites DOM\nthen mermaid.run()"| After

The key insight is that the parser (whether it's CommonMark, marked.js, python-markdown, or anything else) does not need to know about Mermaid at all. The parser does its job β€” escaping the code block as text β€” and the browser-side JS does the diagram rendering. Clean separation of concerns.


🎨 Mermaid diagram types worth knowing

Mermaid supports far more than flowcharts. Here is a quick tour of types useful for architecture documentation:

Flowchart / Graph β€” for decision flows and data flows

graph LR
    A[HTTP Request] --> B{Authenticated?}
    B -->|yes| C[Controller]
    B -->|no| D[Login Redirect]
    C --> E[Service Layer]
    E --> F[(Database)]

Sequence Diagram β€” for request lifecycles and API calls

sequenceDiagram
    Client->>API: POST /reservations
    API->>DB: INSERT reservation
    DB-->>API: OK
    API->>Mailer: sendConfirmation()
    Mailer-->>Client: πŸ“§ email
    API-->>Client: 201 Created

Entity Relationship β€” for data models

erDiagram
    EVENT ||--o{ RESERVATION_TYPE : has
    RESERVATION_TYPE ||--o{ RESERVATION : has
    RESERVATION ||--o{ TICKET : generates
    USER ||--o{ RESERVATION : makes

Pie chart β€” for quick breakdowns

pie title Codebase distribution
    "Controllers" : 22
    "Entities" : 30
    "Services" : 15
    "Templates" : 25
    "Tests" : 8

πŸ’‘ Architecture documentation tips

After wiring up diagram support, here is how to get the most out of it:

βœ… Do

  • Write the diagram the same day you design the feature. It takes 10 minutes when the architecture is in your head and an hour when it is not.
  • Use sequence diagrams for anything async β€” emails, queued jobs, webhooks. They make the order of operations explicit in a way prose cannot.
  • Link from code to docs. A comment with a path to the relevant doc is much more durable than a Slack thread.
  • Keep diagrams close to the code they describe. A diagram in docs/blog/ that describes src/Controller/EventManagement/ is easier to maintain than one in a separate wiki.
  • Treat outdated diagrams as bugs. An architecture diagram that no longer matches the code actively misleads people.

❌ Avoid

  • Mega-diagrams that try to show everything. One diagram per concern. A 40-node flowchart is harder to read than four 10-node ones.
  • Documenting what the code does instead of why. The code already says what it does. Docs should explain the reasoning, the trade-offs, and the non-obvious constraints.
  • Waiting until the project is done. The project is never done. Write as you go.

🏁 What we have now

With the changes described in this post, this blog system supports:

Feature Status
Markdown headings, lists, blockquotes βœ… Always worked
Tables βœ… Via TableExtension
Task lists βœ… Via TaskListExtension
Syntax-highlighted code blocks βœ… Browser-side (no server highlight)
Local image serving βœ… Via docs_image route
Mermaid diagrams βœ… Added today
Table of contents βœ… JS-generated from headings
Mobile-responsive layout βœ… Bootstrap + sticky nav

The full rendering chain, end to end:

graph TB
    subgraph Author ["✍️ Author"]
        MD["Markdown file\n(with mermaid blocks)"]
    end

    subgraph Server ["πŸ–₯️ Symfony Server"]
        Ctrl["MarkdownReaderController"]
        Svc["MarkdownParserService\n(league/commonmark)"]
        HTML["HTML string\n(pre > code.language-mermaid)"]
        Twig["page.html.twig"]
    end

    subgraph Browser ["🌐 Browser"]
        Raw["Rendered HTML"]
        JS["Script rewrites DOM"]
        Mermaid["Mermaid.run()"]
        SVG["✨ SVG Diagrams"]
    end

    MD --> Ctrl --> Svc --> HTML --> Twig --> Raw
    Raw --> JS --> Mermaid --> SVG

Two files changed. Zero new PHP dependencies. Every Markdown file across the entire docs/ tree now renders diagrams automatically.


A good architecture diagram is worth a thousand words of prose β€” and a thousand words of prose written today are worth ten thousand words you will struggle to reconstruct next year.

Contents