Theme assets and npm bundling
When you clone the website repo and run hugo server, the site just works.
You did not install Node.
You did not run npm install.
The chapter map renders.
The events calendar renders.
The dark-mode toggle works.
That is on purpose, and the way it is achieved is worth understanding before you start changing things.
The constraint that shaped the build
Most of the people who contribute to the RLadies+ website are not full-time front-end engineers. They are R-using volunteers who want to fix a typo, add a chapter entry, or write a blog post. Asking them to install a JavaScript toolchain just to preview a markdown change would push most of them away.
So the theme imposes a contract: anyone who only writes content needs only Hugo. Anyone who changes the theme’s CSS, updates a vendor library, or adds a new front-end dependency runs npm — but only inside the theme directory, and they commit the resulting bundles so the next person does not have to.
This is enforced by where things live and what is in .gitignore.
Where the build artefacts go
Inside the theme:
themes/hugo-rladiesplus/
├── package.json # npm scripts and dependencies
├── node_modules/ # installed by `npm install`, gitignored
├── assets/
│ ├── css/
│ │ ├── main.css # Tailwind v4 entry, hand-written
│ │ ├── components/ # hand-written component CSS, imported by main.css
│ │ └── vendor/ # GENERATED — committed
│ │ ├── tailwind.css # compiled by `tailwindcss --minify`
│ │ └── choices.min.css
│ ├── js/
│ │ ├── _d3map.entry.js # esbuild entry for the chapter map
│ │ ├── _fullcalendar.entry.js # esbuild entry for the events calendar
│ │ ├── chapter-filter.js # hand-written, used by /chapters/
│ │ ├── counter.js # hand-written, used everywhere
│ │ ├── darkmode.js # hand-written, inlined in <head>
│ │ ├── map-init.js # hand-written, reads the d3map bundle
│ │ ├── ...
│ │ └── vendor/ # GENERATED — committed
│ │ ├── alpine.min.js
│ │ ├── choices.min.js
│ │ ├── d3map.bundle.min.js
│ │ ├── fullcalendar.bundle.min.js
│ │ ├── mermaid.min.js
│ │ └── shuffle.min.js
│ └── scss/
│ └── vendor/fontawesome/ # GENERATED — committed
└── static/
└── webfonts/ # GENERATED — committed
├── fontawesome/ # Font Awesome woff2
└── google-fonts/ # Poppins, Inconsolata woff2Everything labelled GENERATED is built by an npm script and committed to the repo.
The build script, line by line
The full pipeline lives in themes/hugo-rladiesplus/package.json.
The build script chains together every step:
npm run clean wipe the generated folders
npm run setup recreate empty target folders
npm run build:css tailwindcss --minify -i main.css -o vendor/tailwind.css
npm run build:fullcalendar esbuild bundles FullCalendar plugins
npm run build:d3map esbuild bundles d3-geo, world-atlas, iso-3166-1
npm run sync:fontawesome copies woff2 + scss out of node_modules into the theme
npm run sync:choices copies choices.js
npm run sync:shuffle copies shufflejs
npm run sync:alpine copies Alpine
npm run sync:mermaid copies the mermaid bundle
npm run sync:fonts copies Poppins + Inconsolata woff2 filesThe postinstall script runs npm run build automatically.
Running npm install once gets you a complete, ready-to-commit set of vendor assets.
The two esbuild steps deserve a closer look.
The chapter map uses d3-geo to project a world atlas, topojson-client to parse the topology, and iso-3166-1 to translate country codes.
That is several npm packages that the browser cannot load directly.
assets/js/_d3map.entry.js imports them, attaches the parts the templates need to window.__d3map, and esbuild rolls the lot into a single vendor/d3map.bundle.min.js.
The events calendar follows the same pattern via _fullcalendar.entry.js.
How Hugo Pipes uses the artefacts
Once the bundles exist, Hugo Pipes is what serves them.
The relevant pieces are in partials/head/head.html and partials/footer/scripts.html:
{{ $tw := resources.Get "css/vendor/tailwind.css" }}
{{ $css := slice $tw
| resources.Concat "rladiesplus-bundle.min.css"
| minify
| fingerprint
}}
<link rel="stylesheet" href="{{ $css.RelPermalink }}" integrity="{{ $css.Data.Integrity }}">
{{ $fa := resources.Get "scss/fontawesome.scss" | toCSS | minify | fingerprint }}
<link rel="stylesheet" href="{{ $fa.RelPermalink }}" media="print" onload="this.media='screen'">The Tailwind CSS is concatenated, minified, and fingerprinted.
The FontAwesome SCSS is compiled at build time by the Hugo extended binary’s libsass support, then minified and fingerprinted.
The integrity hash goes into the <link> tag for SRI.
Section-specific JavaScript follows the same pattern.
The events calendar bundle is only loaded by events/list.html, the chapter map only by templates that include partials/map.html, the directory filter only by directory/list.html.
Pages that do not need a feature do not download its bundle.
When you actually need to run npm
Three situations:
You changed something in assets/css/main.css or one of the files under assets/css/components/.
You added a new vendor library to dependencies in package.json.
You bumped a version in package.json.
In all three cases, the workflow is the same. From the theme directory:
cd themes/hugo-rladiesplus
npm install # the postinstall hook rebuilds everything
git status # confirm the regenerated files
git add assets/ static/webfonts/
git commit -m "rebuild theme vendor bundles"If you only edited main.css and want a faster loop while iterating, run just the CSS step:
npm run build:cssThe Tailwind CLI watches your main.css for @import directives, scans every Hugo template under themes/hugo-rladiesplus/layouts/ for class names (Tailwind v4 does this with zero config), and emits a CSS file containing only the utilities your templates actually use.
This is why the production CSS is small: there is no “all of Tailwind” in the bundle, just the rules the site actually references.
When you do not need to run npm
You added or edited a markdown post. You added a new chapter JSON. You translated an i18n key. You added a new layout template that does not introduce new utility classes Hugo cannot find.
You can safely commit and push. The site will rebuild against the existing vendor bundles.
What is gitignored, and why it matters
Inside the theme: node_modules/ and package-lock.json-related lockfile cruft you create incidentally.
The vendor bundles themselves — the things npm produces — are committed.
That is the inversion that makes this work.
If you accidentally check in node_modules/ (a few hundred megabytes), the build will still pass but the diff review will be unpleasant.
If you accidentally git-ignore the vendor bundles, every clone will build a broken site until someone notices.
What about icons and fonts
Font Awesome is the icon set.
The theme syncs the official Free SCSS into assets/scss/vendor/fontawesome/ and the woff2 files into static/webfonts/fontawesome/.
assets/scss/fontawesome.scss imports the parts the site uses.
Hugo compiles that SCSS at build time.
Poppins is the body font, Inconsolata is the code font.
Both are self-hosted via Fontsource, copied into static/webfonts/google-fonts/, and preloaded in <head> to keep the site off Google’s CDN and avoid the layout shift you get from waiting for fonts.
If you want to swap fonts: change the @fontsource/... dependency in package.json, run npm install, update --font-sans / --font-mono in assets/css/main.css, and update the <link rel="preload"> paths in partials/head/head.html.
Plausible analytics
The site uses Plausible for privacy-respecting analytics.
The script is loaded asynchronously from https://plausible.io/js/script.js with defer, gated to the rladies.org domain.
There is no cookie banner because there is no cookie.
If you ever need to disable analytics — for a fork, for a preview, for legal reasons — comment out the two lines at the bottom of partials/head/head.html.