Contents

Enhancing Hugo with Dynamic Markdown, Mermaid, and TOC

Enhancing Hugo with Dynamic Markdown, Mermaid, and Styled TOC

After building my Hugo + LoveIt personal website,
I wanted to explore how to render Markdown dynamically in the browser
instead of pre‑rendering all content at Hugo build time.

In this guide, you will learn how to:

  • Render raw Markdown files dynamically with a GitHub‑style viewer
  • Add Mermaid diagrams for flowcharts and sequence diagrams
  • Enable MathJax for inline math expressions
  • Apply GitHub‑style tables and code block styling
  • Generate a dynamic Table of Contents (TOC) at runtime

1 Why Dynamic Markdown Rendering

By default, Hugo converts Markdown in content/ into static HTML at build time.
This is great for most blogs, but it has limitations if you want:

  • To view standalone .md files directly (perfect for sharing raw notes)
  • To avoid converting Markdown into Hugo shortcodes every time
  • To experiment with a GitHub‑style Markdown viewer on your site

To achieve this, I used Marked.js to render Markdown at runtime directly in the browser.


2 Adding Dynamic Markdown Viewer

To enable runtime Markdown rendering in Hugo, I created:

  1. A custom layoutlayouts/markdown-viewer.html
  2. A JavaScript rendererstatic/js/markdown-viewer.js

