Categories bundle: categories managed in the editor as Hugo term pages, used by news #152

Closed
opened 2026-10-06 00:09:36 +02:00 by dominik.polakovics · 0 comments

This was generated by AI during triage.

Summary

Add a categories feature bundle that makes categories a site-wide part of deckle instead of a news detail. Each category is a Hugo term page under content/categories/. Editors create, rename and delete categories in a Categories module. A new categories field type lets a page type or an element pick from that list. The news bundle uses it for news items and for the "Latest news" filter, replacing its two hand-kept option lists.

Current behavior

  • Categories exist only in the news bundle, as two copies of one option list:

    • news-item.yaml declares a categories multiselect with options: [announcements, company, events, product];
    • news-list.yaml declares a single-valued category select with the same list.

    The sidecar comments and harness add news say to keep the two "in lockstep by hand". Only a repo test (TestNewsBundleSidecarsLoad) compares the shipped copies.

  • Editors can't add, rename or delete a category, because the list lives in sidecars.

  • The news bundle enables Hugo's categories taxonomy through hugo.toml edits (the disableKinds, taxonomies and outputs steps of its featureConfig entry in internal/addcmd). It also ships layouts/taxonomy.html and layouts/term.html, which list news items only.

  • The editor shows the raw value (company), while Hugo shows the term title-cased (Company).

  • On save, a news item's multiselect keeps only values that are in the list. A "Latest news" element that stores a value no longer in the list can't be saved (422 "must be one of…").

Desired behavior

The categories bundle

  • A new bundle, categories (scaffold/features/categories/). harness add categories installs it and harness remove categories removes it. It ships:
    • the /categories/ page, content/categories/_index.md, so it always sits where Hugo needs it. It stays out of the site's main navigation;
    • a page type for that page (only categories as children, hidden from the page tree because the module manages them) and a page type for a category (no children);
    • the Categories module, editor/modules/categories.yaml;
    • the templates for /categories/ and /categories/<slug>/;
    • the hugo.toml edits that enable the categories taxonomy, which move here from the news bundle: re-enable the taxonomy and term kinds, declare category = 'categories', and limit both kinds' output to HTML.
  • It starts with no categories.

Categories

  • A category is a page directly under content/categories/. Pages store its folder name (the slug). Its title is the name shown everywhere: in the editor, and on the site, where Hugo reads term-page titles itself.
  • In the Categories module, editors select the /categories/ page in the tree, then list, create, rename and delete categories, sorted by title. The entry editor has only a Properties tab, and categories have no Enable/Disable.
  • Rename… works as for any page: a new title, the slug re-derived, the old URL kept as an alias, and links rewritten. It also rewrites the slug in every page's categories front matter and in every element's categories param.
  • Delete… deletes the category and removes it from every page's categories front matter and every element's categories param:
    • a list that becomes empty removes the key or param, so a "Latest news" element left without categories shows all news;
    • the confirmation says how many pages will change.
  • Editing the title in Properties changes only the name, as for any page. The slug and the stored values stay.

The categories field type

  • A new field type, categories, allowed in page-type fields: and in element params:. A field or param of this type must be named categories, and a field or param named categories must be of this type. This way, deleting and renaming know exactly where category values are stored.
  • Its options are the site's categories. The editor shows their titles, sorted by title, and stores their slugs. The type takes no options: and no default:.
  • Page-type fields store a list in front matter: categories: [company, product]. Element params store the slugs comma-separated, because shortcode params can't hold lists: categories="company,product".
  • Saving stores exactly the selected categories, and saving with nothing selected removes the key or param. A stored value that names no category isn't shown, and it is dropped at the next save. A save that names a category that doesn't exist is rejected.
  • Editor modules accept the type wherever they accept a multiselect (list columns, card tags, filters), and they show category titles.

