D2 is declarative language to generate diagrams, it's like mermaid on steroids, it has a cli so it's easy to use.
Hugo doesn't support it at the moment of writing this, there is an open issue where the conversation is being done.
So in the meantime official support is added we're going to make our own integration. It will have 2 parts:
- A little HTTP server that receives the
d2code and return a resultingSVG - A custom hugo code block render hook
A simple HTTP server in go
This server only uses standard library and do the follow:
- receive
d2code in aPOSTpayload. - call
d2cli and get itsstd output. - return this
outputas response body.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os/exec"
)
func handleRenderRequest(w http.ResponseWriter, r *http.Request) {
requestBody, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
defer r.Body.Close()
output, err := renderText(string(requestBody))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, output)
}
func renderText(content string) (string, error) {
// d2 -, will render the text received in stdin
command := exec.Command("d2", "-")
command.Stdin = bytes.NewBuffer([]byte(content))
output, err := command.Output()
if err != nil {
return "", err
}
return string(output), nil
}
func main() {
http.HandleFunc("POST /render", handleRenderRequest)
http.ListenAndServe(":8080", nil)
}
We can also use
d2as a package but using it as a CLI is less code.
Now if we send a request the following request:
POST http://localhost:8080/render
shape: sequence_diagram
alice -> bob: What does it mean\nto be well-adjusted?
bob -> alice: The ability to play bridge or\ngolf as if they were games.
Custom code block render hook
Hugo allow us to define custom code block render hooks, we're going to define a custom one using d2 hook so when we define the following code it will call our server and insert its resulting SVG.
D2 code here
Now we need to create a new file in layouts/_default/_markup/render-codeblock-d2.html and put the following code:
{{- $renderHookName := "d2" }}
{{- $inner := trim .Inner "\n\r" }}
{{- $position := .Position }}
{{- $apiEndpoint := "http://localhost:8080/render" }}
{{- $opts := dict "method" "post" "body" $inner }}
{{- with resources.GetRemote $apiEndpoint $opts }}
{{- with .Err }}
{{- errorf "The %q code block render hook was unable to get the remote diagram. See %s. %s" $renderHookName $position . }}
{{- else }}
<div style="width: 600px">
{{ .Content | safeHTML }}
</div>
{{- end }}
{{- else }}
{{- errorf "The %q code block render hook was unable to get the remote diagram. See %s" $renderHookName $position }}
{{- end }}
This code uses resources.GetRemote to make a POST request to our server and then insert the response content as part of the document.
The render will only be done on build time so we don't need to have the render server always on
Conclusion
Hugo allows us to add custom features to our site and go allows us to accomplish task using on only standard library.
Enjoy! π

Top comments (0)