Script loading

Registry contract, theme defaults, shape guards, build details, and security constraints of the plugin loop

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 like name: 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 pageGate set, the plugin is emitted only on pages carrying the named .Page.Store flag.

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 on hasTabs (set by the tabpane shortcode).
  • click-to-copy, deferred:
    • Auto-registered unless params.prism_syntax_highlighting or params.disable_click2copy_chroma is 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).
  • markmap:
    • The registry entry is page-gated on hasMarkmap (set by the markmap code-block render hook).
    • The legacy params.markmap.enable alias 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.

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.docsy is left untouched (the read is gated on reflect.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.js warns (docsy-plugin-missing), regardless of pageGate, 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’s options travel as build params, so the module reads them with import * as params from '@params'. Production builds add minify.
  • 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), and defer when 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.options through safeHTML, safeJS, or safeURL in a companion partial: options are site-config-controlled strings, and Hugo’s contextual autoescaping is the defense; a safe* 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 pageGate so 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.