From 6d2767503b18c90513959dcf4cdaa4e4ddcd4bd6 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:48:56 +0200 Subject: [PATCH 1/2] feat(utilities): add UniqueID and adopt AddModule UniqueID returns a page-unique, build-stable element id from a per-prefix counter in the page's Store. It replaces hashing `now` (which changes the id on every build, so a page carrying the component never renders byte-identically) and `.Ordinal` (which restarts at 0 inside an embedded RenderString, so ids collide). AddModule moves here from Hinode. No gethinode module declares a dependency on Hinode, so a partial there reaches them only because the site merges Hinode's layouts - an undeclared inverted dependency that breaks each module's own exampleSite build. --- layouts/_partials/utilities/AddModule.html | 13 +++++++++ layouts/_partials/utilities/UniqueID.html | 34 ++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 layouts/_partials/utilities/AddModule.html create mode 100644 layouts/_partials/utilities/UniqueID.html diff --git a/layouts/_partials/utilities/AddModule.html b/layouts/_partials/utilities/AddModule.html new file mode 100644 index 0000000..8e868c6 --- /dev/null +++ b/layouts/_partials/utilities/AddModule.html @@ -0,0 +1,13 @@ +{{- $page := .page -}} +{{- if not $page -}} + {{- $page = page -}} +{{- end -}} +{{ with .module }} + {{- $dependencies := $page.Scratch.Get "dependencies" -}} + {{- if reflect.IsSlice $dependencies -}} + {{- $dependencies = $dependencies | append . | uniq -}} + {{ else }} + {{- $dependencies = slice . -}} + {{ end }} + {{- $page.Scratch.Set "dependencies" $dependencies -}} +{{ end }} \ No newline at end of file diff --git a/layouts/_partials/utilities/UniqueID.html b/layouts/_partials/utilities/UniqueID.html new file mode 100644 index 0000000..4d0ef99 --- /dev/null +++ b/layouts/_partials/utilities/UniqueID.html @@ -0,0 +1,34 @@ +{{/* + Returns a page-unique, build-stable element id of the form "-", counting from 1. + + Use this instead of hashing `now` (which changes the id on every build, so a page carrying the + component never renders byte-identically) and instead of `.Ordinal` (which restarts at 0 inside + an embedded RenderString, as the `example` shortcode performs, so ids collide). + + The counter lives in the page's Store, namespaced per prefix, so each component type counts + independently. Gaps are expected and harmless: Hugo renders a page's content more than once + (the search index calls `.Plain`), and a discarded pass consumes a value without emitting an id. + + A DOM id need only be unique within the document that carries it, so no attempt is made to be + unique across pages. + + Arguments: + prefix (string, required) the id's leading segment, e.g. "panel" + page (optional) the page to anchor the counter to; defaults to the current page +*/}} + +{{- $prefix := .prefix -}} +{{- if not $prefix -}} + {{- errorf "partial [utilities/UniqueID.html] - missing required argument 'prefix'" -}} +{{- end -}} + +{{- $page := .page -}} +{{- if not $page -}} + {{- $page = page -}} +{{- end -}} + +{{- $key := printf "hinode-uid-%s" $prefix -}} +{{- $count := add (or ($page.Store.Get $key) 0) 1 -}} +{{- $page.Store.Set $key $count -}} + +{{- return printf "%s-%d" $prefix $count -}} From 7843736b1b50c994034c5fc2d4275d325a1f373b Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Wed, 15 Jul 2026 05:39:43 +0200 Subject: [PATCH 2/2] fix(utilities): make UniqueID immune to heading anchor collisions Hugo de-duplicates repeated headings by appending "-1", "-2", ..., so a page with two "FAQ" headings emits `faq` and `faq-1` - colliding head-on with the old bare `-` id shape. Insert a literal, uppercase "UID" segment that `anchorize` can never produce, making the collision structurally impossible. Co-Authored-By: Claude Opus 4.8 (1M context) --- layouts/_partials/utilities/UniqueID.html | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/layouts/_partials/utilities/UniqueID.html b/layouts/_partials/utilities/UniqueID.html index 4d0ef99..470059b 100644 --- a/layouts/_partials/utilities/UniqueID.html +++ b/layouts/_partials/utilities/UniqueID.html @@ -1,17 +1,31 @@ {{/* - Returns a page-unique, build-stable element id of the form "-", counting from 1. + Returns a page-unique, build-stable element id of the form "-UID-", counting from 1. Use this instead of hashing `now` (which changes the id on every build, so a page carrying the component never renders byte-identically) and instead of `.Ordinal` (which restarts at 0 inside an embedded RenderString, as the `example` shortcode performs, so ids collide). The counter lives in the page's Store, namespaced per prefix, so each component type counts - independently. Gaps are expected and harmless: Hugo renders a page's content more than once - (the search index calls `.Plain`), and a discarded pass consumes a value without emitting an id. + independently. The counter is contiguous: every call consumes and emits one value. A caller may + still drop the id it was handed (Hinode's `nav.html`, for instance, renders no dropdown element + for `tab-type: buttons`), which leaves an apparent gap in the rendered document - the value was + used, just not on an element that carries an id. + + The literal "UID" segment is deliberate and load-bearing: it keeps these ids out of the space + Hugo's `anchorize` can occupy. Hugo derives heading anchors with `autoHeadingIDType = 'github'`, + which lowercases and reduces the heading to [a-z0-9-_], and de-duplicates repeated headings by + appending "-1", "-2", ... A page with two "FAQ" headings therefore emits `faq` and `faq-1`, which + would collide head-on with a bare `faq-1` container id - Bootstrap resolves `data-bs-parent` + with `querySelector`, which would then bind the accordion to the heading. An uppercase segment + is unreachable for `anchorize`, so the collision cannot occur. Do not "simplify" it away. A DOM id need only be unique within the document that carries it, so no attempt is made to be unique across pages. + The counter is read and written non-atomically. That is safe as used, since Hugo renders a given + page's templates on a single goroutine - but never hand this partial a page other than the one + being rendered, as two concurrently rendered pages sharing a counter would race. + Arguments: prefix (string, required) the id's leading segment, e.g. "panel" page (optional) the page to anchor the counter to; defaults to the current page @@ -31,4 +45,4 @@ {{- $count := add (or ($page.Store.Get $key) 0) 1 -}} {{- $page.Store.Set $key $count -}} -{{- return printf "%s-%d" $prefix $count -}} +{{- return printf "%s-UID-%d" $prefix $count -}}