News

  • news-item's categories field becomes type: categories.
  • "Latest news" replaces its category select with a categories param. It shows the news items that are in any of the chosen categories, and its limit applies after filtering.
  • The News module's category filter, card tags and list column work through the new type.
  • The news bundle no longer makes the taxonomy hugo.toml edits (it keeps buildFuture) and no longer ships the /categories/ templates.
  • The bundle version goes up by one. The sidecar comments and the next steps harness add news prints no longer mention hand-kept lists.

Category pages on the site

  • /categories/ lists the categories that have at least one published page, by title, each with its count.
  • /categories/<slug>/ shows the category's title and lists its published pages, newest first, paginated at the site's page size. News items render as the news cards they have today. Other pages render as a title link with their summary.

Docs

scaffold/site/AGENTS.md and the README document (CLAUDE.md rule):

  • the categories field type and its naming rule;
  • the categories bundle;
  • the one line that gives another page type a categories field;
  • what rename and delete do;
  • the news changes.

Decisions

  • Categories are Hugo taxonomy terms with term pages. Pages store their categories in front matter (categories:), and Hugo builds /categories/ and one page per category. Each category's term page holds its title, and Hugo shows that title everywhere (the page heading, <title>, links) without template lookups.
    • Rejected: a list file with separate labels. Hugo can't use those labels as its own term titles.
    • Rejected: free values. There would be no managed list.
  • Editors manage categories in the editor, like TYPO3 category records. This is the maintainer's requirement. Categories are pages, so creating, saving, publishing and the module screen reuse the existing page machinery.
  • A category has one name, and its URL follows it. Renaming changes the name and the URL together and rewrites the stored values. A slug change needs that rewrite anyway, and find and replace is cheap. Uid-style slugs were rejected because they make unreadable URLs.
  • Deleting a category removes it everywhere. Adding the feature changes nothing on pages that already carry categories: such values match as soon as a category with that slug exists, and are otherwise dropped at the page's next save. These are the maintainer's rules.
  • Categories have no Disable. Hugo would still build a disabled category's page from the pages that carry it, just without its title.
  • One fixed key, categories, for every field and param that holds categories. This is the maintainer's requirement, so that delete and rename know where to search. It is also Hugo's taxonomy key.
  • A dedicated field type rather than options on select/multiselect. The options come from content, the editor shows titles but stores slugs, element params store several values comma-separated, and the name is fixed.
  • Categories are their own bundle, not part of news and not part of the starter site. The maintainer's choice: categories are a standard feature that news uses. Marking the bundle standard, so that harness init installs it, is #157.
  • Only news items get a categories field for now. Other page types opt in with one line.
  • The bundle ships the /categories/ page and starts with no categories. Re-running harness add restores missing bundle files, so shipped category files would come back after an editor deleted them.
  • No migration. There are no existing sites, so older news installs need no handling (maintainer).
  • One issue, one pull request. #153 was as broad and landed as one.

Key interfaces

  • Field types: FieldType, validFieldTypes, buildParam, ParamSource, ValidateValues and CoerceValues in internal/element. Page-type fields reuse them (internal/pagetype). The AGENTS.md drift test (internal/element/agentsdoc_test.go) requires the manual to name the new type.
  • Options from content: the server already checks content-dependent values at save time for type: page (CheckPageParams in internal/pageops). The client needs each category's slug and title, through the schema endpoints (/api/pagetypes, /api/elements) or an endpoint of its own.
  • Rename and delete: the page rename and delete operations (internal/pageops; renamePageTo and DeleteDialog in the web app) and the site-wide link rewriting on rename (internal/linkrefs) are the precedent for rewriting categories values across pages.
  • Page actions: pageActions and pageActionItems in web/src/pageactions.ts, for leaving out Enable/Disable on categories.
  • Widgets: fields.tsx (canonicalMultiselectValue, MultiselectField, buildFieldValues) and the module screen's columns, cards and filters.
  • Module rules: checkPlacement and buildFilter in internal/editormodule, and filter matching in internal/server/modules.go.
  • Bundles and config: featureConfig and the taxonomy steps in internal/addcmd (config.go, taxonomy.go; the step IDs are part of the receipt format), and featureHints. In-use detection is in internal/removecmd.
  • The news bundle and its tests: scaffold/features/news/ and internal/theme/news_test.go.

