Adding Custom Short Codes to Hugo Blogs

Not so long ago, this very blog was being hosted on DigitalOcean using Ghost. However, it did not take me long to realize that for the kind of content I have in mind for the blog, a hosting on DigitalOcean with Ghost as a backend isn’t the best option. It was not cost effective and not easy to manage data backups without extra cost.

So, I moved my entire content (which had one post at that time) to Hugo and it has been an incredible experience to get it up and running. Netlify has been a saving grace with their amazing hosting and GitHub integration. All I had to worry about was making sure that I keep my domain name active and configure it the right way.

Base Setup

This entire post is written with the following configuration in mind.

  1. Hugo v0.73.0/extended
  2. Theme Pure by xiaoheiAh (with personal customizations)
▲ ~/Documents/personal-blog.harshanarayana.dev hugo version                                                 45s  ⇡ staging :: 36m :: ⬡
Hugo Static Site Generator v0.73.0/extended darwin/amd64 BuildDate: unknown

File Structure and Purpose

Relative Path in the RepoFile Description
config.tomlHugo Static Site Generator global configuration file
archetypes/default.mdDefault Configuration used to create a new Post
data/git-example.yamlGit Graph Commit Information Data file
themes/pure/assets/js/application.jsCustomized version of the base JavaScript setup for the Blog theme
themes/pure/layouts/partials/script.htmlPartial template that enables additional imports for JavaScript and CSS
themes/pure/layouts/shortcodes/mermaid.htmlShort Code to use for MermaidJS
themes/pure/layouts/shortcodes/gitgraph.htmlShort Code to use for GitGraph.Js Inclusion

Short Code for MermaidJS

Basic Configurations

Include Custom JavaScript and CSS Files

Let us make a few changes in the config.toml to include a few items that indicate all the required js and css files to be included into the static site to get the mermaidJS diagrams working.

# config.toml
[params.publicCDN.mermaidJS]
javascript = '<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.5.2/mermaid.min.js" integrity="sha256-y9inIfeUjJK2oCN6/ER6D/iC661lxlvgYGHt6WnZ/xk=" crossorigin="anonymous"></script>'

Conditionally Enable MermaidJS on posts

Now, we need to include the Javascript configured above in the config.toml so that it can be included while generating the static site if required.

<!-- 
  file: themes/pure/layouts/partials/script.html

  We selectively check if the post asks for MermaidJS to be included or not and include the JS reqired for it only
  when that parameter is set to true. This is done in order to ensure that we don't include unwanted js and slow
  down the page load time.
-->
{{- if and ( .Params.mermaidJS.enable ) (or .IsPage .IsHome) -}}
{{ .Site.Params.publicCDN.mermaidJS.javascript | safeHTML }}
{{- end }}

Create a shortcode file

<!-- file: themes/pure/layouts/shortcodes/mermaid.html -->
<div class="mermaid">
    {{.Inner}}
</div>

Activate MermaidJS templates

Initialize the mermaidjs instance if the window has a property of mermaid. Which happens only if the inclusion was performed in the step above.

// themes/pure/assets/js/application.js
if (window.mermaid) {
    var config = {
      theme: "null",
      logLevel: "fatal",
      securityLevel: "strict",
      startOnLoad: true,
      arrowMarkerAbsolute: false,
      flowchart: {
        htmlLabels: true,
        curve: "linear",
      },
      sequence: {
        diagramMarginX: 50,
        diagramMarginY: 10,
        actorMargin: 50,
        width: 150,
        height: 65,
        boxMargin: 10,
        boxTextMargin: 5,
        noteMargin: 10,
        messageMargin: 35,
        messageAlign: "center",
        mirrorActors: true,
        bottomMarginAdj: 1,
        useMaxWidth: true,
        rightAngles: false,
        showSequenceNumbers: false,
      },
      gantt: {
        titleTopMargin: 25,
        barHeight: 20,
        barGap: 4,
        topPadding: 50,
        leftPadding: 75,
        gridLineStartPadding: 35,
        fontSize: 11,
        fontFamily: '"Open-Sans", "sans-serif"',
        numberSectionStyles: 4,
        axisFormat: "%Y-%m-%d",
      },
    };
    window.mermaid.initialize(config);
  }

Setup Default options for ShortCode configuration

Configure the archetypes/default.md file to include the default values for shortCode configuration

---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
mermaidJS:
  enable: false
---

Consuiming MermaidJS Short Codes

Now we can create a new Post using the hugo command line option.

▲ ~/Documents/personal-blog.harshanarayana.dev hugo new content/posts/example.md                       ⇡ staging :: 10m :: ⬢
content/posts/example.md created
---
title: "Example"
date: 2020-07-03T17:14:38+05:30
draft: true
mermaidJS:
  enable: true
---
{{< mermaid >}}
sequenceDiagram
  A->B: Normal line
  B-->C: Dashed line
  C->>D: Open arrow
  D-->>A: Dashed open arrow
{{</ mermaid >}}

This will render into a sequence diagram shown below.

sequenceDiagram A->B: Normal line B-->C: Dashed line C->>D: Open arrow D-->>A: Dashed open arrow

Further Reading

  1. MermaidJS Documentation to explore the possible options and usage examples

Short Code for GitGraph.js

While exploring the possibilities of mermaidjs I noticed they have a primitive support for the gitGraph that can be used to render a commit graph of your git logs. However, it is very primitive and not really extendable yet as it is still in experimental phase. But there is a great alternative in the form of GitGraph.js

Including this one took a bit more effort that including the mermaidjs since the integration wasn’t really that simple and I wanted it to be backed by a data file instead of hard-coding the contents of the commit graph in the markdown document of the Blog.

Basic Configurations

Include Custom JavaScript and CSS Files

