feat: page tree browsing — content tree API + SPA tree/element panes #21

Merged
dominik.polakovics merged 3 commits from afk/4 into main 2026-07-12 15:22:41 +02:00

Closes #4

What

The TYPO3-style left pane, read-only: the harness now serves the site's page tree per language, and the SPA renders it with expand/collapse, selection, and an element list in the center pane.

Backend

  • internal/content: FrontMatter.Decode() (Hugo's own metadecoders, all four formats) and ParseFrontMatter() — front-matter extraction that survives body parse failures, so a page whose body the round-trip engine rejects still appears in the tree. Decode-only; #9 builds the write half on the same mapping.
  • internal/pagetree (new): walks a language's content root into sections (_index.md), leaf bundles (index.md), and plain .md pages; title/weight/draft from front matter with slug fallback plus a problem field instead of dropped nodes; Hugo-style ordering (weighted ascending before unweighted, then title case-insensitively, then path). Page identity is (path, lang) throughout; the language→content-root mapping lives in exactly one function (ContentRoot), so content/en/-style layouts later change only that.
  • internal/server: GET /api/tree?lang= and GET /api/page?path=&lang= (replaces /api/debug/tree; README updated). Traversal outside the content root → 400; directory paths, unknown lang, missing files → 404; body parse failure → 422. Strictly read-only.
  • Sample site: weighted guides/ + posts/ sections, three leaf bundles (10/20/unweighted to exercise ordering), shortcodes (note, badge) with layout templates; existing plain pages and the draft page kept as-is.

Frontend

  • TreePane: real tree (server order), chevron expand/collapse, single selection, drafts dimmed + draft-badged, ⚠ tooltip for metadata problems; path-less sections (no _index.md) expandable but not selectable.
  • State survival: expansion/selection keyed by stable node identity outside the fetched data, and the /api/tree poll writes no signal when the payload is byte-identical — same discipline that keeps the preview iframe's LiveReload alive. A refetch that removes the selected page clears selection gracefully.
  • PagePane (replaces the FormPane placeholder): elements of the selected page in document order, labels/icons via lookupElementSchema() (stub until #3) with shortcode-name + per-kind-glyph fallback; whitespace-only text runs hidden without renumbering document indices.
  • #3 seam: createSelection() exposes selectedElement: {page: {path, lang}, elementPath: number[]}; the placeholder detail pane in PagePane marks exactly where the schema-generated form mounts.

Verification

  • go vet ./..., go test ./... (content, pagetree, server, supervisor) — green
  • npx tsc --noEmit, npm test (19 tests incl. same-DOM-node-across-identical-refetch and deleted-selected-page pruning), npm run build — green
  • Live smoke on the built binary: tree ordering (Guides=10 < Posts=20 < unweighted Home; 10/20/unweighted within Guides), draft flag, (path, lang) in /api/page, traversal→400, directory→404, unknown lang→404, old debug route→404, SPA served
  • hugo builds the extended sample site

🤖 Generated with Claude Code

https://claude.ai/code/session_01HHa5His6Hs6xbbAW4ETLY2

Closes #4 ## What The TYPO3-style left pane, read-only: the harness now serves the site's page tree per language, and the SPA renders it with expand/collapse, selection, and an element list in the center pane. ### Backend - `internal/content`: `FrontMatter.Decode()` (Hugo's own metadecoders, all four formats) and `ParseFrontMatter()` — front-matter extraction that survives body parse failures, so a page whose body the round-trip engine rejects still appears in the tree. Decode-only; #9 builds the write half on the same mapping. - `internal/pagetree` (new): walks a language's content root into sections (`_index.md`), leaf bundles (`index.md`), and plain `.md` pages; title/weight/draft from front matter with slug fallback plus a `problem` field instead of dropped nodes; Hugo-style ordering (weighted ascending before unweighted, then title case-insensitively, then path). Page identity is **(path, lang)** throughout; the language→content-root mapping lives in exactly one function (`ContentRoot`), so `content/en/`-style layouts later change only that. - `internal/server`: `GET /api/tree?lang=` and `GET /api/page?path=&lang=` (replaces `/api/debug/tree`; README updated). Traversal outside the content root → 400; directory paths, unknown lang, missing files → 404; body parse failure → 422. Strictly read-only. - Sample site: weighted `guides/` + `posts/` sections, three leaf bundles (10/20/unweighted to exercise ordering), shortcodes (`note`, `badge`) with layout templates; existing plain pages and the draft page kept as-is. ### Frontend - `TreePane`: real tree (server order), chevron expand/collapse, single selection, drafts dimmed + `draft`-badged, ⚠ tooltip for metadata problems; path-less sections (no `_index.md`) expandable but not selectable. - State survival: expansion/selection keyed by stable node identity outside the fetched data, and the `/api/tree` poll writes no signal when the payload is byte-identical — same discipline that keeps the preview iframe's LiveReload alive. A refetch that removes the selected page clears selection gracefully. - `PagePane` (replaces the `FormPane` placeholder): elements of the selected page in document order, labels/icons via `lookupElementSchema()` (stub until #3) with shortcode-name + per-kind-glyph fallback; whitespace-only text runs hidden without renumbering document indices. - #3 seam: `createSelection()` exposes `selectedElement: {page: {path, lang}, elementPath: number[]}`; the placeholder detail pane in `PagePane` marks exactly where the schema-generated form mounts. ## Verification - `go vet ./...`, `go test ./...` (content, pagetree, server, supervisor) — green - `npx tsc --noEmit`, `npm test` (19 tests incl. same-DOM-node-across-identical-refetch and deleted-selected-page pruning), `npm run build` — green - Live smoke on the built binary: tree ordering (Guides=10 < Posts=20 < unweighted Home; 10/20/unweighted within Guides), draft flag, (path, lang) in `/api/page`, traversal→400, directory→404, unknown lang→404, old debug route→404, SPA served - `hugo` builds the extended sample site 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01HHa5His6Hs6xbbAW4ETLY2
Add front-matter metadata decoding to the content engine
(FrontMatter.Decode via Hugo's own metadecoders, plus ParseFrontMatter
that survives body parse failures) and a new internal/pagetree package
that walks a language's content root into a nested tree of sections
(_index.md), leaf bundles (index.md), and plain pages, each carrying
title/weight/draft with Hugo-style sibling ordering (weighted ascending
before unweighted, then title). Metadata problems never drop a node:
the slug stands in as title and the reason is surfaced.

The server gains GET /api/tree?lang= and GET /api/page?path=&lang=
(replacing /api/debug/tree). Page identity is (path, lang) on the wire;
the language content root resolves in exactly one place
(pagetree.ContentRoot), paths outside the content root are rejected,
and everything is strictly read-only.

Extend the sample site with weighted sections, leaf bundles, and
shortcodes so tree ordering and element listing are demonstrable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHa5His6Hs6xbbAW4ETLY2
feat(web): page tree pane, element list, and element-selection seam
All checks were successful
ci / build-and-test (pull_request) Successful in 1m2s
5f7c0dc01b
Replace the placeholder tree with the real page tree from /api/tree:
expand/collapse sections, single selection, dimmed+badged drafts, and a
tooltip for metadata problems. Expansion and selection are keyed by
stable node identity outside the fetched data, and the poll only writes
signals when the payload actually changed, so tree state survives
LiveReload and refetches; a deleted selected page clears selection
gracefully.

The center pane lists the selected page's elements in document order
with schema-derived label/icon lookup (stubbed until issue #3's sidecar
schemas land) falling back to shortcode name and per-kind glyphs.
Selecting an element flows through a narrow selection store
({page: {path, lang}, elementPath}) into a placeholder detail pane —
the exact seam #3's schema-generated form replaces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHa5His6Hs6xbbAW4ETLY2
Merge main into afk/4: integrate #20's element editor with #21's page tree
All checks were successful
ci / build-and-test (pull_request) Successful in 1m2s
0ef75651a1
#21 branched before #20 merged; both restructured the SPA and the /api/page
surface. Resolved as the integration #21 was designed for:

- Server: unified endpoint set — /api/tree + /api/page (keyed by (path,lang),
  #21) alongside /api/elements + /api/page/save (#20), refactored onto
  pagetree content-root resolution; save now carries lang; /api/debug/tree
  dropped. Response identity is {path,lang} throughout.
- Client: TreePane (page tree) + PagePane + PreviewPane. PagePane lists the
  page's shortcodes recursively (depth-indented) and mounts #20's
  schema-generated FormPane in its detail seam; ElementsPane superseded.
  api.ts unifies both type systems (ShortcodeNode = BlockNode | InlineNode).
- Tests reconciled against the unified contract; ElementsPane/placeholder
  tests replaced by the tree->element->form flow.

Verified: go vet/test ./..., gofmt, 28 web tests, tsc, vite build, hugo
build of the sample site, and a live end-to-end save through the built
harness (GET tree/page/elements, POST save with lang, canonical rewrite).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvYx5Znqp3RDsgbudYgvKJ
Author
Owner

This was generated by AI while landing a PR.

Landing audit — #21 (page tree) × #20 (element editor) integration

#21 branched before #20 merged; the two collided across the API surface, the client data layer, and the SPA shell. Resolved via a merge of main into afk/4 (two-parent merge commit 0ef7565), keeping both features:

  • Server — unified endpoints: /api/tree + /api/page (keyed by (path, lang)) alongside /api/elements + /api/page/save, all refactored onto pagetree content-root resolution; save now carries lang; /api/debug/tree dropped. Page identity is {path, lang} throughout.
  • ClientTreePane + PagePane + PreviewPane. PagePane lists a page’s shortcodes recursively (depth-indented) and mounts #20’s schema-generated FormPane in its detail seam; ElementsPane superseded. api.ts unifies both type systems (ShortcodeNode = BlockNode | InlineNode).
  • Tests — reconciled to the unified contract; ElementsPane/placeholder tests replaced by the tree→element→form flow.

Verification (resolution re-verified — no prior gate had seen it): go vet/go test ./..., gofmt, 28 web tests, tsc, vite build, hugo build of the sample site, plus a live end-to-end save through the built harness (GET tree/page/elements, POST save with lang, canonical file rewrite). CI green on the merge commit (run #15). main is now an ancestor of afk/4 — the merge into main is conflict-free. Body carries Closes #4.

Verdict: PASS — awaiting free-text merge confirmation.

> *This was generated by AI while landing a PR.* **Landing audit — #21 (page tree) × #20 (element editor) integration** #21 branched before #20 merged; the two collided across the API surface, the client data layer, and the SPA shell. Resolved via a merge of `main` into `afk/4` (two-parent merge commit `0ef7565`), keeping both features: - **Server** — unified endpoints: `/api/tree` + `/api/page` (keyed by `(path, lang)`) alongside `/api/elements` + `/api/page/save`, all refactored onto `pagetree` content-root resolution; `save` now carries `lang`; `/api/debug/tree` dropped. Page identity is `{path, lang}` throughout. - **Client** — `TreePane` + `PagePane` + `PreviewPane`. `PagePane` lists a page’s shortcodes recursively (depth-indented) and mounts #20’s schema-generated `FormPane` in its detail seam; `ElementsPane` superseded. `api.ts` unifies both type systems (`ShortcodeNode = BlockNode | InlineNode`). - **Tests** — reconciled to the unified contract; `ElementsPane`/placeholder tests replaced by the tree→element→form flow. **Verification (resolution re-verified — no prior gate had seen it):** `go vet`/`go test ./...`, `gofmt`, 28 web tests, `tsc`, `vite build`, `hugo` build of the sample site, plus a live end-to-end save through the built harness (`GET` tree/page/elements, `POST` save with `lang`, canonical file rewrite). CI green on the merge commit (run #15). `main` is now an ancestor of `afk/4` — the merge into `main` is conflict-free. Body carries `Closes #4`. **Verdict: PASS** — awaiting free-text merge confirmation.
Sign in to join this conversation.
No reviewers
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!21
No description provided.