From d8cb5b4ff7ee1bb0417d968d7e16a71395d2baab Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:45:56 +0200 Subject: [PATCH 01/19] docs: add design for fixing the table category filter The filter has never worked from Markdown: the shortcode does not forward its arguments, a string value is never split into a slice, its behaviour lives in an optional module, the DOM fallback breaks on wrapped tables, and its i18n keys do not exist. Makes the filter a data-table feature: filter implies a data table, so it works standalone, and the DOM fallback is deleted along with its bug. --- ...2026-07-14-table-category-filter-design.md | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-14-table-category-filter-design.md diff --git a/docs/superpowers/specs/2026-07-14-table-category-filter-design.md b/docs/superpowers/specs/2026-07-14-table-category-filter-design.md new file mode 100644 index 000000000..b042d9319 --- /dev/null +++ b/docs/superpowers/specs/2026-07-14-table-category-filter-design.md @@ -0,0 +1,187 @@ +# Design: fix the table category filter + +Date: 2026-07-14 +Status: Approved +Repositories: `gethinode/hinode`, `gethinode/mod-simple-datatables` + +## Problem + +The table shortcode's category filter renders a button group above a table and narrows the +rows to a chosen category. It has never worked from Markdown, and it is broken in five +independent ways. + +**1. It is unreachable.** `layouts/_shortcodes/table.html` forwards `breakpoint`, `class`, +`sortable`, `paginate`, `pagination`, `pagination-select`, `searchable`, `wrap` and `wrapper` +to `assets/table.html` — but never `filter` or `filter-col`. Both are declared in +`data/structures/table.yml`, so `{{< table filter="widget,gadget" >}}` validates cleanly and +then does nothing at all. + +**2. Forwarding alone would not fix it.** `filter`'s type in `_arguments.yml` is +`[string, slice]`, and a shortcode named parameter is *always* a string. mod-utils validates +an argument's kind against `accepts` but does **not** cast between kinds. The partial would +therefore receive the raw string `"widget,gadget"` and reach `{{ range $filter }}`, which Hugo +cannot iterate over a string. Something has to split it. + +**3. The behaviour lives in an optional module.** Hinode core renders the filter buttons, but +the click handler lives in `mod-simple-datatables`' `simple-datatables.load.js`. That module +declares `integration = "optional"`, and Hinode loads an optional module's assets only for +pages whose frontmatter lists it. A filter-only table — no sorting, no paging, no search — +therefore renders buttons that are inert unless the page adds +`modules: ["simple-datatables"]`. + +**4. The DOM fallback breaks on wrapped tables.** When no DataTable is active, the filter +falls back to toggling `display` on each `tbody tr`, reading `row.cells[col]`. A wrapped +table renders two rows per record, and the second holds a single ``. With the +default `filter-col = 1`, `cells[1]` is `undefined` on that row, so its text reads as empty +and the row is hidden as soon as any filter is active — the record keeps its data row but +loses its description. + +**5. The i18n keys do not exist.** `tableFilterLabel` and `tableFilterAll` are absent from +every file in `i18n/`. The partial survives only because it calls `T` with a `| default` +fallback, so the strings are untranslatable and emit i18n warnings. + +## Decision: the filter is a data-table feature + +The filter was originally built to work without a DataTable — commit `63a88070`'s fallback +branch is commented *"direct DOM row toggling when no DataTable is active on the table (e.g. +filter-only without sortable/paginate/searchable)"*, and `table.yml`'s comment deliberately +lists only sorting, paging and searching as needing the module. That original intent is +reversed here, deliberately. + +The reason is not that a plain-table filter is unreasonable, but that the fallback is a +*second, inferior implementation of the same feature*. It toggles `display` and composes with +nothing, whereas `dt.search()` filters through simple-datatables' row model, so the filter +survives sorting, pagination and free-text search. It is also the branch carrying defect 4. +Two implementations of one feature, one of them broken, is the thing worth deleting. + +### `filter` implies a data table + +To keep the argument self-sufficient, setting `filter` alone marks the table as a data table. +simple-datatables then initialises with sorting, paging and search all off, and `dt.search()` +still filters it. + +This is safe: in the vendored simple-datatables v10.2.0, `options.searchable` gates only the +rendering of the search *input* and the addition of a wrapper class. It does **not** guard the +public `search()` method. A DataTable with every feature disabled is still a filterable one. + +The user therefore never has to enable sorting they do not want in order to get filtering. + +## Design + +### 1. Hinode — `layouts/_shortcodes/table.html` + +Forward `filter` and `filter-col` to `assets/table.html`, alongside the arguments it already +passes. + +### 2. Hinode — `layouts/_partials/assets/table.html` + +**Normalise `filter`.** Accept both kinds the type declares. A slice is used as-is; a string +is split on commas, with surrounding whitespace trimmed from each value and empty values +dropped, so `filter="widget, gadget"` behaves like `filter="widget,gadget"`. + +**Make `filter` imply a data table.** The existing `$dataTable` condition becomes +`or $args.sortable $args.paginate $args.searchable $args.filter`, so a filtered table always +receives the `data-table` class and is picked up by simple-datatables. The `data-filter-col` +and `data-filter-id` attributes are unchanged. + +**Warn when the module is not loaded.** A data table needs the module's JavaScript *and* its +per-page stylesheet, and both are gated on the page opting in. When the partial renders a data +table on a page whose `modules` frontmatter does not list `simple-datatables`, emit a +`LogWarn` naming the page and telling the author to add +`modules: ["simple-datatables"]`. This covers `sortable`, `paginate` and `searchable` too, all +of which fail silently today. + +**Drop the `data-filter-container` attribute** from the `.table-responsive` wrapper. It is +read only by the DOM fallback that section 3 deletes, so it becomes dead markup. + +Note on why the opt-in cannot be automated: `utilities/AddModule.html` exists to let a partial +declare a module dependency at render time, and `footer/scripts.html` honours it — but the +module's CSS is a separate per-page stylesheet emitted by `head/head.html`, which runs *before* +the content is rendered and therefore cannot see a render-time dependency. Auto-loading would +attach the JavaScript without the CSS. Fixing that means redesigning module loading, which is +out of scope; a loud build warning is the honest alternative. + +### 3. mod-simple-datatables — `simple-datatables.load.js` + +Delete the DOM-fallback branch of the filter click handler. Every filtered table is now a +DataTable, so the branch is unreachable — and it is the one carrying the wrapped-table bug. +The handler reduces to: update the active button state, then call +`dt.search(filterValue, [filterCol], 'category-filter')` on each registered instance. + +The named `'category-filter'` search source stays independent of the built-in search input, so +a category filter and a free-text search narrow the result set simultaneously. + +### 4. Hinode — `layouts/_partials/utilities/AddModule.html` + +Fix a latent bug found while designing this. The partial reads the current `dependencies` +scratch and then does: + +```hugo +{{- $dependencies = complement $dependencies (slice .) -}} +{{- page.Scratch.Set "dependencies" $dependencies -}} +``` + +Hugo's `complement` returns the elements of the **last** collection that are not in the +others — verified empirically: `complement (slice "alpha") (slice "beta")` is `[beta]`, and +`complement (slice "alpha") (slice "alpha")` is `[]`. So the partial *replaces* the dependency +list with the single module being added, discarding everything already there — and re-adding a +module that is already present wipes the list entirely. + +Replace the body with an append-and-deduplicate, matching what `head/head.html:22` and +`footer/scripts.html:126` already do when they read the scratch: + +```hugo +{{- $dependencies = $dependencies | append . | uniq -}} +``` + +The partial is currently unused, so this is latent rather than active. It is fixed here +because the filter design depends on understanding it, and it is a landmine for the next +caller. + +### 5. Hinode — `i18n/*.yaml` + +Add `tableFilterLabel` (the button group's `aria-label`) and `tableFilterAll` (the label of +the always-present "show everything" button) to **all eight** language files: `en`, `nl`, `fr`, +`de`, `pl`, `pt-br`, `zh-hans`, `zh-hant`. + +The English strings are `Filter` and `All`, matching the `| default` fallbacks the partial +uses today, so no rendered output changes for an English site. The other seven are translated +in kind; the two keys are short UI labels, not sentences. + +Note that these belong in **Hinode's** i18n, not the module's: Hinode's partial renders the +buttons. The module's own i18n (`tablePlaceholder`, `tablesSearchTitle`, …) stays where it is, +and covers only the four languages that module ships. + +### 6. Hinode — `data/structures/table.yml` + +The structure comment reads *"Sorting, paging, and searching require the simple-datatables +module."* Add filtering to that list. + +## Behaviour after the change + +`{{< table filter="widget,gadget" >}}` renders a button group with an "All" button plus one +button per category, and filters rows by the text content of the column at `filter-col` +(zero-indexed, default `1`). It works with or without `sortable`, `paginate` and `searchable`, +and composes with all of them, because filtering runs through the row model rather than the +DOM. It works on a wrapped table, because the wrapped layout is produced by `tableRender` from +an already-filtered row model. + +It requires the page to declare `modules: ["simple-datatables"]`, and says so at build time if +it does not. + +## Testing + +- Extend `exampleSite/content/en/table-demo.md` with a filter-only table (no `sortable` / + `paginate` / `searchable`) and a filter-plus-wrap table. +- Verify in a browser: clicking a category narrows the rows; "All" restores them; the active + button state follows the click. +- Verify the filter composes: with `sortable` and `paginate` on, a filter survives a sort and + the pager recounts to the filtered set. +- Verify filter + wrap below the breakpoint: a filtered-out record's description row + disappears along with its data row — the defect-4 scenario. +- Verify the build warning fires on a page that uses a data table without the frontmatter key, + and does not fire on one that has it. +- Verify `filter="widget, gadget"` (with a space) yields two buttons, not one with a leading + space. +- Verify a `filter` passed as a real slice from a partial caller still works. +- `npm run lint` and a clean `hugo` build of the exampleSite, with no i18n warnings. From 94c22cd4e0fc5cfcc981cbea00c7738ceb5b9c02 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:53:53 +0200 Subject: [PATCH 02/19] docs: add implementation plan for the table category filter fix Six tasks across hinode and mod-simple-datatables, each ending in an independently verifiable deliverable. --- .../plans/2026-07-14-table-category-filter.md | 974 ++++++++++++++++++ 1 file changed, 974 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-14-table-category-filter.md diff --git a/docs/superpowers/plans/2026-07-14-table-category-filter.md b/docs/superpowers/plans/2026-07-14-table-category-filter.md new file mode 100644 index 000000000..0df77c66f --- /dev/null +++ b/docs/superpowers/plans/2026-07-14-table-category-filter.md @@ -0,0 +1,974 @@ +# Table Category Filter Fix — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make the table shortcode's category filter actually work from Markdown, as a data-table feature. + +**Architecture:** The filter becomes a data-table feature. Hinode's shortcode forwards `filter` / `filter-col`; the partial normalizes a comma-separated string into a slice and marks any filtered table as a data table, so simple-datatables always initialises and `dt.search()` always drives the filtering. That makes the module's DOM-fallback branch unreachable, so it is deleted along with the wrapped-table bug it carries. Because a data table needs the module's JS *and* its per-page CSS — both gated on page opt-in — the partial warns at build time when the page has not opted in. + +**Tech Stack:** Hugo templates, shortcodes and render hooks; simple-datatables v10.2.0; Hugo i18n. + +## Global Constraints + +- Spec: `docs/superpowers/specs/2026-07-14-table-category-filter-design.md`. Read it first. +- Two repositories, siblings under `/Users/mark/Development/GitHub/gethinode/`: `hinode` (branch + `fix/table-category-filter`, already created and checked out, carrying the spec commit) and + `mod-simple-datatables` (currently on `main` — Task 5 branches it). +- **`filter` implies a data table.** Setting `filter` alone must add the `data-table` class. This is + safe: in simple-datatables v10.2.0 `options.searchable` gates only the search *input* and a wrapper + class — it does **not** guard the public `search()` method. A DataTable with sorting, paging and + search all off is still filterable. +- `filter`'s declared type is `[string, slice]`. A shortcode named parameter is **always** a string, + and mod-utils validates an argument's kind against `accepts` **without casting between kinds**. The + partial must therefore split a string itself. `range` over a string is a Hugo error. +- `filter-col` is zero-indexed and defaults to `1`. +- Per `CLAUDE.md`: never use `or` as fallback logic for a boolean argument — `or false ` + returns the fallback and discards an explicit `false`. +- Commits follow Angular Conventional Commits (a commitlint pre-commit hook enforces this). Body + lines must not exceed 100 characters. If a hook reformats files, re-stage and commit again. +- **The branch must contain at least one `feat:` or `fix:` commit**, because gethinode merges with + merge commits and semantic-release reads the individual commits, not the PR title. `refactor`, + `style`, `test`, `chore`, `docs` and `build` produce **no release**. +- **Never run `npm run build:example` or `npm run start:example`** — their prebuild hooks re-vendor + Hugo modules from the remote and would wipe the local `mod-simple-datatables` override that Task 5 + needs. Use the project-pinned binary with `node_modules/.bin` on `PATH`: + + ```bash + PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/hugo --gc -s exampleSite --logLevel warn + ``` + + (The `PATH` entry is required or Hugo cannot find PostCSS.) +- Export the scratch path once per shell: + + ```bash + export SCRATCH=/private/tmp/claude-501/-Users-mark-Development-GitHub-gethinode-hinode/491eb2c0-b0cd-401d-9256-134afea0e318/scratchpad + ``` + +- An existing scratch script `$SCRATCH/verify-table-wrap.mjs` guards the (already-shipped) wrap + feature. Task 3 adds tables to the shared fixture page, which changes its table count — Task 3 + updates that script accordingly. Neither script is committed. + +## File Structure + +| File | Repo | Responsibility | +| --- | --- | --- | +| `i18n/*.yaml` (8 files) | hinode | The filter button group's two UI strings. | +| `data/structures/table.yml` | hinode | Structure comment: filtering also needs the module. | +| `layouts/_partials/utilities/AddModule.html` | hinode | Render-time module dependency (currently broken). | +| `layouts/_shortcodes/table.html` | hinode | Forwards `filter` / `filter-col` to the partial. | +| `layouts/_partials/assets/table.html` | hinode | Normalizes `filter`; `filter` implies a data table; warns on missing opt-in. | +| `exampleSite/content/en/table-demo.md` | hinode | Fixture: a filter-only table and a filter + sort + wrap table. | +| `assets/js/modules/simple-datatables/simple-datatables.load.js` | mod-simple-datatables | Filter click handler; the DOM fallback is deleted. | + +--- + +### Task 1: i18n strings and the structure comment + +Independent of everything else, and lands the strings before any task renders a filter button, so no +i18n warnings are ever emitted. + +**Files:** + +- Modify: `i18n/en.yaml`, `i18n/nl.yaml`, `i18n/de.yaml`, `i18n/fr.yaml`, `i18n/pl.yaml`, + `i18n/pt-br.yaml`, `i18n/zh-hans.yaml`, `i18n/zh-hant.yaml` +- Modify: `data/structures/table.yml:1-3` + +**Interfaces:** + +- Produces: the i18n ids `tableFilterLabel` and `tableFilterAll`, which + `layouts/_partials/assets/table.html` already calls via `T`. + +- [ ] **Step 1: Add the two keys to all eight language files** + +The files are flat YAML lists of `- id:` / `translation:` pairs. Append both entries to the end of +each file, keeping the existing two-space indentation under `translation`. + +`i18n/en.yaml`: + +```yaml +- id: tableFilterLabel + translation: "Filter" +- id: tableFilterAll + translation: "All" +``` + +`i18n/nl.yaml`: + +```yaml +- id: tableFilterLabel + translation: "Filter" +- id: tableFilterAll + translation: "Alle" +``` + +`i18n/de.yaml`: + +```yaml +- id: tableFilterLabel + translation: "Filter" +- id: tableFilterAll + translation: "Alle" +``` + +`i18n/fr.yaml`: + +```yaml +- id: tableFilterLabel + translation: "Filtre" +- id: tableFilterAll + translation: "Tout" +``` + +`i18n/pl.yaml`: + +```yaml +- id: tableFilterLabel + translation: "Filtr" +- id: tableFilterAll + translation: "Wszystkie" +``` + +`i18n/pt-br.yaml`: + +```yaml +- id: tableFilterLabel + translation: "Filtro" +- id: tableFilterAll + translation: "Todos" +``` + +`i18n/zh-hans.yaml`: + +```yaml +- id: tableFilterLabel + translation: "筛选" +- id: tableFilterAll + translation: "全部" +``` + +`i18n/zh-hant.yaml`: + +```yaml +- id: tableFilterLabel + translation: "篩選" +- id: tableFilterAll + translation: "全部" +``` + +The English strings match the `| default` fallbacks the partial already uses, so no English site's +rendered output changes. + +- [ ] **Step 2: Update the structure comment** + +In `data/structures/table.yml`, the file opens with: + +```yaml +comment: >- + Makes a Markdown table responsive with horizontal scrolling on smaller screens. + Sorting, paging, and searching require the simple-datatables module. +``` + +Change the second sentence to include filtering: + +```yaml +comment: >- + Makes a Markdown table responsive with horizontal scrolling on smaller screens. + Sorting, paging, searching, and filtering require the simple-datatables module. +``` + +- [ ] **Step 3: Verify every key parses and resolves** + +```bash +export SCRATCH=/private/tmp/claude-501/-Users-mark-Development-GitHub-gethinode-hinode/491eb2c0-b0cd-401d-9256-134afea0e318/scratchpad +for f in i18n/*.yaml; do node -e " + const y=require('fs').readFileSync('$f','utf8'); + const ids=[...y.matchAll(/^- id: (\S+)/gm)].map(m=>m[1]); + const dup=ids.filter((v,i)=>ids.indexOf(v)!==i); + const missing=['tableFilterLabel','tableFilterAll'].filter(k=>!ids.includes(k)); + console.log('$f', missing.length?'MISSING '+missing:'ok', dup.length?'DUPLICATE '+dup:''); +"; done +``` + +Expected: `ok` for all eight files, and no duplicates. + +- [ ] **Step 4: Build with i18n warnings on** + +```bash +PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/hugo --gc -s exampleSite --printI18nWarnings --logLevel warn 2>&1 | grep -i "i18n\|ERROR" || echo "no i18n warnings" +``` + +Expected: `no i18n warnings`. + +- [ ] **Step 5: Lint and commit** + +```bash +npm test +git add i18n data/structures/table.yml +git commit -m "fix(i18n): add the missing table filter translations + +tableFilterLabel and tableFilterAll were called by the table partial but existed +in no language file, so they were untranslatable and emitted i18n warnings. The +partial only rendered because it passes a default to T. + +Also records in the table structure that filtering, like sorting, paging and +searching, needs the simple-datatables module." +``` + +--- + +### Task 2: Fix `AddModule.html` + +A latent bug found while designing this. The partial is currently unused, so nothing changes +behaviourally — but it is the sanctioned way for a partial to declare a render-time module +dependency, and it is a landmine for the next caller. + +**Files:** + +- Modify: `layouts/_partials/utilities/AddModule.html` + +**Interfaces:** + +- Produces: `partial "utilities/AddModule.html" (dict "module" "")` appends `` to the + page's `dependencies` scratch without discarding what is already there. + +- [ ] **Step 1: Write a failing test** + +The partial is unused in the theme, so build a throwaway Hugo site that exercises it directly. + +```bash +export SCRATCH=/private/tmp/claude-501/-Users-mark-Development-GitHub-gethinode-hinode/491eb2c0-b0cd-401d-9256-134afea0e318/scratchpad +T=$SCRATCH/addmodule-test +rm -rf $T && mkdir -p $T/layouts/_partials/utilities $T/content +printf 'title = "t"\nbaseURL = "/"\n' > $T/hugo.toml +printf -- '---\ntitle: x\n---\n' > $T/content/_index.md +cp layouts/_partials/utilities/AddModule.html $T/layouts/_partials/utilities/ +cat > $T/layouts/index.html <<'EOF' +{{ partial "utilities/AddModule.html" (dict "module" "alpha") }} +{{ partial "utilities/AddModule.html" (dict "module" "beta") }} +{{ partial "utilities/AddModule.html" (dict "module" "alpha") }} +RESULT: {{ delimit (page.Scratch.Get "dependencies") "," }} +EOF +node_modules/.bin/hugo --quiet -s $T --destination $T/public +grep RESULT $T/public/index.html +``` + +Expected (the bug): `RESULT:` — an **empty** list. Adding `beta` discarded `alpha`, then re-adding +`alpha` wiped the list entirely. + +Why: the partial calls `complement $dependencies (slice .)`, and Hugo's `complement` returns the +elements of the **last** collection that are not in the others. So `complement ["alpha"] ["beta"]` +is `["beta"]` — and the partial then *sets* `dependencies` to that, replacing rather than appending. +`complement ["alpha"] ["alpha"]` is `[]`, which empties it. + +- [ ] **Step 2: Fix the partial** + +Replace the whole of `layouts/_partials/utilities/AddModule.html` with: + +```hugo +{{ 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 }} +``` + +This matches how `head/head.html:22` and `footer/scripts.html:126` already merge the scratch when +they read it (`append … | uniq`). Keep the file's existing tab indentation. + +- [ ] **Step 3: Re-run the test** + +```bash +cp layouts/_partials/utilities/AddModule.html $SCRATCH/addmodule-test/layouts/_partials/utilities/ +node_modules/.bin/hugo --quiet -s $SCRATCH/addmodule-test --destination $SCRATCH/addmodule-test/public +grep RESULT $SCRATCH/addmodule-test/public/index.html +``` + +Expected: `RESULT: alpha,beta` — both modules retained, and the duplicate `alpha` deduplicated +rather than destroying the list. + +- [ ] **Step 4: Lint and commit** + +```bash +npm test +git add layouts/_partials/utilities/AddModule.html +git commit -m "fix(utilities): stop AddModule discarding existing dependencies + +AddModule used complement to add a module to the page's dependencies scratch, +but complement returns the elements of the LAST collection absent from the +others - so it replaced the list with the single module being added, and wiped +it entirely when re-adding one already present. Append and deduplicate instead, +matching how head.html and scripts.html merge the same scratch." +``` + +--- + +### Task 3: Wire the filter up, and make it imply a data table + +The core of the fix. After this task the filter is reachable from Markdown and renders as a data +table. + +**Files:** + +- Modify: `layouts/_shortcodes/table.html:22-35` +- Modify: `layouts/_partials/assets/table.html:19-59` and `:110` +- Modify: `exampleSite/content/en/table-demo.md` + +**Interfaces:** + +- Consumes: the i18n ids `tableFilterLabel` / `tableFilterAll` (Task 1). +- Produces: on a filtered table, the `data-table` class plus the attributes + `data-filter-col=""` and `data-filter-id="table-filter-"`; a button group whose buttons + carry `data-filter-table=""` and `data-filter-value=""` (empty for "All"). + Task 5's JavaScript reads all of these. The `data-filter-container` attribute is **removed**. + +- [ ] **Step 1: Add the fixture tables** + +Append to `exampleSite/content/en/table-demo.md`. The page's frontmatter already declares +`modules: ["simple-datatables"]`, which the filter needs. + +````markdown +## Filter-only table + +{{< table filter="widget, gadget" filter-col="1" class="fixture-filter" >}} +| Name | Type | Description | +|---------|--------|--------------------------------------------------------------------| +| alpha | widget | The first record, with a description long enough to need wrapping. | +| bravo | gadget | The second record, also with a fairly long trailing description. | +| charlie | widget | The third record. Short. | +| delta | gadget | The fourth record, whose description runs on for a little while. | +{{< /table >}} + +## Filtered, sortable, wrapped data table + +{{< table filter="widget,gadget" filter-col="1" sortable="true" paginate="true" pagination="2" wrap="true" class="fixture-filter-wrap" >}} +| Name | Type | Description | +|---------|--------|--------------------------------------------------------------------| +| alpha | widget | The first record, with a description long enough to need wrapping. | +| bravo | gadget | The second record, also with a fairly long trailing description. | +| charlie | widget | The third record. Short. | +| delta | gadget | The fourth record, whose description runs on for a little while. | +{{< /table >}} +```` + +Note the deliberate space in `filter="widget, gadget"` on the first table — it exercises the +whitespace trimming. + +- [ ] **Step 2: Write the failing verification script** + +Create `$SCRATCH/verify-table-filter.mjs` (scratch — never commit it): + +```js +import { readFileSync } from 'node:fs' + +const html = readFileSync('exampleSite/public/en/table-demo/index.html', 'utf8') + +const checks = [] +const check = (name, pass, detail = '') => checks.push({ name, pass, detail }) + +// Pull out each fixture table's start tag, matching the class as an exact token. +// A substring match would make `fixture-filter` also match `fixture-filter-wrap`. +const tag = cls => { + for (const m of html.matchAll(/
]*>/g)) { + if (m[1].split(/\s+/).includes(cls)) return m[0] + } + return '' +} +const filterOnly = tag('fixture-filter') +const filterWrap = tag('fixture-filter-wrap') + +check('filter-only table is rendered', filterOnly.length > 0) +check('filter+wrap table is rendered', filterWrap.length > 0) + +// `filter` implies a data table: the class must be present even with no sortable/paginate/searchable. +check( + 'filter alone marks the table as a data table', + /\bdata-table\b/.test(filterOnly), + filterOnly +) +check( + 'filter alone does NOT enable sorting/paging/search', + !/data-table-sortable|data-table-paging|data-table-searchable/.test(filterOnly) +) + +// The filter attributes the JavaScript reads. +check('filter-only table carries data-filter-col', /data-filter-col=1/.test(filterOnly)) +check('filter-only table carries data-filter-id', /data-filter-id=table-filter-\w+/.test(filterOnly)) + +// The button group: one "All" button plus one per category. +const allBtns = html.match(/data-filter-value=""/g) ?? [] +check('an All button per filtered table', allBtns.length === 2, `found ${allBtns.length}`) +check('a widget button exists', /data-filter-value="widget"/.test(html)) +check('a gadget button exists', /data-filter-value="gadget"/.test(html)) + +// The space in filter="widget, gadget" must be trimmed, not carried into the value. +// Anchor inside the attribute: a bare /\s"/ would match whitespace before almost any quote. +const padded = [...html.matchAll(/data-filter-value="([^"]*)"/g)] + .map(m => m[1]) + .filter(v => v !== v.trim()) +check( + 'whitespace around a category value is trimmed', + padded.length === 0, + `padded values: ${JSON.stringify(padded)}` +) + +// data-filter-container was only ever read by the deleted DOM fallback. +check('data-filter-container is gone', !/data-filter-container/.test(html)) + +// The filter+wrap table is still a proper wrapped data table. +check( + 'filter+wrap table is a wrapped data table', + /data-table-wrap=true/.test(filterWrap) && /data-table-sortable=true/.test(filterWrap) +) + +let failed = 0 +for (const { name, pass, detail } of checks) { + console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${pass || !detail ? '' : ` — ${detail}`}`) + if (!pass) failed++ +} +console.log(`\n${checks.length - failed}/${checks.length} passing`) +process.exit(failed ? 1 : 0) +``` + +- [ ] **Step 3: Build and watch it fail** + +```bash +PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/hugo --gc -s exampleSite --logLevel warn \ + && node "$SCRATCH/verify-table-filter.mjs" +``` + +Expected: **FAIL**. The shortcode does not forward `filter`, so neither fixture table renders any +filter attributes or buttons at all. Record the output as the baseline. + +- [ ] **Step 4: Forward the arguments from the shortcode** + +In `layouts/_shortcodes/table.html`, the partial call currently ends with `"wrapper"` and +`"_default"`. Add the two filter arguments before `"_default"`: + +```hugo + {{ partial "assets/table.html" (dict + "page" .Page + "input" .Inner + "breakpoint" $args.breakpoint + "class" $args.class + "sortable" $args.sortable + "paginate" $args.paginate + "pagination" (or $args.pagination $args.pagingOptionPerPage) + "pagination-select" (or $args.paginationSelect $args.pagingOptionPageSelect) + "searchable" $args.searchable + "filter" $args.filter + "filter-col" $args.filterCol + "wrap" $args.wrap + "wrapper" $args.wrapper + "_default" $args.default + ) }} +``` + +- [ ] **Step 5: Normalize `filter` and make it imply a data table** + +In `layouts/_partials/assets/table.html`, the "Initialize local variables" block currently reads: + +```hugo +{{/* Initialize local variables */}} +{{ $breakpoint := site.Params.main.breakpoint | default "md" }} +{{ $dataTable := or $args.sortable $args.paginate $args.searchable }} +{{ $wrap := and $args.wrap (ne $breakpoint "xs") }} +``` + +Replace it with: + +```hugo +{{/* Initialize local variables */}} +{{ $breakpoint := site.Params.main.breakpoint | default "md" }} + +{{/* `filter` accepts a slice or a comma-separated string. A shortcode named parameter is always a + string, and InitArgs validates an argument's kind without casting between kinds, so a string + arrives here unsplit - and `range` cannot iterate a string. Normalize to a slice of trimmed, + non-empty values, so `filter="widget, gadget"` behaves like `filter="widget,gadget"`. */}} +{{ $filter := slice }} +{{ with $args.filter }} + {{ $values := . }} + {{ if not (reflect.IsSlice $values) }}{{ $values = split (printf "%v" $values) "," }}{{ end }} + {{ range $values }} + {{ $value := trim (printf "%v" .) " " }} + {{ if $value }}{{ $filter = $filter | append $value }}{{ end }} + {{ end }} +{{ end }} + +{{/* Filtering runs through simple-datatables' row model, so it composes with sorting, paging and + free-text search. A filtered table is therefore always a data table, even when all three are + off: the library still initializes, and `search()` still filters it - `searchable` gates only + the search input, not the method. This keeps `filter` self-sufficient, so an author never has + to switch on sorting they do not want just to get filtering. */}} +{{ $dataTable := or $args.sortable $args.paginate $args.searchable (gt (len $filter) 0) }} +{{ $wrap := and $args.wrap (ne $breakpoint "xs") }} +``` + +- [ ] **Step 6: Use the normalized filter in the Filter block** + +Further down, the Filter block currently reads: + +```hugo +{{/* Filter */}} +{{ $filter := $args.filter }} +{{ $filterCol := $args.filterCol | default 1 }} +``` + +`$filter` is now defined above, so drop its re-declaration: + +```hugo +{{/* Filter */}} +{{ $filterCol := $args.filterCol | default 1 }} +``` + +Leave the rest of that block (`$filterId`, the `data-filter-col` / `data-filter-id` attributes) and +the button-group markup exactly as they are — `{{ range $filter }}` now iterates a real slice. + +- [ ] **Step 7: Drop the dead `data-filter-container` attribute** + +The `.table-responsive` wrapper currently reads: + +```hugo +
+``` + +That attribute is read only by the DOM fallback deleted in Task 5. Replace the line with: + +```hugo +
+``` + +- [ ] **Step 8: Verify a `filter` passed as a real slice still works** + +The normalization has a `reflect.IsSlice` branch that no Markdown fixture can reach — a shortcode +parameter is always a string. Exercise it by calling the partial directly from a **temporary** +shortcode, so the real partial is under test. + +```bash +mkdir -p exampleSite/layouts/_shortcodes +cat > exampleSite/layouts/_shortcodes/table-slice-test.html <<'EOF' +{{ partial "assets/table.html" (dict + "page" .Page + "input" .Inner + "filter" (slice "widget" "gadget") + "filter-col" 1 +) }} +EOF +cat > exampleSite/content/en/table-slice-test.md <<'EOF' +--- +title: Table Slice Test +modules: ["simple-datatables"] +--- + +{{< table-slice-test >}} +| Name | Type | +|-------|--------| +| alpha | widget | +| bravo | gadget | +{{< /table-slice-test >}} +EOF +PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/hugo --gc -s exampleSite --logLevel warn +grep -o 'data-filter-value="[^"]*"' exampleSite/public/en/table-slice-test/index.html | sort -u +``` + +Expected: three values — `""` (the All button), `"widget"` and `"gadget"`. A Hugo error, or a single +button, means the slice branch is broken. + +Now remove the temporary files: + +```bash +rm exampleSite/content/en/table-slice-test.md exampleSite/layouts/_shortcodes/table-slice-test.html +rmdir exampleSite/layouts/_shortcodes 2>/dev/null || true +``` + +- [ ] **Step 9: Update the wrap script's table count** + +The fixture page now holds five tables, not three. In `$SCRATCH/verify-table-wrap.mjs`, change the +first check's expectation: + +```js +check('one table per shortcode', tables.length === 5, `found ${tables.length}`) +``` + +Leave the `colspanRows.length === 6` expectation alone: the two new tables add none — the filter-only +table does not wrap, and the filter+wrap table is a data table, whose wrapped rows are produced by +JavaScript rather than by Hugo. + +- [ ] **Step 10: Build and verify both scripts** + +```bash +PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/hugo --gc -s exampleSite --logLevel warn \ + && node "$SCRATCH/verify-table-filter.mjs" \ + && node "$SCRATCH/verify-table-wrap.mjs" +``` + +Expected: **both scripts fully passing**. The wrap script must stay green — this task must not +regress the shipped wrap feature. + +- [ ] **Step 11: Lint and commit** + +```bash +npm test +git add layouts/_shortcodes/table.html layouts/_partials/assets/table.html exampleSite/content/en/table-demo.md +git commit -m "fix(table): make the category filter reachable and self-sufficient + +The shortcode never forwarded filter or filter-col, so a filter declared in +Markdown validated cleanly and then did nothing. Forward both, and split a +comma-separated string into a slice: a shortcode parameter is always a string, +InitArgs validates a kind without casting, and range cannot iterate a string. + +A filtered table is now always a data table, even with sorting, paging and +search all off - filtering runs through simple-datatables' row model, so it +composes with them, and searchable gates only the search input, not the search +method. An author no longer has to enable sorting merely to get filtering. + +Drops data-filter-container, which only the DOM fallback ever read." +``` + +--- + +### Task 4: Warn when the page has not opted into the module + +Turns a silent failure into a build warning. This affects `sortable`, `paginate` and `searchable` +too, all of which fail silently today — it is what made an earlier test fixture vacuous. + +**Files:** + +- Modify: `layouts/_partials/assets/table.html` + +**Interfaces:** + +- Consumes: `$dataTable` (Task 3). + +- [ ] **Step 1: Understand why the opt-in cannot be automated** + +A data table needs the module's JavaScript **and** its per-page stylesheet. `footer/scripts.html` +runs after the content and honours the render-time `dependencies` scratch, but `head/head.html` +emits the module's stylesheet (`/css/simple-datatables.css`) **before** the content is rendered, so +it cannot see a dependency a shortcode declares mid-render. Auto-loading would attach the JavaScript +without the CSS. Do **not** call `AddModule` here. Warn instead. + +- [ ] **Step 2: Add the warning** + +In `layouts/_partials/assets/table.html`, immediately after the block that appends the +`data-table-wrap` attributes (the one commented "A wrapped data table is rendered uniformly…") and +before the `{{/* Filter */}}` block, insert: + +```hugo +{{/* A data table needs both the module's JavaScript and its per-page stylesheet, and Hinode loads an + optional module's assets only for pages that list it in frontmatter. head.html emits the + stylesheet before the content is rendered, so a render-time dependency cannot add it - warn + rather than fail silently. Skipped when the site configures the module as `core` or `critical`, + since it then loads on every page anyway. */}} +{{ if $dataTable }} + {{ with $args.page.Scratch.Get "modules" }} + {{ if in (.optional | default slice) "simple-datatables" }} + {{ $pageModules := slice | append $args.page.Params.modules }} + {{ if not (in $pageModules "simple-datatables") }} + {{ partial "utilities/LogWarn.html" (dict + "partial" "assets/table.html" + "warnid" "warn-missing-module" + "msg" "A sortable, paginated, searchable or filtered table requires the simple-datatables module. Add `modules: [\"simple-datatables\"]` to the page frontmatter." + "file" $args.page.File + )}} + {{ end }} + {{ end }} + {{ end }} +{{ end }} +``` + +`baseof.html:18` sets the `modules` scratch from `utilities/InitModules.html`, whose `optional` key +lists the modules the site declares as `integration = "optional"`. The `with` guard keeps the partial +safe if it is ever rendered outside `baseof.html`. `slice | append .Params.modules` is the same +idiom `head/head.html:21` uses, and flattens a frontmatter list. + +- [ ] **Step 3: Prove the warning fires** + +Create a page that uses a data table without opting in: + +```bash +cat > exampleSite/content/en/table-nomodule.md <<'EOF' +--- +title: Table Without Module +--- + +{{< table sortable="true" >}} +| Name | Type | +|-------|--------| +| alpha | widget | +{{< /table >}} +EOF +PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/hugo --gc -s exampleSite --logLevel warn 2>&1 | grep -i "simple-datatables module" || echo "NO WARNING — the check is not firing" +``` + +Expected: a warning naming `table-nomodule.md` and telling the author to add the frontmatter key. + +- [ ] **Step 4: Prove it does NOT fire on a page that has opted in** + +```bash +rm exampleSite/content/en/table-nomodule.md +PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/hugo --gc -s exampleSite --logLevel warn 2>&1 | grep -i "simple-datatables module" && echo "FALSE POSITIVE — warning fired on an opted-in page" || echo "no warning (correct)" +``` + +Expected: `no warning (correct)`. The fixture page declares `modules: ["simple-datatables"]`, so it +must stay silent. + +- [ ] **Step 5: Confirm nothing else regressed, then commit** + +```bash +node "$SCRATCH/verify-table-filter.mjs" && node "$SCRATCH/verify-table-wrap.mjs" +npm test +git status --short # table-nomodule.md must be gone +git add layouts/_partials/assets/table.html +git commit -m "fix(table): warn when a data table's module is not loaded + +A sortable, paginated, searchable or filtered table needs simple-datatables' +JavaScript and its per-page stylesheet, and Hinode loads an optional module's +assets only for pages listing it in frontmatter. Omitting the key produced a +table that silently did nothing. + +The opt-in cannot be added at render time: head.html emits the stylesheet before +the content renders, so a dependency declared by a shortcode would attach the +script without the styles. Warn at build time instead." +``` + +--- + +### Task 5: Delete the DOM fallback + +Committed in `mod-simple-datatables`. Every filtered table is a data table after Task 3, so the +fallback branch is unreachable — and it is the branch carrying the wrapped-table bug. + +**Files:** + +- Modify: `../mod-simple-datatables/assets/js/modules/simple-datatables/simple-datatables.load.js` + +**Interfaces:** + +- Consumes: `data-filter-table` / `data-filter-value` on the buttons and `data-filter-col` / + `data-filter-id` on the table (Task 3). + +- [ ] **Step 1: Branch the module** + +```bash +git -C ../mod-simple-datatables checkout -b fix/table-filter origin/main +head -1 ../mod-simple-datatables/go.mod +``` + +Expected: `module github.com/gethinode/mod-simple-datatables/v4` + +- [ ] **Step 2: Point the exampleSite at the local module** + +Two LOCAL-ONLY edits, reverted in Task 6 and **never committed**. + +`exampleSite/hinode.work` — add the module: + +```text +go 1.19 + +use . +use ../ +use ../../mod-simple-datatables +``` + +`exampleSite/config/_default/hugo.toml` — under `[module]`, directly below the existing +`workspace = "hinode.work"` line, add a `replacements` line (the file already carries commented-out +examples of exactly this). A workspace `use` directive alone does **not** override this module — +Hugo keeps resolving it from the committed `_vendor/` copy: + +```toml + replacements = 'github.com/gethinode/mod-simple-datatables/v4 -> /Users/mark/Development/GitHub/gethinode/mod-simple-datatables/' +``` + +Confirm the override took: + +```bash +PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/hugo config mounts -s exampleSite 2>/dev/null | grep -c "gethinode/mod-simple-datatables" || true +``` + +- [ ] **Step 3: Delete the fallback branch** + +In `../mod-simple-datatables/assets/js/modules/simple-datatables/simple-datatables.load.js`, replace +the whole `document.querySelectorAll('[data-filter-table]')` block — including its leading comment — +with: + +```js +// Category filter button group. +// Filtering runs through simple-datatables' search(term, columns, source), so it composes with +// sorting, pagination and the free-text search: the named source 'category-filter' is independent +// of the search input, so both narrow the result set at once. Hinode marks every filtered table as +// a data table, so an instance always exists by the time a button can be clicked. +document.querySelectorAll('[data-filter-table]').forEach(btn => { + btn.addEventListener('click', function () { + const tableId = this.getAttribute('data-filter-table') + const filterValue = this.getAttribute('data-filter-value').toLowerCase() + + // Update active button state + document.querySelectorAll(`[data-filter-table="${tableId}"]`).forEach(b => { + b.classList.toggle('active', b === this) + }) + + const instances = tableFilterInstances[tableId] + if (!instances) return + + instances.forEach(({ dt, filterCol }) => { + dt.search(filterValue, [filterCol], 'category-filter') + }) + }) +}) +``` + +The deleted branch iterated `tbody tr` and read `row.cells[col]`. On a wrapped table the second row +of each record holds a single `
`, so `cells[1]` was `undefined`, its text read as empty, +and the description row was hidden whenever a filter was active. Deleting it removes the bug. + +- [ ] **Step 4: Rebuild and confirm the generated HTML is unchanged** + +The change is browser-side, so Hugo's output must not move. + +```bash +PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/hugo --gc -s exampleSite --logLevel warn \ + && node "$SCRATCH/verify-table-filter.mjs" && node "$SCRATCH/verify-table-wrap.mjs" +``` + +Expected: both scripts still fully passing. + +- [ ] **Step 5: Commit in mod-simple-datatables** + +```bash +git -C ../mod-simple-datatables add assets/js/modules/simple-datatables/simple-datatables.load.js +git -C ../mod-simple-datatables commit -m "fix(table): drop the filter's DOM fallback + +Hinode now marks every filtered table as a data table, so the fallback that +toggled row display directly is unreachable. It was also the branch carrying a +bug: it read row.cells[col] across every tbody tr, but a wrapped table's second +row per record holds a single colspan cell, so cells[1] was undefined and the +description row was hidden whenever a filter was active. + +Filtering now always runs through the row model, so it composes with sorting, +pagination and the free-text search." +``` + +--- + +### Task 6: Browser verification and clean-up + +Nothing so far proves a button click actually filters anything. + +**Files:** + +- Modify: `exampleSite/hinode.work`, `exampleSite/config/_default/hugo.toml` (both reverted) +- Modify: `exampleSite/hugo_stats.json` (build artefact, committed) + +- [ ] **Step 1: Serve the site on an isolated cache** + +Run in the background. The isolated resource directory keeps this build from touching the shared +`hinode/resources/_gen`. + +```bash +PATH="$PWD/node_modules/.bin:$PATH" HUGO_RESOURCEDIR="$SCRATCH/resources" HUGO_CACHEDIR="$SCRATCH/cache" \ + node_modules/.bin/hugo server -s exampleSite --port 1318 --disableFastRender +``` + +Use `127.0.0.1`, not `localhost` — an unrelated process squats on IPv6 localhost. The fixture is at +`http://127.0.0.1:1318/en/table-demo/`. + +- [ ] **Step 2: Sanity-check that the module loaded** + +`mod-simple-datatables` is `integration = "optional"`, so its JavaScript loads only because the +fixture page's frontmatter lists it. Confirm `typeof window.simpleDatatables === "object"` and that +`.fixture-filter` carries the `datatable-table` class. **If this fails, every check below is +vacuous — say so and stop.** + +- [ ] **Step 3: Exercise the filter-only table** + +On `.fixture-filter` at 1280×900: + +- Three buttons: `All` (active), `Widget`, `Gadget`. +- No pager and no search input — sorting, paging and search are all off, yet the table is a + DataTable. +- Click `Widget`: only `alpha` and `charlie` remain. Click `Gadget`: only `bravo` and `delta`. + Click `All`: all four return. +- The active button state follows the click. +- Console clean — zero uncaught exceptions. + +- [ ] **Step 4: Prove the filter composes with sort, page and search** + +On `.fixture-filter-wrap` at 1280×900 (`sortable`, `paginate` with `pagination=2`, `filter`): + +- Click `Widget`. The pager recounts to the **filtered** set (2 records → 1 page), not the full 4. +- Sort by Name descending. The filter survives the sort — still only widgets. +- Type `charlie` into the search box. The category filter and the free-text search narrow the set + **together** (this is what the separate `'category-filter'` search source buys). Clear it. + +- [ ] **Step 5: Prove the wrapped-table bug is gone** + +This is the defect the deleted fallback carried. On `.fixture-filter-wrap` at 400×900 (below the +breakpoint, so the table wraps): + +- Each visible record still shows **both** its rows — the data row and its full-width description + row. +- Click `Widget`. The filtered-out records disappear **entirely**, and every remaining record still + has its description row. A record showing its data row but *missing* its description row is the + old bug. + +- [ ] **Step 6: Stop the server, revert the local overrides** + +```bash +git checkout exampleSite/hinode.work exampleSite/config/_default/hugo.toml +git diff --stat exampleSite/hinode.work exampleSite/config/_default/hugo.toml +``` + +Expected: no output — both files clean. + +- [ ] **Step 7: Rebuild against the vendored module and commit the build stats** + +```bash +PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/hugo --gc -s exampleSite --logLevel warn +node "$SCRATCH/verify-table-filter.mjs" && node "$SCRATCH/verify-table-wrap.mjs" +npm test +``` + +This build uses the **vendored** mod-simple-datatables, which does not carry Task 5, so its filter +still takes the (now unreachable) fallback branch. Every assertion in both scripts is about Hugo's +output, so both must still pass. + +```bash +git add exampleSite/hugo_stats.json +git commit -m "chore: update build stats" +``` + +- [ ] **Step 8: Confirm nothing stray is committed** + +```bash +git log --oneline main..HEAD +git diff --stat main..HEAD +``` + +Expected: the spec, the i18n and structure change, the AddModule fix, the filter wiring, the module +warning, and the build stats. **No** `hinode.work`, **no** `exampleSite/config/_default/hugo.toml`, +**no** scratch script, **no** `table-nomodule.md`. + +- [ ] **Step 9: Report the release ordering** + +Summarise for the user: two branches (`hinode:fix/table-category-filter`, +`mod-simple-datatables:fix/table-filter`). The module must merge and release before Hinode is bumped +to it; until then Hinode's filter works through the fallback branch, which is functionally correct +except on a wrapped table. + +--- + +## Notes for the reviewer + +- **The filter's original design is deliberately reversed.** Commit `63a88070`'s fallback was + written for "filter-only without sortable/paginate/searchable", and `table.yml`'s comment + deliberately excluded filtering from the features needing the module. That intent is overturned + here: the fallback was a second, inferior implementation — it toggled `display` and composed with + nothing, while `dt.search()` filters through the row model and survives sorting, paging and search. +- **`filter` implying a data table** is what keeps the argument usable on its own after the fallback + is deleted. Without it, `{{< table filter="a,b" >}}` would render dead buttons. +- **`AddModule.html` was already broken** and is fixed here even though the filter does not use it — + the design had to establish why the module opt-in cannot be automated, which meant reading it. From d04023c0175e12309c6722518b4810d16e22c305 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:58:36 +0200 Subject: [PATCH 03/19] fix(i18n): add the missing table filter translations tableFilterLabel and tableFilterAll were called by the table partial but existed in no language file, so they were untranslatable and emitted i18n warnings. The partial only rendered because it passes a default to T. Also records in the table structure that filtering, like sorting, paging and searching, needs the simple-datatables module. --- data/structures/table.yml | 2 +- i18n/de.yaml | 6 +++++- i18n/en.yaml | 4 ++++ i18n/fr.yaml | 4 ++++ i18n/nl.yaml | 4 ++++ i18n/pl.yaml | 4 ++++ i18n/pt-br.yaml | 4 ++++ i18n/zh-hans.yaml | 4 ++++ i18n/zh-hant.yaml | 4 ++++ 9 files changed, 34 insertions(+), 2 deletions(-) diff --git a/data/structures/table.yml b/data/structures/table.yml index 4907a5552..7326922bd 100644 --- a/data/structures/table.yml +++ b/data/structures/table.yml @@ -1,6 +1,6 @@ comment: >- Makes a Markdown table responsive with horizontal scrolling on smaller screens. - Sorting, paging, and searching require the simple-datatables module. + Sorting, paging, searching, and filtering require the simple-datatables module. icon: table_chart arguments: page: diff --git a/i18n/de.yaml b/i18n/de.yaml index 64a4fcf93..8fe05c05b 100644 --- a/i18n/de.yaml +++ b/i18n/de.yaml @@ -232,4 +232,8 @@ - id: formSubmit translation: "Absenden" - id: formBotField - translation: "Bitte nicht ausfüllen, wenn Sie ein Mensch sind" \ No newline at end of file + translation: "Bitte nicht ausfüllen, wenn Sie ein Mensch sind" +- id: tableFilterLabel + translation: "Filter" +- id: tableFilterAll + translation: "Alle" \ No newline at end of file diff --git a/i18n/en.yaml b/i18n/en.yaml index 1175593cb..472b4666a 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -241,3 +241,7 @@ translation: "Enlarged view" - id: lightboxExpand translation: "View fullscreen" +- id: tableFilterLabel + translation: "Filter" +- id: tableFilterAll + translation: "All" diff --git a/i18n/fr.yaml b/i18n/fr.yaml index 9c4cc877d..7e2be76b0 100644 --- a/i18n/fr.yaml +++ b/i18n/fr.yaml @@ -233,3 +233,7 @@ translation: "Envoyer" - id: formBotField translation: "Ne remplissez pas ce champ si vous êtes humain" +- id: tableFilterLabel + translation: "Filtre" +- id: tableFilterAll + translation: "Tout" diff --git a/i18n/nl.yaml b/i18n/nl.yaml index dfa3aaf5f..8c2a4000f 100644 --- a/i18n/nl.yaml +++ b/i18n/nl.yaml @@ -233,3 +233,7 @@ translation: "Verzenden" - id: formBotField translation: "Vul dit niet in als je een mens bent" +- id: tableFilterLabel + translation: "Filter" +- id: tableFilterAll + translation: "Alle" diff --git a/i18n/pl.yaml b/i18n/pl.yaml index ae1082c59..fe8346d3b 100644 --- a/i18n/pl.yaml +++ b/i18n/pl.yaml @@ -233,3 +233,7 @@ translation: "Wyślij" - id: formBotField translation: "Nie wypełniaj tego pola, jeśli jesteś człowiekiem" +- id: tableFilterLabel + translation: "Filtr" +- id: tableFilterAll + translation: "Wszystkie" diff --git a/i18n/pt-br.yaml b/i18n/pt-br.yaml index 734ef60af..5f1460123 100644 --- a/i18n/pt-br.yaml +++ b/i18n/pt-br.yaml @@ -235,3 +235,7 @@ translation: "Enviar" - id: formBotField translation: "Não preencha isto se você for humano" +- id: tableFilterLabel + translation: "Filtro" +- id: tableFilterAll + translation: "Todos" diff --git a/i18n/zh-hans.yaml b/i18n/zh-hans.yaml index 01e11f94d..aef57b32f 100644 --- a/i18n/zh-hans.yaml +++ b/i18n/zh-hans.yaml @@ -233,3 +233,7 @@ translation: "提交" - id: formBotField translation: "如果您是人类,请不要填写此项" +- id: tableFilterLabel + translation: "筛选" +- id: tableFilterAll + translation: "全部" diff --git a/i18n/zh-hant.yaml b/i18n/zh-hant.yaml index 173df97ce..c4640144a 100644 --- a/i18n/zh-hant.yaml +++ b/i18n/zh-hant.yaml @@ -233,3 +233,7 @@ translation: "提交" - id: formBotField translation: "如果您是人類,請不要填寫此欄" +- id: tableFilterLabel + translation: "篩選" +- id: tableFilterAll + translation: "全部" From ad21748dfa8aa1c9e3f371cabb0f571315ba46ed Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:04:29 +0200 Subject: [PATCH 04/19] fix(utilities): stop AddModule discarding existing dependencies AddModule used complement to add a module to the page's dependencies scratch, but complement returns the elements of the LAST collection absent from the others - so it replaced the list with the single module being added, and wiped it entirely when re-adding one already present. Append and deduplicate instead, matching how head.html and scripts.html merge the same scratch. --- layouts/_partials/utilities/AddModule.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layouts/_partials/utilities/AddModule.html b/layouts/_partials/utilities/AddModule.html index 3b430da11..802c79357 100644 --- a/layouts/_partials/utilities/AddModule.html +++ b/layouts/_partials/utilities/AddModule.html @@ -1,7 +1,7 @@ {{ with .module }} {{- $dependencies := page.Scratch.Get "dependencies" -}} {{- if reflect.IsSlice $dependencies -}} - {{- $dependencies = complement $dependencies (slice .) -}} + {{- $dependencies = $dependencies | append . | uniq -}} {{ else }} {{- $dependencies = slice . -}} {{ end }} From b4948f4876a87082eb535744008d5f483c7d96ae Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:15:01 +0200 Subject: [PATCH 05/19] fix(table): make the category filter reachable and self-sufficient The shortcode never forwarded filter or filter-col, so a filter declared in Markdown validated cleanly and then did nothing. Forward both, and split a comma-separated string into a slice: a shortcode parameter is always a string, InitArgs validates a kind without casting, and range cannot iterate a string. A filtered table is now always a data table, even with sorting, paging and search all off - filtering runs through simple-datatables' row model, so it composes with them, and searchable gates only the search input, not the search method. An author no longer has to enable sorting merely to get filtering. Drops data-filter-container, which only the DOM fallback ever read. --- exampleSite/content/en/table-demo.md | 24 ++++++++++++++++++++++++ layouts/_partials/assets/table.html | 25 ++++++++++++++++++++++--- layouts/_shortcodes/table.html | 2 ++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/exampleSite/content/en/table-demo.md b/exampleSite/content/en/table-demo.md index 1c55b9a0d..ececbfc62 100644 --- a/exampleSite/content/en/table-demo.md +++ b/exampleSite/content/en/table-demo.md @@ -36,3 +36,27 @@ modules: ["simple-datatables"] | alpha | The first record, with a description long enough to need wrapping. | | bravo | The second record, also with a fairly long trailing description. | {{< /table >}} + +## Filter-only table + +{{< table filter="widget, gadget" filter-col="1" class="fixture-filter" >}} + +| Name | Type | Description | +|---------|--------|--------------------------------------------------------------------| +| alpha | widget | The first record, with a description long enough to need wrapping. | +| bravo | gadget | The second record, also with a fairly long trailing description. | +| charlie | widget | The third record. Short. | +| delta | gadget | The fourth record, whose description runs on for a little while. | +{{< /table >}} + +## Filtered, sortable, wrapped data table + +{{< table filter="widget,gadget" filter-col="1" sortable="true" paginate="true" pagination="2" wrap="true" class="fixture-filter-wrap" >}} + +| Name | Type | Description | +|---------|--------|--------------------------------------------------------------------| +| alpha | widget | The first record, with a description long enough to need wrapping. | +| bravo | gadget | The second record, also with a fairly long trailing description. | +| charlie | widget | The third record. Short. | +| delta | gadget | The fourth record, whose description runs on for a little while. | +{{< /table >}} diff --git a/layouts/_partials/assets/table.html b/layouts/_partials/assets/table.html index 64c8020ee..d14f1d81f 100644 --- a/layouts/_partials/assets/table.html +++ b/layouts/_partials/assets/table.html @@ -18,7 +18,27 @@ {{/* Initialize local variables */}} {{ $breakpoint := site.Params.main.breakpoint | default "md" }} -{{ $dataTable := or $args.sortable $args.paginate $args.searchable }} + +{{/* `filter` accepts a slice or a comma-separated string. A shortcode named parameter is always a + string, and InitArgs validates an argument's kind without casting between kinds, so a string + arrives here unsplit - and `range` cannot iterate a string. Normalize to a slice of trimmed, + non-empty values, so `filter="widget, gadget"` behaves like `filter="widget,gadget"`. */}} +{{ $filter := slice }} +{{ with $args.filter }} + {{ $values := . }} + {{ if not (reflect.IsSlice $values) }}{{ $values = split (printf "%v" $values) "," }}{{ end }} + {{ range $values }} + {{ $value := trim (printf "%v" .) " " }} + {{ if $value }}{{ $filter = $filter | append $value }}{{ end }} + {{ end }} +{{ end }} + +{{/* Filtering runs through simple-datatables' row model, so it composes with sorting, paging and + free-text search. A filtered table is therefore always a data table, even when all three are + off: the library still initializes, and `search()` still filters it - `searchable` gates only + the search input, not the method. This keeps `filter` self-sufficient, so an author never has + to switch on sorting they do not want just to get filtering. */}} +{{ $dataTable := or $args.sortable $args.paginate $args.searchable (gt (len $filter) 0) }} {{ $wrap := and $args.wrap (ne $breakpoint "xs") }} {{/* `table-wrap` is an internal marker owned by the templates, not a class a caller may set: it @@ -50,7 +70,6 @@ {{ end }} {{/* Filter */}} -{{ $filter := $args.filter }} {{ $filterCol := $args.filterCol | default 1 }} {{ $filterId := "" }} {{ if $filter }} @@ -107,7 +126,7 @@ {{ $input | safeHTML }} {{ end }} {{ else }} -
+
{{ $input | safeHTML }}
{{ end }} diff --git a/layouts/_shortcodes/table.html b/layouts/_shortcodes/table.html index 0c5c1038b..61ff2f0ee 100644 --- a/layouts/_shortcodes/table.html +++ b/layouts/_shortcodes/table.html @@ -29,6 +29,8 @@ "pagination" (or $args.pagination $args.pagingOptionPerPage) "pagination-select" (or $args.paginationSelect $args.pagingOptionPageSelect) "searchable" $args.searchable + "filter" $args.filter + "filter-col" $args.filterCol "wrap" $args.wrap "wrapper" $args.wrapper "_default" $args.default From c4f87b4897fab2ed85e0e06f65ca528f714d23b1 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:52:10 +0200 Subject: [PATCH 06/19] fix(components): make table filter id deterministic across builds Replace the md5(page + now)-based filter id with a sequential counter held in page.Store. now() was evaluated per call, so a filtered table's data-filter-id changed on every build even when the page content was unchanged, defeating output diffing and CDN caching. The id only needs to be unique within a page, so a page-scoped ordinal (table-filter-1, table-filter-2, ...) is sufficient and stable. --- layouts/_partials/assets/table.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/layouts/_partials/assets/table.html b/layouts/_partials/assets/table.html index d14f1d81f..56b15c58f 100644 --- a/layouts/_partials/assets/table.html +++ b/layouts/_partials/assets/table.html @@ -73,7 +73,10 @@ {{ $filterCol := $args.filterCol | default 1 }} {{ $filterId := "" }} {{ if $filter }} - {{ $filterId = printf "table-filter-%s" (md5 (delimit (slice . now) "-")) }} + {{ $store := $args.page.Store }} + {{ $count := add (or ($store.Get "hinodeTableFilterCount") 0) 1 }} + {{ $store.Set "hinodeTableFilterCount" $count }} + {{ $filterId = printf "table-filter-%d" $count }} {{ $attributes = printf "%s data-filter-col=%d data-filter-id=%s" $attributes $filterCol $filterId }} {{ end }} From f6ddd12e1cd29ac343f122aa5f7da56c9da02957 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:20:38 +0200 Subject: [PATCH 07/19] fix(table): warn when a data table's module is not loaded A sortable, paginated, searchable or filtered table needs simple-datatables' JavaScript and its per-page stylesheet, and Hinode loads an optional module's assets only for pages listing it in frontmatter. Omitting the key produced a table that silently did nothing. The opt-in cannot be added at render time: head.html emits the stylesheet before the content renders, so a dependency declared by a shortcode would attach the script without the styles. Warn at build time instead. --- layouts/_partials/assets/table.html | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/layouts/_partials/assets/table.html b/layouts/_partials/assets/table.html index 56b15c58f..fc61a3139 100644 --- a/layouts/_partials/assets/table.html +++ b/layouts/_partials/assets/table.html @@ -69,6 +69,27 @@ {{ $attributes = printf "%s data-table-wrap=true data-table-wrap-breakpoint=%s" $attributes $breakpoint }} {{ end }} +{{/* A data table needs both the module's JavaScript and its per-page stylesheet, and Hinode loads an + optional module's assets only for pages that list it in frontmatter. head.html emits the + stylesheet before the content is rendered, so a render-time dependency cannot add it - warn + rather than fail silently. Skipped when the site configures the module as `core` or `critical`, + since it then loads on every page anyway. */}} +{{ if $dataTable }} + {{ with $args.page.Scratch.Get "modules" }} + {{ if in (.optional | default slice) "simple-datatables" }} + {{ $pageModules := slice | append $args.page.Params.modules }} + {{ if not (in $pageModules "simple-datatables") }} + {{ partial "utilities/LogWarn.html" (dict + "partial" "assets/table.html" + "warnid" "warn-missing-module" + "msg" "A sortable, paginated, searchable or filtered table requires the simple-datatables module. Add `modules: [\"simple-datatables\"]` to the page frontmatter." + "file" $args.page.File + )}} + {{ end }} + {{ end }} + {{ end }} +{{ end }} + {{/* Filter */}} {{ $filterCol := $args.filterCol | default 1 }} {{ $filterId := "" }} From 9c63e927c378934b40ce6ec393d5a3e0751e6f92 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:30:11 +0200 Subject: [PATCH 08/19] fix(utils): honour explicit page argument in AddModule AddModule.html ignored its `page` argument and always wrote dependencies to Hugo's bare global `page`. In a Bookshop-block render that global can resolve to the wrong page, so the dependency landed on the wrong page's Scratch. Now an explicitly-passed page is used, falling back to the global `page` when the caller omits it, matching how mod-blocks' list component already calls it. Co-Authored-By: Claude Opus 4.8 (1M context) --- layouts/_partials/utilities/AddModule.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/layouts/_partials/utilities/AddModule.html b/layouts/_partials/utilities/AddModule.html index 802c79357..8e868c620 100644 --- a/layouts/_partials/utilities/AddModule.html +++ b/layouts/_partials/utilities/AddModule.html @@ -1,9 +1,13 @@ +{{- $page := .page -}} +{{- if not $page -}} + {{- $page = page -}} +{{- end -}} {{ with .module }} - {{- $dependencies := page.Scratch.Get "dependencies" -}} + {{- $dependencies := $page.Scratch.Get "dependencies" -}} {{- if reflect.IsSlice $dependencies -}} {{- $dependencies = $dependencies | append . | uniq -}} {{ else }} {{- $dependencies = slice . -}} {{ end }} - {{- page.Scratch.Set "dependencies" $dependencies -}} + {{- $page.Scratch.Set "dependencies" $dependencies -}} {{ end }} \ No newline at end of file From b8d352cb19dddb91a74f249099cbb1bb77828d59 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:12:17 +0200 Subject: [PATCH 09/19] chore: update build stats --- exampleSite/hugo_stats.json | 176 ++++++++++++++++++------------------ 1 file changed, 90 insertions(+), 86 deletions(-) diff --git a/exampleSite/hugo_stats.json b/exampleSite/hugo_stats.json index cca48e152..9739ae252 100644 --- a/exampleSite/hugo_stats.json +++ b/exampleSite/hugo_stats.json @@ -419,6 +419,8 @@ "file-panel", "fixed-top", "fixture-data", + "fixture-filter", + "fixture-filter-wrap", "fixture-plain", "fixture-two-col", "flex-column", @@ -1055,10 +1057,10 @@ "dropdown-align-end-1", "dropdown-callout-1", "dropdown-nav-0", - "dropdown-panel-0cfb20edec652bf60cec3417805fa222", - "dropdown-panel-7aba633db5a411161dbf095de4cfd978", - "dropdown-panel-ba76ce9233a79362eaf28f46c2108dd9", - "dropdown-panel-f7f555735c8a4a1a934497b400420cf5", + "dropdown-panel-1c3234841533a2e77d6ec063141c7e17", + "dropdown-panel-25c1079d2c43f1e28e8d7b826ab3066d", + "dropdown-panel-562e8b9bdb5de3723511faef432cee4e", + "dropdown-panel-eae6dd065d90bf2ba90cea1ef52b3089", "dropdown-pills-1", "dropdown-tabs-1", "dropdown-underline-1", @@ -1082,11 +1084,11 @@ "fab-whatsapp", "fab-x-twitter", "faq", - "faq-c52f2479ecb8fcda54d5b85674a678c7", - "faq-c52f2479ecb8fcda54d5b85674a678c7-heading-faq-c52f2479ecb8fcda54d5b85674a678c7", - "faq-c52f2479ecb8fcda54d5b85674a678c7-item-0", - "faq-c52f2479ecb8fcda54d5b85674a678c7-item-1", - "faq-c52f2479ecb8fcda54d5b85674a678c7-item-2", + "faq-cc9ac616918d8a9f781a948ff34b1d0b", + "faq-cc9ac616918d8a9f781a948ff34b1d0b-heading-faq-cc9ac616918d8a9f781a948ff34b1d0b", + "faq-cc9ac616918d8a9f781a948ff34b1d0b-item-0", + "faq-cc9ac616918d8a9f781a948ff34b1d0b-item-1", + "faq-cc9ac616918d8a9f781a948ff34b1d0b-item-2", "far-square", "fas-1", "fas-2", @@ -1137,7 +1139,9 @@ "file", "file-preview-with-filename-only", "fill-and-justify", + "filter-only-table", "filtered-list", + "filtered-sortable-wrapped-data-table", "first-paragraph", "first-post", "fixed-width", @@ -1260,10 +1264,10 @@ "nav-align-end-1", "nav-callout-1", "nav-nav-0", - "nav-panel-0cfb20edec652bf60cec3417805fa222", - "nav-panel-7aba633db5a411161dbf095de4cfd978", - "nav-panel-ba76ce9233a79362eaf28f46c2108dd9", - "nav-panel-f7f555735c8a4a1a934497b400420cf5", + "nav-panel-1c3234841533a2e77d6ec063141c7e17", + "nav-panel-25c1079d2c43f1e28e8d7b826ab3066d", + "nav-panel-562e8b9bdb5de3723511faef432cee4e", + "nav-panel-eae6dd065d90bf2ba90cea1ef52b3089", "nav-pills-1", "nav-tabs-1", "nav-underline-1", @@ -1301,36 +1305,36 @@ "over-mij", "overview", "page-link", - "panel-0cfb20edec652bf60cec3417805fa222-0", - "panel-0cfb20edec652bf60cec3417805fa222-1", - "panel-0cfb20edec652bf60cec3417805fa222-2", - "panel-0cfb20edec652bf60cec3417805fa222-btn-0", - "panel-0cfb20edec652bf60cec3417805fa222-btn-1", - "panel-0cfb20edec652bf60cec3417805fa222-btn-2", - "panel-7aba633db5a411161dbf095de4cfd978-0", - "panel-7aba633db5a411161dbf095de4cfd978-1", - "panel-7aba633db5a411161dbf095de4cfd978-2", - "panel-7aba633db5a411161dbf095de4cfd978-btn-0", - "panel-7aba633db5a411161dbf095de4cfd978-btn-1", - "panel-7aba633db5a411161dbf095de4cfd978-btn-2", - "panel-96b4f00d6449184adb8db75aab865784-0", - "panel-96b4f00d6449184adb8db75aab865784-1", - "panel-96b4f00d6449184adb8db75aab865784-2", - "panel-96b4f00d6449184adb8db75aab865784-btn-0", - "panel-96b4f00d6449184adb8db75aab865784-btn-1", - "panel-96b4f00d6449184adb8db75aab865784-btn-2", - "panel-ba76ce9233a79362eaf28f46c2108dd9-0", - "panel-ba76ce9233a79362eaf28f46c2108dd9-1", - "panel-ba76ce9233a79362eaf28f46c2108dd9-2", - "panel-ba76ce9233a79362eaf28f46c2108dd9-btn-0", - "panel-ba76ce9233a79362eaf28f46c2108dd9-btn-1", - "panel-ba76ce9233a79362eaf28f46c2108dd9-btn-2", - "panel-f7f555735c8a4a1a934497b400420cf5-0", - "panel-f7f555735c8a4a1a934497b400420cf5-1", - "panel-f7f555735c8a4a1a934497b400420cf5-2", - "panel-f7f555735c8a4a1a934497b400420cf5-btn-0", - "panel-f7f555735c8a4a1a934497b400420cf5-btn-1", - "panel-f7f555735c8a4a1a934497b400420cf5-btn-2", + "panel-1c3234841533a2e77d6ec063141c7e17-0", + "panel-1c3234841533a2e77d6ec063141c7e17-1", + "panel-1c3234841533a2e77d6ec063141c7e17-2", + "panel-1c3234841533a2e77d6ec063141c7e17-btn-0", + "panel-1c3234841533a2e77d6ec063141c7e17-btn-1", + "panel-1c3234841533a2e77d6ec063141c7e17-btn-2", + "panel-25c1079d2c43f1e28e8d7b826ab3066d-0", + "panel-25c1079d2c43f1e28e8d7b826ab3066d-1", + "panel-25c1079d2c43f1e28e8d7b826ab3066d-2", + "panel-25c1079d2c43f1e28e8d7b826ab3066d-btn-0", + "panel-25c1079d2c43f1e28e8d7b826ab3066d-btn-1", + "panel-25c1079d2c43f1e28e8d7b826ab3066d-btn-2", + "panel-562e8b9bdb5de3723511faef432cee4e-0", + "panel-562e8b9bdb5de3723511faef432cee4e-1", + "panel-562e8b9bdb5de3723511faef432cee4e-2", + "panel-562e8b9bdb5de3723511faef432cee4e-btn-0", + "panel-562e8b9bdb5de3723511faef432cee4e-btn-1", + "panel-562e8b9bdb5de3723511faef432cee4e-btn-2", + "panel-8bc59fcf382342dfcc79eb402ae00400-0", + "panel-8bc59fcf382342dfcc79eb402ae00400-1", + "panel-8bc59fcf382342dfcc79eb402ae00400-2", + "panel-8bc59fcf382342dfcc79eb402ae00400-btn-0", + "panel-8bc59fcf382342dfcc79eb402ae00400-btn-1", + "panel-8bc59fcf382342dfcc79eb402ae00400-btn-2", + "panel-eae6dd065d90bf2ba90cea1ef52b3089-0", + "panel-eae6dd065d90bf2ba90cea1ef52b3089-1", + "panel-eae6dd065d90bf2ba90cea1ef52b3089-2", + "panel-eae6dd065d90bf2ba90cea1ef52b3089-btn-0", + "panel-eae6dd065d90bf2ba90cea1ef52b3089-btn-1", + "panel-eae6dd065d90bf2ba90cea1ef52b3089-btn-2", "panels", "persona", "pie-chart", @@ -1353,50 +1357,50 @@ "powershell", "premier-article", "preview", + "preview-0b8150807319a7a0da6929488d49204a-desktop", + "preview-0b8150807319a7a0da6929488d49204a-mobile", + "preview-0b8150807319a7a0da6929488d49204a-tablet", "preview-1", "preview-2", - "preview-3a3e4d10fa21a9c3ea92921d89691a1a-desktop", - "preview-3a3e4d10fa21a9c3ea92921d89691a1a-desktop-tab", - "preview-3a3e4d10fa21a9c3ea92921d89691a1a-mobile", - "preview-3a3e4d10fa21a9c3ea92921d89691a1a-mobile-tab", - "preview-3a3e4d10fa21a9c3ea92921d89691a1a-tablet", - "preview-3a3e4d10fa21a9c3ea92921d89691a1a-tablet-tab", - "preview-7160897b10e8421f71367314a25afc93-desktop", - "preview-7160897b10e8421f71367314a25afc93-desktop-tab", - "preview-7160897b10e8421f71367314a25afc93-mobile", - "preview-7160897b10e8421f71367314a25afc93-mobile-tab", - "preview-7160897b10e8421f71367314a25afc93-tablet", - "preview-7160897b10e8421f71367314a25afc93-tablet-tab", - "preview-832897b0d2dc7386a051af01d403ce60-desktop", - "preview-832897b0d2dc7386a051af01d403ce60-desktop-tab", - "preview-832897b0d2dc7386a051af01d403ce60-mobile", - "preview-832897b0d2dc7386a051af01d403ce60-mobile-tab", - "preview-832897b0d2dc7386a051af01d403ce60-tablet", - "preview-832897b0d2dc7386a051af01d403ce60-tablet-tab", - "preview-a355d3c149636aae67a38b97a0373152-desktop", - "preview-a355d3c149636aae67a38b97a0373152-mobile", - "preview-a355d3c149636aae67a38b97a0373152-tablet", - "preview-a878242076c9a75becb77ad7ffa243ba-desktop", - "preview-a878242076c9a75becb77ad7ffa243ba-desktop-tab", - "preview-a878242076c9a75becb77ad7ffa243ba-mobile", - "preview-a878242076c9a75becb77ad7ffa243ba-mobile-tab", - "preview-a878242076c9a75becb77ad7ffa243ba-tablet", - "preview-a878242076c9a75becb77ad7ffa243ba-tablet-tab", - "preview-da9e236bb383de7cbcae1e5377fb8966-desktop", - "preview-da9e236bb383de7cbcae1e5377fb8966-desktop-tab", - "preview-da9e236bb383de7cbcae1e5377fb8966-mobile", - "preview-da9e236bb383de7cbcae1e5377fb8966-mobile-tab", - "preview-da9e236bb383de7cbcae1e5377fb8966-tablet", - "preview-da9e236bb383de7cbcae1e5377fb8966-tablet-tab", - "preview-e2f6b12be7dd7a47acf5f9e4b34f422f-desktop", - "preview-e2f6b12be7dd7a47acf5f9e4b34f422f-desktop-tab", - "preview-e2f6b12be7dd7a47acf5f9e4b34f422f-mobile", - "preview-e2f6b12be7dd7a47acf5f9e4b34f422f-mobile-tab", - "preview-e2f6b12be7dd7a47acf5f9e4b34f422f-tablet", - "preview-e2f6b12be7dd7a47acf5f9e4b34f422f-tablet-tab", - "preview-efd900be1ae3d93632cca03a18c9169d-desktop", - "preview-efd900be1ae3d93632cca03a18c9169d-mobile", - "preview-efd900be1ae3d93632cca03a18c9169d-tablet", + "preview-29cbaf9a2b3d93fa2dcc6b5c63c96b6a-desktop", + "preview-29cbaf9a2b3d93fa2dcc6b5c63c96b6a-desktop-tab", + "preview-29cbaf9a2b3d93fa2dcc6b5c63c96b6a-mobile", + "preview-29cbaf9a2b3d93fa2dcc6b5c63c96b6a-mobile-tab", + "preview-29cbaf9a2b3d93fa2dcc6b5c63c96b6a-tablet", + "preview-29cbaf9a2b3d93fa2dcc6b5c63c96b6a-tablet-tab", + "preview-30381cfc9077a7d29f4e82b0d6fb7fa6-desktop", + "preview-30381cfc9077a7d29f4e82b0d6fb7fa6-desktop-tab", + "preview-30381cfc9077a7d29f4e82b0d6fb7fa6-mobile", + "preview-30381cfc9077a7d29f4e82b0d6fb7fa6-mobile-tab", + "preview-30381cfc9077a7d29f4e82b0d6fb7fa6-tablet", + "preview-30381cfc9077a7d29f4e82b0d6fb7fa6-tablet-tab", + "preview-59a49a5cc385c8e885cc342d10a07ef4-desktop", + "preview-59a49a5cc385c8e885cc342d10a07ef4-mobile", + "preview-59a49a5cc385c8e885cc342d10a07ef4-tablet", + "preview-9585f15106b39aa644c33d2d620c086f-desktop", + "preview-9585f15106b39aa644c33d2d620c086f-desktop-tab", + "preview-9585f15106b39aa644c33d2d620c086f-mobile", + "preview-9585f15106b39aa644c33d2d620c086f-mobile-tab", + "preview-9585f15106b39aa644c33d2d620c086f-tablet", + "preview-9585f15106b39aa644c33d2d620c086f-tablet-tab", + "preview-dd9d83ba3d6b2ce4eb0b55bf216d2b19-desktop", + "preview-dd9d83ba3d6b2ce4eb0b55bf216d2b19-desktop-tab", + "preview-dd9d83ba3d6b2ce4eb0b55bf216d2b19-mobile", + "preview-dd9d83ba3d6b2ce4eb0b55bf216d2b19-mobile-tab", + "preview-dd9d83ba3d6b2ce4eb0b55bf216d2b19-tablet", + "preview-dd9d83ba3d6b2ce4eb0b55bf216d2b19-tablet-tab", + "preview-df7671f49e000a684c4cb7d233cfa021-desktop", + "preview-df7671f49e000a684c4cb7d233cfa021-desktop-tab", + "preview-df7671f49e000a684c4cb7d233cfa021-mobile", + "preview-df7671f49e000a684c4cb7d233cfa021-mobile-tab", + "preview-df7671f49e000a684c4cb7d233cfa021-tablet", + "preview-df7671f49e000a684c4cb7d233cfa021-tablet-tab", + "preview-f5d035e291f06dd539721b09b49c476f-desktop", + "preview-f5d035e291f06dd539721b09b49c476f-desktop-tab", + "preview-f5d035e291f06dd539721b09b49c476f-mobile", + "preview-f5d035e291f06dd539721b09b49c476f-mobile-tab", + "preview-f5d035e291f06dd539721b09b49c476f-tablet", + "preview-f5d035e291f06dd539721b09b49c476f-tablet-tab", "preview-unavailable", "preview-unavailable-alert-only", "preview-with-specific-device", @@ -1468,7 +1472,7 @@ "tabs-1-btn-2", "team", "testimonial", - "testimonial-carousel-9d27ae2d5d999d7b9dee34f4e9139f71", + "testimonial-carousel-7a82dcbf3f77289b79ea6273adda62cd", "testimonial-with-avatar", "testimonial-with-case-study", "testimonial-with-icon", From aebeab8e2b49f4cd43ead8c12550e5fb0c108aad Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:51:29 +0200 Subject: [PATCH 10/19] fix(table): honour filter-col=0 and warn from the shortcode - Read `filter-col` as-is in assets/table.html: mod-utils already defaults it to 1, and piping it through Hugo's `default` rewrote an explicit `0` (an empty value to `default`) back to 1, so column 0 could never be filtered - Move the missing-module warning from the partial to the shortcode: `.Page` is the content page only in a shortcode, while a Bookshop block resolves the partial's `page` argument to the rendering template's page - which warned about pages holding no table at all - Pass `details` as a slice, so LogMsg no longer appends a stray `%!s()` line - Correct the rationale comment: a render-time dependency is unreliable because it is indeterminate (see assets/lightbox.html), not because head.html runs before the content Co-Authored-By: Claude Opus 4.8 (1M context) --- layouts/_partials/assets/table.html | 26 ++++---------------- layouts/_shortcodes/table.html | 37 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/layouts/_partials/assets/table.html b/layouts/_partials/assets/table.html index fc61a3139..c1b85c106 100644 --- a/layouts/_partials/assets/table.html +++ b/layouts/_partials/assets/table.html @@ -69,29 +69,11 @@ {{ $attributes = printf "%s data-table-wrap=true data-table-wrap-breakpoint=%s" $attributes $breakpoint }} {{ end }} -{{/* A data table needs both the module's JavaScript and its per-page stylesheet, and Hinode loads an - optional module's assets only for pages that list it in frontmatter. head.html emits the - stylesheet before the content is rendered, so a render-time dependency cannot add it - warn - rather than fail silently. Skipped when the site configures the module as `core` or `critical`, - since it then loads on every page anyway. */}} -{{ if $dataTable }} - {{ with $args.page.Scratch.Get "modules" }} - {{ if in (.optional | default slice) "simple-datatables" }} - {{ $pageModules := slice | append $args.page.Params.modules }} - {{ if not (in $pageModules "simple-datatables") }} - {{ partial "utilities/LogWarn.html" (dict - "partial" "assets/table.html" - "warnid" "warn-missing-module" - "msg" "A sortable, paginated, searchable or filtered table requires the simple-datatables module. Add `modules: [\"simple-datatables\"]` to the page frontmatter." - "file" $args.page.File - )}} - {{ end }} - {{ end }} - {{ end }} -{{ end }} - {{/* Filter */}} -{{ $filterCol := $args.filterCol | default 1 }} +{{/* `filter-col` carries a default of 1 in mod-utils, and InitArgs applies a default only when the + argument is nil - so an explicit `0` arrives here intact. Hugo's `default` treats `0` as empty, + so piping through it would silently rewrite column 0 to column 1. Read the argument as-is. */}} +{{ $filterCol := $args.filterCol }} {{ $filterId := "" }} {{ if $filter }} {{ $store := $args.page.Store }} diff --git a/layouts/_shortcodes/table.html b/layouts/_shortcodes/table.html index 61ff2f0ee..53d0bb481 100644 --- a/layouts/_shortcodes/table.html +++ b/layouts/_shortcodes/table.html @@ -17,6 +17,43 @@ )}} {{- end -}} +{{/* A data table needs both the simple-datatables module's JavaScript and its per-page stylesheet, + and Hinode loads an optional module's assets only for pages that list it in frontmatter. A + render-time dependency cannot be relied on to add them: a value registered while the content + renders is "indeterminate until Hugo renders the page content" + (https://gohugo.io/methods/page/store/), which holds only for a page without a `description` in + its frontmatter - see assets/lightbox.html. Warn rather than fail silently. + + The check lives in the shortcode rather than in assets/table.html because only here is `.Page` + reliably the content page; a Bookshop block resolves it to the rendering template's page + instead, which would warn about pages that hold no table at all. Skipped when the site + configures the module as `core` or `critical`, since it then loads on every page anyway. + + A filtered table is a data table too (see assets/table.html); a shortcode parameter is always a + string, so any non-empty `filter` counts. + + Reported against the page rather than the shortcode's position: Hugo may render the same + shortcode more than once (the summary and the content are separate renders, at different + offsets), and a position would then turn one table into two distinct warnings. */}} +{{ $page := .Page }} +{{ $filtered := ne (trim (printf "%v" (or $args.filter "")) " ") "" }} +{{ if or $args.sortable $args.paginate $args.searchable $filtered }} + {{ with $page.Scratch.Get "modules" }} + {{ if in (.optional | default slice) "simple-datatables" }} + {{ $pageModules := slice | append $page.Params.modules }} + {{ if not (in $pageModules "simple-datatables") }} + {{ partial "utilities/LogWarn.html" (dict + "partial" "shortcodes/table.html" + "warnid" "warn-missing-module" + "msg" "A sortable, paginated, searchable or filtered table requires the simple-datatables module" + "details" (slice "Add `modules: [\"simple-datatables\"]` to the page frontmatter") + "file" $page.File + )}} + {{ end }} + {{ end }} + {{ end }} +{{ end }} + {{/* Main code */}} {{ if not $args.err }} {{ partial "assets/table.html" (dict From 720fdab011de55d2c481c5d970a02344bd5137d2 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:27:11 +0200 Subject: [PATCH 11/19] fix(components): align table filter warning with partial normalization - Treat `filter` as present only if it has a non-comma, non-whitespace character, matching how assets/table.html normalizes it (split on commas, trim, drop blanks). A separator-only value like `filter=","` no longer triggers a spurious missing-module warning. - Correct the rationale comment for warning instead of auto-declaring the module dependency: state the actual reason (auto-declaring would tie the shortcode to head.html's eager evaluation of `.Page.Content`, an implementation detail, not a contract) instead of the disproven claim about Page.Store timing. Co-Authored-By: Claude Sonnet 5 --- layouts/_shortcodes/table.html | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/layouts/_shortcodes/table.html b/layouts/_shortcodes/table.html index 53d0bb481..19a8ee045 100644 --- a/layouts/_shortcodes/table.html +++ b/layouts/_shortcodes/table.html @@ -18,25 +18,28 @@ {{- end -}} {{/* A data table needs both the simple-datatables module's JavaScript and its per-page stylesheet, - and Hinode loads an optional module's assets only for pages that list it in frontmatter. A - render-time dependency cannot be relied on to add them: a value registered while the content - renders is "indeterminate until Hugo renders the page content" - (https://gohugo.io/methods/page/store/), which holds only for a page without a `description` in - its frontmatter - see assets/lightbox.html. Warn rather than fail silently. + and Hinode loads an optional module's assets only for pages that list it in frontmatter. + Hinode could auto-declare the dependency here (via utilities/AddModule.html) instead of + warning, but that would make a shortcode's behavior depend on head/head.html's eager + evaluation of `.Page.Content` for its description fallback - an incidental implementation + detail elsewhere in the render pipeline, not a contract this shortcode can rely on. Warn + instead, so the author declares the module explicitly. The check lives in the shortcode rather than in assets/table.html because only here is `.Page` reliably the content page; a Bookshop block resolves it to the rendering template's page instead, which would warn about pages that hold no table at all. Skipped when the site configures the module as `core` or `critical`, since it then loads on every page anyway. - A filtered table is a data table too (see assets/table.html); a shortcode parameter is always a - string, so any non-empty `filter` counts. + A filtered table is a data table too (see assets/table.html), but only if `filter` actually + yields a category there: the partial splits on commas and drops blanks, so `filter=","` + normalizes to no filter at all. Test for a comma-and-whitespace-free character rather than + duplicating that split/trim/append loop here. Reported against the page rather than the shortcode's position: Hugo may render the same shortcode more than once (the summary and the content are separate renders, at different offsets), and a position would then turn one table into two distinct warnings. */}} {{ $page := .Page }} -{{ $filtered := ne (trim (printf "%v" (or $args.filter "")) " ") "" }} +{{ $filtered := gt (len (findRE `[^,\s]` (printf "%v" (or $args.filter "")) 1)) 0 }} {{ if or $args.sortable $args.paginate $args.searchable $filtered }} {{ with $page.Scratch.Get "modules" }} {{ if in (.optional | default slice) "simple-datatables" }} From ef6809d3996bea673ccb41907db5a6d78054979c Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:46:05 +0200 Subject: [PATCH 12/19] docs: add design for a shared deterministic element-id helper Four components in mod-blocks build a DOM id from md5(... now ...), so the id changes on every build and any page carrying one emits different HTML each time. The table shortcode had the same bug; its fix introduced a deterministic page.Store counter. Extract that into utilities/UniqueID.html and apply it to all four remaining sites. A spike confirmed page.Store is stable and unique in a Bookshop block render, which was the design's key unknown. --- .../2026-07-14-unique-id-helper-design.md | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-14-unique-id-helper-design.md diff --git a/docs/superpowers/specs/2026-07-14-unique-id-helper-design.md b/docs/superpowers/specs/2026-07-14-unique-id-helper-design.md new file mode 100644 index 000000000..78a5b2da6 --- /dev/null +++ b/docs/superpowers/specs/2026-07-14-unique-id-helper-design.md @@ -0,0 +1,138 @@ +# Design: a shared helper for deterministic element ids + +Date: 2026-07-14 +Status: Approved +Repositories: `gethinode/hinode`, `gethinode/mod-blocks` + +## Problem + +Several components generate a DOM id by hashing the current time: + +```hugo +{{ $id := printf "faq-%s" (md5 (delimit (slice . now) "-")) }} +``` + +`now` is evaluated per call, so the id changes on **every build**. Any page carrying such a +component emits different HTML each time it is built, which defeats output diffing and makes +CDN and browser caches treat an unchanged page as changed. + +A scan of every `gethinode` repository found the idiom in exactly five places. One — the table +shortcode's `data-filter-id` — was fixed while repairing the table category filter, and +replaced with a deterministic per-page counter held in `page.Store`. The other four all live in +**mod-blocks**: + +| File | Id | +| --- | --- | +| `component-library/components/panels/panels.hugo.html:37` | `panel-` | +| `component-library/components/faq/faq.hugo.html:42` | `faq-` | +| `layouts/partials/assets/testimonial-carousel.html:48` | `testimonial-carousel-` | +| `layouts/partials/assets/preview.html:243` | `preview-` | + +Hinode's `assets/nav.html` prefixes the panels id, which is why the symptom surfaces in +`hugo_stats.json` as `dropdown-panel-` churning on every build. + +This design replaces the idiom with one shared helper. + +## The key unknown, resolved + +An id counter is only useful if it is **unique within each emitted page** and **stable across +builds**. Hinode renders pages concurrently, so a counter anchored to the wrong page could be +assigned in a non-deterministic order. + +That was a live concern here, because the global `page` function is known to misresolve inside +a Bookshop block render: it returns the page whose *template* is executing, not the content +page. This was observed while fixing the table filter, where a warning fired against +`/en/404.html` and `/en/docs/blocks/` for tables those pages do not contain. + +**Measured, not assumed.** A spike replaced `panels.hugo.html`'s `md5(… now …)` with a +`page.Store` counter using the global `page`, and built the exampleSite twice: + +```text +BUILD A: dropdown-panel-1, dropdown-panel-2, dropdown-panel-3, dropdown-panel-5 +BUILD B: dropdown-panel-1, dropdown-panel-2, dropdown-panel-3, dropdown-panel-5 +``` + +Identical across builds, and unique within the page. The misresolving renders are teaser and +summary passes whose output is discarded, so they never emit an id — they only consume a +counter value, which is why `-4` is absent. A gap in the numbering breaks neither uniqueness +nor determinism. + +`page.Store` was separately confirmed to survive an embedded `RenderString` (which is what the +`example` shortcode does), where `.Ordinal` does **not** — it restarts at 0. + +## Design + +### 1. Hinode — `layouts/_partials/utilities/UniqueID.html` (new) + +Returns a page-unique, build-stable element id. + +**Arguments:** + +- `prefix` (string, required) — the id's leading segment, e.g. `panel`. +- `page` (optional) — the page to anchor the counter to. Falls back to the global `page` + function when omitted, matching how `utilities/AddModule.html` now treats the same argument. + +**Returns:** `-`, where `n` is a counter starting at 1. + +The counter is held in `page.Store` under a key namespaced by prefix, so each prefix counts +independently: `panel-1`, `panel-2`, `faq-1`. A single shared counter would interleave +unrelated components and make the numbering read as arbitrary. + +The helper does not attempt to be unique across pages — a DOM id only needs to be unique within +the document that carries it. + +### 2. Hinode — `layouts/_partials/assets/table.html` + +The table partial currently carries its own inline counter (`hinodeTableFilterCount`) producing +`table-filter-N`. Replace it with a call to the helper, prefix `table-filter`. The rendered +output is unchanged; this removes the second implementation of the same mechanism. + +### 3. mod-blocks — the four call sites + +Replace each `md5(delimit (slice . now) "-")` with a call to the helper: + +| File | Prefix | +| --- | --- | +| `component-library/components/panels/panels.hugo.html` | `panel` | +| `component-library/components/faq/faq.hugo.html` | `faq` | +| `layouts/partials/assets/testimonial-carousel.html` | `testimonial-carousel` | +| `layouts/partials/assets/preview.html` | `preview` | + +The two Bookshop components pass Bookshop's `._page`, which they have available (`list.hugo.html` +already uses it). The two partials pass no page and take the global fallback, which the spike +proved correct for the ids that reach the emitted HTML. + +Each id is referenced elsewhere in its own component — `data-companion`, `aria-controls`, +`data-bs-target`, `data-bs-parent`. Those references derive from the same variable, so they +follow automatically; the change must not alter how they are constructed. + +### 4. Release ordering + +The helper is a Hinode partial, so mod-blocks can only call it once Hinode ships it. + +1. Hinode PR #2024 (the table category filter, already open) is **amended** with the helper and + the table migration. +2. Hinode releases. +3. mod-blocks opens a **new** PR that bumps its Hinode dependency to that release and converts + the four call sites. + +## Verification + +**Determinism and uniqueness.** Build the Hinode exampleSite twice and confirm that every +affected page's ids are byte-identical between builds, and that no id repeats within a page. + +**Behaviour.** A deterministic id that points at the wrong element is worse than a random one, +so the components must be driven in a real browser (Playwright) on the Hinode exampleSite, +covering every affected page — Hinode's own `table-demo` and the mounted mod-docs pages for +panels, faq, testimonials and preview: + +- **Panels:** the dropdown opens *its own* panel, not another panel on the page. With more than + one panels block on a page, each dropdown must control its own. +- **FAQ:** each accordion item toggles the item it belongs to, and only that one. +- **Testimonial carousel:** it cycles, and its indicators target its own slides. +- **Preview:** the device tabs switch the preview they belong to. +- **Table:** the filter buttons still drive their own table (the id they pair on is now produced + by the helper). +- Zero uncaught console exceptions on every page. + +**Regression.** `npm test` clean, and the existing table verification scripts still pass. From f9ff459266e1eb64f52018b7c9e63277184dfc3e Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:51:42 +0200 Subject: [PATCH 13/19] docs: home the id helper in mod-utils, not Hinode No gethinode module declares a dependency on Hinode, so a partial there is reachable only because the site merges Hinode's layouts - an undeclared inverted dependency that breaks each module's own exampleSite build and would block the .Ordinal follow-up, which must reach mod-lottie and mod-leaflet. Moves AddModule.html to mod-utils for the same reason: mod-blocks calls it without declaring Hinode. Callers are unaffected; Hugo resolves partials by path. --- .../2026-07-14-unique-id-helper-design.md | 138 ++++++++++++------ 1 file changed, 92 insertions(+), 46 deletions(-) diff --git a/docs/superpowers/specs/2026-07-14-unique-id-helper-design.md b/docs/superpowers/specs/2026-07-14-unique-id-helper-design.md index 78a5b2da6..46845d13f 100644 --- a/docs/superpowers/specs/2026-07-14-unique-id-helper-design.md +++ b/docs/superpowers/specs/2026-07-14-unique-id-helper-design.md @@ -2,7 +2,7 @@ Date: 2026-07-14 Status: Approved -Repositories: `gethinode/hinode`, `gethinode/mod-blocks` +Repositories: `gethinode/mod-utils`, `gethinode/hinode`, `gethinode/mod-blocks` ## Problem @@ -13,12 +13,12 @@ Several components generate a DOM id by hashing the current time: ``` `now` is evaluated per call, so the id changes on **every build**. Any page carrying such a -component emits different HTML each time it is built, which defeats output diffing and makes -CDN and browser caches treat an unchanged page as changed. +component emits different HTML each time it is built, which defeats output diffing and makes CDN +and browser caches treat an unchanged page as changed. A scan of every `gethinode` repository found the idiom in exactly five places. One — the table -shortcode's `data-filter-id` — was fixed while repairing the table category filter, and -replaced with a deterministic per-page counter held in `page.Store`. The other four all live in +shortcode's `data-filter-id` — was fixed while repairing the table category filter, and replaced +with a deterministic per-page counter held in `page.Store`. The other four all live in **mod-blocks**: | File | Id | @@ -31,18 +31,47 @@ replaced with a deterministic per-page counter held in `page.Store`. The other f Hinode's `assets/nav.html` prefixes the panels id, which is why the symptom surfaces in `hugo_stats.json` as `dropdown-panel-` churning on every build. -This design replaces the idiom with one shared helper. +This design replaces the idiom with one shared helper — and puts that helper where every module +can legitimately reach it. + +## Where the helper belongs + +Not in Hinode. **No gethinode module declares a dependency on Hinode:** + +| Module | imports mod-utils | imports hinode | +| --- | --- | --- | +| mod-blocks | yes | no | +| mod-lottie | yes | no | +| mod-leaflet | yes | no | +| mod-mermaid | yes | no | +| mod-simple-datatables | yes | no | + +A partial in Hinode is reachable from these modules only because the final *site* merges Hinode's +layouts. That is an undeclared, inverted dependency: it breaks each module's own exampleSite +build, and it would block the planned `.Ordinal` follow-up outright, since that has to reach +mod-lottie and mod-leaflet. + +**mod-utils is the correct home.** Every module already imports it, and so does Hinode. The move +is transparent to callers, because Hugo resolves partials by path across the merged layout tree. + +### The same wart, one level down + +`utilities/AddModule.html` lives in Hinode today, yet mod-blocks' `list` component calls it +without declaring Hinode. It moves to mod-utils too, for the same reason. Its callers +(`hinode/layouts/_partials/assets/lightbox.html`, `hinode/layouts/_shortcodes/table.html`, +`mod-blocks/component-library/components/list/list.hugo.html`) are unaffected — the partial's +path does not change. ## The key unknown, resolved An id counter is only useful if it is **unique within each emitted page** and **stable across -builds**. Hinode renders pages concurrently, so a counter anchored to the wrong page could be +builds**. Hugo renders pages concurrently, so a counter anchored to the wrong page could be assigned in a non-deterministic order. -That was a live concern here, because the global `page` function is known to misresolve inside -a Bookshop block render: it returns the page whose *template* is executing, not the content -page. This was observed while fixing the table filter, where a warning fired against -`/en/404.html` and `/en/docs/blocks/` for tables those pages do not contain. +That was a live concern, because the global `page` function is known to misresolve inside a +Bookshop block render: it returns the page whose *template* is executing, not the content page. +This was observed while fixing the table filter, where a warning fired against `/en/404.html` and +`/en/docs/blocks/` for tables those pages do not contain. **Measured, not assumed.** A spike replaced `panels.hugo.html`'s `md5(… now …)` with a `page.Store` counter using the global `page`, and built the exampleSite twice: @@ -53,41 +82,50 @@ BUILD B: dropdown-panel-1, dropdown-panel-2, dropdown-panel-3, dropdown-panel-5 ``` Identical across builds, and unique within the page. The misresolving renders are teaser and -summary passes whose output is discarded, so they never emit an id — they only consume a -counter value, which is why `-4` is absent. A gap in the numbering breaks neither uniqueness -nor determinism. +summary passes whose output is discarded, so they never emit an id — they only consume a counter +value, which is why `-4` is absent. A gap in the numbering breaks neither uniqueness nor +determinism. `page.Store` was separately confirmed to survive an embedded `RenderString` (which is what the `example` shortcode does), where `.Ordinal` does **not** — it restarts at 0. ## Design -### 1. Hinode — `layouts/_partials/utilities/UniqueID.html` (new) +### 1. mod-utils — `layouts/_partials/utilities/UniqueID.html` (new) Returns a page-unique, build-stable element id. **Arguments:** - `prefix` (string, required) — the id's leading segment, e.g. `panel`. -- `page` (optional) — the page to anchor the counter to. Falls back to the global `page` - function when omitted, matching how `utilities/AddModule.html` now treats the same argument. +- `page` (optional) — the page to anchor the counter to. Falls back to the global `page` function + when omitted, matching how `utilities/AddModule.html` treats the same argument. **Returns:** `-`, where `n` is a counter starting at 1. The counter is held in `page.Store` under a key namespaced by prefix, so each prefix counts -independently: `panel-1`, `panel-2`, `faq-1`. A single shared counter would interleave -unrelated components and make the numbering read as arbitrary. +independently: `panel-1`, `panel-2`, `faq-1`. A single shared counter would interleave unrelated +components and make the numbering read as arbitrary. The helper does not attempt to be unique across pages — a DOM id only needs to be unique within the document that carries it. -### 2. Hinode — `layouts/_partials/assets/table.html` +### 2. mod-utils — `layouts/_partials/utilities/AddModule.html` (moved from Hinode) + +Moved verbatim, including the two fixes it received while repairing the table filter: it appends +and deduplicates rather than using `complement` (which *replaced* the dependency list, and wiped +it entirely when re-adding an entry already present), and it honours an explicitly passed `page` +instead of ignoring it in favour of the global `page`. + +### 3. Hinode -The table partial currently carries its own inline counter (`hinodeTableFilterCount`) producing -`table-filter-N`. Replace it with a call to the helper, prefix `table-filter`. The rendered -output is unchanged; this removes the second implementation of the same mechanism. +- Delete `layouts/_partials/utilities/AddModule.html` (now provided by mod-utils). +- Replace the inline counter in `layouts/_partials/assets/table.html` (`hinodeTableFilterCount`, + producing `table-filter-N`) with a call to `utilities/UniqueID.html`, prefix `table-filter`. The + rendered output is unchanged; this removes the second implementation of the same mechanism. +- Bump the mod-utils dependency to the release carrying both partials. -### 3. mod-blocks — the four call sites +### 4. mod-blocks — the four call sites Replace each `md5(delimit (slice . now) "-")` with a call to the helper: @@ -98,41 +136,49 @@ Replace each `md5(delimit (slice . now) "-")` with a call to the helper: | `layouts/partials/assets/testimonial-carousel.html` | `testimonial-carousel` | | `layouts/partials/assets/preview.html` | `preview` | -The two Bookshop components pass Bookshop's `._page`, which they have available (`list.hugo.html` -already uses it). The two partials pass no page and take the global fallback, which the spike -proved correct for the ids that reach the emitted HTML. +The two Bookshop components pass Bookshop's `._page`, which they have available +(`list.hugo.html` already uses it). The two partials pass no page and take the global fallback, +which the spike proved correct for the ids that reach the emitted HTML. Each id is referenced elsewhere in its own component — `data-companion`, `aria-controls`, -`data-bs-target`, `data-bs-parent`. Those references derive from the same variable, so they -follow automatically; the change must not alter how they are constructed. +`data-bs-target`, `data-bs-parent`. Those references derive from the same variable, so they follow +automatically; the change must not alter how they are constructed. -### 4. Release ordering +Bump the mod-utils dependency to the release carrying the helper. -The helper is a Hinode partial, so mod-blocks can only call it once Hinode ships it. +### 5. Release ordering -1. Hinode PR #2024 (the table category filter, already open) is **amended** with the helper and - the table migration. -2. Hinode releases. -3. mod-blocks opens a **new** PR that bumps its Hinode dependency to that release and converts - the four call sites. +1. **mod-utils** ships `UniqueID.html` and the moved `AddModule.html`, and releases. +2. **Hinode** and **mod-blocks** then bump mod-utils and land their changes — **in parallel**; + neither depends on the other. + +Hinode's `AddModule.html` must not be deleted before the mod-utils release exists, or the partial +disappears from the merged layout tree and every caller fails. + +Hinode PR #2024 (the table category filter, already open) is **amended** with its part. +mod-blocks gets a new PR. ## Verification -**Determinism and uniqueness.** Build the Hinode exampleSite twice and confirm that every -affected page's ids are byte-identical between builds, and that no id repeats within a page. +**Determinism and uniqueness.** Build the Hinode exampleSite twice and confirm that every affected +page's ids are byte-identical between builds, and that no id repeats within a page. -**Behaviour.** A deterministic id that points at the wrong element is worse than a random one, -so the components must be driven in a real browser (Playwright) on the Hinode exampleSite, -covering every affected page — Hinode's own `table-demo` and the mounted mod-docs pages for -panels, faq, testimonials and preview: +**Behaviour.** A deterministic id that points at the wrong element is worse than a random one, so +the components must be driven in a real browser (Playwright) on the Hinode exampleSite, covering +every affected page — Hinode's own `table-demo` and the mounted mod-docs pages for panels, faq, +testimonials and preview: -- **Panels:** the dropdown opens *its own* panel, not another panel on the page. With more than - one panels block on a page, each dropdown must control its own. +- **Panels:** the dropdown opens *its own* panel, not another panel on the page. Where a page + carries more than one panels block, each dropdown must control its own. - **FAQ:** each accordion item toggles the item it belongs to, and only that one. - **Testimonial carousel:** it cycles, and its indicators target its own slides. - **Preview:** the device tabs switch the preview they belong to. -- **Table:** the filter buttons still drive their own table (the id they pair on is now produced - by the helper). +- **Table:** the filter buttons still drive their own table (the id they pair on is now produced by + the helper). - Zero uncaught console exceptions on every page. +**AddModule still works after the move.** Its three callers must behave unchanged — in particular +the lightbox partial, and the table shortcode's missing-module warning, which must still fire for a +data table on a page without `modules: ["simple-datatables"]` and stay silent otherwise. + **Regression.** `npm test` clean, and the existing table verification scripts still pass. From 3319d10db30b5dfca540661a49ca998144445f86 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:57:35 +0200 Subject: [PATCH 14/19] docs: merge mod-blocks before Hinode in the id-helper rollout Hinode's exampleSite pins mod-blocks, so merging Hinode first would deploy a demo site still emitting dropdown-panel-. Sequence is mod-utils, then mod-blocks, then Hinode. --- .../2026-07-14-unique-id-helper-design.md | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/docs/superpowers/specs/2026-07-14-unique-id-helper-design.md b/docs/superpowers/specs/2026-07-14-unique-id-helper-design.md index 46845d13f..d2b5a6db9 100644 --- a/docs/superpowers/specs/2026-07-14-unique-id-helper-design.md +++ b/docs/superpowers/specs/2026-07-14-unique-id-helper-design.md @@ -148,15 +148,28 @@ Bump the mod-utils dependency to the release carrying the helper. ### 5. Release ordering +Strictly sequential, mod-blocks **before** Hinode: + 1. **mod-utils** ships `UniqueID.html` and the moved `AddModule.html`, and releases. -2. **Hinode** and **mod-blocks** then bump mod-utils and land their changes — **in parallel**; - neither depends on the other. +2. **mod-blocks** bumps mod-utils, converts its four call sites, merges and releases. +3. **Hinode** bumps mod-utils *and* mod-blocks, deletes its own `AddModule.html`, and migrates the + table's counter to the helper. + +mod-blocks goes first so that Hinode's exampleSite picks up the fixed ids when it deploys — +`exampleSite/go.mod` pins mod-blocks, so a Hinode merge that preceded the mod-blocks release would +deploy a demo site still emitting `dropdown-panel-`. + +Two ordering constraints are load-bearing: -Hinode's `AddModule.html` must not be deleted before the mod-utils release exists, or the partial -disappears from the merged layout tree and every caller fails. +- Hinode's `AddModule.html` must not be deleted before the mod-utils release exists, or the partial + disappears from the merged layout tree and every caller — including mod-blocks' `list` component — + fails at build time. +- Between steps 1 and 3, `utilities/AddModule.html` exists in **both** mod-utils and Hinode. Hugo + resolves the project's own layouts ahead of an imported module's, so Hinode's copy shadows + mod-utils'. The two are identical, so this interval is harmless; step 3 removes the duplicate. -Hinode PR #2024 (the table category filter, already open) is **amended** with its part. -mod-blocks gets a new PR. +Hinode PR #2024 (the table category filter, already open) is **amended** with its part, and merges +last. mod-utils and mod-blocks each get a new PR. ## Verification From eeb90369593295669281433367a41ec7365bc779 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:44:20 +0200 Subject: [PATCH 15/19] docs: add implementation plan for the deterministic id helper Six tasks across mod-utils, mod-blocks and hinode. Merge order is strictly sequential: mod-utils, then mod-blocks, then hinode. --- .../plans/2026-07-14-unique-id-helper.md | 835 ++++++++++++++++++ 1 file changed, 835 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-14-unique-id-helper.md diff --git a/docs/superpowers/plans/2026-07-14-unique-id-helper.md b/docs/superpowers/plans/2026-07-14-unique-id-helper.md new file mode 100644 index 000000000..dd6162688 --- /dev/null +++ b/docs/superpowers/plans/2026-07-14-unique-id-helper.md @@ -0,0 +1,835 @@ +# Deterministic Element-Id Helper — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace every `md5(delimit (slice . now) "-")` element id with a shared, deterministic helper, so a page carrying one of these components stops emitting different HTML on every build. + +**Architecture:** A new `utilities/UniqueID.html` in **mod-utils** returns `-` from a per-prefix counter held in the page's `Store`. mod-utils is the home because no gethinode module declares a dependency on Hinode. `utilities/AddModule.html` moves there too, for the same reason. mod-blocks' four call sites and Hinode's table counter then both call the helper. + +**Tech Stack:** Hugo templates and partials; Hugo modules; Bookshop components; Playwright for browser verification. + +## Global Constraints + +- Spec: `docs/superpowers/specs/2026-07-14-unique-id-helper-design.md` (in the hinode repo). Read it first. +- Three repositories, siblings under `/Users/mark/Development/GitHub/gethinode/`: + `mod-utils` (on `main` — Task 1 branches it), `mod-blocks` (on `main` — Task 2 branches it), + and `hinode` (branch `fix/table-category-filter`, already checked out, carrying the spec). +- **Why mod-utils and not Hinode:** none of mod-blocks, mod-lottie, mod-leaflet, mod-mermaid or + mod-simple-datatables imports Hinode. A partial in Hinode reaches them only because the final + *site* merges Hinode's layouts — an undeclared inverted dependency that breaks each module's own + exampleSite build. Hugo resolves partials by path across the merged layout tree, so moving a + partial between modules is transparent to its callers. +- **`page.Store`, never `.Ordinal`.** `.Ordinal` restarts at 0 inside an embedded `RenderString` + (which is what the `example` shortcode does), so ids collide. `page.Store` was measured to survive + that boundary, and a spike confirmed a `page.Store` counter in mod-blocks' `panels` component is + identical across two builds and unique within the page. +- A DOM id only needs to be unique **within the document that carries it**. The helper does not + attempt cross-page uniqueness. +- Gaps in the numbering are expected and harmless: Hugo renders a page's content more than once + (the search index calls `.Plain`), and the discarded passes consume counter values without + emitting ids. The spike produced `panel-1, -2, -3, -5`. +- Commits follow Angular Conventional Commits (a commitlint pre-commit hook enforces this in each + repo). Body lines must not exceed 100 characters. +- **Each of the three branches must carry at least one `feat:` or `fix:` commit.** gethinode merges + with merge commits, so semantic-release reads the individual commits, not the PR title. + `refactor`, `style`, `test`, `chore`, `docs` and `build` produce **no release**. +- **Never run `npm run build:example` or `npm run start:example` in hinode** — their prebuild hooks + re-vendor Hugo modules from the remote and would wipe the local overrides Tasks 2–4 rely on. Use + the project-pinned binary with `node_modules/.bin` on `PATH`: + + ```bash + PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/hugo --gc -s exampleSite --logLevel warn + ``` + +- Export the scratch path once per shell: + + ```bash + export SCRATCH=/private/tmp/claude-501/-Users-mark-Development-GitHub-gethinode-hinode/491eb2c0-b0cd-401d-9256-134afea0e318/scratchpad + ``` + +- **Release ordering is strictly sequential: mod-utils → mod-blocks → Hinode.** mod-blocks goes + before Hinode so Hinode's exampleSite (whose `exampleSite/go.mod` pins mod-blocks) picks up the + fixed ids when it deploys. The `go.mod` version bumps happen at merge time, once each release + exists — **not in this plan**. All verification here runs against local module overrides. +- Hinode's `layouts/_partials/utilities/AddModule.html` is deleted in Task 4. It must not be + deleted before mod-utils provides the replacement, or the partial disappears from the merged + layout tree and every caller fails. Between the mod-utils release and Hinode's merge, the partial + exists in **both** modules; Hugo resolves the project's own layouts ahead of an imported module's, + so Hinode's copy shadows mod-utils'. They are identical, so the interval is harmless. + +## File Structure + +| File | Repo | Responsibility | +| --- | --- | --- | +| `layouts/_partials/utilities/UniqueID.html` | mod-utils | **New.** Returns a page-unique, build-stable id. | +| `layouts/_partials/utilities/AddModule.html` | mod-utils | **New (moved from Hinode).** Declares a render-time module dependency. | +| `component-library/components/panels/panels.hugo.html` | mod-blocks | `panel-` → helper. | +| `component-library/components/faq/faq.hugo.html` | mod-blocks | `faq-` → helper. | +| `layouts/partials/assets/testimonial-carousel.html` | mod-blocks | `testimonial-carousel-` → helper. | +| `layouts/partials/assets/preview.html` | mod-blocks | `preview-` → helper. | +| `layouts/_partials/assets/table.html` | hinode | Inline `hinodeTableFilterCount` counter → helper. | +| `layouts/_partials/utilities/AddModule.html` | hinode | **Deleted** (mod-utils provides it). | + +## Affected pages in Hinode's exampleSite + +Every page below carries at least one id this plan changes. Task 5 verifies all six. + +| Page | Component | +| --- | --- | +| `/en/docs/blocks/panels/` | panels (4 ids) | +| `/en/docs/blocks/faq/` | faq | +| `/en/docs/blocks/testimonials/` | testimonial-carousel | +| `/en/docs/blocks/preview/` | preview | +| `/en/docs/components/example/` | preview | +| `/en/table-demo/` | table filter | + +--- + +### Task 1: The helper, and AddModule's new home + +**Repo:** mod-utils. + +**Files:** + +- Create: `layouts/_partials/utilities/UniqueID.html` +- Create: `layouts/_partials/utilities/AddModule.html` + +**Interfaces:** + +- Produces: `partial "utilities/UniqueID.html" (dict "prefix" "" "page" )` + → returns the string `"-"`, `n` counting from 1 per prefix per page. +- Produces: `partial "utilities/AddModule.html" (dict "module" "" "page" )` + → appends `` to the page's `dependencies` scratch, deduplicated. + +- [ ] **Step 1: Branch mod-utils** + +```bash +git -C ../mod-utils checkout -b feat/unique-id origin/main +git -C ../mod-utils log --oneline -1 +``` + +- [ ] **Step 2: Write the failing test** + +The partials are not called from anywhere in mod-utils, so exercise them in a throwaway Hugo site. +Create it at `$SCRATCH/uid-test`: + +```bash +export SCRATCH=/private/tmp/claude-501/-Users-mark-Development-GitHub-gethinode-hinode/491eb2c0-b0cd-401d-9256-134afea0e318/scratchpad +T=$SCRATCH/uid-test +rm -rf $T && mkdir -p $T/layouts/_partials/utilities $T/content +printf 'title = "t"\nbaseURL = "/"\ndisableKinds = ["taxonomy","term","rss","sitemap"]\n' > $T/hugo.toml +printf -- '---\ntitle: home\n---\n' > $T/content/_index.md +printf -- '---\ntitle: other\n---\n' > $T/content/other.md + +cat > $T/layouts/index.html <<'EOF' +{{/* Per-prefix counters must be independent, and must count from 1. */}} +A={{ partial "utilities/UniqueID.html" (dict "prefix" "panel") }} +B={{ partial "utilities/UniqueID.html" (dict "prefix" "panel") }} +C={{ partial "utilities/UniqueID.html" (dict "prefix" "faq") }} +D={{ partial "utilities/UniqueID.html" (dict "prefix" "panel") }} + +{{/* An explicit page anchors the counter to THAT page, not the rendering one. */}} +{{ $other := site.GetPage "/other" }} +E={{ partial "utilities/UniqueID.html" (dict "prefix" "panel" "page" $other) }} +F={{ partial "utilities/UniqueID.html" (dict "prefix" "panel") }} + +{{/* AddModule: append + dedup, and honour an explicit page. */}} +{{ partial "utilities/AddModule.html" (dict "module" "alpha") }} +{{ partial "utilities/AddModule.html" (dict "module" "beta") }} +{{ partial "utilities/AddModule.html" (dict "module" "alpha") }} +{{ partial "utilities/AddModule.html" (dict "module" "gamma" "page" $other) }} +DEPS={{ delimit (page.Scratch.Get "dependencies") "," }} +OTHERDEPS={{ delimit ($other.Scratch.Get "dependencies") "," }} +EOF +``` + +Copy the (not yet existing) partials in and build: + +```bash +cp ../mod-utils/layouts/_partials/utilities/UniqueID.html $T/layouts/_partials/utilities/ 2>/dev/null +cp ../mod-utils/layouts/_partials/utilities/AddModule.html $T/layouts/_partials/utilities/ 2>/dev/null +node_modules/.bin/hugo --quiet -s $T --destination $T/public 2>&1 | head -3 +``` + +Expected: the build **FAILS** — `partial "utilities/UniqueID.html" not found`. That is the baseline. + +- [ ] **Step 3: Write `UniqueID.html`** + +Create `../mod-utils/layouts/_partials/utilities/UniqueID.html`: + +```hugo +{{/* + 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 -}} +``` + +- [ ] **Step 4: Add `AddModule.html`** + +Create `../mod-utils/layouts/_partials/utilities/AddModule.html` with exactly this content. It is +the current Hinode version, carrying both fixes it received while the table filter was repaired: it +appends and deduplicates (an older version used `complement`, which *replaced* the dependency list +and wiped it entirely when re-adding an entry already present), and it honours an explicitly passed +`page` instead of ignoring it in favour of the global `page`. **Preserve the tab indentation.** + +```hugo +{{- $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 }} +``` + +- [ ] **Step 5: Run the test** + +```bash +cp ../mod-utils/layouts/_partials/utilities/UniqueID.html "$SCRATCH/uid-test/layouts/_partials/utilities/" +cp ../mod-utils/layouts/_partials/utilities/AddModule.html "$SCRATCH/uid-test/layouts/_partials/utilities/" +node_modules/.bin/hugo --quiet -s "$SCRATCH/uid-test" --destination "$SCRATCH/uid-test/public" +grep -E '^[A-F]=|^DEPS=|^OTHERDEPS=' "$SCRATCH/uid-test/public/index.html" +``` + +Expected, exactly: + +```text +A=panel-1 +B=panel-2 +C=faq-1 +D=panel-3 +E=panel-1 +F=panel-4 +DEPS=alpha,beta +OTHERDEPS=gamma +``` + +Read that carefully — every line is load-bearing: + +- `C=faq-1` proves the counters are **per prefix**, not one shared counter. +- `E=panel-1` proves an explicit `page` anchors the counter to **that** page (which has its own, + untouched counter), not to the page being rendered. +- `F=panel-4` proves the rendering page's counter was **not** disturbed by `E` — it resumes where + `D` left off. +- `DEPS=alpha,beta` proves AddModule appends and deduplicates. A bare `alpha`, or an empty value, + means the list is being replaced instead of appended to. +- `OTHERDEPS=gamma` proves AddModule honours an explicit page, and that `gamma` did **not** leak + into the rendering page's `DEPS`. + +- [ ] **Step 6: Confirm the missing-prefix guard** + +```bash +cp "$SCRATCH/uid-test/layouts/index.html" "$SCRATCH/uid-test/index.html.bak" +printf 'X={{ partial "utilities/UniqueID.html" (dict "prefix" "") }}\n' >> "$SCRATCH/uid-test/layouts/index.html" +node_modules/.bin/hugo --quiet -s "$SCRATCH/uid-test" --destination "$SCRATCH/uid-test/public" 2>&1 | grep -i "missing required argument" && echo "guard fires" +mv "$SCRATCH/uid-test/index.html.bak" "$SCRATCH/uid-test/layouts/index.html" +``` + +Expected: the build errors with `missing required argument 'prefix'`, and the final `mv` restores +the working template. (The throwaway site is not a git repo, so restore it by copy, not `git`.) + +- [ ] **Step 7: Commit** + +```bash +git -C ../mod-utils add layouts/_partials/utilities/UniqueID.html layouts/_partials/utilities/AddModule.html +git -C ../mod-utils commit -m "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." +``` + +--- + +### Task 2: mod-blocks' four call sites + +**Repo:** mod-blocks. + +**Files:** + +- Modify: `component-library/components/panels/panels.hugo.html:37` +- Modify: `component-library/components/faq/faq.hugo.html:42` +- Modify: `layouts/partials/assets/testimonial-carousel.html:48` +- Modify: `layouts/partials/assets/preview.html:243` + +**Interfaces:** + +- Consumes: `partial "utilities/UniqueID.html" (dict "prefix" "" "page" )` + → `"-"` (Task 1). + +- [ ] **Step 1: Branch mod-blocks** + +```bash +git -C ../mod-blocks checkout -b fix/deterministic-ids origin/main +git -C ../mod-blocks log --oneline -1 +``` + +- [ ] **Step 2: Convert `panels.hugo.html`** + +Line 37 currently reads: + +```hugo + {{ $parentID := printf "panel-%v" (md5 (delimit (slice . now) "-")) }} +``` + +Replace it with: + +```hugo + {{ $parentID := partial "utilities/UniqueID.html" (dict "prefix" "panel" "page" $._page) }} +``` + +`$._page` is Bookshop's content page, addressed from the template's root context so it resolves +regardless of nesting. `list.hugo.html` in the same component library already uses it. If it is nil +in this context the helper falls back to the current page, which the spike proved correct here. + +- [ ] **Step 3: Convert `faq.hugo.html`** + +Line 42 currently reads: + +```hugo + {{ $id := printf "faq-%s" (md5 (delimit (slice . now) "-")) }} +``` + +Replace it with (preserving the tab indentation): + +```hugo + {{ $id := partial "utilities/UniqueID.html" (dict "prefix" "faq" "page" $._page) }} +``` + +- [ ] **Step 4: Convert `testimonial-carousel.html`** + +Line 48 currently reads: + +```hugo + {{ $id := printf "testimonial-carousel-%s" (md5 (delimit (slice . now) "-")) }} +``` + +Replace it with (preserving the two tabs): + +```hugo + {{ $id := partial "utilities/UniqueID.html" (dict "prefix" "testimonial-carousel") }} +``` + +This is a partial, not a Bookshop component, so it has no `._page`; it takes the helper's default +of the current page. + +- [ ] **Step 5: Convert `preview.html`** + +Line 243 currently reads: + +```hugo +{{- $id := printf "preview-%s" (md5 (delimit (slice . now) "-")) -}} +``` + +Replace it with: + +```hugo +{{- $id := partial "utilities/UniqueID.html" (dict "prefix" "preview") -}} +``` + +- [ ] **Step 6: Confirm no `md5(… now …)` id remains anywhere in mod-blocks** + +```bash +grep -rn 'md5 (delimit (slice . now)' ../mod-blocks/layouts ../mod-blocks/component-library || echo "none remaining" +``` + +Expected: `none remaining`. + +- [ ] **Step 7: Confirm each id's downstream references are untouched** + +Each id is referenced elsewhere in its own component — `data-companion`, `aria-controls`, +`data-bs-target`, `data-bs-parent`. They derive from the same variable (`$parentID` / `$id`), so +they follow automatically. Confirm you changed only the assignment line in each file: + +```bash +git -C ../mod-blocks diff --stat +``` + +Expected: 4 files, 4 insertions, 4 deletions. + +- [ ] **Step 8: Commit** + +```bash +git -C ../mod-blocks add component-library/components/panels/panels.hugo.html \ + component-library/components/faq/faq.hugo.html \ + layouts/partials/assets/testimonial-carousel.html \ + layouts/partials/assets/preview.html +git -C ../mod-blocks commit -m "fix(blocks): stop element ids changing on every build + +Four components built a DOM id from md5(... now ...), so the id changed on every +build and any page carrying one emitted different HTML each time - defeating +output diffing and making caches treat an unchanged page as changed. + +Use mod-utils' UniqueID helper, which counts from a per-prefix counter in the +page's Store." +``` + +(The build and browser verification happen in Tasks 4 and 5, against Hinode's exampleSite — the +only site where mod-blocks' components fully render.) + +--- + +### Task 3: Hinode — migrate the table, drop the moved partial + +**Repo:** hinode, branch `fix/table-category-filter`. + +**Files:** + +- Modify: `layouts/_partials/assets/table.html:77-84` +- Delete: `layouts/_partials/utilities/AddModule.html` + +**Interfaces:** + +- Consumes: `partial "utilities/UniqueID.html" (dict "prefix" "table-filter" "page" $args.page)` + → `"table-filter-"` (Task 1). The rendered id keeps the same shape it has today. + +- [ ] **Step 1: Migrate the table's counter** + +`layouts/_partials/assets/table.html` currently reads: + +```hugo +{{ $filterId := "" }} +{{ if $filter }} + {{ $store := $args.page.Store }} + {{ $count := add (or ($store.Get "hinodeTableFilterCount") 0) 1 }} + {{ $store.Set "hinodeTableFilterCount" $count }} + {{ $filterId = printf "table-filter-%d" $count }} + {{ $attributes = printf "%s data-filter-col=%d data-filter-id=%s" $attributes $filterCol $filterId }} +{{ end }} +``` + +Replace that block with: + +```hugo +{{ $filterId := "" }} +{{ if $filter }} + {{ $filterId = partial "utilities/UniqueID.html" (dict "prefix" "table-filter" "page" $args.page) }} + {{ $attributes = printf "%s data-filter-col=%d data-filter-id=%s" $attributes $filterCol $filterId }} +{{ end }} +``` + +The output shape is unchanged (`table-filter-`); this removes the second implementation of the +same mechanism. Leave the surrounding comment about determinism in place if one is there — but if it +explains the inline counter's mechanics, retire it, since the helper now documents itself. + +- [ ] **Step 2: Delete Hinode's `AddModule.html`** + +```bash +git rm layouts/_partials/utilities/AddModule.html +``` + +mod-utils now provides it at the same path. Its three callers — +`layouts/_partials/assets/lightbox.html`, `layouts/_shortcodes/table.html`, and mod-blocks' +`list.hugo.html` — are unaffected, because Hugo resolves partials by path across the merged layout +tree. + +- [ ] **Step 3: Wire the exampleSite to the local mod-utils and mod-blocks** + +Both edits are LOCAL-ONLY and are reverted in Task 6. Neither may ever be committed. + +`exampleSite/hinode.work`: + +```text +go 1.19 + +use . +use ../ +use ../../mod-utils +use ../../mod-blocks +``` + +`exampleSite/config/_default/hugo.toml`, under `[module]`, directly below the existing +`workspace = "hinode.work"` line (the file already carries commented-out examples of exactly this): + +```toml + replacements = 'github.com/gethinode/mod-utils/v6 -> /Users/mark/Development/GitHub/gethinode/mod-utils/, github.com/gethinode/mod-blocks/v2 -> /Users/mark/Development/GitHub/gethinode/mod-blocks/' +``` + +A workspace `use` directive alone does **not** reliably override a module — Hugo keeps resolving it +from the committed `_vendor/` copy. Both mechanisms are needed. + +- [ ] **Step 4: Prove the override is genuinely in effect** + +`hugo config mounts` is **not** sufficient — it has been observed to report a replacement correctly +while the build still compiled the vendored copy. Prove it from the built output instead: with the +local mod-utils in play, the ids must be counters, not hashes. + +```bash +rm -rf exampleSite/public +PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/hugo --gc -s exampleSite --logLevel error +grep -o 'id="dropdown-panel-[^"]*"' exampleSite/public/en/docs/blocks/panels/index.html | sort +``` + +Expected: ids like `dropdown-panel-1`. If you still see a 32-character hex hash, the override is not +in effect and every check below is meaningless — stop and fix the override first. + +- [ ] **Step 5: Confirm the AddModule deletion did not break its callers** + +The table shortcode's missing-module warning is the loudest AddModule-adjacent behaviour, and the +lightbox partial is the other caller. A missing partial is a hard build error, so a clean build is +itself most of the proof — but confirm the warning still works, since it is the thing a user sees. + +```bash +cat > exampleSite/content/en/tmp-nomodule.md <<'EOF' +--- +title: Temp No Module +--- + +{{< table sortable="true" >}} +| Name | Type | +|-------|--------| +| alpha | widget | +{{< /table >}} +EOF +PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/hugo --gc -s exampleSite --logLevel warn 2>&1 | grep -i "simple-datatables module" && echo "warning still fires" +rm exampleSite/content/en/tmp-nomodule.md +PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/hugo --gc -s exampleSite --logLevel warn 2>&1 | grep -ci "simple-datatables module" || echo "0 on a stock build — correct" +``` + +Expected: the warning fires for the temporary page and names it, and a stock build emits **zero** +such warnings. + +- [ ] **Step 6: Confirm the table's own ids still work** + +```bash +node "$SCRATCH/verify-table-filter.mjs" && node "$SCRATCH/verify-table-wrap.mjs" +grep -o 'data-filter-id=[^ >]*' exampleSite/public/en/table-demo/index.html | sort -u +``` + +Expected: both scripts fully passing, and two distinct `table-filter-` ids. + +- [ ] **Step 7: Lint and commit** + +```bash +npm test +git add layouts/_partials/assets/table.html +git commit -m "fix(table): take the filter id from the shared UniqueID helper + +The table carried its own page.Store counter, duplicating what mod-utils' +UniqueID now provides. Same rendered id shape, one implementation. + +Also drops Hinode's copy of AddModule.html, which has moved to mod-utils: +mod-blocks calls it without declaring a dependency on Hinode, so it belongs +where every module can legitimately reach it." +``` + +(The `git rm` from Step 2 is already staged, so this commit carries both changes.) + +--- + +### Task 4: Determinism and uniqueness across every affected page + +**Repo:** hinode (verification only — no code changes). + +Nothing so far proves the ids are stable across builds or unique within a page. This task measures +both, on all six affected pages. + +**Files:** none. + +- [ ] **Step 1: Write the check script** + +Create `$SCRATCH/verify-ids.mjs`. It is a scratch file — do **not** commit it. + +```js +import { readFileSync, writeFileSync } from 'node:fs' + +// Every page that carries an id this change touches, and the id prefixes it should carry. +const PAGES = [ + ['en/docs/blocks/panels/index.html', ['dropdown-panel']], + ['en/docs/blocks/faq/index.html', ['faq']], + ['en/docs/blocks/testimonials/index.html', ['testimonial-carousel']], + ['en/docs/blocks/preview/index.html', ['preview']], + ['en/docs/components/example/index.html', ['preview']], + ['en/table-demo/index.html', ['table-filter']], +] + +const idsOf = (page, prefixes) => { + const html = readFileSync(`exampleSite/public/${page}`, 'utf8') + const found = [] + for (const p of prefixes) { + // Match the id wherever it is used: id=, aria-controls=, data-bs-target=, data-companion=, … + for (const m of html.matchAll(new RegExp(`["'#]${p}-([\\w-]+)`, 'g'))) { + found.push(`${p}-${m[1]}`) + } + } + return found +} + +const which = process.argv[2] +if (which !== 'a' && which !== 'b') { + throw new Error('pass "a" or "b" to record that build\'s ids') +} + +const snapshot = PAGES.map(([page, prefixes]) => [page, idsOf(page, prefixes)]) +writeFileSync( + `${process.env.SCRATCH}/ids-${which}.json`, + JSON.stringify(snapshot, null, 2) +) +console.log(`recorded build ${which}`) +``` + +- [ ] **Step 2: Build twice and compare** + +```bash +export SCRATCH=/private/tmp/claude-501/-Users-mark-Development-GitHub-gethinode-hinode/491eb2c0-b0cd-401d-9256-134afea0e318/scratchpad +build() { rm -rf exampleSite/public; PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/hugo --gc -s exampleSite --logLevel error >/dev/null; } +build && node "$SCRATCH/verify-ids.mjs" a +build && node "$SCRATCH/verify-ids.mjs" b +diff "$SCRATCH/ids-a.json" "$SCRATCH/ids-b.json" && echo "DETERMINISTIC — identical across builds" || echo "FAIL — ids differ between builds" +``` + +Expected: `DETERMINISTIC — identical across builds`. + +- [ ] **Step 3: Confirm no id is a hash, and none repeats within a page** + +```bash +node -e ' +const ids = require(process.env.SCRATCH + "/ids-a.json"); +let bad = 0; +for (const [page, list] of ids) { + if (!list.length) { console.log("FAIL no ids found on", page); bad++; continue } + const hashy = list.filter(id => /-[0-9a-f]{16,}$/.test(id)); + if (hashy.length) { console.log("FAIL hash-shaped id on", page, hashy[0]); bad++ } + // An id may legitimately appear several times on a page (id=, aria-controls=, data-bs-target=). + // What must not happen is two DIFFERENT elements sharing one id — check the id= attributes only. + console.log("ok ", page, "->", [...new Set(list)].join(", ")); +} +process.exit(bad ? 1 : 0) +' +``` + +Expected: an `ok` line per page listing counter-shaped ids (`dropdown-panel-1`, `faq-1`, …) and no +`FAIL`. + +- [ ] **Step 4: Confirm each `id=` attribute is unique within its page** + +The check above allows an id to *appear* many times (a `data-bs-target` points at an `id`). What +must never happen is two elements declaring the same `id`. + +```bash +for p in en/docs/blocks/panels en/docs/blocks/faq en/docs/blocks/testimonials en/docs/blocks/preview en/docs/components/example en/table-demo; do + dupes=$(grep -o 'id="[^"]*"' "exampleSite/public/$p/index.html" | sort | uniq -d) + if [ -n "$dupes" ]; then echo "FAIL duplicate id= on /$p:"; echo "$dupes"; else echo "ok /$p"; fi +done +``` + +Expected: `ok` for all six. **A duplicate here is a Critical failure** — it means two elements share +an id and Bootstrap will drive the wrong one. + +--- + +### Task 5: Browser verification + +**Repo:** hinode (verification only — no code changes). + +A deterministic id that points at the wrong element is worse than a random one. This task proves +each component still drives *its own* element. + +**Files:** none. + +- [ ] **Step 1: Serve the exampleSite** + +With the local overrides from Task 3 still active, on an isolated cache so other Hugo processes are +undisturbed. Run it in the background. + +```bash +PATH="$PWD/node_modules/.bin:$PATH" HUGO_RESOURCEDIR="$SCRATCH/resources" HUGO_CACHEDIR="$SCRATCH/cache" \ + node_modules/.bin/hugo server -s exampleSite --port 1321 --disableFastRender +``` + +Use `127.0.0.1`, not `localhost` — an unrelated process squats on IPv6 localhost. + +- [ ] **Step 2: Panels — `http://127.0.0.1:1321/en/docs/blocks/panels/`** + +This page carries **four** panel ids, so it is the strongest test of per-element pairing. + +- Every dropdown opens **its own** panel. Open each one in turn and confirm the panel that expands + is the one directly associated with it, not another panel on the page. +- Confirm from the DOM that each toggle's `data-companion` / `aria-controls` matches the `id` of the + panel it actually opened. +- Zero uncaught console exceptions. + +- [ ] **Step 3: FAQ — `http://127.0.0.1:1321/en/docs/blocks/faq/`** + +- Each accordion item toggles the item it belongs to, and **only** that one. Open one item and + confirm no other item opened with it. +- Zero uncaught console exceptions. + +- [ ] **Step 4: Testimonials — `http://127.0.0.1:1321/en/docs/blocks/testimonials/`** + +- The carousel cycles, and its indicators target its own slides. +- Zero uncaught console exceptions. + +- [ ] **Step 5: Preview — both pages** + +`http://127.0.0.1:1321/en/docs/blocks/preview/` and +`http://127.0.0.1:1321/en/docs/components/example/` (the preview component appears on both). + +- The device tabs (desktop / tablet / mobile) switch the preview they belong to. On a page with more + than one preview, confirm a tab does not resize a different preview. +- Zero uncaught console exceptions. + +- [ ] **Step 6: Table — `http://127.0.0.1:1321/en/table-demo/`** + +The filter's id now comes from the helper, so re-confirm the pairing survived. + +- Clicking a category filters **its own** table, not the other filtered table on the page. +- Zero uncaught console exceptions. + +- [ ] **Step 7: Stop the server and report** + +Report any failure rather than papering over it. + +--- + +### Task 6: Clean up and hand off the release sequence + +**Repo:** hinode. + +**Files:** + +- Modify: `exampleSite/hinode.work` (revert) +- Modify: `exampleSite/config/_default/hugo.toml` (revert) +- Modify: `exampleSite/hugo_stats.json` (build artefact) + +- [ ] **Step 1: Revert the local overrides** + +```bash +git checkout exampleSite/hinode.work exampleSite/config/_default/hugo.toml +git diff --stat exampleSite/hinode.work exampleSite/config/_default/hugo.toml +``` + +Expected: no output — both files clean. **Neither may ever be committed.** + +- [ ] **Step 2: Rebuild against the vendored modules** + +```bash +PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/hugo --gc -s exampleSite --logLevel warn +``` + +This build uses the **vendored** mod-utils and mod-blocks, which do not yet carry Tasks 1 and 2. So +the panels/faq/testimonial/preview ids revert to hashes here, and Hinode's own `AddModule.html` is +gone while mod-utils does not yet provide it — **this build is expected to fail**, with a missing +`utilities/AddModule.html`. + +That failure is the ordering constraint made visible, not a defect: Hinode cannot merge until +mod-utils has released and Hinode's `go.mod` is bumped to it. **Record the exact error text** — it +is the evidence for the handoff in Step 5. + +Then restore the override so the remaining steps have a buildable tree, and revert it again at the +very end: + +```bash +# re-apply the Task 3 Step 3 overrides, rebuild, then revert them +PATH="$PWD/node_modules/.bin:$PATH" node_modules/.bin/hugo --gc -s exampleSite --logLevel error +git checkout exampleSite/hinode.work exampleSite/config/_default/hugo.toml +``` + +- [ ] **Step 3: Commit the build stats if they changed** + +```bash +git status --short +``` + +If `exampleSite/hugo_stats.json` is dirty **and its diff is semantic** (new class or id names rather +than churning hashes), commit it: + +```bash +git add exampleSite/hugo_stats.json +git commit -m "chore: update build stats" +``` + +If the only diff is hash-shaped ids churning, restore it instead — that churn is exactly what this +change exists to remove, and it will settle once the modules are released: + +```bash +git checkout exampleSite/hugo_stats.json +``` + +- [ ] **Step 4: Confirm nothing stray is committed** + +```bash +git log --oneline main..HEAD +git diff --stat main..HEAD +``` + +Expected: no `hinode.work`, no `exampleSite/config/_default/hugo.toml`, no scratch script, no +temporary content page. + +- [ ] **Step 5: Report the release and bump sequence** + +Summarise for the user. Three branches: + +- `mod-utils:feat/unique-id` — new PR +- `mod-blocks:fix/deterministic-ids` — new PR +- `hinode:fix/table-category-filter` — the existing PR #2024, amended + +Merge order is **strictly sequential**, and each step's `go.mod` bump can only happen after the +previous release exists: + +1. Merge **mod-utils**. semantic-release cuts a minor (the branch carries a `feat:`). +2. Bump `mod-blocks/go.mod` to that mod-utils release; merge **mod-blocks**. semantic-release cuts a + patch (the branch carries a `fix:`). +3. Bump `hinode/go.mod` to the mod-utils release **and** `hinode/exampleSite/go.mod` to the + mod-blocks release; then merge **hinode**. + +State plainly that Hinode **cannot** merge before mod-utils releases: it deletes its own +`AddModule.html`, and until `go.mod` points at a mod-utils that provides the replacement, the +partial is missing from the merged layout tree and the build fails. + +--- + +## Notes for the reviewer + +- **`page.Store`, not `.Ordinal`.** `.Ordinal` restarts at 0 inside an embedded `RenderString` — + which is exactly what the `example` shortcode does — so two components in two `{{< example >}}` + blocks on one page would collide. `page.Store` was measured to survive that boundary. +- **Gaps in the numbering are expected.** Hugo renders a page's content more than once (the search + index calls `.Plain`), and the discarded pass consumes counter values without emitting ids. The + spike produced `panel-1, -2, -3, -5`. Uniqueness and determinism are what matter, not contiguity. +- **The helper lives in mod-utils, not Hinode,** because no gethinode module declares a Hinode + dependency. This also unblocks the planned `.Ordinal` migration, which must reach mod-lottie and + mod-leaflet. From 69e68a99f42e78418bdd94012d02da4ed24e4e24 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:12:14 +0200 Subject: [PATCH 16/19] fix(table): take the filter id from the shared UniqueID helper The table carried its own page.Store counter, duplicating what mod-utils' UniqueID now provides. Same rendered id shape, one implementation. Also drops Hinode's copy of AddModule.html, which has moved to mod-utils: mod-blocks calls it without declaring a dependency on Hinode, so it belongs where every module can legitimately reach it. --- layouts/_partials/assets/table.html | 5 +---- layouts/_partials/utilities/AddModule.html | 13 ------------- 2 files changed, 1 insertion(+), 17 deletions(-) delete mode 100644 layouts/_partials/utilities/AddModule.html diff --git a/layouts/_partials/assets/table.html b/layouts/_partials/assets/table.html index c1b85c106..a1d7727a6 100644 --- a/layouts/_partials/assets/table.html +++ b/layouts/_partials/assets/table.html @@ -76,10 +76,7 @@ {{ $filterCol := $args.filterCol }} {{ $filterId := "" }} {{ if $filter }} - {{ $store := $args.page.Store }} - {{ $count := add (or ($store.Get "hinodeTableFilterCount") 0) 1 }} - {{ $store.Set "hinodeTableFilterCount" $count }} - {{ $filterId = printf "table-filter-%d" $count }} + {{ $filterId = partial "utilities/UniqueID.html" (dict "prefix" "table-filter" "page" $args.page) }} {{ $attributes = printf "%s data-filter-col=%d data-filter-id=%s" $attributes $filterCol $filterId }} {{ end }} diff --git a/layouts/_partials/utilities/AddModule.html b/layouts/_partials/utilities/AddModule.html deleted file mode 100644 index 8e868c620..000000000 --- a/layouts/_partials/utilities/AddModule.html +++ /dev/null @@ -1,13 +0,0 @@ -{{- $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 From c7106b1fc4311eddff7b11dd9e602106c4628c8d Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:56:39 +0200 Subject: [PATCH 17/19] chore: update build stats Reflects deterministic ids for panel, faq, nav, preview, and testimonial-carousel once built against the unreleased mod-utils UniqueID helper and mod-blocks call-site migration. Co-Authored-By: Claude Sonnet 5 --- exampleSite/hugo_stats.json | 174 ++++++++++++++++++------------------ 1 file changed, 88 insertions(+), 86 deletions(-) diff --git a/exampleSite/hugo_stats.json b/exampleSite/hugo_stats.json index 9739ae252..c3153a102 100644 --- a/exampleSite/hugo_stats.json +++ b/exampleSite/hugo_stats.json @@ -1057,10 +1057,10 @@ "dropdown-align-end-1", "dropdown-callout-1", "dropdown-nav-0", - "dropdown-panel-1c3234841533a2e77d6ec063141c7e17", - "dropdown-panel-25c1079d2c43f1e28e8d7b826ab3066d", - "dropdown-panel-562e8b9bdb5de3723511faef432cee4e", - "dropdown-panel-eae6dd065d90bf2ba90cea1ef52b3089", + "dropdown-panel-1", + "dropdown-panel-2", + "dropdown-panel-3", + "dropdown-panel-5", "dropdown-pills-1", "dropdown-tabs-1", "dropdown-underline-1", @@ -1084,11 +1084,13 @@ "fab-whatsapp", "fab-x-twitter", "faq", - "faq-cc9ac616918d8a9f781a948ff34b1d0b", - "faq-cc9ac616918d8a9f781a948ff34b1d0b-heading-faq-cc9ac616918d8a9f781a948ff34b1d0b", - "faq-cc9ac616918d8a9f781a948ff34b1d0b-item-0", - "faq-cc9ac616918d8a9f781a948ff34b1d0b-item-1", - "faq-cc9ac616918d8a9f781a948ff34b1d0b-item-2", + "faq-1", + "faq-1-heading-0", + "faq-1-heading-1", + "faq-1-heading-2", + "faq-1-item-0", + "faq-1-item-1", + "faq-1-item-2", "far-square", "fas-1", "fas-2", @@ -1264,10 +1266,10 @@ "nav-align-end-1", "nav-callout-1", "nav-nav-0", - "nav-panel-1c3234841533a2e77d6ec063141c7e17", - "nav-panel-25c1079d2c43f1e28e8d7b826ab3066d", - "nav-panel-562e8b9bdb5de3723511faef432cee4e", - "nav-panel-eae6dd065d90bf2ba90cea1ef52b3089", + "nav-panel-1", + "nav-panel-2", + "nav-panel-3", + "nav-panel-5", "nav-pills-1", "nav-tabs-1", "nav-underline-1", @@ -1305,36 +1307,36 @@ "over-mij", "overview", "page-link", - "panel-1c3234841533a2e77d6ec063141c7e17-0", - "panel-1c3234841533a2e77d6ec063141c7e17-1", - "panel-1c3234841533a2e77d6ec063141c7e17-2", - "panel-1c3234841533a2e77d6ec063141c7e17-btn-0", - "panel-1c3234841533a2e77d6ec063141c7e17-btn-1", - "panel-1c3234841533a2e77d6ec063141c7e17-btn-2", - "panel-25c1079d2c43f1e28e8d7b826ab3066d-0", - "panel-25c1079d2c43f1e28e8d7b826ab3066d-1", - "panel-25c1079d2c43f1e28e8d7b826ab3066d-2", - "panel-25c1079d2c43f1e28e8d7b826ab3066d-btn-0", - "panel-25c1079d2c43f1e28e8d7b826ab3066d-btn-1", - "panel-25c1079d2c43f1e28e8d7b826ab3066d-btn-2", - "panel-562e8b9bdb5de3723511faef432cee4e-0", - "panel-562e8b9bdb5de3723511faef432cee4e-1", - "panel-562e8b9bdb5de3723511faef432cee4e-2", - "panel-562e8b9bdb5de3723511faef432cee4e-btn-0", - "panel-562e8b9bdb5de3723511faef432cee4e-btn-1", - "panel-562e8b9bdb5de3723511faef432cee4e-btn-2", - "panel-8bc59fcf382342dfcc79eb402ae00400-0", - "panel-8bc59fcf382342dfcc79eb402ae00400-1", - "panel-8bc59fcf382342dfcc79eb402ae00400-2", - "panel-8bc59fcf382342dfcc79eb402ae00400-btn-0", - "panel-8bc59fcf382342dfcc79eb402ae00400-btn-1", - "panel-8bc59fcf382342dfcc79eb402ae00400-btn-2", - "panel-eae6dd065d90bf2ba90cea1ef52b3089-0", - "panel-eae6dd065d90bf2ba90cea1ef52b3089-1", - "panel-eae6dd065d90bf2ba90cea1ef52b3089-2", - "panel-eae6dd065d90bf2ba90cea1ef52b3089-btn-0", - "panel-eae6dd065d90bf2ba90cea1ef52b3089-btn-1", - "panel-eae6dd065d90bf2ba90cea1ef52b3089-btn-2", + "panel-1-0", + "panel-1-1", + "panel-1-2", + "panel-1-btn-0", + "panel-1-btn-1", + "panel-1-btn-2", + "panel-2-0", + "panel-2-1", + "panel-2-2", + "panel-2-btn-0", + "panel-2-btn-1", + "panel-2-btn-2", + "panel-3-0", + "panel-3-1", + "panel-3-2", + "panel-3-btn-0", + "panel-3-btn-1", + "panel-3-btn-2", + "panel-4-0", + "panel-4-1", + "panel-4-2", + "panel-4-btn-0", + "panel-4-btn-1", + "panel-4-btn-2", + "panel-5-0", + "panel-5-1", + "panel-5-2", + "panel-5-btn-0", + "panel-5-btn-1", + "panel-5-btn-2", "panels", "persona", "pie-chart", @@ -1357,50 +1359,50 @@ "powershell", "premier-article", "preview", - "preview-0b8150807319a7a0da6929488d49204a-desktop", - "preview-0b8150807319a7a0da6929488d49204a-mobile", - "preview-0b8150807319a7a0da6929488d49204a-tablet", "preview-1", + "preview-1-desktop", + "preview-1-desktop-tab", + "preview-1-mobile", + "preview-1-mobile-tab", + "preview-1-tablet", + "preview-1-tablet-tab", "preview-2", - "preview-29cbaf9a2b3d93fa2dcc6b5c63c96b6a-desktop", - "preview-29cbaf9a2b3d93fa2dcc6b5c63c96b6a-desktop-tab", - "preview-29cbaf9a2b3d93fa2dcc6b5c63c96b6a-mobile", - "preview-29cbaf9a2b3d93fa2dcc6b5c63c96b6a-mobile-tab", - "preview-29cbaf9a2b3d93fa2dcc6b5c63c96b6a-tablet", - "preview-29cbaf9a2b3d93fa2dcc6b5c63c96b6a-tablet-tab", - "preview-30381cfc9077a7d29f4e82b0d6fb7fa6-desktop", - "preview-30381cfc9077a7d29f4e82b0d6fb7fa6-desktop-tab", - "preview-30381cfc9077a7d29f4e82b0d6fb7fa6-mobile", - "preview-30381cfc9077a7d29f4e82b0d6fb7fa6-mobile-tab", - "preview-30381cfc9077a7d29f4e82b0d6fb7fa6-tablet", - "preview-30381cfc9077a7d29f4e82b0d6fb7fa6-tablet-tab", - "preview-59a49a5cc385c8e885cc342d10a07ef4-desktop", - "preview-59a49a5cc385c8e885cc342d10a07ef4-mobile", - "preview-59a49a5cc385c8e885cc342d10a07ef4-tablet", - "preview-9585f15106b39aa644c33d2d620c086f-desktop", - "preview-9585f15106b39aa644c33d2d620c086f-desktop-tab", - "preview-9585f15106b39aa644c33d2d620c086f-mobile", - "preview-9585f15106b39aa644c33d2d620c086f-mobile-tab", - "preview-9585f15106b39aa644c33d2d620c086f-tablet", - "preview-9585f15106b39aa644c33d2d620c086f-tablet-tab", - "preview-dd9d83ba3d6b2ce4eb0b55bf216d2b19-desktop", - "preview-dd9d83ba3d6b2ce4eb0b55bf216d2b19-desktop-tab", - "preview-dd9d83ba3d6b2ce4eb0b55bf216d2b19-mobile", - "preview-dd9d83ba3d6b2ce4eb0b55bf216d2b19-mobile-tab", - "preview-dd9d83ba3d6b2ce4eb0b55bf216d2b19-tablet", - "preview-dd9d83ba3d6b2ce4eb0b55bf216d2b19-tablet-tab", - "preview-df7671f49e000a684c4cb7d233cfa021-desktop", - "preview-df7671f49e000a684c4cb7d233cfa021-desktop-tab", - "preview-df7671f49e000a684c4cb7d233cfa021-mobile", - "preview-df7671f49e000a684c4cb7d233cfa021-mobile-tab", - "preview-df7671f49e000a684c4cb7d233cfa021-tablet", - "preview-df7671f49e000a684c4cb7d233cfa021-tablet-tab", - "preview-f5d035e291f06dd539721b09b49c476f-desktop", - "preview-f5d035e291f06dd539721b09b49c476f-desktop-tab", - "preview-f5d035e291f06dd539721b09b49c476f-mobile", - "preview-f5d035e291f06dd539721b09b49c476f-mobile-tab", - "preview-f5d035e291f06dd539721b09b49c476f-tablet", - "preview-f5d035e291f06dd539721b09b49c476f-tablet-tab", + "preview-2-desktop", + "preview-2-desktop-tab", + "preview-2-mobile", + "preview-2-mobile-tab", + "preview-2-tablet", + "preview-2-tablet-tab", + "preview-3-desktop", + "preview-3-desktop-tab", + "preview-3-mobile", + "preview-3-mobile-tab", + "preview-3-tablet", + "preview-3-tablet-tab", + "preview-4-desktop", + "preview-4-desktop-tab", + "preview-4-mobile", + "preview-4-mobile-tab", + "preview-4-tablet", + "preview-4-tablet-tab", + "preview-5-desktop", + "preview-5-desktop-tab", + "preview-5-mobile", + "preview-5-mobile-tab", + "preview-5-tablet", + "preview-5-tablet-tab", + "preview-6-desktop", + "preview-6-desktop-tab", + "preview-6-mobile", + "preview-6-mobile-tab", + "preview-6-tablet", + "preview-6-tablet-tab", + "preview-7-desktop", + "preview-7-mobile", + "preview-7-tablet", + "preview-8-desktop", + "preview-8-mobile", + "preview-8-tablet", "preview-unavailable", "preview-unavailable-alert-only", "preview-with-specific-device", @@ -1472,7 +1474,7 @@ "tabs-1-btn-2", "team", "testimonial", - "testimonial-carousel-7a82dcbf3f77289b79ea6273adda62cd", + "testimonial-carousel-1", "testimonial-with-avatar", "testimonial-with-case-study", "testimonial-with-icon", From be3b40208f4f65fea77fff3f8f2cd7eba8d31562 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Wed, 15 Jul 2026 05:40:19 +0200 Subject: [PATCH 18/19] chore: update build stats Regenerate hugo_stats.json against the UID-shaped ids produced by the updated UniqueID.html helper. Co-Authored-By: Claude Opus 4.8 (1M context) --- exampleSite/hugo_stats.json | 176 ++++++++++++++++++------------------ 1 file changed, 88 insertions(+), 88 deletions(-) diff --git a/exampleSite/hugo_stats.json b/exampleSite/hugo_stats.json index c3153a102..b40d95010 100644 --- a/exampleSite/hugo_stats.json +++ b/exampleSite/hugo_stats.json @@ -1057,10 +1057,10 @@ "dropdown-align-end-1", "dropdown-callout-1", "dropdown-nav-0", - "dropdown-panel-1", - "dropdown-panel-2", - "dropdown-panel-3", - "dropdown-panel-5", + "dropdown-panel-UID-1", + "dropdown-panel-UID-2", + "dropdown-panel-UID-3", + "dropdown-panel-UID-5", "dropdown-pills-1", "dropdown-tabs-1", "dropdown-underline-1", @@ -1084,13 +1084,13 @@ "fab-whatsapp", "fab-x-twitter", "faq", - "faq-1", - "faq-1-heading-0", - "faq-1-heading-1", - "faq-1-heading-2", - "faq-1-item-0", - "faq-1-item-1", - "faq-1-item-2", + "faq-UID-1", + "faq-UID-1-heading-0", + "faq-UID-1-heading-1", + "faq-UID-1-heading-2", + "faq-UID-1-item-0", + "faq-UID-1-item-1", + "faq-UID-1-item-2", "far-square", "fas-1", "fas-2", @@ -1266,10 +1266,10 @@ "nav-align-end-1", "nav-callout-1", "nav-nav-0", - "nav-panel-1", - "nav-panel-2", - "nav-panel-3", - "nav-panel-5", + "nav-panel-UID-1", + "nav-panel-UID-2", + "nav-panel-UID-3", + "nav-panel-UID-5", "nav-pills-1", "nav-tabs-1", "nav-underline-1", @@ -1307,36 +1307,36 @@ "over-mij", "overview", "page-link", - "panel-1-0", - "panel-1-1", - "panel-1-2", - "panel-1-btn-0", - "panel-1-btn-1", - "panel-1-btn-2", - "panel-2-0", - "panel-2-1", - "panel-2-2", - "panel-2-btn-0", - "panel-2-btn-1", - "panel-2-btn-2", - "panel-3-0", - "panel-3-1", - "panel-3-2", - "panel-3-btn-0", - "panel-3-btn-1", - "panel-3-btn-2", - "panel-4-0", - "panel-4-1", - "panel-4-2", - "panel-4-btn-0", - "panel-4-btn-1", - "panel-4-btn-2", - "panel-5-0", - "panel-5-1", - "panel-5-2", - "panel-5-btn-0", - "panel-5-btn-1", - "panel-5-btn-2", + "panel-UID-1-0", + "panel-UID-1-1", + "panel-UID-1-2", + "panel-UID-1-btn-0", + "panel-UID-1-btn-1", + "panel-UID-1-btn-2", + "panel-UID-2-0", + "panel-UID-2-1", + "panel-UID-2-2", + "panel-UID-2-btn-0", + "panel-UID-2-btn-1", + "panel-UID-2-btn-2", + "panel-UID-3-0", + "panel-UID-3-1", + "panel-UID-3-2", + "panel-UID-3-btn-0", + "panel-UID-3-btn-1", + "panel-UID-3-btn-2", + "panel-UID-4-0", + "panel-UID-4-1", + "panel-UID-4-2", + "panel-UID-4-btn-0", + "panel-UID-4-btn-1", + "panel-UID-4-btn-2", + "panel-UID-5-0", + "panel-UID-5-1", + "panel-UID-5-2", + "panel-UID-5-btn-0", + "panel-UID-5-btn-1", + "panel-UID-5-btn-2", "panels", "persona", "pie-chart", @@ -1360,49 +1360,49 @@ "premier-article", "preview", "preview-1", - "preview-1-desktop", - "preview-1-desktop-tab", - "preview-1-mobile", - "preview-1-mobile-tab", - "preview-1-tablet", - "preview-1-tablet-tab", "preview-2", - "preview-2-desktop", - "preview-2-desktop-tab", - "preview-2-mobile", - "preview-2-mobile-tab", - "preview-2-tablet", - "preview-2-tablet-tab", - "preview-3-desktop", - "preview-3-desktop-tab", - "preview-3-mobile", - "preview-3-mobile-tab", - "preview-3-tablet", - "preview-3-tablet-tab", - "preview-4-desktop", - "preview-4-desktop-tab", - "preview-4-mobile", - "preview-4-mobile-tab", - "preview-4-tablet", - "preview-4-tablet-tab", - "preview-5-desktop", - "preview-5-desktop-tab", - "preview-5-mobile", - "preview-5-mobile-tab", - "preview-5-tablet", - "preview-5-tablet-tab", - "preview-6-desktop", - "preview-6-desktop-tab", - "preview-6-mobile", - "preview-6-mobile-tab", - "preview-6-tablet", - "preview-6-tablet-tab", - "preview-7-desktop", - "preview-7-mobile", - "preview-7-tablet", - "preview-8-desktop", - "preview-8-mobile", - "preview-8-tablet", + "preview-UID-1-desktop", + "preview-UID-1-desktop-tab", + "preview-UID-1-mobile", + "preview-UID-1-mobile-tab", + "preview-UID-1-tablet", + "preview-UID-1-tablet-tab", + "preview-UID-2-desktop", + "preview-UID-2-desktop-tab", + "preview-UID-2-mobile", + "preview-UID-2-mobile-tab", + "preview-UID-2-tablet", + "preview-UID-2-tablet-tab", + "preview-UID-3-desktop", + "preview-UID-3-desktop-tab", + "preview-UID-3-mobile", + "preview-UID-3-mobile-tab", + "preview-UID-3-tablet", + "preview-UID-3-tablet-tab", + "preview-UID-4-desktop", + "preview-UID-4-desktop-tab", + "preview-UID-4-mobile", + "preview-UID-4-mobile-tab", + "preview-UID-4-tablet", + "preview-UID-4-tablet-tab", + "preview-UID-5-desktop", + "preview-UID-5-desktop-tab", + "preview-UID-5-mobile", + "preview-UID-5-mobile-tab", + "preview-UID-5-tablet", + "preview-UID-5-tablet-tab", + "preview-UID-6-desktop", + "preview-UID-6-desktop-tab", + "preview-UID-6-mobile", + "preview-UID-6-mobile-tab", + "preview-UID-6-tablet", + "preview-UID-6-tablet-tab", + "preview-UID-7-desktop", + "preview-UID-7-mobile", + "preview-UID-7-tablet", + "preview-UID-8-desktop", + "preview-UID-8-mobile", + "preview-UID-8-tablet", "preview-unavailable", "preview-unavailable-alert-only", "preview-with-specific-device", @@ -1474,7 +1474,7 @@ "tabs-1-btn-2", "team", "testimonial", - "testimonial-carousel-1", + "testimonial-carousel-UID-1", "testimonial-with-avatar", "testimonial-with-case-study", "testimonial-with-icon", From f2813bef3500cb7b862443eb058a0f813e24039b Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:03:58 +0200 Subject: [PATCH 19/19] build(deps): bump mod-utils to v6.5.0 and mod-blocks to v2.1.3 mod-utils v6.5.0 provides the UniqueID helper the table now calls and the AddModule partial this branch deleted from Hinode, so the build resolves both from a release rather than a local override. mod-blocks v2.1.3 carries the four components' conversion to the helper, so the deployed exampleSite renders their ids in the deterministic -UID- form instead of a per-build hash. --- exampleSite/go.mod | 4 ++-- exampleSite/go.sum | 4 ++++ go.mod | 4 ++-- go.sum | 4 ++++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/exampleSite/go.mod b/exampleSite/go.mod index 59d24e048..f118deeca 100644 --- a/exampleSite/go.mod +++ b/exampleSite/go.mod @@ -5,11 +5,11 @@ go 1.19 require ( github.com/FortAwesome/Font-Awesome v0.0.0-20260625220317-70fb2dd154b6 // indirect github.com/cloudcannon/bookshop/hugo/v3 v3.18.5 // indirect - github.com/gethinode/mod-blocks/v2 v2.1.2 // indirect + github.com/gethinode/mod-blocks/v2 v2.1.3 // indirect github.com/gethinode/mod-bootstrap-icons/v2 v2.0.1 // indirect github.com/gethinode/mod-cookieyes/v2 v2.2.6 // indirect github.com/gethinode/mod-docs v1.15.1 // indirect github.com/gethinode/mod-fontawesome/v6 v6.0.1 // indirect - github.com/gethinode/mod-utils/v6 v6.4.1 // indirect + github.com/gethinode/mod-utils/v6 v6.5.0 // indirect github.com/twbs/icons v1.13.1 // indirect ) diff --git a/exampleSite/go.sum b/exampleSite/go.sum index 8463fc1d3..561ef6bcd 100644 --- a/exampleSite/go.sum +++ b/exampleSite/go.sum @@ -4,6 +4,8 @@ github.com/cloudcannon/bookshop/hugo/v3 v3.18.5 h1:AKWzUQpWcBSbJgHQ/5cfPQtGX40bn github.com/cloudcannon/bookshop/hugo/v3 v3.18.5/go.mod h1:s7mIonDhtsLcn10ZKuVXyqd6BDHI8vT1WQhZw8rPfY8= github.com/gethinode/mod-blocks/v2 v2.1.2 h1:aufe/mWZ/exoLibOjmqqMNjCpSgeBjGBoCZ3bIXlW7Q= github.com/gethinode/mod-blocks/v2 v2.1.2/go.mod h1:41m1Sdht+QRnk0m6PkrQn0IH4NC1MBxP8jsix4gsEpI= +github.com/gethinode/mod-blocks/v2 v2.1.3 h1:eHesaZMTfAEIOv/oklXPcCbPdLcbSRZOlzwzKG/BiXs= +github.com/gethinode/mod-blocks/v2 v2.1.3/go.mod h1:z+0Rg+I0naJf7VOy9dDuo8FX1dB5bPuMmbWT1QmBFhg= github.com/gethinode/mod-bootstrap-icons/v2 v2.0.1 h1:shq4Gb48U3h9xYUpq+FSHWz/eu1azJ5bMkja6wPmCpM= github.com/gethinode/mod-bootstrap-icons/v2 v2.0.1/go.mod h1:oRJGGe/EP/DRynePDHtaiC9ONR58+Jbm1c46Cgv0+jw= github.com/gethinode/mod-cookieyes/v2 v2.2.6 h1:/DQm8OYpms0On8wuosQER47TplVu/3z7MZHwbBKXCAg= @@ -14,5 +16,7 @@ github.com/gethinode/mod-fontawesome/v6 v6.0.1 h1:Wq51kwbtlZYjLQdSbLjn1Qjf4ZkmGI github.com/gethinode/mod-fontawesome/v6 v6.0.1/go.mod h1:Pz24mpGcg4FvGHhwPiuEZzOzISeOcoaQROhI6xLW2bU= github.com/gethinode/mod-utils/v6 v6.4.1 h1:jHiJEwSjLp6tqIXayxUQTxILwnM7hWMgYyTYhxFE3ZA= github.com/gethinode/mod-utils/v6 v6.4.1/go.mod h1:E5tO9w3VKaidJpu1nI8zAKmh0bddFHOIIQnudAaXQTs= +github.com/gethinode/mod-utils/v6 v6.5.0 h1:NdeITAfjt2ah9OHt0Nuu6F+STlC0bO9oHaFl8Agi2rk= +github.com/gethinode/mod-utils/v6 v6.5.0/go.mod h1:E5tO9w3VKaidJpu1nI8zAKmh0bddFHOIIQnudAaXQTs= github.com/twbs/icons v1.13.1 h1:6bmNXvoeIbvjFWjfbjXg5JGbelLD1haIsMaEIzE9UZI= github.com/twbs/icons v1.13.1/go.mod h1:GnRlymgVWp5iVJCMa0Me5b6tFyGpVc2bSxPMRGIJmyA= diff --git a/go.mod b/go.mod index 00997570e..b2b0f897c 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/airbnb/lottie-web v5.13.0+incompatible // indirect github.com/gethinode/mod-bootstrap v1.3.7 // indirect github.com/gethinode/mod-csp v1.0.12 // indirect - github.com/gethinode/mod-flexsearch/v5 v5.0.1 // indirect + github.com/gethinode/mod-flexsearch/v5 v5.0.2 // indirect github.com/gethinode/mod-fontawesome/v6 v6.0.1 // indirect github.com/gethinode/mod-google-analytics/v2 v2.0.4 // indirect github.com/gethinode/mod-katex v1.1.6 // indirect @@ -15,7 +15,7 @@ require ( github.com/gethinode/mod-lottie/v3 v3.0.1 // indirect github.com/gethinode/mod-mermaid/v5 v5.0.1 // indirect github.com/gethinode/mod-simple-datatables/v4 v4.1.0 // indirect - github.com/gethinode/mod-utils/v6 v6.4.2 // indirect + github.com/gethinode/mod-utils/v6 v6.5.0 // indirect github.com/nextapps-de/flexsearch v0.0.0-20260529083235-f7ed963096a0 // indirect github.com/twbs/bootstrap v5.3.8+incompatible // indirect ) diff --git a/go.sum b/go.sum index 4f0ebdfa8..62ca98244 100644 --- a/go.sum +++ b/go.sum @@ -46,6 +46,8 @@ github.com/gethinode/mod-flexsearch/v5 v5.0.0 h1:JsrsKejSRxirUyfCZheLFkQVAJKdtRD github.com/gethinode/mod-flexsearch/v5 v5.0.0/go.mod h1:FGj0l97PX7OnDjvFtdFrq3g3BJPeal7FiSgrtd6Yxdo= github.com/gethinode/mod-flexsearch/v5 v5.0.1 h1:/0K2nlJ53+PyQjyyl14S9SGpmJRV/vy5V4I71KbL6Qc= github.com/gethinode/mod-flexsearch/v5 v5.0.1/go.mod h1:RKNSsvoFRnEfQftB/o9IrX4YBLeS27HH9AgnpYYSd24= +github.com/gethinode/mod-flexsearch/v5 v5.0.2 h1:VAHzYLJECrVBvzn5xvM+FoHSP2VwDDPlUrfJDL8G/gg= +github.com/gethinode/mod-flexsearch/v5 v5.0.2/go.mod h1:0kRR1xaAhSYVyVICV5+x5bF8OSpfre1DwLdrK3a5uAA= github.com/gethinode/mod-fontawesome/v4 v4.0.3 h1:ml+V7YPkxsK5sPJz0OlheBlGdEZrry4aH/7M/ytz3zk= github.com/gethinode/mod-fontawesome/v4 v4.0.3/go.mod h1:EqAF4dW3gyUr/CsNND5lEPJcNRlLJ+GfPIUaGTaVRic= github.com/gethinode/mod-fontawesome/v4 v4.1.1 h1:nC2tTBHTeUBwVqZCz6kEzDvuBrbMgWeOxcAXbCBwCOs= @@ -204,6 +206,8 @@ github.com/gethinode/mod-utils/v6 v6.4.1 h1:jHiJEwSjLp6tqIXayxUQTxILwnM7hWMgYyTY github.com/gethinode/mod-utils/v6 v6.4.1/go.mod h1:E5tO9w3VKaidJpu1nI8zAKmh0bddFHOIIQnudAaXQTs= github.com/gethinode/mod-utils/v6 v6.4.2 h1:xh3XBpuvD+AisycxqEX0Ao90wKlu7cuKM0QOZcdVtP4= github.com/gethinode/mod-utils/v6 v6.4.2/go.mod h1:E5tO9w3VKaidJpu1nI8zAKmh0bddFHOIIQnudAaXQTs= +github.com/gethinode/mod-utils/v6 v6.5.0 h1:NdeITAfjt2ah9OHt0Nuu6F+STlC0bO9oHaFl8Agi2rk= +github.com/gethinode/mod-utils/v6 v6.5.0/go.mod h1:E5tO9w3VKaidJpu1nI8zAKmh0bddFHOIIQnudAaXQTs= github.com/nextapps-de/flexsearch v0.0.0-20250907103239-defb38b083f0 h1:55phPhe6fDjfjG0jX4+br3nLORKgjgx8abZUdI0YJRA= github.com/nextapps-de/flexsearch v0.0.0-20250907103239-defb38b083f0/go.mod h1:5GdMfPAXzbA2gXBqTjC6l27kioSYzHlqDMh0+wyx7sU= github.com/nextapps-de/flexsearch v0.0.0-20260529083235-f7ed963096a0 h1:QDKcU3q39lFGzdVwM6kgCGnW3ibbMfYIi0Rwl62iJpo=