The script:

  • Fetches Markdown files from static/files/*.md
  • Renders them with Marked.js
  • Applies syntax highlighting with Highlight.js

HTML Structure:

<div id="markdown-container" data-file="/files/sample.md">
  Loading markdown...
</div>
<!-- Include scripts -->
<link rel="stylesheet" href="/css/github-markdown.min.css">
<script src="/js/marked.min.js"></script>
<script src="/js/highlight.min.js"></script>
<script src="/js/markdown-viewer.js"></script>

3 Table Styling and GitHub Look

By default, Markdown tables in Hugo are plain and lack visual distinction.
To improve readability, I added GitHub‑style table formatting:

  border-collapse: collapse;
  margin: 1rem 0;
  width: 100%;
}

.markdown-body th,
.markdown-body td {
  border: 1px solid #d0d7de;
  padding: 6px 13px;
}

.markdown-body tr:nth-child(even) {
  background-color: #f6f8fa;
}

And ensured markdown-viewer.js adds .markdown-body class for styling. All tables automatically inherit GitHub-like styling.


4 Adding Mermaid Diagram Support

LoveIt does not natively support Mermaid for dynamically loaded Markdown.

Steps I followed:

  1. Added Mermaid.js to static/js/mermaid.min.js
  2. In markdown-viewer.js, converted fenced code blocks:
// Convert mermaid code fences into 
container.querySelectorAll('pre code.language-mermaid').forEach(block => { const div = document.createElement('div'); div.classList.add('mermaid'); div.textContent = block.textContent; block.parentNode.replaceWith(div); }); if (window.mermaid) { mermaid.initialize({ startOnLoad: false }); mermaid.run({ querySelector: ".mermaid" }); }

Now my Markdown with:

 ```mermaid graph TD A[Start] --> B[Process] --> C[End] ``` 

…renders a live Mermaid diagram in the browser.


5 MathJax for Math Expressions

To support LaTeX‑style math like:

$$ 10 \mod 7 = 3 $$

I included MathJax v3:

<script>
window.MathJax = {
  tex: { inlineMath: [['$', '$'], ['\\(', '\\)']] },
  svg: { fontCache: 'global' }
};
</script>
<script src="/js/tex-svg.js"></script>

Then triggered it in JS:

if (window.MathJax) {
  window.MathJax.typesetPromise();
}

6 Dynamic Table of Contents (TOC)

Hugo normally builds TOC at build time. Since my Markdown is dynamic, I built the TOC after rendering:

  • Detected headings h1, h2, h3 in #markdown-container
  • Populated the existing LoveIt <nav id="TableOfContents"> dynamically

Simplified Example:

const tocNav = document.getElementById("TableOfContents");
const headings = container.querySelectorAll("h1, h2, h3");

if (tocNav && headings.length > 0) {
  const ul = document.createElement("ul");
  headings.forEach(h => {
    const id = h.id || h.textContent.trim().replace(/\s+/g, "-").toLowerCase();
    h.id = id;

    const li = document.createElement("li");
    const a = document.createElement("a");
    a.textContent = h.textContent;
    a.href = "#" + id;
    li.appendChild(a);
    ul.appendChild(li);
  });
  tocNav.innerHTML = "";
  tocNav.appendChild(ul);
}

Now LoveIt’s floating TOC shows headings for dynamically loaded Markdown too.

7 I’ll show step‑by‑step how to

  • Create a HTML viewer layout in Hugo
  • Add a JavaScript file to render Markdown dynamically
  • Support Mermaid diagrams, MathJax formulas, and GitHub‑style tables
  • Generate a dynamic Table of Contents (TOC) for the loaded Markdown file

7.1 Create a Hugo Layout for the Markdown Viewer

Inside your Hugo project:

layouts/markdown-viewer.html

Example:

html
{{- define "title" }}{{ .Title }} - {{ .Site.Title }}{{ end -}}

{{- define "content" -}}
  <!-- Markdown Viewer Scripts -->
  <!-- MathJax v3 Configuration -->
  <script>
  window.MathJax = {
    tex: { inlineMath: [['$', '$'], ['\\(', '\\)']] },
    svg: { fontCache: 'global' }
  };
  </script>
  <!-- Load MathJax AFTER config -->
  <script src="/js/tex-svg.js"></script>
<!--  <link rel="stylesheet" href="/css/markdown-viewer.css"> -->
  <link rel="stylesheet" href="/css/github.min.css">
  <link rel="stylesheet" href="/css/github-markdown.min.css">
  <script src="/js/marked.min.js"></script>
  <script src="/js/highlight.min.js"></script>
  <script src="/js/markdown-viewer.js"></script>
  <script src="/js/mermaid.min.js"></script>
  
  <script>
    mermaid.initialize({ startOnLoad: false });
  </script>
  <!-- Markdown Viewer Scripts -->
    {{- $params := .Scratch.Get "params" -}}

    {{- $toc := $params.toc -}}
    {{- if eq $toc true -}}
        {{- $toc = .Site.Params.page.toc | default dict -}}
    {{- else if eq $toc false -}}
        {{- $toc = dict "enable" false -}}
    {{- end -}}

    {{- /* Auto TOC */ -}}
    {{- if ne $toc.enable false -}}
        <div class="toc" id="toc-auto">
            <h2 class="toc-title">{{ T "contents" }}</h2>
            <div class="toc-content{{ if eq $toc.auto false }} always-active{{ end }}" id="toc-content-auto"></div>
        </div>
    {{- end -}}

    <article class="page single">
        {{- /* Title */ -}}
        <h1 class="single-title animate__animated animate__flipInX">{{ .Title | emojify }}</h1>

        {{- /* Subtitle */ -}}
        {{- with $params.subtitle -}}
            <h2 class="single-subtitle">{{ . }}</h2>
        {{- end -}}

        {{- /* Meta */ -}}
        <div class="post-meta">
            <div class="post-meta-line">
                {{- $author := $params.author | default .Site.Params.Author.name | default (T "author") -}}
                {{- $authorLink := $params.authorlink | default .Site.Params.Author.link | default .Site.Home.RelPermalink -}}
                <span class="post-author">
                    {{- $options := dict "Class" "author" "Destination" $authorLink "Title" "Author" "Rel" "author" "Icon" (dict "Class" "fas fa-user-circle fa-fw") "Content" $author -}}
                    {{- partial "plugin/a.html" $options -}}
                </span>

                {{- $categories := slice -}}
                {{- range .Params.categories -}}
                    {{- $category := partialCached "function/path.html" . . | printf "/categories/%v" | $.Site.GetPage -}}
                    {{- $categories = $categories | append (printf `<a href="%v"><i class="far fa-folder fa-fw" aria-hidden="true"></i>%v</a>` $category.RelPermalink $category.Title) -}}
                {{- end -}}
                {{- with delimit $categories "&nbsp;" -}}
                    &nbsp;<span class="post-category">
                        {{- dict "Categories" . | T "includedInCategories" | safeHTML -}}
                    </span>
                {{- end -}}
            </div>
            <div class="post-meta-line">
                {{- with .Site.Params.dateformat | default "2006-01-02" | .PublishDate.Format -}}
                    <i class="far fa-calendar-alt fa-fw" aria-hidden="true"></i>&nbsp;<time datetime="{{ . }}">{{ . }}</time>&nbsp;
                {{- end -}}
                <i class="fas fa-pencil-alt fa-fw" aria-hidden="true"></i>&nbsp;{{ T "wordCount" .WordCount }}&nbsp;
                <i class="far fa-clock fa-fw" aria-hidden="true"></i>&nbsp;{{ T "readingTime" .ReadingTime }}&nbsp;
                {{- $comment := .Scratch.Get "comment" | default dict -}}
                {{- if $comment.enable | and $comment.valine.enable | and $comment.valine.visitor -}}
                    <span id="{{ .RelPermalink }}" class="leancloud_visitors" data-flag-title="{{ .Title }}">
                        <i class="far fa-eye fa-fw" aria-hidden="true"></i>&nbsp;<span class=leancloud-visitors-count></span>&nbsp;{{ T "views" }}
                    </span>&nbsp;
                {{- end -}}
            </div>
        </div>

        {{- /* Featured image */ -}}
        {{- $image := $params.featuredimage -}}
        {{- with .Resources.GetMatch "featured-image" -}}
            {{- $image = .RelPermalink -}}
        {{- end -}}
        {{- with $image -}}
            <div class="featured-image">
                {{- dict "Src" . "Title" $.Description "Resources" $.Resources | partial "plugin/img.html" -}}
            </div>
        {{- end -}}

        {{- /* Static TOC */ -}}
        {{- if ne $toc.enable false -}}
            <div class="details toc" id="toc-static"  data-kept="{{ if $toc.keepStatic }}true{{ end }}">
                <div class="details-summary toc-title">
                    <span>{{ T "contents" }}</span>
                    <span><i class="details-icon fas fa-angle-right" aria-hidden="true"></i></span>
                </div>
                <div class="details-content toc-content" id="toc-content-static">
                    {{- dict "Content" .TableOfContents "Ruby" $params.ruby "Fraction" $params.fraction "Fontawesome" $params.fontawesome | partial "function/content.html" | safeHTML -}}
                </div>
            </div>
        {{- end -}}

        {{- /* Content */ -}}
        <div class="content" id="content">
            {{- dict "Content" .Content "Ruby" $params.ruby "Fraction" $params.fraction "Fontawesome" $params.fontawesome | partial "function/content.html" | safeHTML -}}
        </div>

        {{- /* Footer */ -}}
        {{- partial "single/footer.html" . -}}

        {{- /* Comment */ -}}
        {{- partial "comment.html" . -}}
    </article>
{{- end -}}

