Hugo: Hoe je Stimulus integreert met een import map

Door Pieter Vogelaar 7 Maart 2026

Ontdek hoe je Stimulus integreert met Hugo om moderne JavaScript aan je statische site toe te voegen. Deze tutorial maakt gebruik van een import map en de ingebouwde asset pipeline. Er zijn geen build tools nodig (zoals Webpack), wat het werken ermee erg fijn en snel maakt.

Hoewel uitgebreide JavaScript frameworks vaak aanvoelen als overkill voor een statische site generator, biedt Stimulus een HTML-first benadering die perfect aansluit bij de filosofie van Hugo. Het neemt je volledige frontend niet over, maar je voegt simpelweg data-controller attributen toe aan je bestaande HTML.

Stimulus wordt standaard gebruikt in het Ruby on Rails framework, maar het kan ook uitstekend worden ingezet als een compact en krachtig op zichzelf staand JavaScript framework in combinatie met Hugo.

Deze tutorial gaat ervan uit dat je je eigen thema kunt aanpassen. Als je een standaardthema gebruikt en dit niet direct wilt wijzigen, kun je waarschijnlijk gebruikmaken van de theme overrides die Hugo standaard biedt. Alle voorbeeldpaden zijn relatief aan de theme-map, bijvoorbeeld themes/my-theme.

We hanteren de volgende mappenstructuur (alleen relevante bestanden):

themes/
  my-theme/
    assets/
      js/
        controllers/
          application.js
          contact_form_controller.js
        utilities/                        # optioneel
          index.js                        # optioneel
        application.js
      vendor/
        js/
          @hotwired--stimulus.js
          @popperjs--core.js              # optioneel
          bootstrap.esm.min.js            # optioneel
    layouts/
      partials/
        headers.html
        importmap.html
    static/
      bootstrap.esm.min.js.map            # optioneel

De Bootstrap framework bestanden zijn toegevoegd om een completer voorbeeld te geven van het werken met ESM (JavaScript modules). Het bootstrap esm bestand kan worden gedownload uit de source files ZIP. Bootstrap vereist ook @popperjs/core. Uiteraard is het Bootstrap gedeelte volledig optioneel.

Hugo asset pipeline

Wanneer hugo build wordt uitgevoerd, worden bestanden uit de static map ongewijzigd naar de public map gekopieerd. Hugo biedt echter een asset pipeline voor de assets map. Een bestand zoals assets/js/application.js wordt dan bijvoorbeeld js/application.b9cd4be4fcb1aad651fcbc2e41c7a7edc940ba726286c460f6ab072f368e76af.min.js in de public map. Er wordt een sha256 fingerprint gemaakt van de inhoud van het bestand, en die hash wordt onderdeel van de bestandsnaam. Dit is ideaal voor caching: het bestand kan voor onbepaalde tijd worden gecached en zodra de inhoud wijzigt, verandert de hash. Zo krijgt de gebruiker altijd de meest recente versie.

Import map

Voeg in het layout bestand dat de <head> tag beschrijft (bijvoorbeeld layouts/partials/headers.html) het volgende toe:

{{ partial "importmap.html" . }}

Je bent misschien gewend om <script> tags vlak boven de </body> tag te plaatsen. Omdat ES Modules echter standaard deferred (uitgesteld) worden geladen, hoef je je geen zorgen te maken over het blokkeren van de HTML parsing.

Maak layouts/partials/importmap.html aan met de volgende inhoud:

{{- $imports := dict -}}
{{- $vendorJS := resources.Match "vendor/js/**.js" -}}
{{- $customJS := resources.Match "js/**.js" -}}
{{- $allJS := $vendorJS | append $customJS -}}

{{- range $allJS -}}
  {{- $file := . | fingerprint -}}

  {{- if not (strings.HasPrefix .Name "/vendor/") -}}
    {{- $file = $file | minify -}}
  {{- end -}}

  {{- $key := .Name |
      strings.TrimPrefix "/vendor/js/" |
      strings.TrimPrefix "/js/" |
      strings.TrimSuffix ".esm.min.js" |
      strings.TrimSuffix ".min.js" |
      strings.TrimSuffix ".js" |
      strings.TrimSuffix "/index" |
      replaceRE "--" "/"
  -}}

  {{- $imports = merge $imports (dict $key $file.RelPermalink) -}}
{{- end -}}

{{- $importMap := dict "imports" $imports -}}

<script type="importmap">
{{ $importMap | jsonify (dict "indent" "  ") }}
</script>

<script type="module">import 'application'</script>

Alle JS-paden worden (recursief) gezocht in de mappen assets/vendor/js en assets/js. Het resultaat is een import map die er ongeveer zo uitziet:

<script type="importmap">
{
  "imports": {
    "@hotwired/stimulus": "/vendor/js/@hotwired--stimulus.d6c1c73b686d843e46e47599e038ee4deacfed039aa787a640b73a16dbf0a27b.js",
    "@popperjs/core": "/vendor/js/@popperjs--core.d1913da117b5d1e240698b51e32666f99e97bcb496aa2675f12b71ce733725d8.js",
    "application": "/js/application.b9cd4be4fcb1aad651fcbc2e41c7a7edc940ba726286c460f6ab072f368e76af.min.js",
    "bootstrap": "/vendor/js/bootstrap.esm.min.f9280841ae00ff8c36baa9e829833dd9fc893c70d67b63c552f601a88fdfbc81.js",
    "controllers/application": "/js/controllers/application.470f468ab36d303fd4bf350f951bbd30b8a2b02e49617252bc1631dd64a432c6.min.js",
    "controllers/contact_form_controller": "/js/controllers/contact_form_controller.bb95cadaae65a9257549aab6f90885241150e84a7b28b70c1934b09cec044873.min.js",
    "controllers/user/profile_controller": "/js/controllers/user/profile_controller.2a8966c2d475172588b56f935ef271c489f673e95cacff3f1a1476d1010d3791.min.js",
    "utilities": "/js/utilities/index.0b732d11c184e9b16c037a5f8a4376c1497332df644b254b79a810ece8dce65a.min.js"
  }
}
</script>

Wanneer een module een andere module importeert, bijvoorbeeld met import 'utilities', weet de browser dat het bestand /js/utilities/index.0b732d11c184e9b16c037a5f8a4376c1497332df644b254b79a810ece8dce65a.min.js geladen moet worden.

Onze applicatie wordt gestart met:

<script type="module">import 'application'</script>

De inhoud van assets/js/application.js:

// Main application

// Stimulus import
import 'controllers/application';

// Other imports

Stimulus starten

De inhoud van assets/js/controllers/application.js:

import { Application } from '@hotwired/stimulus';

const application = Application.start();
const importMapElement = document.querySelector('script[type="importmap"]');

if (importMapElement) {
  // Get the import map by parsing the raw JSON string from the element
  const importMap = JSON.parse(importMapElement.textContent);

  if ('imports' in importMap) {
    Object.entries(importMap.imports).forEach(([key, url]) => {
      if (key.startsWith('controllers/') && key != 'controllers/application') {
        // Derive the Stimulus identifier, e.g.:
        // "controllers/contact_form_controller" -> "contact-form"
        // "controllers/user/profile_controller" -> "user--profile"
        const identifier = key.replace(/^controllers\//, '')
          .replace(/_controller$/, '')
          .replace(/\//g, '--')
          .replace(/_/g, '-');

        // Eager load the controller
        import(key).then(module => {
          application.register(identifier, module.default);
        }).catch(error => {
          console.error(`Failed to register controller: ${identifier}`, error);
        });
      }
    });
  }
}

// Configure Stimulus development experience
application.debug = false;
window.Stimulus = application;

export { application };

Deze code laadt alle Stimulus controllers en registreert ze. Dit gaat razendsnel; zelfs 100 controller bestanden zijn geen probleem, zeker niet met HTTP/2 multiplexing.

Stimulus gebruiken

Stimulus reageert zodra het DOM-elementen vindt met een data-controller of data-action attribuut. Lees er meer over in de officiële documentatie.

<form method="post" data-controller="contact-form" data-action="contact-form#send">
  <button type="submit" class="btn btn-primary">Verstuur bericht</button>
</form>

De inhoud van assets/js/controllers/contact_form_controller.js:

import { Controller } from '@hotwired/stimulus'
import { Utilities } from 'utilities'

export default class extends Controller {
  send(event) {
    event.preventDefault();
    this.element.querySelector('button[type="submit"]').disabled = true;

    Utilities.successMessage('Het bericht is succesvol verzonden');
  }
}

Let op: importeer nooit met een relatief pad zoals import { Utilities } from '../utilities/index.js'. De exacte importnaam moet overeenkomen met de naam in de import map.

Conclusie

Het integreren van Stimulus via import maps is fantastisch voor Hugo projecten. Het respecteert het statische karakter van de tool terwijl het je een fijne structuur geeft om je scripts te beheren. Kort samengevat:

  • Geen build tools zoals Webpack nodig: Alles wordt afgehandeld door Hugo en de browser.
  • Automatische fingerprinting: Hugo zorgt ervoor dat gebruikers altijd de nieuwste code hebben.
  • Fijne structuur: Je JavaScript blijft modulair en makkelijk te onderhouden.
Zoek je een partner met deze expertise voor jouw project?
Vogelaar Solutions helpt organisaties met DevOps, platform engineering en web development. Neem contact op voor een vrijblijvend gesprek.