🎺 How to Create a Widget for Hugo.

How to create a custom sidebar widget in a Hugo static website.

The context

For my blog, I use HBStack with the hbcards/theme, which relies heavily on Hugo modules.

This setup makes configuration easier and provides a clean, modular architecture.

One powerful feature of this theme is the availability of hooks at different stages of the rendering process. These hooks allow us to customize the blog without modifying the theme itself.

In this article, we will develop a custom sidebar widget that displays a random citation — a quote, joke, or piece of technical wisdom — each time a page is loaded.

Data in Hugo

At the project root, the ./data directory is used by Hugo at build time to populate the .Site.Data object.

Hugo supports several data formats, including JSON, TOML, YAML, and XML.

For this exercise, I created three data files, each representing a different category of citations:

1data
2└── sidebar
3    ├── jokes.yaml
4    ├── quotes.yaml
5    └── wisdom.yaml

Because Hugo generates a static website, this data cannot be queried dynamically at runtime.

Instead, the data must be rendered into the HTML during the build process.

For this, we use a hook provided by the HB theme:

layouts/partials/hugopress/modules/hb-custom/hooks/hb-blog-sidebar.html

To keep the page clean, we embed the data as hidden HTML elements using data-* attributes.

JavaScript can then read these attributes once the page has been loaded in the browser.

A loop based on site.Data.sidebar.<filename> is used to include the data in the generated website.

 1<div class="hb-module text-center">
 2  <aside class="hb-sidebar">
 3
 4    <section class="hb-sidebar-box random-citation js-random-citation">
 5      <h5 id="citation-title"></h5>
 6
 7      <blockquote id="citation-content"></blockquote>
 8
 9      <!-- Hidden category data -->
10      <div class="citation-data" hidden>
11
12        <!-- Quotes -->
13        <div class="citation-category" data-category="quote" data-title="💬 Quote">
14          {{ range site.Data.sidebar.quotes }}
15            <div
16              data-text="{{ .text }}"
17              data-author="{{ .author }}">
18            </div>
19          {{ end }}
20        </div>
21
22        <!-- Jokes -->
23        <div class="citation-category" data-category="joke" data-title="😂 IT Joke">
24          {{ range site.Data.sidebar.jokes }}
25            <div data-text="{{ . }}"></div>
26          {{ end }}
27        </div>
28
29        <!-- Wisdom -->
30        <div class="citation-category" data-category="wisdom" data-title="🧠 Tech Wisdom">
31          {{ range site.Data.sidebar.wisdom }}
32            <div data-text="{{ . }}"></div>
33          {{ end }}
34        </div>
35
36      </div>
37    </section>
38
39  </aside>
40</div>

TypeScript or JavaScript?

JavaScript is the language executed by the browser, while TypeScript is a superset of JavaScript that adds static typing and improved tooling.

In this project, we write our code in TypeScript (.ts) because:

  • It catches many errors at build time
  • It provides better autocompletion
  • It improves code readability and maintainability
  • It compiles down to plain JavaScript for the browser

Hugo Pipes can compile the TypeScript file into JavaScript, so the browser never sees the .ts file directly.

The theme does not automatically include custom JavaScript files.

Instead, we explicitly register our TypeScript file using a Hugo hook so that it can be compiled and injected into the page.

Let’s take the following example, located at:

./assets/hb/modules/custom/js/index.ts

 1console.log("✅ Random citation script loaded");
 2
 3// Random citation (no dependencies)
 4document.addEventListener("DOMContentLoaded", () => {
 5  const container = document.querySelector<HTMLElement>(".js-random-citation");
 6  if (!container) return;
 7
 8  const categories = container.querySelectorAll<HTMLElement>(
 9    ".citation-category"
10  );
11  if (!categories.length) return;
12
13  // 1️⃣ Pick a random category
14  const category =
15    categories[Math.floor(Math.random() * categories.length)];
16
17  const title = container.querySelector<HTMLElement>("#citation-title");
18  const blockquote = container.querySelector<HTMLElement>("#citation-content");
19
20  if (!title || !blockquote) return;
21
22  title.textContent = category.dataset.title ?? "";
23
24  // 2️⃣ Pick a random item inside the category
25  const items = category.querySelectorAll<HTMLElement>("div");
26  if (!items.length) return;
27
28  const chosen = items[Math.floor(Math.random() * items.length)];
29
30  const text = chosen.dataset.text ?? "";
31  const author = chosen.dataset.author;
32
33  blockquote.innerHTML = `
34    “${text}”
35    ${author ? `<footer>— ${author}</footer>` : ""}
36  `;
37});
38// End of random citation

How to use it

Hugo generates a static website, meaning all processed files are written to the ./public directory, which is then served by a web server.

After compilation, our TypeScript code is bundled into a JavaScript file inside the public directory.

To make the script available on the website, we load it using another HBStack hook located at:

layouts/partials/hugopress/modules/hb-custom/hooks/hb-head-end.html

1{{/* Load custom sidebar JS */}}
2{{ $js := resources.Get "hb/modules/custom/js/index.ts" | js.Build | minify | fingerprint }}
3<script src="{{ $js.RelPermalink }}" defer></script>

This ensures that the generated JavaScript is included in the page and executed by the browser.

The result

The result is a sidebar widget that displays a different citation each time a page is loaded.

The site remains fully static, while the browser randomly selects which citation to display at runtime.

Random citation widget

Troubleshooting

  1. Inspect the page source and search for the random-citation class to verify that the data is correctly embedded.

  2. Add a log statement at the top of index.ts:

1console.log("Random citation script loaded");

Open DevTools → Console and reload the page.

If the message does not appear, the script is not being loaded.

Verify the hb-head-end.html hook and the resources.Get path.

  1. Test DOM access manually.

In DevTools → Console, run:

1document.querySelector(".citation-category div")?.dataset

This allows you to verify that the data-* attributes are accessible from JavaScript.

Data flow diagram

The following diagram illustrates how data flows from Hugo to the browser and finally into the rendered widget.

At no point does JavaScript access Hugo data directly.

All data access happens through the DOM via data-* attributes that were generated at build time.

 1 BUILD TIME (Hugo)
 2 ─────────────────────────────────────────────────
 3
 4   data/sidebar/quotes.yaml
 5   data/sidebar/jokes.yaml
 6   data/sidebar/wisdom.yaml
 7              │
 8              ▼
 9      ┌─────────────────────┐
10      │   .Site.Data        │
11      │   (Hugo context)    │
12      └─────────┬───────────┘
13                │ Go templates
14                ▼
15      ┌─────────────────────┐
16      │ Generated HTML      │
17      │ hidden <div> nodes  │
18      │ data-* attributes   │
19      └─────────┬───────────┘
20                │
21                │ static HTML
22                ▼
23
24 RUNTIME (Browser)
25 ─────────────────────────────────────────────────
26
27      ┌─────────────────────┐
28      │ Browser DOM         │
29      │ (parsed HTML)       │
30      └─────────┬───────────┘
31                │
32                │ JavaScript
33                ▼
34      ┌─────────────────────┐
35      │ index.ts            │
36      │ - select category   │
37      │ - select citation   │
38      └─────────┬───────────┘
39                │
40                ▼
41      ┌─────────────────────┐
42      │ Visible Sidebar     │
43      │ Random citation     │
44      └─────────────────────┘
Sunday, October 4, 2026 Tuesday, January 13, 2026