Key points:

  • data-file="{{ .Params.markdownFile }}" → Tells JS which Markdown to load
  • TOC <nav> is pre‑rendered for LoveIt floating TOC

7.2 Create the JavaScript Renderer

File:

static/js/markdown-viewer.js

Full Script (Development Version)

document.addEventListener("DOMContentLoaded", () => {
  const container = document.getElementById("markdown-container");
  if (!container) return;

  const file = container.dataset.file;

  fetch(file)
    .then(res => {
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.text();
    })
    .then(md => {
      // 1 Render Markdown to HTML using Marked.js
      container.innerHTML = marked.parse(md);

      // 2 Highlight regular code blocks
      hljs.highlightAll();

      // 3 Convert mermaid fenced code blocks to <div class="mermaid">
      container.querySelectorAll('pre code.language-mermaid').forEach(block => {
        const div = document.createElement('div');
        div.classList.add('mermaid');
        div.textContent = block.textContent; // keep inner text as raw mermaid code
        block.parentNode.replaceWith(div);
      });
      
      // 4 Force table styling if no CSS is applied
  container.querySelectorAll('table').forEach(tbl => {
    tbl.classList.add('table', 'table-striped', 'table-bordered', 'table-hover');

    // Inline fallback styling (guaranteed)
    tbl.style.borderCollapse = 'collapse';
    tbl.style.border = '1px solid #d0d7de';
    tbl.style.margin = '1rem 0';
    tbl.style.width = '100%';

    tbl.querySelectorAll('th, td').forEach(cell => {
      cell.style.border = '1px solid #d0d7de';
      cell.style.padding = '6px 13px';
    });

    tbl.querySelectorAll('tr:nth-child(even)').forEach(row => {
      row.style.backgroundColor = '#f6f8fa'; // GitHub-like striped rows
    });
  });

      // 5 Trigger Mermaid rendering if available
      if (window.mermaid) {
        mermaid.initialize({ startOnLoad: false });
        mermaid.run({ querySelector: ".mermaid" });
      }
      
      // 6 Trigger MathJax typesetting for equations
      if (window.MathJax) {
        window.MathJax.typesetPromise();
      }
      
     // 7 Minimal LoveIt-style Dynamic TOC generation
	
		
	console.log("🔹 Starting dynamic TOC generation...");

	const tocAuto = document.getElementById("toc-content-auto");
	if (!tocAuto) {
	  console.warn("⚠️ TOC container #toc-content-auto NOT found in DOM.");
	} else {
	  console.log("✅ Found #toc-content-auto container:", tocAuto);
	}

	const markdownContainer = document.getElementById("markdown-container");
	if (!markdownContainer) {
	  console.warn("⚠️ Markdown container #markdown-container NOT found in DOM.");
	} else {
	  console.log("✅ Found #markdown-container container:", markdownContainer);
	}

	if (tocAuto && markdownContainer) {
	  const tocNav = document.getElementById("TableOfContents");
	  if (!tocNav) {
	    console.warn("⚠️ <nav id='TableOfContents'> NOT found inside #toc-auto.");
	  } else {
	    console.log("✅ Found <nav id='TableOfContents'> inside #toc-auto:", tocNav);
	  }

	  const headings = markdownContainer.querySelectorAll("h1, h2, h3");
	  console.log("🔹 Headings detected inside #markdown-container:", headings.length);

	  if (tocNav && headings.length > 0) {
	    const ul = document.createElement("ul");

	    headings.forEach((h, idx) => {
	      // Generate a safe ID for the heading
	      const headingId =
		h.id ||
		h.textContent
		  .trim()
		  .replace(/\s+/g, "-")
		  .replace(/[^\w-]/g, "")
		  .toLowerCase();

	      h.id = headingId;
	      console.log(`  ➜ Heading ${idx + 1}: "${h.textContent}" -> #${headingId}`);

	      const li = document.createElement("li");
	      const a = document.createElement("a");
	      a.textContent = h.textContent;
	      a.href = "#" + headingId;

	      // Mark first heading as active for LoveIt styling
	      if (idx === 0) {
		li.classList.add("has-active");
		a.classList.add("active");
	      }

	      li.appendChild(a);
	      ul.appendChild(li);
	    });

	    // Clear any existing content and append generated TOC
	    tocNav.innerHTML = "";
	    tocNav.appendChild(ul);

	    console.log("✅ Dynamic TOC successfully generated with", headings.length, "entries.");
	  } else {
	    console.warn("⚠️ TOC generation skipped: either no headings or tocNav missing.");
	  }
	}


      
    })
    .catch(err => {
      container.innerHTML = `<p style="color:red">Failed to load markdown: ${err}</p>`;
      console.error(err);
    });
});

