This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Implementation

Code-level structure, Hugo/Docsy templates, SCSS/JS customizations, patches, and internal shims

Subsystems

  • Script loading: registry contract, shape guards, and build details of the plugin loop

Patches and workarounds

1 - 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.

2 - ScrollSpy patch

Runtime patch for Bootstrap ScrollSpy to handle invalid CSS selector IDs.

As of Docsy 0.13.0

Problem

As of Bootstrap 5.3.8 (the version used by Docsy 0.13.0), ScrollSpy fails if a page contains a heading ID that is not also a valid CSS # selector. This can happen, for example, if a heading ID starts with a digit. For technical details about this bug, see #2329.

Solution

Docsy 0.13.0 implements a runtime patch for ScrollSpy that intercepts ScrollSpy’s initialization to properly handle heading IDs starting with digits or containing other characters that form invalid CSS selectors. This allows active TOC entry tracking to work correctly without altering the original heading IDs, so links to headings continue to work as expected.

The patch is automatically applied when ScrollSpy is enabled (which is the default). For implementation details, see #2382, #2383.

Maintenance

CI/CD automatically keeps the patch up-to-date when Bootstrap is updated. The _test:full:pre script extracts the method from Bootstrap, applies the patch, and updates the runtime patch file. If the Bootstrap method code has changed to a degree that the patch no longer works, CI will fail, indicating that the patch file needs manual review and updates.

Until the upstream ScrollSpy fix is released in a future Bootstrap version, this patch ensures that active TOC entry tracking works reliably for all pages.

References