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.
Vogelaar Solutions helpt organisaties met DevOps, platform engineering en web development. Neem contact op voor een vrijblijvend gesprek.