This script:

  • Loads Markdown dynamically
  • Highlights code and styles tables
  • Supports Mermaid and MathJax
  • Generates LoveIt‑style TOC

7.3 Create a Sample Hugo Content Page

Inside your Hugo project:

content/posts/markdown-demo/index.md

Front Matter:

---
title: "Dynamic Markdown Demo"
date: 2025-08-01
---
<div id="markdown-container" data-file="/files/sample.md">
  Loading markdown...
</div>
  • Uses the markdown-viewer layout
  • Loads Markdown from /static/files/sample.md

7.4 Provide a Sample Markdown File

static/files/sample.md

Example Markdown:

# 1️⃣ Introduction

This is a dynamic markdown demo with **Mermaid**, **MathJax**, and **TOC**.

```mermaid
graph TD
A[Start] --> B[Process] --> C[End]
```

$10 \mod 7 = 3$

| Item   | Price |
| ------ | ----- |
| Apple  | $1    |
| Banana | $2    |

8 Test in Local Hugo Server

hugo server -D

Open the page:

http://localhost:1313/posts/markdown-demo/

You should see:

  • Rendered Markdown
  • Highlighted code
  • Mermaid diagram
  • Math formula
  • GitHub‑style table
  • Floating TOC

Technically authored by me, accelerated with insights from ChatGPT by OpenAI.” Refer: Leverage ChatGPT

Happy Learning