Acceptance criteria

  • harness add categories on a fresh site writes the bundle, including content/categories/_index.md, enables the categories taxonomy in hugo.toml, and records everything in the receipt. harness remove categories undoes it. Right after an add, with no categories created and nothing customised, the remove succeeds without --purge.
  • A Categories rail entry lists the site's categories by title. Creating "Company news" writes content/categories/company-news/_index.md, and the category then appears in a news item's properties and in the "Latest news" category picker, with no sidecar edit.
  • A news item with two categories saves categories: [company-news, product], and clearing them removes the key. A stored value that names no category isn't shown, and it is gone after the next save.
  • A "Latest news" element with two categories stores categories="company-news,product". It shows only the news items in either category, newest first, with the limit applied after filtering.
  • Renaming "Company news" to "Company updates" changes the slug, every news item's front matter and every "Latest news" param that used it, and the old category URL redirects.
  • Deleting a category removes it from every news item and every "Latest news" element, and an element left with no categories shows all news. The confirmation states how many pages will change.
  • Categories have no Enable/Disable action, and their entry editor has no Content tab.
  • Each of these is a load error that names the file:
    • a field or param of type categories that isn't named categories;
    • a field named categories with another type;
    • options: or default: on a categories field.
  • The News module's category filter, card tags and list column show category titles and filter by slug.
  • The site's /categories/ page lists the categories that have published pages, by title, with counts. A category page shows its title and its published pages newest first, with news items as news cards.
  • The news bundle carries no category option list, makes no taxonomy hugo.toml edits and ships no /categories/ templates, and its version is bumped.
  • AGENTS.md and the README document the field type and its naming rule, the categories bundle, opting other page types in, rename and delete, and the news changes.

How to verify

  • make test. It builds web/dist first, which go test ./... needs. The theme's Hugo build tests run only with hugo on PATH (CI installs 0.163.3).
  • cd web && npm test.
  • In a browser, against harness serve on a fresh site with harness add categories and harness add news:
    • create three categories, assign them to news items, and filter "Latest news" by two of them;
    • check /categories/ and one category page in the preview;
    • rename a category, then delete one, and check the news items, the element and the old URL.

Assumptions to check

  • Hugo builds a category's page from its term file even when no page carries the category yet.
  • The editor's create flow stamps an explicit type on new pages. Check that Hugo 0.163 still renders typed category pages, and the /categories/ page, through the bundle's templates.
  • Rendering news items as news cards on a generic category page: Hugo's content views (.Render) are one way. Check how they behave under Hugo 0.163's template system.
  • Whether site.Home.Pages, which the theme's header navigation iterates over, includes the /categories/ page, and what keeps that page out of the navigation.
  • internal/removecmd counts pages of a bundle's page types as content in use. The shipped /categories/ page alone must not block removing a bundle that has no categories. deckle-hosted's TestRealHarnessLifecycle (harness/real_test.go, run when DECKLE_HOSTED_TEST_HARNESS is set) adds and removes the first feature the harness lists, and that becomes categories.
  • Deckle slugs ([a-z0-9-]) are exactly what Hugo derives for a term's URL, so a stored slug always matches its term page.

