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 simple HTTP server in go

This server only uses standard library and do the follow:

  • receive d2 code in a POST payload.
  • call d2 cli and get its std output.
  • return this output as 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 d2 as 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.

/images/blog/add-d2-support-to-hugo/render-post-result.png

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
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! 🎉