For most JavaScript building, js.Build is the right choice. The new js.Batch function added in Hugo v0.140.0 can be used for more advanced use cases. This function allows coordinated creation of a JavaScript bundle from multiple sources (e.g. shortcodes, render hooks, page templates etc.).

Some key features:

  • You can partition your scripts into groups (e.g. one group per section).
  • A script can have multiple instances (e.g. multiple instances of the same React components with different options).
  • A group can have one or more runners that will receive a data structure with all instances for that group with a binding of the JavaScript import of the defined export.
  • You can control how imports gets resolved in importContext. This allows you to build scripts inside page bundles with relative imports resolved in the bundle. Combine it with mount for even more control.
  • This enables code splitting with sharing of common code and dependencies (e.g. React)

js.Batch

Args: ID The Batch ID is used to create the base directory for this batch. Forward slashes are allowed. The outline for creating a batch with one script group:

js.BatcGhro"ujpCRSIsoucn/"nnrsmmfnityyiepabggrtnarSSScStoe"e"eeecutmtmtthpOyOyO"O""prpspmptutctytinirisionoioconenpnrnsrstsis""pt""myinstance"

Build

The Build method returns

GroupsRe(smoaupr)ces(slice)

As a consequence of the concurrent building, the building and inclusion of any output will need to be done in a templates.Defer block. The example below shows the common use case of including one group’s resources in a template:

{{ $group := .group }}
{{ with (templates.Defer (dict "key" $group "data" $group )) }}
  {{ with (js.Batch "js/mybatch") }}
    {{ with .Build }}
      {{ with index .Groups $ }}
        {{ range . }}
          {{ $s := . }}
          {{ if eq $s.MediaType.SubType "css" }}
            <link href="{{ $s.RelPermalink }}" rel="stylesheet" />
          {{ else }}
            <script src="{{ $s.RelPermalink }}" type="module"></script>
          {{ end }}
        {{ end }}
      {{ end }}
  {{ end }}
{{ end }}

Config

Returns a OptionsSetter that can be used to set config options for the batch.

See js.Build options, but note that:

  • targetPath is set automatically (there may be multiple outputs).
  • format must be esm, currently the only format supporting code splitting.
  • params will be available in th @params/config namespace in the scripts. This way you can import both the script or runner params and the config params with:
import * as params from "@params";
import * as config from "@params/config";
{{ with js.Batch "js/mybatch" }}
  {{ with .Config }}
       {{ .SetOptions (dict
        "target" "es2023"
        "format" "esm"
        "jsx" "automatic"
        "loaders" (dict ".png" "dataurl")
        "minify" true
        "params" (dict "param1" "value1")
        )
      }}
  {{ end }}
{{ end }}

Group

Args: ID. No slashes.

Most of the building blocks can be seen in hdx shortcode in this project:

{{ $path := .Get "r" }}
{{ $class := .Get "c" | default "" }}
{{ $r := or (.Page.Resources.Get $path) (resources.Get $path) }}
{{ if not $r }}
  {{ errorf "resource not found: %s" $path }}
{{ end }}
{{ $batch := (js.Batch "js/mybatch") }}
{{ $scriptID := $path | anchorize }}
{{ $instanceID :=  .Ordinal | string }}
{{ $group := .Page.RelPermalink | anchorize }}
{{ $props := .Params | default dict }}
{{ $export := .Get "export" | default "default" }}
{{ $innerData := .Inner | transform.Unmarshal | default dict }}
{{ with  $innerData }}
  {{ $props = $props | merge .props }}
{{ end }}
{{ with $batch.Group $group }}
  {{ with .Runner "create-elements" }}
    {{ .SetOptions (dict "resource" (resources.Get "js/batch/react-create-elements.js")) }}
  {{ end }}
  {{ with .Script $scriptID }}
    {{ $common := resources.Match "/js/headlessui/*.*" }}
    {{ $importContext := (slice $.Page ($common.Mount "/js/headlessui" ".")) }}
    {{ .SetOptions (dict
      "resource" $r
      "export" $export
      "importContext" $importContext )
    }}
  {{ end }}
  {{ with .Instance $scriptID $instanceID }}
    {{ .SetOptions (dict "params" $props) }}
  {{ end }}
{{ end }}
<div
  id="{{ printf `%s-%s` $scriptID $instanceID }}"
  class="mt-6 mb-8 p-3 overflow-auto rounded-lg bg-linear-45 from-purple-900 via-blue-900 to-purple-800 {{ $class }} mystyles"></div>