Out of scope

  • Marking the categories bundle standard and installing it from harness init (#157).
  • One Publish for all pending changes (#156).
  • Tags and other taxonomies.
  • Categories on page types other than news items, beyond documenting the one-line opt-in.
  • Nested categories: Hugo's taxonomies are flat.
  • Migrating existing sites: there are none.
  • deckle-hosted.

Blocked by

  • #151 (no functional dependency; it also changes news-list.yaml and its template, so this issue lands after it)
> *This was generated by AI during triage.* ## Summary Add a `categories` feature bundle that makes categories a site-wide part of deckle instead of a news detail. Each category is a Hugo term page under `content/categories/`. Editors create, rename and delete categories in a Categories module. A new `categories` field type lets a page type or an element pick from that list. The news bundle uses it for news items and for the "Latest news" filter, replacing its two hand-kept option lists. ## Current behavior - Categories exist only in the news bundle, as two copies of one option list: - `news-item.yaml` declares a `categories` multiselect with `options: [announcements, company, events, product]`; - `news-list.yaml` declares a single-valued `category` select with the same list. The sidecar comments and `harness add news` say to keep the two "in lockstep by hand". Only a repo test (`TestNewsBundleSidecarsLoad`) compares the shipped copies. - Editors can't add, rename or delete a category, because the list lives in sidecars. - The news bundle enables Hugo's `categories` taxonomy through `hugo.toml` edits (the `disableKinds`, `taxonomies` and `outputs` steps of its `featureConfig` entry in `internal/addcmd`). It also ships `layouts/taxonomy.html` and `layouts/term.html`, which list news items only. - The editor shows the raw value (`company`), while Hugo shows the term title-cased (`Company`). - On save, a news item's multiselect keeps only values that are in the list. A "Latest news" element that stores a value no longer in the list can't be saved (422 "must be one of…"). ## Desired behavior ### The categories bundle - A new bundle, `categories` (`scaffold/features/categories/`). `harness add categories` installs it and `harness remove categories` removes it. It ships: - the `/categories/` page, `content/categories/_index.md`, so it always sits where Hugo needs it. It stays out of the site's main navigation; - a page type for that page (only categories as children, hidden from the page tree because the module manages them) and a page type for a category (no children); - the Categories module, `editor/modules/categories.yaml`; - the templates for `/categories/` and `/categories/<slug>/`; - the `hugo.toml` edits that enable the `categories` taxonomy, which move here from the news bundle: re-enable the `taxonomy` and `term` kinds, declare `category = 'categories'`, and limit both kinds' output to HTML. - It starts with no categories. ### Categories - A category is a page directly under `content/categories/`. Pages store its folder name (the slug). Its title is the name shown everywhere: in the editor, and on the site, where Hugo reads term-page titles itself. - In the Categories module, editors select the `/categories/` page in the tree, then list, create, rename and delete categories, sorted by title. The entry editor has only a Properties tab, and categories have no Enable/Disable. - **Rename…** works as for any page: a new title, the slug re-derived, the old URL kept as an alias, and links rewritten. It also rewrites the slug in every page's `categories` front matter and in every element's `categories` param. - **Delete…** deletes the category and removes it from every page's `categories` front matter and every element's `categories` param: - a list that becomes empty removes the key or param, so a "Latest news" element left without categories shows all news; - the confirmation says how many pages will change. - Editing the title in Properties changes only the name, as for any page. The slug and the stored values stay. ### The `categories` field type - A new field type, `categories`, allowed in page-type `fields:` and in element `params:`. A field or param of this type must be named `categories`, and a field or param named `categories` must be of this type. This way, deleting and renaming know exactly where category values are stored. - Its options are the site's categories. The editor shows their titles, sorted by title, and stores their slugs. The type takes no `options:` and no `default:`. - Page-type fields store a list in front matter: `categories: [company, product]`. Element params store the slugs comma-separated, because shortcode params can't hold lists: `categories="company,product"`. - Saving stores exactly the selected categories, and saving with nothing selected removes the key or param. A stored value that names no category isn't shown, and it is dropped at the next save. A save that names a category that doesn't exist is rejected. - Editor modules accept the type wherever they accept a multiselect (list columns, card `tags`, `filters`), and they show category titles. ### News - `news-item`'s `categories` field becomes `type: categories`. - "Latest news" replaces its `category` select with a `categories` param. It shows the news items that are in any of the chosen categories, and its limit applies after filtering. - The News module's category filter, card tags and list column work through the new type. - The news bundle no longer makes the taxonomy `hugo.toml` edits (it keeps `buildFuture`) and no longer ships the `/categories/` templates. - The bundle `version` goes up by one. The sidecar comments and the next steps `harness add news` prints no longer mention hand-kept lists. ### Category pages on the site - `/categories/` lists the categories that have at least one published page, by title, each with its count. - `/categories/<slug>/` shows the category's title and lists its published pages, newest first, paginated at the site's page size. News items render as the news cards they have today. Other pages render as a title link with their summary. ### Docs `scaffold/site/AGENTS.md` and the README document (CLAUDE.md rule): - the `categories` field type and its naming rule; - the categories bundle; - the one line that gives another page type a categories field; - what rename and delete do; - the news changes. ## Decisions - **Categories are Hugo taxonomy terms with term pages.** Pages store their categories in front matter (`categories:`), and Hugo builds `/categories/` and one page per category. Each category's term page holds its title, and Hugo shows that title everywhere (the page heading, `<title>`, links) without template lookups. - Rejected: a list file with separate labels. Hugo can't use those labels as its own term titles. - Rejected: free values. There would be no managed list. - **Editors manage categories in the editor, like TYPO3 category records.** This is the maintainer's requirement. Categories are pages, so creating, saving, publishing and the module screen reuse the existing page machinery. - **A category has one name, and its URL follows it.** Renaming changes the name and the URL together and rewrites the stored values. A slug change needs that rewrite anyway, and find and replace is cheap. Uid-style slugs were rejected because they make unreadable URLs. - **Deleting a category removes it everywhere.** Adding the feature changes nothing on pages that already carry categories: such values match as soon as a category with that slug exists, and are otherwise dropped at the page's next save. These are the maintainer's rules. - **Categories have no Disable.** Hugo would still build a disabled category's page from the pages that carry it, just without its title. - **One fixed key, `categories`, for every field and param that holds categories.** This is the maintainer's requirement, so that delete and rename know where to search. It is also Hugo's taxonomy key. - **A dedicated field type rather than options on select/multiselect.** The options come from content, the editor shows titles but stores slugs, element params store several values comma-separated, and the name is fixed. - **Categories are their own bundle, not part of news and not part of the starter site.** The maintainer's choice: categories are a standard feature that news uses. Marking the bundle standard, so that `harness init` installs it, is #157. - **Only news items get a categories field for now.** Other page types opt in with one line. - **The bundle ships the `/categories/` page and starts with no categories.** Re-running `harness add` restores missing bundle files, so shipped category files would come back after an editor deleted them. - **No migration.** There are no existing sites, so older news installs need no handling (maintainer). - **One issue, one pull request.** #153 was as broad and landed as one. ## Key interfaces - **Field types:** `FieldType`, `validFieldTypes`, `buildParam`, `ParamSource`, `ValidateValues` and `CoerceValues` in `internal/element`. Page-type fields reuse them (`internal/pagetype`). The AGENTS.md drift test (`internal/element/agentsdoc_test.go`) requires the manual to name the new type. - **Options from content:** the server already checks content-dependent values at save time for `type: page` (`CheckPageParams` in `internal/pageops`). The client needs each category's slug and title, through the schema endpoints (`/api/pagetypes`, `/api/elements`) or an endpoint of its own. - **Rename and delete:** the page rename and delete operations (`internal/pageops`; `renamePageTo` and `DeleteDialog` in the web app) and the site-wide link rewriting on rename (`internal/linkrefs`) are the precedent for rewriting `categories` values across pages. - **Page actions:** `pageActions` and `pageActionItems` in `web/src/pageactions.ts`, for leaving out Enable/Disable on categories. - **Widgets:** `fields.tsx` (`canonicalMultiselectValue`, `MultiselectField`, `buildFieldValues`) and the module screen's columns, cards and filters. - **Module rules:** `checkPlacement` and `buildFilter` in `internal/editormodule`, and filter matching in `internal/server/modules.go`. - **Bundles and config:** `featureConfig` and the taxonomy steps in `internal/addcmd` (`config.go`, `taxonomy.go`; the step IDs are part of the receipt format), and `featureHints`. In-use detection is in `internal/removecmd`. - **The news bundle and its tests:** `scaffold/features/news/` and `internal/theme/news_test.go`. ## Acceptance criteria - [ ] `harness add categories` on a fresh site writes the bundle, including `content/categories/_index.md`, enables the `categories` taxonomy in `hugo.toml`, and records everything in the receipt. `harness remove categories` undoes it. Right after an add, with no categories created and nothing customised, the remove succeeds without `--purge`. - [ ] A Categories rail entry lists the site's categories by title. Creating "Company news" writes `content/categories/company-news/_index.md`, and the category then appears in a news item's properties and in the "Latest news" category picker, with no sidecar edit. - [ ] A news item with two categories saves `categories: [company-news, product]`, and clearing them removes the key. A stored value that names no category isn't shown, and it is gone after the next save. - [ ] A "Latest news" element with two categories stores `categories="company-news,product"`. It shows only the news items in either category, newest first, with the limit applied after filtering. - [ ] Renaming "Company news" to "Company updates" changes the slug, every news item's front matter and every "Latest news" param that used it, and the old category URL redirects. - [ ] Deleting a category removes it from every news item and every "Latest news" element, and an element left with no categories shows all news. The confirmation states how many pages will change. - [ ] Categories have no Enable/Disable action, and their entry editor has no Content tab. - [ ] Each of these is a load error that names the file: - a field or param of type `categories` that isn't named `categories`; - a field named `categories` with another type; - `options:` or `default:` on a `categories` field. - [ ] The News module's category filter, card tags and list column show category titles and filter by slug. - [ ] The site's `/categories/` page lists the categories that have published pages, by title, with counts. A category page shows its title and its published pages newest first, with news items as news cards. - [ ] The news bundle carries no category option list, makes no taxonomy `hugo.toml` edits and ships no `/categories/` templates, and its `version` is bumped. - [ ] AGENTS.md and the README document the field type and its naming rule, the categories bundle, opting other page types in, rename and delete, and the news changes. ## How to verify - `make test`. It builds `web/dist` first, which `go test ./...` needs. The theme's Hugo build tests run only with `hugo` on PATH (CI installs 0.163.3). - `cd web && npm test`. - In a browser, against `harness serve` on a fresh site with `harness add categories` and `harness add news`: - create three categories, assign them to news items, and filter "Latest news" by two of them; - check `/categories/` and one category page in the preview; - rename a category, then delete one, and check the news items, the element and the old URL. ## Assumptions to check - Hugo builds a category's page from its term file even when no page carries the category yet. - The editor's create flow stamps an explicit `type` on new pages. Check that Hugo 0.163 still renders typed category pages, and the `/categories/` page, through the bundle's templates. - Rendering news items as news cards on a generic category page: Hugo's content views (`.Render`) are one way. Check how they behave under Hugo 0.163's template system. - Whether `site.Home.Pages`, which the theme's header navigation iterates over, includes the `/categories/` page, and what keeps that page out of the navigation. - `internal/removecmd` counts pages of a bundle's page types as content in use. The shipped `/categories/` page alone must not block removing a bundle that has no categories. deckle-hosted's `TestRealHarnessLifecycle` (`harness/real_test.go`, run when `DECKLE_HOSTED_TEST_HARNESS` is set) adds and removes the first feature the harness lists, and that becomes `categories`. - Deckle slugs (`[a-z0-9-]`) are exactly what Hugo derives for a term's URL, so a stored slug always matches its term page. ## Out of scope - Marking the categories bundle standard and installing it from `harness init` (#157). - One Publish for all pending changes (#156). - Tags and other taxonomies. - Categories on page types other than news items, beyond documenting the one-line opt-in. - Nested categories: Hugo's taxonomies are flat. - Migrating existing sites: there are none. - deckle-hosted. ## Blocked by - #151 (no functional dependency; it also changes `news-list.yaml` and its template, so this issue lands after it)
dominik.polakovics changed title from Category system shared by every element and page type that uses categories to Categories bundle: categories managed in the editor as Hugo term pages, used by news 2026-10-07 00:39:02 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle#152
No description provided.