Same as we did while performing mermaidjs integration, we will go ahead and include a configuration file record to include javascript and css files that will bring in the gitgraph.js support.

[params.publicCDN.gitGraph]
javascript = '<script src="https://cdn.jsdelivr.net/npm/@gitgraph/js" crossorigin="anonymous"></script>'

Conditionally Enable GitGraph on posts

<!-- 
  file: themes/pure/layouts/partials/script.html

  We selectively check if the post asks for MermaidJS to be included or not and include the JS reqired for it only
  when that parameter is set to true. This is done in order to ensure that we don't include unwanted js and slow
  down the page load time.
-->
{{- if and ( .Params.gitGraph.enable ) (or .IsPage .IsHome) -}}
{{ .Site.Params.publicCDN.gitGraph.javascript | safeHTML }}
{{- end }}

Activate GitGraph instance

  if (window.GitgraphJS) {
    const graphContainer = document.getElementById("graph");
    var template = {
      colors: ["#6963FF", "#47E8D4", "#6BDB52", "#E84BA5", "#FFA657"],
      arrow: { size: 5, color: null, offset: -1.5 },
      branch: {
        color: "#e0ebeb",
        lineWidth: 3,
        mergeStyle: "straight",
        spacing: 30,
        label: {
          display: true,
          bgColor: "white",
          font: "normal 10pt 'Fira Mono', monospace",
          borderRadius: 5,
        },
      },
      commit: {
        spacing: 50,
        hasTooltipInCompactMode: true,
        dot: {
          size: 5,
          strokeWidth: 1,
          strokeColor: "#e0ebeb",
          font: "normal 10pt 'Fira Mono', monospace",
        },
        message: {
          display: true,
          displayAuthor: false,
          displayHash: true,
          color: "black",
          font: "normal 10pt 'Fira Mono', monospace",
        },
      },
      tag: {},
    };
    var gitGraph = window.GitgraphJS.createGitgraph(graphContainer, {
      template: template,
      author: "Harsha Narayana <harsha2k4@gmail.com>",
      orientation: "vertical",
      layout: "responsive",
      svgClasses: ["svc-content"],
    });
    window.gitGraph = gitGraph;

Setup Default options for ShortCode configuration

Configure the archetypes/default.md file to include the default values for shortCode configuration

---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
gitGraph:
  enable: false
---

Setup data Rendering using Data Files and ShortCode

Hugo has an incredible concept called data templates that lets you use toml yaml or json file as a source of data and use them in your shortcode templates as you see fit. In order to activate this all you need to do is create the following.

Create a file data/example.toml and you are done. Now you can access the data from the file using example arg and combine it with a magic sauce {{ $dataFile := index .Site.Data "example" }}

//  file: themes/pure/layouts/shortcodes/gitgraph.html
<div id="graph" class="svg-container"></div>

{{ $arg := .Get 0 }}
{{ $dataFile := index .Site.Data $arg }}

<script>
  window.onload = (event) => {
    var g = window.gitGraph;

    function createNewGitBranch(data){
      /**
       * Check if there is a property for the window object that matches the
       * name of the branch in question. If it is present, that means a branch
       * has already been created and there is nothing else that needs to be done.
       * 
       * However, if the is no property matching the branch name is present then
       * this is a new branch and needs to be created.
       * 
       * You are free to customize the way these properties are managed at the
       * window lever for better optimization.
       */
      var parentBranch = data.meta.parent;
      var branchName = data.meta.name

      if ( window.hasOwnProperty(branchName) ) {
        return branchName;
      }

      if ( parentBranch === "" ) {
        var branch = g.branch(branchName);
      } else {
        var branch = window[parentBranch].branch(branchName);
      }
      window[branchName] = branch;
      return branchName
    }

    /**
     * Conditionally add comments and other branch level components so that the
     * graph is built and rendered on the UI.
     */
    {{ range $branch := $dataFile.gitGraph }}
    var branchName = createNewGitBranch({{ .branch }});
    {{ range $commit := .branch.commit }}
    window[branchName].commit({{ . }});
    {{ end }}
    {{ if index .branch "merge" }}
    var baseBranch = {{ .branch.merge.base }};
    var mergeCommit = {{ .branch.merge.commit }};
    window[baseBranch].merge(window[branchName], mergeCommit);
    {{ end }}
    {{ if index .branch "tag" }}
    var branchToTag = {{ .branch.tag.branch }};
    var tagVersion = {{ .branch.tag.version }};
    window[branchToTag].tag(tagVersion);
    {{ end }}
    {{ end }}
  }
</script>

Create an Example Data File

# data/git-example.yaml
gitGraph:
  - branch:
      meta:
        parent: ""
        name: master
      commit:
        - Initial commit
  - branch:
      meta:
        name: develop
        parent: master
      commit:
        - Cut new Dev Branch
  - branch:
      meta:
        name: feature/f1
        parent: develop
      commit:
        - "feat: introduce form layout"
  - branch:
      meta:
        name: feature/f2
        parent: develop
      commit:
        - "feat: introduce responsive layout"
        - "enh: add media query support"
        - "enh: add parameterized template override"
      merge:
        base: develop
        commit: "feat: branch responsive layout into main dev"

Consuming GitGraph shortcode

---
title: "Example"
date: 2020-07-03T17:14:38+05:30
draft: true
gitGraph:
  enable: true
---
{{< gitgraph "git-example" >}}

Which will render a gitgraph as the one below.

Further Reading

  1. GitGraph.js Documentation has a few great examples and documentation to indicate the usage.

To Do

  1. If you carefully look at the current shortcode implementation, it can support only one graph per page due to how the branches are managed. This needs to be customized to add an additional property in the data file to take the name of the graph and store all properties inside that named property on window object.