Adding D2 to Hugo

- 2 mins read

I currently use a fork of a theme called poison for this site. It supports mermaid for generating certain visualisations (such as this one)

flowchart LR A --> B --> C

While mermaid is a fantastic tool, I prefer D2 because I think the syntax is a bit cleaner for certain diagrams and the output by default looks a bit nicer. It also supports more advanced diagrams like SQL tables.

direction:right A --> B --> C
users { shape: sql_table id: int { constraint: primary_key } email: string name: string } servers { shape: sql_table id: int { constraint: primary_key } name: string created_at: timestamp } user_server_junction { shape: sql_table id: int user_id: int server_id: int joined_at: timestamp } user_server_junction.user_id -> users.id user_server_junction.server_id -> servers.id

Shortcode

Hugo supports something called shortcodes, which are essentially commands that can be replaced at build time with custom output.

{{<d2>}}
users {
    shape: sql_table
    id: int { constraint: primary_key }
    email: string
    name: string
}

servers {
    shape: sql_table
    id: int { constraint: primary_key }
    name: string
    created_at: timestamp
}

user_server_junction {
    shape: sql_table
    id: int
    user_id: int
    server_id: int
    joined_at: timestamp
}

user_server_junction.user_id -> users.id
user_server_junction.server_id -> servers.id
{{</d2>}}

The source for the sql diagram generated before is wrapped in the shortcode.

At build time if the page contains d2 then a script is inserted that will generate the appropriate svg for each element on the page.

Note that d2 is a custom shortcode, one that can be found in the layouts/shortcodes directory for the theme.

d2.html

Although the source could be improved vastly, I’ve provided it here incase anyone would like to recreate it for themselves.

{{ if ne (.Page.Scratch.Get "hasD2") true}}
<script type="module">
    import { D2 } from 'https://esm.sh/@terrastruct/d2';
    const d2 = new D2();

    const d2Elements = document.querySelectorAll('.d2');
    for (const element of d2Elements) {
        const useElk = element.textContent.toLowerCase().includes('sql_table');
        const result = await d2.compile(element.textContent, { layout: useElk ? 'elk' : 'dagre' });
        const svg = await d2.render(result.diagram, { ...result.renderOptions, noXMLTag: true, pad: 25, scale: useElk ? null : 1 });
        element.innerHTML = svg;
    }
</script>
{{ .Page.Scratch.Set "hasD2" true}}
{{ end }}

 <div class="d2">
    {{- .Inner | safeHTML }}
 </div>

Future Improvement - Hugo Pipes

One improvement I would like to make in the future is to generate the diagrams once on the server rather then have every client do it. In order to do this I will need to use Hugo Pipes, which is a series of asset processing functions along with a native copy of d2 (the client uses d2.js).