Script loading
Code-level notes for _partials/scripts/plugins.html, the loop
behind params.docsy.plugins. For the architecture and ordering decisions, see
the design notes.
Registry contract
params.docsy.plugins is a list of plugin entries:
params:
docsy:
plugins:
- name: NAME # resolves assets/js/plugins/NAME.js
enable: true # optional; false skips the entry
defer: true # optional; adds `defer` to the script tag
pageGate: FLAG # optional; a .Page.Store flag name
options: {} # optional; reaches the module as @params
- NAME # bare-name shorthand for { name: NAME }
- Names are coerced to strings (
printf "%v"): YAML auto-types entries likename: 2048, and the loop must resolve the same asset path either way. - Names are restricted to
^[A-Za-z0-9_-]+$: they address assets and partials by path, so anything that could traverse outside the plugin namespaces is refused with a warning (docsy-plugin-name). - Registry order is emission order.
- With
pageGateset, the plugin is emitted only on pages carrying the named.Page.Storeflag.
Theme-default entries and legacy aliases
The loop appends theme-default entries after the site-declared registry. A
site-declared entry of the same name supersedes its auto-registration but
inherits the default fields it leaves unset (pageGate, defer); an
explicit value, pageGate: '' included, always wins:
tabpane-persist, page-gated onhasTabs(set by the tabpane shortcode).click-to-copy, deferred:- Auto-registered unless
params.prism_syntax_highlightingorparams.disable_click2copy_chromais set (the pre-plugin gates keep their semantics for the default entry). - A site-declared entry always emits; combining it with prism draws a warning
(
docsy-c2c-prism).
- Auto-registered unless
markmap:- The registry entry is page-gated on
hasMarkmap(set by the markmap code-block render hook). - The legacy
params.markmap.enablealias registers it ungated (exact pre-0.18 site-wide behavior: custom render hooks and raw markmap HTML never set the flag) and warns (docsy-markmap-legacy) for its deprecation cycle.
- The registry entry is page-gated on
An entry with enable: false is skipped; the string "false" counts as false
(YAML strings are truthy in Go templates).
Shape guards
The registry read must not break sites that already carry a params.docsy
value:
- A scalar
params.docsyis left untouched (the read is gated onreflect.IsMap) and the site registry is treated as empty; theme-default entries still emit. - A non-list
params.docsy.plugins, falsy scalars included, warns (docsy-plugins-config) and is ignored. - An entry with no usable name warns (
docsy-plugin-unnamed) and is skipped. - A name registered more than once warns (
warnf): plugins load once, and a second registration is a misconfiguration. - An enabled registration with no asset at
assets/js/plugins/NAME.jswarns (docsy-plugin-missing), regardless ofpageGate, so a typo can’t hide behind a gate.
Except the duplicate-name warnf, warnings are issued with warnidf, so each
is suppressible through Hugo’s ignoreLogs.
Build and emission
- The script asset resolves through the union file system: theme, project, or module-mounted, with a same-named project file shadowing the theme’s.
- Each plugin builds individually with
js.Build; the entry’soptionstravel as buildparams, so the module reads them withimport * as params from '@params'. Production builds addminify. - Fingerprinting runs in every environment, not just production: distinct builds of one source (per-language sites) must publish distinct paths, or the last write wins site-wide.
- The script tag carries SRI attributes (
integrity,crossorigin), anddeferwhen the entry sets it.
Companions
Optional companions resolve by naming convention and emit before the script tag (why):
_partials/scripts/plugins/NAME.html: a partial for vendored libraries, component markup, and config-provider patterns; invoked with(dict "Page" PAGE "Plugin" ENTRY).assets/scss/plugins/NAME.scss: a stylesheet compiled through the SCSS pipeline, minified in production, fingerprinted, and emitted with SRI.
Security constraints
- Never pipe
Plugin.optionsthroughsafeHTML,safeJS, orsafeURLin a companion partial: options are site-config-controlled strings, and Hugo’s contextual autoescaping is the defense; asafe*cast disables it. - Plugin
options(and any value reaching a module as@params) ship world-readable in the built JS: never route secrets or tokens through them. - Third-party libraries are pinned and vendored: no CDN-
latest, and no loader that pulls unpinned secondary code (SRI on a loader is worthless if the loader fetches unpinned dependencies). - Remote-capable plugins get a
pageGateso their code ships only where used. - Residual exposure, named: the vendored markmap autoloader’s runtime libraries still load from the CDN, pinned by the autoloader itself but without SRI.