Editor modules declared in YAML by feature bundles; News module replaces the List module #153

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

This was generated by AI during triage.

Summary

Let a feature bundle, or a site owner, add an editor module declared in YAML (editor/modules/<name>.yaml). The YAML says which page types the module manages, how their pages are shown (a list or cards), what each entry shows, and which controls (search, sort, filters, paging) surround them.

The page tree stays beside the module. Selecting a page that may hold the entries lists them, and that page is where new ones are created.

The news bundle uses this to ship a News module, in which news items are listed, created and edited without leaving the module. The generic List module is removed. Re-running harness add on an installed feature adds the bundle files the site lacks, so existing news sites get the module file.

Current behavior

  • Modules are a fixed registry in the web app (MODULES in web/src/modules.tsx): Page, View, List and Media, plus the hidden element edit page. Neither a site nor a feature can add one. The rail, route repair and return validation all check module ids synchronously against that registry.
  • News items are managed in the generic List module (#79): a table of a section's child pages built from the /api/tree payload, with columns Title, Type, Date and Status and the row actions Rename…, Enable/Disable and Delete…. A title opens the page module with return to the list (#149). The create dialog (title, required fields, fields marked create: true) is defined inside ListModule.tsx.
  • hideChildPages on a page type hides all of that type's child pages from the tree; they are "curated through the List module instead". The page module points such a section to the List ("Open list", an inline hint). #149's fallbacks for a hidden page (its back button, the landing after deleting it) lead to the parent's List.
  • harness add <feature> on an installed feature (its receipt exists) changes nothing and reports already-installed. Files added to a bundle later therefore never reach sites that installed it earlier (README, "Not covered yet").
  • The schema error banner lists element and backend-layout sidecar load errors.

Desired behavior

Module files

  • A new sidecar family: editor/modules/<name>.yaml.
    • It follows the shared internal/sidecardir scan rules: one module per file, the file name is the module id, strict decoding.
    • Files are read fresh on each request, like the other families.
    • Load errors appear in the schema error banner as their own family, and one bad file never hides the others.
  • A feature bundle ships its module files under editor/modules/; harness add vendors them and harness remove removes them, like any bundle file. A site owner, or an agent following AGENTS.md, can also write one by hand.
  • Every valid module file adds a rail entry (its label and icon) after the built-in modules, in id order.
  • These ids are reserved, compared case-insensitively; using one is a load error:
    • the built-in modules page, view, media and edit;
    • api: the server answers every /api/* path itself, so the editor can never show a module there;
    • internal: a host in front of the editor may block /internal/* (deckle-hosted's proxy answers 404 there on editor hosts).

Schema

version: 1                    # required, exactly 1
label: News                   # optional; defaults to the module id
icon: lucide:newspaper        # optional; the icon syntax other sidecars use
pageTypes: [news-item]        # required, non-empty: the page types this module lists and creates

views:                        # required, at least one; the first is the default; two or more give a view switcher
  - layout: list
    columns: [title, date, categories, author, status]
  - layout: cards
    card:
      image: image
      title: title            # defaults to the built-in title
      meta: [date, author]
      text: teaser
      tags: categories

sort: date desc               # optional, "<field> [asc|desc]" (asc when omitted); default: the tree's order of the container's children
search: [title, teaser]       # optional; the fields the search box looks in; default [title]
filters: [categories, status] # optional; default none
pageSize: 20                  # optional; default 20, at most 100

edit: [properties, content]   # optional; the entry editor's tabs, in order; default [properties, content]
  • There is no key for where entries live. The tree selection and the page types' children lists decide that (see below).

  • Field names refer to the fields the listed page types declare, plus three built-ins:

    • title;
    • date (front-matter date);
    • status: the disabled marker (front-matter draft) and the publish state (the git badge).

    A declared field named title or date is the same front-matter key as the built-in; its label is used wherever a label shows.

  • Where a field may be used:

    • list columns and card meta: any field or built-in;
    • card image: an image field;
    • card title: title or a text field;
    • card text: a text or textarea field;
    • card tags: a select or multiselect field;
    • search: title, and text and textarea fields;
    • sort: title, date, and text, number, datetime and select fields;
    • filters: status, and select, multiselect and bool fields.
  • Filter values:

    • a select or multiselect filter offers the field's options;
    • a bool filter offers yes/no;
    • status offers Enabled/Disabled. The publish state is shown but cannot be filtered on in v1.
  • Load errors, each naming the file and the reason:

    • an unknown key, layout, card role, tab or sort direction;
    • no views, a list view without columns, or a pageSize outside 1–100;
    • a pageTypes entry that is not a loaded page type;
    • a field name that is neither a built-in nor declared by any listed page type;
    • a field used where its type is not allowed (every listed type's declaration of that name must fit);
    • a listed page type that declares its own status field (it would shadow the built-in);
    • a page type another module already lists (the module first in id order keeps it);
    • a listed page type whose own children explicitly names one of the module's page types. Such an entry could hold entries of its own module, and one URL would then mean both the list and the entry;
    • a reserved id.

The page tree in a feature module

  • The page tree stays as the module's left column, in a selection-only mode:

    • no ⋯ or right-click menus;
    • no drag and drop;
    • no inline rename, inline create or "Home (missing)" create.

    Page management stays in the Page module.

  • Which pages are selectable:

    • A page is a container, and selectable, when its effective type explicitly lists one of the module's page types in children (news: pages of type news, whose children is [news-item]).
    • Every other page stays visible but greyed out, and can still be expanded, so a nested container is reachable.
    • A type without a children key, which allows any child, does not count; otherwise every such page would become a container.
  • The module's URL follows the other page-tree modules: /<module>/<lang>/<page path>. The path names one of two things:

    • a container: the module screen;
    • an entry, meaning a page whose effective type is one of the module's page types and whose parent is a container: the entry editor.

    Route repair judges the path against the tree as usual.

  • When the URL names neither (for example, Home still selected after switching modules), the module asks for a container, naming the selectable type: "Select a News page in the tree". Nothing is selected automatically, consistent with the editor's rule that it never auto-selects a page (#33).

  • While an entry is open, its container is the tree's selected row (styling and aria-current). The row is revealed and scrolled into view as in #149, whether or not the container hides its children.

  • Switching modules keeps the page path, as today. An entry open in the Page module opens in the entry editor when you switch to News; a container opens in the Page module when you switch back.

Hiding managed pages from the tree

  • hideChildPages keeps its place in the page-type schema, with a narrower meaning: a container whose type sets it hides its managed children from the page tree, in every module. A managed child is a child page whose effective type is one of the page types of a module the page is a container for. Its other children stay in the tree, and its row expands when any remain.
  • When no loaded module manages a type's children, hideChildPages hides nothing. So a hidden page always has a module that lists it. A site without the module file (news installed before this change, harness add news not re-run) still reaches its items in the tree.
  • Hidden pages stay in the /api/tree payload, because route repair and the link pickers read them. A hidden page, or anything beneath one, selects its nearest drawn ancestor in the tree (#149).

The module screen

  • With a container selected, the module lists that page's child pages of the module's page types, in the declared views. Several views give a switcher.

  • Search, sort, filters and paging apply to the whole set, on the server:

    • search is a case-insensitive substring match over the search fields;
    • sort is by column headers in a list and a sort menu for cards, over sortable fields only.
  • Search terms, filters, sort, the active view and the page number live in the URL query, so reload, Back and a shared link keep them.

  • Until the user picks a sort, entries follow sort, else the tree's order of the container's children. For news that is date desc: newest first, undated last.

  • status renders as the List does today: the disabled marker and the published / modified / unpublished badge.

  • A "New " button appears for each of the module's page types that the container's type allows as a child (singletons excluded).

    • It opens the existing create dialog and creates the entry under the container.
    • The new entry then opens in the module on its first tab, with return to the list.
    • An empty container shows an empty state with its New buttons.
  • An entry's title (in a list) or its card opens the entry inside the module, with return set to the list's full URL.

  • A server endpoint serves the entries:

    • in: module, container path, lang, search, sort, filters and page;
    • out: the page of entries (identity, type, title, draft, and the values of the fields the views use) and the total count.

    It reads the front matter of the container's matching children. Every datetime value goes out as RFC3339 (see #133), including a native TOML datetime.

  • Another endpoint lists the loaded modules and their load errors.

The entry editor, inside the module

  • One tab per item in edit:

    • Properties: the page settings form;
    • Content: the page module's element list for that page (insert, reorder, delete, open element edit pages).

    The active tab lives in the URL query, so reload keeps it and element edit pages opened from Content return to Content.

  • The toolbar has:

    • back to the list, labelled with the container's title;
    • Save and Save & close;
    • View, Publish, Undo and Discard…, as in the page module;
    • the shared page actions from #149 (Rename…, Enable/Disable, Delete…).
  • Save and Save & close act on the Properties form from either tab. Switching tabs keeps unsaved edits. Leaving the entry with unsaved edits goes through the unsaved-changes guard.

  • Close and Save & close go to return: the list with its search, filters, sort, view and page intact. Without a return (a module switch, a link, a typed URL) they go to the entry's container.

  • Element edit pages opened from Content return to the entry, using #149's return.

  • Renaming the open entry follows it to its new path and keeps its return. Deleting it lands on its return, else on its container.

Modules from the server

  • The rail, route repair and return validation know the feature modules as well as the built-ins. Nothing judges a module id before the module list has loaded.
    • Reloading a News module URL stays in the News module.
    • An element edit page whose return names a module URL goes back there.

News

  • The news bundle ships editor/modules/news.yaml, along the lines of the example above, and its manifest version becomes 2.
  • In the page module, a container page offers "Manage in " (a docheader button and the inline hint) where the List affordances were: "Manage in News" on a News page.
  • A managed page reached another way (preview navigation, a link):
    • keeps #149's behavior: its container is selected in the tree;
    • offers "Open in " ("Open in News");
    • its back-button fallback, and the landing after deleting it, go to its container in that module.
  • Bundle comments and harness add news next steps that mention the List module point to the News module instead.

List module removed

  • The List module, its rail entry and its affordances ("Open list", the page-module hint) are removed. The create dialog survives as a shared component.
  • Old /list/… URLs and return values naming list are treated like any unknown module: route repair lands on the page module, and a stale return is ignored.

harness add fills gaps

  • Re-running harness add <feature> on an installed feature writes every bundle file missing on disk, including one the receipt lists but the owner deleted.
    • It never touches a path that exists.
    • It runs the same safety checks as a fresh add: no writing through symlinked directories, and nothing is written if a check fails.
  • It records only the files it wrote, each with the hash of the bytes written. An existing path it left alone is never recorded, or harness remove would delete the owner's own file.
  • The receipt's version becomes the bundle's current version. hugo.toml edits and page-type allowances are not re-applied.
  • It reports status added, exit 0, in text and --json output:
    • files lists only the files it wrote;
    • config, allowances and nextSteps are empty;
    • the --json contract stays formatVersion: 1.
  • A feature with nothing missing is still a no-op reporting already-installed. This is how existing news sites get editor/modules/news.yaml.

Docs

scaffold/site/AGENTS.md and the README document:

  • the new family: layout, schema, load errors and reserved ids;
  • the tree in feature modules;
  • the narrowed hideChildPages;
  • where the create dialog now lives (create: true fields; a new entry opens in the module);
  • the gap-filling add, including the README's --json table (the added row) and "Not covered yet".

Decisions

  • Modules are declared in YAML. The maintainer's requirement ("it needs to work to specify it in yaml"): the YAML picks the presentation (list or cards), what each entry shows, and which page types are shown.
  • Modules manage pages, not elements. The List module (#79) and its TYPO3-style redesign (#150, closed as superseded) don't fit Hugo.
    • TYPO3 lists records from uniform tables. In Hugo a page is a file, and an element is a shortcode at a position inside a page body, with no identity, metadata or creation date of its own.
    • What does map onto Hugo is a page holding child pages of one type, such as news items under a News page. It is also how TYPO3's news works: you pick the storage folder in the page tree, and the module shows and creates records there.
  • The page tree stays visible, and the selection decides where entries are listed and created. The maintainer's choice. It is TYPO3's storage-folder model, and it fits Hugo, where news items really are children of the News page. It also removes any need for a "where do entries live" key or a section picker.
  • Only pages whose type explicitly allows an entry type are selectable; the rest are greyed out. The maintainer's choice.
  • The tree is selection-only in feature modules. The maintainer's choice: page management stays in the Page module, and greyed-out rows would conflict with page menus.
  • No automatic selection. A message asks for a page instead, as the maintainer chose, consistent with #33.
  • No runtime JS plugins. Feature bundles and the editor both ship inside the harness binary. The editor provides the building blocks (layouts, controls, the entry editor) and YAML composes them.
  • News items are edited inside the News module. The maintainer's choice; you never switch into the Page module to edit one.
    • This also fixes, at the root, the navigation problems this started from: a news title opening a page the tree does not show, and Save & close not returning to the list.
    • #149 fixed the navigation around it.
  • The List module is removed. The maintainer's choice; the News module covers its only real use.
  • hideChildPages hides only pages a module manages (maintainer, in triage).
    • Existing sidecars stay valid and keep their meaning, owners still choose whether managed pages leave the tree, and a hidden page always has a module that lists it.
    • Keeping the old rule while removing the List would hide the items of every site that installed news before this change, with nothing listing them until harness add news is re-run. deckle-hosted never re-runs it.
    • Retiring the key was rejected. Strict decoding means every existing news.yaml must still load, so the key would linger as a dead key.
  • Schema names and rules (maintainer, in triage):
    • pageTypes, not entries: it says what the list holds, and leaves room for a later data: source (Hugo data files) beside it.
    • One module per page type, and no entry types that hold entries of their own module. "The module that manages a page" and the list-or-entry reading of a URL are then never ambiguous.
    • The status filter is the disabled marker only: filtering on the publish state would need git state on every query.
    • The five card roles (image, title, meta, text, tags) are enough for v1.
  • api and internal are reserved module ids besides the built-ins: /api/* never reaches the editor, and a host may block /internal/* (deckle-hosted does).
  • The entry editor also has Undo and Discard… (maintainer, in triage), so an entry is no lesser page than in the page module.
  • The tab, view and sort live in the URL, because the URL is the editor's source of truth (web/src/routing.ts).
  • harness add fills gaps for installed features. The maintainer's choice, so that existing news sites get the module file.
  • A gap-filling add reports added (maintainer, in triage). deckle-hosted parses --json strictly and rejects unknown statuses; reusing added keeps it working unchanged. No deckle-hosted change is part of this work.
  • One issue, one pull request (maintainer, in triage), gap filling included.
  • Docs change with the code. A new sidecar family, a scaffold layout change and a changed harness add mean scaffold/site/AGENTS.md and the README change in the same pull request (CLAUDE.md rule).

Key interfaces

  • Registry and shell:

    • MODULES / moduleById / ModuleDef (web/src/modules.tsx);
    • ModuleRail, which reads MODULES directly;
    • the shell (web/src/App.tsx): selectModule, returnFacts, the route-repair wiring;
    • repairRoute's unknown-module rule (web/src/routerepair.ts);
    • parseReturn / readReturn (web/src/routing.ts) and ReturnContext (web/src/returnnav.ts).

    Built-ins stay in the registry and feature modules come from the server; all of these must see both.

  • The page-tree column: PAGE_TREE_NAV (web/src/modules.tsx), TreePane / TreeRow (web/src/components/TreePane.tsx), and createTreeController (web/src/stores/treeController.ts: reveal, selectedRow). It needs a per-module selectable rule and a selection-only mode. TreeBrowser's isSelectable (greyed label, live chevron) is the precedent.

  • The hidden-page rule moves from "under a hideChildPages section" to "managed and hidden". It lives in:

    • hidingSection / parentPage (web/src/pagemanage.ts);
    • deleteLanding (web/src/stores/docheaderActions.ts);
    • PagePane's backTarget and isListSection / openList;
    • TreeRow's hidesChildren;
    • the tree node flag (HideChildPages in internal/pagetree).
  • Loader: internal/sidecardir is the scan contract. A new loader package goes beside internal/element, internal/pagetype and internal/backendlayout; it validates against the loaded page types.

  • Server (internal/server): the module-list and entries endpoints.

    • Pages are read via internal/pagetree, front matter the way the settings read does (ReadSettings / fieldValue in internal/pageops/settings.go).
    • fieldValue turns a native TOML datetime into Go's time.Time string (#133), while pagetree already formats date as RFC3339. #151 fixes the settings read, so share one conversion.
  • SchemaErrorBanner (web/src/components/SchemaErrorBanner.tsx): a new family.

  • Entry editor tabs: PageSettings (web/src/components/PageSettings.tsx; expose, footerSave) for Properties, and PagePane (web/src/components/PagePane.tsx) for Content. PagePane's docheader is built in, so the element list has to render without it.

  • The create dialog: CreateDialog, hasHiddenFields and seedFieldValues in web/src/components/ListModule.tsx.

  • Page actions and return: pageActionItems (web/src/pageactions.ts), createDocheaderActions, RenameDialog, DeleteDialog, withReturn (web/src/routing.ts) and useReturn (web/src/returnnav.ts).

  • Gap filling: internal/addcmd (Run, Result, Receipt, scanDestinations, featureHints) and the add output in cmd/harness/features.go (addCmd, addResultOutput, printAddResult). The JSON contract is the README's "Machine-readable output".

  • The news bundle: scaffold/features/news/ (feature.yaml, and the comments in editor/pagetypes/news.yaml and news-item.yaml).

Acceptance criteria

  • A site with the news feature shows a "News" rail entry after the built-ins, and a site without it does not. A module file written by hand also appears.

  • A module file is reported in the error banner, naming the file, and adds no rail entry, when it has any of:

    • an unknown key, page type, field, layout, role or tab;
    • a field of the wrong type for where it is used;
    • a reserved id (page, view, media, edit, api, internal, in any case);
    • a page type another module already lists;
    • a listed type that declares status;
    • a listed type whose children names one of the module's types.

    Other modules still load.

  • Reloading a News module URL (a list, a filtered list, an entry) stays in the News module. An element edit page whose return names a module URL goes back there.

  • In the News module, the tree shows every page, but only pages whose type explicitly lists news-item in children can be selected; the others are greyed out and still expandable.

  • The News module's tree has no ⋯ or right-click menus, no drag and drop, and no inline rename or create.

  • Switching to News with a non-news page selected shows "Select a News page in the tree", and nothing is selected automatically.

  • Selecting a News page lists its news items, newest first, in the declared views; with two views a switcher appears. A second News page lists only its own items.

  • Search, sort, filters (category, status) and paging work over the whole set, and survive reload and Back.

  • Every datetime the entries endpoint serves is RFC3339, including a front-matter date written as a native TOML datetime.

  • "New News item" creates the item under the selected News page and opens it in the module.

  • Opening an item shows its Properties and Content tabs inside the module, with its News page selected in the tree; reload keeps the tab.

  • Save & close and Close return to the list with its search, filters, sort, view and page.

  • Element edit pages opened from Content return to the item's Content tab.

  • Rename…, Enable/Disable, Delete…, View, Publish, Undo and Discard… work from the entry editor; Delete… lands on the list.

  • With the news module file present, news items are hidden from the tree in every module, while a child of a News page that is not a news item stays visible. With editor/modules/news.yaml deleted, news items show in the tree.

  • The List module and its affordances are gone, and the page module offers "Manage in News" on a News page.

  • A news item reached through the preview offers "Open in News". Its back-button fallback and post-delete landing go to its News page in the News module.

  • harness add news on a site that installed news before the module file existed:

    • adds editor/modules/news.yaml;
    • records it, and bundle version 2, in the receipt;
    • reports added listing just that file, in text and --json.
  • Re-running harness add news leaves an existing file at a bundle path alone and does not record it. On an up-to-date site it is a no-op reporting already-installed.

  • harness remove news removes the module file.

  • AGENTS.md and the README document module files, their schema and load errors, the tree behavior, the narrowed hideChildPages, the create dialog's new home and the gap-filling add.

  • No docs, hints or bundle comments still refer to the List module.

How to verify

  • make test. It builds web/dist first, which web/embed.go needs; a bare go test ./... fails in a fresh worktree. The Go side covers:

    • the module loader and its load errors;
    • both endpoints (search, sort, filters, paging, RFC3339);
    • gap filling (addcmd and the CLI output).

    go test ./internal/... was green at triage.

  • cd web && npm test. It covers:

    • the registry and shell with server modules (rail, route repair, return);
    • the tree's selectable, selection-only and hiding behavior;
    • the module screen and the entry editor.

    The suite was green at triage: 60 files, 1693 tests.

  • In a browser against harness serve, on a site with harness add news, a git remote (so publishing works) and two News pages:

    • select each in the tree, then create, find, edit, disable, publish and delete items without leaving the module;
    • reload on a filtered list and on an entry.

    The #149 analysis used Playwright with the system Chromium.

  • Gap filling, on a site with news installed:

    • delete editor/modules/news.yaml and its receipt entry, re-run harness add news, and check that the file and the receipt entry come back;
    • repeat with the receipt entry kept;
    • repeat with a hand-written file at that path: it is left alone and not recorded.

Assumptions to check

  • That PagePane's element list can render inside another module. It reads the selected page from the URL-backed selection, which under the module's route names the entry. But its docheader and some follows (properties, crumbs) assume the page module.

Out of scope

  • An aggregate view of entries across all News pages: each News page lists its own items.
  • Module sources other than pages. Hugo data files (data/*.yaml) are the obvious next source, since they are the closest thing Hugo has to records. The schema leaves room, but v1 manages pages only.
  • Listing elements (#150's idea).
  • deckle-hosted, which keeps working unchanged. Until something re-runs harness add news there, existing hosted news sites keep their items in the tree and show "version 1 installed; version 2 current".
  • Updating bundle files that changed since the add: gap filling never overwrites.
  • Filtering on the publish state.
  • A shared category system (#152).
  • Scheduled publishing (#78).
  • #151's fixes: the settings read's datetime bug, shared news cards, layout pickers.

Blocked by

None - can start immediately

> *This was generated by AI during triage.* ## Summary Let a feature bundle, or a site owner, add an editor module declared in YAML (`editor/modules/<name>.yaml`). The YAML says which page types the module manages, how their pages are shown (a list or cards), what each entry shows, and which controls (search, sort, filters, paging) surround them. The page tree stays beside the module. Selecting a page that may hold the entries lists them, and that page is where new ones are created. The news bundle uses this to ship a **News module**, in which news items are listed, created and edited without leaving the module. The generic List module is removed. Re-running `harness add` on an installed feature adds the bundle files the site lacks, so existing news sites get the module file. ## Current behavior - Modules are a fixed registry in the web app (`MODULES` in `web/src/modules.tsx`): Page, View, List and Media, plus the hidden element edit page. Neither a site nor a feature can add one. The rail, route repair and `return` validation all check module ids synchronously against that registry. - News items are managed in the generic List module (#79): a table of a section's child pages built from the `/api/tree` payload, with columns Title, Type, Date and Status and the row actions Rename…, Enable/Disable and Delete…. A title opens the page module with `return` to the list (#149). The create dialog (title, required fields, fields marked `create: true`) is defined inside `ListModule.tsx`. - `hideChildPages` on a page type hides all of that type's child pages from the tree; they are "curated through the List module instead". The page module points such a section to the List ("Open list", an inline hint). #149's fallbacks for a hidden page (its back button, the landing after deleting it) lead to the parent's List. - `harness add <feature>` on an installed feature (its receipt exists) changes nothing and reports `already-installed`. Files added to a bundle later therefore never reach sites that installed it earlier (README, "Not covered yet"). - The schema error banner lists element and backend-layout sidecar load errors. ## Desired behavior ### Module files - A new sidecar family: `editor/modules/<name>.yaml`. - It follows the shared `internal/sidecardir` scan rules: one module per file, the file name is the module id, strict decoding. - Files are read fresh on each request, like the other families. - Load errors appear in the schema error banner as their own family, and one bad file never hides the others. - A feature bundle ships its module files under `editor/modules/`; `harness add` vendors them and `harness remove` removes them, like any bundle file. A site owner, or an agent following AGENTS.md, can also write one by hand. - Every valid module file adds a rail entry (its label and icon) after the built-in modules, in id order. - These ids are reserved, compared case-insensitively; using one is a load error: - the built-in modules `page`, `view`, `media` and `edit`; - `api`: the server answers every `/api/*` path itself, so the editor can never show a module there; - `internal`: a host in front of the editor may block `/internal/*` (deckle-hosted's proxy answers 404 there on editor hosts). ### Schema ```yaml version: 1 # required, exactly 1 label: News # optional; defaults to the module id icon: lucide:newspaper # optional; the icon syntax other sidecars use pageTypes: [news-item] # required, non-empty: the page types this module lists and creates views: # required, at least one; the first is the default; two or more give a view switcher - layout: list columns: [title, date, categories, author, status] - layout: cards card: image: image title: title # defaults to the built-in title meta: [date, author] text: teaser tags: categories sort: date desc # optional, "<field> [asc|desc]" (asc when omitted); default: the tree's order of the container's children search: [title, teaser] # optional; the fields the search box looks in; default [title] filters: [categories, status] # optional; default none pageSize: 20 # optional; default 20, at most 100 edit: [properties, content] # optional; the entry editor's tabs, in order; default [properties, content] ``` - There is no key for where entries live. The tree selection and the page types' `children` lists decide that (see below). - Field names refer to the fields the listed page types declare, plus three built-ins: - `title`; - `date` (front-matter `date`); - `status`: the disabled marker (front-matter `draft`) and the publish state (the git badge). A declared field named `title` or `date` is the same front-matter key as the built-in; its label is used wherever a label shows. - Where a field may be used: - list `columns` and card `meta`: any field or built-in; - card `image`: an image field; - card `title`: `title` or a text field; - card `text`: a text or textarea field; - card `tags`: a select or multiselect field; - `search`: `title`, and text and textarea fields; - `sort`: `title`, `date`, and text, number, datetime and select fields; - `filters`: `status`, and select, multiselect and bool fields. - Filter values: - a select or multiselect filter offers the field's options; - a bool filter offers yes/no; - `status` offers Enabled/Disabled. The publish state is shown but cannot be filtered on in v1. - Load errors, each naming the file and the reason: - an unknown key, layout, card role, tab or sort direction; - no views, a list view without columns, or a `pageSize` outside 1–100; - a `pageTypes` entry that is not a loaded page type; - a field name that is neither a built-in nor declared by any listed page type; - a field used where its type is not allowed (every listed type's declaration of that name must fit); - a listed page type that declares its own `status` field (it would shadow the built-in); - a page type another module already lists (the module first in id order keeps it); - a listed page type whose own `children` explicitly names one of the module's page types. Such an entry could hold entries of its own module, and one URL would then mean both the list and the entry; - a reserved id. ### The page tree in a feature module - The page tree stays as the module's left column, in a selection-only mode: - no ⋯ or right-click menus; - no drag and drop; - no inline rename, inline create or "Home (missing)" create. Page management stays in the Page module. - Which pages are selectable: - A page is a **container**, and selectable, when its effective type explicitly lists one of the module's page types in `children` (news: pages of type `news`, whose `children` is `[news-item]`). - Every other page stays visible but greyed out, and can still be expanded, so a nested container is reachable. - A type without a `children` key, which allows any child, does not count; otherwise every such page would become a container. - The module's URL follows the other page-tree modules: `/<module>/<lang>/<page path>`. The path names one of two things: - a container: the module screen; - an **entry**, meaning a page whose effective type is one of the module's page types and whose parent is a container: the entry editor. Route repair judges the path against the tree as usual. - When the URL names neither (for example, Home still selected after switching modules), the module asks for a container, naming the selectable type: "Select a News page in the tree". Nothing is selected automatically, consistent with the editor's rule that it never auto-selects a page (#33). - While an entry is open, its container is the tree's selected row (styling and `aria-current`). The row is revealed and scrolled into view as in #149, whether or not the container hides its children. - Switching modules keeps the page path, as today. An entry open in the Page module opens in the entry editor when you switch to News; a container opens in the Page module when you switch back. ### Hiding managed pages from the tree - `hideChildPages` keeps its place in the page-type schema, with a narrower meaning: a container whose type sets it hides its **managed** children from the page tree, in every module. A managed child is a child page whose effective type is one of the page types of a module the page is a container for. Its other children stay in the tree, and its row expands when any remain. - When no loaded module manages a type's children, `hideChildPages` hides nothing. So a hidden page always has a module that lists it. A site without the module file (news installed before this change, `harness add news` not re-run) still reaches its items in the tree. - Hidden pages stay in the `/api/tree` payload, because route repair and the link pickers read them. A hidden page, or anything beneath one, selects its nearest drawn ancestor in the tree (#149). ### The module screen - With a container selected, the module lists that page's child pages of the module's page types, in the declared views. Several views give a switcher. - Search, sort, filters and paging apply to the whole set, on the server: - search is a case-insensitive substring match over the `search` fields; - sort is by column headers in a list and a sort menu for cards, over sortable fields only. - Search terms, filters, sort, the active view and the page number live in the URL query, so reload, Back and a shared link keep them. - Until the user picks a sort, entries follow `sort`, else the tree's order of the container's children. For news that is `date desc`: newest first, undated last. - `status` renders as the List does today: the `disabled` marker and the published / modified / unpublished badge. - A "New <type label>" button appears for each of the module's page types that the container's type allows as a child (singletons excluded). - It opens the existing create dialog and creates the entry under the container. - The new entry then opens in the module on its first tab, with `return` to the list. - An empty container shows an empty state with its New buttons. - An entry's title (in a list) or its card opens the entry inside the module, with `return` set to the list's full URL. - A server endpoint serves the entries: - in: module, container path, lang, search, sort, filters and page; - out: the page of entries (identity, type, title, draft, and the values of the fields the views use) and the total count. It reads the front matter of the container's matching children. Every datetime value goes out as RFC3339 (see #133), including a native TOML datetime. - Another endpoint lists the loaded modules and their load errors. ### The entry editor, inside the module - One tab per item in `edit`: - **Properties**: the page settings form; - **Content**: the page module's element list for that page (insert, reorder, delete, open element edit pages). The active tab lives in the URL query, so reload keeps it and element edit pages opened from Content return to Content. - The toolbar has: - back to the list, labelled with the container's title; - Save and Save & close; - View, Publish, Undo and Discard…, as in the page module; - the shared page actions from #149 (Rename…, Enable/Disable, Delete…). - Save and Save & close act on the Properties form from either tab. Switching tabs keeps unsaved edits. Leaving the entry with unsaved edits goes through the unsaved-changes guard. - Close and Save & close go to `return`: the list with its search, filters, sort, view and page intact. Without a `return` (a module switch, a link, a typed URL) they go to the entry's container. - Element edit pages opened from Content return to the entry, using #149's `return`. - Renaming the open entry follows it to its new path and keeps its `return`. Deleting it lands on its `return`, else on its container. ### Modules from the server - The rail, route repair and `return` validation know the feature modules as well as the built-ins. Nothing judges a module id before the module list has loaded. - Reloading a News module URL stays in the News module. - An element edit page whose `return` names a module URL goes back there. ### News - The news bundle ships `editor/modules/news.yaml`, along the lines of the example above, and its manifest `version` becomes 2. - In the page module, a container page offers "Manage in <module label>" (a docheader button and the inline hint) where the List affordances were: "Manage in News" on a News page. - A managed page reached another way (preview navigation, a link): - keeps #149's behavior: its container is selected in the tree; - offers "Open in <module label>" ("Open in News"); - its back-button fallback, and the landing after deleting it, go to its container in that module. - Bundle comments and `harness add news` next steps that mention the List module point to the News module instead. ### List module removed - The List module, its rail entry and its affordances ("Open list", the page-module hint) are removed. The create dialog survives as a shared component. - Old `/list/…` URLs and `return` values naming `list` are treated like any unknown module: route repair lands on the page module, and a stale `return` is ignored. ### `harness add` fills gaps - Re-running `harness add <feature>` on an installed feature writes every bundle file missing on disk, including one the receipt lists but the owner deleted. - It never touches a path that exists. - It runs the same safety checks as a fresh add: no writing through symlinked directories, and nothing is written if a check fails. - It records only the files it wrote, each with the hash of the bytes written. An existing path it left alone is never recorded, or `harness remove` would delete the owner's own file. - The receipt's `version` becomes the bundle's current version. `hugo.toml` edits and page-type allowances are not re-applied. - It reports status `added`, exit 0, in text and `--json` output: - `files` lists only the files it wrote; - `config`, `allowances` and `nextSteps` are empty; - the `--json` contract stays `formatVersion: 1`. - A feature with nothing missing is still a no-op reporting `already-installed`. This is how existing news sites get `editor/modules/news.yaml`. ### Docs `scaffold/site/AGENTS.md` and the README document: - the new family: layout, schema, load errors and reserved ids; - the tree in feature modules; - the narrowed `hideChildPages`; - where the create dialog now lives (`create: true` fields; a new entry opens in the module); - the gap-filling `add`, including the README's `--json` table (the `added` row) and "Not covered yet". ## Decisions - **Modules are declared in YAML.** The maintainer's requirement ("it needs to work to specify it in yaml"): the YAML picks the presentation (list or cards), what each entry shows, and which page types are shown. - **Modules manage pages, not elements.** The List module (#79) and its TYPO3-style redesign (#150, closed as superseded) don't fit Hugo. - TYPO3 lists records from uniform tables. In Hugo a page is a file, and an element is a shortcode at a position inside a page body, with no identity, metadata or creation date of its own. - What does map onto Hugo is a page holding child pages of one type, such as news items under a News page. It is also how TYPO3's news works: you pick the storage folder in the page tree, and the module shows and creates records there. - **The page tree stays visible, and the selection decides where entries are listed and created.** The maintainer's choice. It is TYPO3's storage-folder model, and it fits Hugo, where news items really are children of the News page. It also removes any need for a "where do entries live" key or a section picker. - **Only pages whose type explicitly allows an entry type are selectable; the rest are greyed out.** The maintainer's choice. - **The tree is selection-only in feature modules.** The maintainer's choice: page management stays in the Page module, and greyed-out rows would conflict with page menus. - **No automatic selection.** A message asks for a page instead, as the maintainer chose, consistent with #33. - **No runtime JS plugins.** Feature bundles and the editor both ship inside the harness binary. The editor provides the building blocks (layouts, controls, the entry editor) and YAML composes them. - **News items are edited inside the News module.** The maintainer's choice; you never switch into the Page module to edit one. - This also fixes, at the root, the navigation problems this started from: a news title opening a page the tree does not show, and Save & close not returning to the list. - #149 fixed the navigation around it. - **The List module is removed.** The maintainer's choice; the News module covers its only real use. - **`hideChildPages` hides only pages a module manages** (maintainer, in triage). - Existing sidecars stay valid and keep their meaning, owners still choose whether managed pages leave the tree, and a hidden page always has a module that lists it. - Keeping the old rule while removing the List would hide the items of every site that installed news before this change, with nothing listing them until `harness add news` is re-run. deckle-hosted never re-runs it. - Retiring the key was rejected. Strict decoding means every existing `news.yaml` must still load, so the key would linger as a dead key. - **Schema names and rules** (maintainer, in triage): - `pageTypes`, not `entries`: it says what the list holds, and leaves room for a later `data:` source (Hugo data files) beside it. - One module per page type, and no entry types that hold entries of their own module. "The module that manages a page" and the list-or-entry reading of a URL are then never ambiguous. - The `status` filter is the disabled marker only: filtering on the publish state would need git state on every query. - The five card roles (image, title, meta, text, tags) are enough for v1. - **`api` and `internal` are reserved module ids** besides the built-ins: `/api/*` never reaches the editor, and a host may block `/internal/*` (deckle-hosted does). - **The entry editor also has Undo and Discard…** (maintainer, in triage), so an entry is no lesser page than in the page module. - **The tab, view and sort live in the URL**, because the URL is the editor's source of truth (`web/src/routing.ts`). - **`harness add` fills gaps for installed features.** The maintainer's choice, so that existing news sites get the module file. - **A gap-filling add reports `added`** (maintainer, in triage). deckle-hosted parses `--json` strictly and rejects unknown statuses; reusing `added` keeps it working unchanged. No deckle-hosted change is part of this work. - **One issue, one pull request** (maintainer, in triage), gap filling included. - **Docs change with the code.** A new sidecar family, a scaffold layout change and a changed `harness add` mean `scaffold/site/AGENTS.md` and the README change in the same pull request (CLAUDE.md rule). ## Key interfaces - **Registry and shell:** - `MODULES` / `moduleById` / `ModuleDef` (`web/src/modules.tsx`); - `ModuleRail`, which reads `MODULES` directly; - the shell (`web/src/App.tsx`): `selectModule`, `returnFacts`, the route-repair wiring; - `repairRoute`'s unknown-module rule (`web/src/routerepair.ts`); - `parseReturn` / `readReturn` (`web/src/routing.ts`) and `ReturnContext` (`web/src/returnnav.ts`). Built-ins stay in the registry and feature modules come from the server; all of these must see both. - **The page-tree column:** `PAGE_TREE_NAV` (`web/src/modules.tsx`), `TreePane` / `TreeRow` (`web/src/components/TreePane.tsx`), and `createTreeController` (`web/src/stores/treeController.ts`: reveal, `selectedRow`). It needs a per-module selectable rule and a selection-only mode. `TreeBrowser`'s `isSelectable` (greyed label, live chevron) is the precedent. - **The hidden-page rule** moves from "under a `hideChildPages` section" to "managed and hidden". It lives in: - `hidingSection` / `parentPage` (`web/src/pagemanage.ts`); - `deleteLanding` (`web/src/stores/docheaderActions.ts`); - `PagePane`'s `backTarget` and `isListSection` / `openList`; - `TreeRow`'s `hidesChildren`; - the tree node flag (`HideChildPages` in `internal/pagetree`). - **Loader:** `internal/sidecardir` is the scan contract. A new loader package goes beside `internal/element`, `internal/pagetype` and `internal/backendlayout`; it validates against the loaded page types. - **Server** (`internal/server`): the module-list and entries endpoints. - Pages are read via `internal/pagetree`, front matter the way the settings read does (`ReadSettings` / `fieldValue` in `internal/pageops/settings.go`). - `fieldValue` turns a native TOML datetime into Go's `time.Time` string (#133), while `pagetree` already formats `date` as RFC3339. #151 fixes the settings read, so share one conversion. - `SchemaErrorBanner` (`web/src/components/SchemaErrorBanner.tsx`): a new family. - **Entry editor tabs:** `PageSettings` (`web/src/components/PageSettings.tsx`; `expose`, `footerSave`) for Properties, and `PagePane` (`web/src/components/PagePane.tsx`) for Content. `PagePane`'s docheader is built in, so the element list has to render without it. - **The create dialog:** `CreateDialog`, `hasHiddenFields` and `seedFieldValues` in `web/src/components/ListModule.tsx`. - **Page actions and `return`:** `pageActionItems` (`web/src/pageactions.ts`), `createDocheaderActions`, `RenameDialog`, `DeleteDialog`, `withReturn` (`web/src/routing.ts`) and `useReturn` (`web/src/returnnav.ts`). - **Gap filling:** `internal/addcmd` (`Run`, `Result`, `Receipt`, `scanDestinations`, `featureHints`) and the add output in `cmd/harness/features.go` (`addCmd`, `addResultOutput`, `printAddResult`). The JSON contract is the README's "Machine-readable output". - **The news bundle:** `scaffold/features/news/` (`feature.yaml`, and the comments in `editor/pagetypes/news.yaml` and `news-item.yaml`). ## Acceptance criteria - [ ] A site with the news feature shows a "News" rail entry after the built-ins, and a site without it does not. A module file written by hand also appears. - [ ] A module file is reported in the error banner, naming the file, and adds no rail entry, when it has any of: - an unknown key, page type, field, layout, role or tab; - a field of the wrong type for where it is used; - a reserved id (`page`, `view`, `media`, `edit`, `api`, `internal`, in any case); - a page type another module already lists; - a listed type that declares `status`; - a listed type whose `children` names one of the module's types. Other modules still load. - [ ] Reloading a News module URL (a list, a filtered list, an entry) stays in the News module. An element edit page whose `return` names a module URL goes back there. - [ ] In the News module, the tree shows every page, but only pages whose type explicitly lists `news-item` in `children` can be selected; the others are greyed out and still expandable. - [ ] The News module's tree has no ⋯ or right-click menus, no drag and drop, and no inline rename or create. - [ ] Switching to News with a non-news page selected shows "Select a News page in the tree", and nothing is selected automatically. - [ ] Selecting a News page lists its news items, newest first, in the declared views; with two views a switcher appears. A second News page lists only its own items. - [ ] Search, sort, filters (category, status) and paging work over the whole set, and survive reload and Back. - [ ] Every datetime the entries endpoint serves is RFC3339, including a front-matter `date` written as a native TOML datetime. - [ ] "New News item" creates the item under the selected News page and opens it in the module. - [ ] Opening an item shows its Properties and Content tabs inside the module, with its News page selected in the tree; reload keeps the tab. - [ ] Save & close and Close return to the list with its search, filters, sort, view and page. - [ ] Element edit pages opened from Content return to the item's Content tab. - [ ] Rename…, Enable/Disable, Delete…, View, Publish, Undo and Discard… work from the entry editor; Delete… lands on the list. - [ ] With the news module file present, news items are hidden from the tree in every module, while a child of a News page that is not a news item stays visible. With `editor/modules/news.yaml` deleted, news items show in the tree. - [ ] The List module and its affordances are gone, and the page module offers "Manage in News" on a News page. - [ ] A news item reached through the preview offers "Open in News". Its back-button fallback and post-delete landing go to its News page in the News module. - [ ] `harness add news` on a site that installed news before the module file existed: - adds `editor/modules/news.yaml`; - records it, and bundle version 2, in the receipt; - reports `added` listing just that file, in text and `--json`. - [ ] Re-running `harness add news` leaves an existing file at a bundle path alone and does not record it. On an up-to-date site it is a no-op reporting `already-installed`. - [ ] `harness remove news` removes the module file. - [ ] AGENTS.md and the README document module files, their schema and load errors, the tree behavior, the narrowed `hideChildPages`, the create dialog's new home and the gap-filling `add`. - [ ] No docs, hints or bundle comments still refer to the List module. ## How to verify - `make test`. It builds `web/dist` first, which `web/embed.go` needs; a bare `go test ./...` fails in a fresh worktree. The Go side covers: - the module loader and its load errors; - both endpoints (search, sort, filters, paging, RFC3339); - gap filling (`addcmd` and the CLI output). `go test ./internal/...` was green at triage. - `cd web && npm test`. It covers: - the registry and shell with server modules (rail, route repair, `return`); - the tree's selectable, selection-only and hiding behavior; - the module screen and the entry editor. The suite was green at triage: 60 files, 1693 tests. - In a browser against `harness serve`, on a site with `harness add news`, a git remote (so publishing works) and two News pages: - select each in the tree, then create, find, edit, disable, publish and delete items without leaving the module; - reload on a filtered list and on an entry. The #149 analysis used Playwright with the system Chromium. - Gap filling, on a site with news installed: - delete `editor/modules/news.yaml` and its receipt entry, re-run `harness add news`, and check that the file and the receipt entry come back; - repeat with the receipt entry kept; - repeat with a hand-written file at that path: it is left alone and not recorded. ## Assumptions to check - That `PagePane`'s element list can render inside another module. It reads the selected page from the URL-backed selection, which under the module's route names the entry. But its docheader and some follows (properties, crumbs) assume the page module. ## Out of scope - An aggregate view of entries across all News pages: each News page lists its own items. - Module sources other than pages. Hugo data files (`data/*.yaml`) are the obvious next source, since they are the closest thing Hugo has to records. The schema leaves room, but v1 manages pages only. - Listing elements (#150's idea). - deckle-hosted, which keeps working unchanged. Until something re-runs `harness add news` there, existing hosted news sites keep their items in the tree and show "version 1 installed; version 2 current". - Updating bundle files that changed since the add: gap filling never overwrites. - Filtering on the publish state. - A shared category system (#152). - Scheduled publishing (#78). - #151's fixes: the settings read's datetime bug, shared news cards, layout pickers. ## Blocked by None - can start immediately
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#153
No description provided.