Script

Args: ID. No slashes.

Returns a OptionsSetter that can be used to set script options for this script.

{{ with js.Batch "js/mybatch" }}
  {{ with .Group "mygroup" }}
      {{ with .Script "myscript" }}
          {{ .SetOptions (dict "resource" (resources.Get "myscript.js")) }}
      {{ end }}
  {{ end }}
{{ end }}

Instance

Args: SCRIPT_ID, INSTANCE_ID. No slashes.

Returns a OptionsSetter that can be used to set params options for this instance.

{{ with js.Batch "js/mybatch" }}
  {{ with .Group "mygroup" }}
      {{ with .Instance "myscript" "myinstance" }}
          {{ .SetOptions (dict "params" (dict "param1" "value1")) }}
      {{ end }}
  {{ end }}
{{ end }}

Runner

Returns a OptionsSetter that can be used to set script options for this runner.

{{ with js.Batch "js/mybatch" }}
  {{ with .Group "mygroup" }}
      {{ with .Runner "myrunner" }}
          {{ .SetOptions (dict "resource" (resources.Get "myrunner.js")) }}
      {{ end }}
  {{ end }}
{{ end }}

The runner script used in this project:

import * as ReactDOM from 'react-dom/client';
import * as React from 'react';

export default function Run(group) {
	console.log('Running react-create-elements.js', group);
	const scripts = group.scripts;
	for (const script of scripts) {
		for (const instance of script.instances) {
			/* This is a convention in this project. */
			let elId = `${script.id}-${instance.id}`;
			let el = document.getElementById(elId);
			if (!el) {
				console.warn(`Element with id ${elId} not found`);
				continue;
			}
			const root = ReactDOM.createRoot(el);
			//console.log('create', elId, 'with', instance.params);
			const reactEl = React.createElement(script.binding, instance.params);
			root.render(reactEl);
		}
	}
}

SetOptions takes a map of options.

Mount

Resources.Mount. See Buttons for more details, but in short the common use case would be to mount component folders inside /assets to be on the same level as the Page bundle, so you can do:

import { ButtonBasic } from "./button.jsx";

Instead of:

import { ButtonBasic } from "/js/headlessui/button.jsx";

It’s not in the example, but I guess this could be more visibly useful in situations where you need to override certain files (e.g. CSS) in the component folder.

OptionsSetter

An OptionsSetter is a special object that is returned once only. This means that you should wrap it with with:

{{ with .Script "myscript" }}
    {{ .SetOptions (dict "resource" (resources.Get "myscript.js"))}}
{{ end }}

Script Options

resource
The resource to build. This can be a file resource or a virtual resource.
export
The export to bind the runner to. Set it to * to export the entire namespace. Default is default for runner scripts and * for other scripts.
importContext
An additional context for resolving imports. Hugo will always check this one first before falling back to assets and node_modules. A common use of this is to resolve imports inside a page bundle.
params
A map of parameters that will be passed to the script as JSON. These gets bound to the @params namespace:
import * as params from '@params';

Import Context

Params Options

params
A map of parameters that will be passed to the script as JSON.

type Batcher interface {
	Build(context.Context) (BatchPackage, error)
	Config(ctx context.Context) OptionsSetter
	Group(ctx context.Context, id string) BatcherGroup
}
type BatcherGroup interface {
	Script(id string) OptionsSetter
	Instance(sid, iid string) OptionsSetter
	Runner(id string) OptionsSetter
}
type OptionsSetter interface {
	SetOptions(map[string]any) string
}