feat: page management — create, rename, move, delete, reorder, enable/disable with page types #28

Closed
dominik.polakovics wants to merge 1 commit from afk/9 into main

Turns #4's read-only page tree into full page management with guardrails, per the issue's agent brief.

What's here

Page types (internal/pagetype, editor/pagetypes/*.yaml): developer-defined sidecars declaring label/icon, kind: page|section, root (creatable at the content root), allowed children types, an elements allow-list (validated against element schemas, served for #6 to enforce), and page-level front-matter fields in the exact element-param grammar. Same loading discipline as internal/element: strict decode, per-file actionable errors, invalid files never block valid ones. Served fresh on GET /api/pagetypes.

Front-matter write engine (internal/content): the write half meta.go promised — EncodeFrontMatter (canonical: deterministic bytes, keys sorted at every level, per-format guards self-checked against the serializer), UpdateFrontMatter, and ReplaceFrontMatter, which splices an encoded block in front of verbatim body bytes so weight/draft/alias edits work even on pages whose bodies Parse rejects. Org front matter stays decode-only with a loud error.

Operations (internal/pageops + seven POST /api/page/* endpoints):

  • create — offers only types the parent allows (root: true at the root, the section type's children elsewhere); writes a leaf bundle with the type's defaults, recorded type, and draft = true; slugs auto-generate from the title, collision-safe, overridable
  • rename / moveos.Rename of the whole bundle; the old published URL is appended to aliases (every descendant's URL on section renames/moves), existing aliases preserved, never duplicated; multi-file rewrites are prepared and validated before the irreversible rename
  • reorder / move placement — weights renumbered 10, 20, 30… in tree order (sibling order comes from pagetree.Build, so it cannot drift from what the client sees)
  • enable — flips draft; delete — file / bundle / recursive section
  • frontmatter — declared fields set or removed, unmanaged keys and body bytes always preserved
  • All paths confined to the content root; existing targets never overwritten; every write atomic; precise 400/404/409/422 mapping (field errors in the element-save shape)

SPA (web/): per-row action cluster (new child, enable/disable, rename, delete), create dialog with type picker + live slug preview, delete confirmation naming the page and its descendant count, HTML5 drag & drop for same-section reorder and cross-section moves (legality mirrors the server; the server stays authoritative), and a page-settings form generated from the page type's fields — widget code shared with the element form via the new FieldInput. Tree refreshes immediately after each mutation; expansion state survives; selection follows renamed/moved pages to their new identity.

Sample site: section + page types exercising the child-type constraints (page not allowed at root, allowed under sections); all content carries its type.

Verification

  • go vet ./... clean; go test ./... green (9 packages) — all mutation tests run against temp copies of the site, never the checked-in one
  • web: 58 vitest tests green, tsc --noEmit clean, production build OK
  • End-to-end smoke test against the built binary on a temp site copy: create (incl. root-forbidden 400 and slug-collision 409), rename with alias, section rename fanning aliases to every descendant, move with alias + weight renumbering at the drop index, reorder (incl. stale-order 409), enable, frontmatter save (unmanaged date key preserved, 422 field mapping), recursive section delete, type on the tree, meta on /api/page — all against a live hugo server that kept building cleanly throughout

Out of scope (per the brief)

Element CRUD enforcement of the elements allow-list (#6), git safety commits/publish (#10, #11), locks/CSRF (#13), media (#12), multi-language UI.

Closes #9

🤖 Generated with Claude Code

https://claude.ai/code/session_01DUR1jRbZoNqohK2qDQSZG6

Turns #4's read-only page tree into full page management with guardrails, per the issue's agent brief. ## What's here **Page types** (`internal/pagetype`, `editor/pagetypes/*.yaml`): developer-defined sidecars declaring label/icon, `kind: page|section`, `root` (creatable at the content root), allowed `children` types, an `elements` allow-list (validated against element schemas, served for #6 to enforce), and page-level front-matter `fields` in the exact element-param grammar. Same loading discipline as `internal/element`: strict decode, per-file actionable errors, invalid files never block valid ones. Served fresh on `GET /api/pagetypes`. **Front-matter write engine** (`internal/content`): the write half `meta.go` promised — `EncodeFrontMatter` (canonical: deterministic bytes, keys sorted at every level, per-format guards self-checked against the serializer), `UpdateFrontMatter`, and `ReplaceFrontMatter`, which splices an encoded block in front of **verbatim body bytes** so weight/draft/alias edits work even on pages whose bodies `Parse` rejects. Org front matter stays decode-only with a loud error. **Operations** (`internal/pageops` + seven `POST /api/page/*` endpoints): - **create** — offers only types the parent allows (`root: true` at the root, the section type's `children` elsewhere); writes a leaf bundle with the type's defaults, recorded `type`, and `draft = true`; slugs auto-generate from the title, collision-safe, overridable - **rename / move** — `os.Rename` of the whole bundle; the old published URL is appended to `aliases` (every descendant's URL on section renames/moves), existing aliases preserved, never duplicated; multi-file rewrites are prepared and validated **before** the irreversible rename - **reorder / move placement** — weights renumbered 10, 20, 30… in tree order (sibling order comes from `pagetree.Build`, so it cannot drift from what the client sees) - **enable** — flips `draft`; **delete** — file / bundle / recursive section - **frontmatter** — declared fields set or removed, unmanaged keys and body bytes always preserved - All paths confined to the content root; existing targets never overwritten; every write atomic; precise 400/404/409/422 mapping (field errors in the element-save shape) **SPA** (`web/`): per-row action cluster (new child, enable/disable, rename, delete), create dialog with type picker + live slug preview, delete confirmation naming the page and its descendant count, HTML5 drag & drop for same-section reorder and cross-section moves (legality mirrors the server; the server stays authoritative), and a page-settings form generated from the page type's fields — widget code shared with the element form via the new `FieldInput`. Tree refreshes immediately after each mutation; expansion state survives; selection follows renamed/moved pages to their new identity. **Sample site**: `section` + `page` types exercising the child-type constraints (page not allowed at root, allowed under sections); all content carries its `type`. ## Verification - `go vet ./...` clean; `go test ./...` green (9 packages) — all mutation tests run against temp copies of the site, never the checked-in one - `web`: 58 vitest tests green, `tsc --noEmit` clean, production build OK - End-to-end smoke test against the built binary on a temp site copy: create (incl. root-forbidden 400 and slug-collision 409), rename with alias, section rename fanning aliases to every descendant, move with alias + weight renumbering at the drop index, reorder (incl. stale-order 409), enable, frontmatter save (unmanaged `date` key preserved, 422 field mapping), recursive section delete, `type` on the tree, `meta` on `/api/page` — all against a live `hugo server` that kept building cleanly throughout ## Out of scope (per the brief) Element CRUD enforcement of the `elements` allow-list (#6), git safety commits/publish (#10, #11), locks/CSRF (#13), media (#12), multi-language UI. Closes #9 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DUR1jRbZoNqohK2qDQSZG6
feat: page management — create, rename, move, delete, reorder, enable/disable with page types
All checks were successful
ci / build-and-test (pull_request) Successful in 1m18s
96e2e0bc20
Turn the read-only page tree into full tree editing with guardrails:

- internal/pagetype: developer-defined page-type sidecars
  (editor/pagetypes/*.yaml) with label/icon/kind/root/children/elements
  and front-matter fields sharing the element param grammar; strict
  per-file validation, invalid files never block valid ones.
- internal/content: the front-matter write half — EncodeFrontMatter
  (canonical, deterministic, per-format ordered encoding),
  UpdateFrontMatter, and ReplaceFrontMatter (splice a new block in front
  of verbatim body bytes, so mutations work on pages whose bodies
  Parse rejects). Org stays decode-only, loudly.
- internal/pageops: create (leaf bundles, draft: true, collision-safe
  slugs from titles), rename/move (os.Rename with old published URLs
  appended to aliases — every descendant's URL on section moves),
  reorder (weights renumbered 10, 20, 30…), enable (draft toggle),
  save-fields (declared fields set/removed, unmanaged keys preserved),
  delete. Multi-file operations prepare and validate every rewrite
  before the irreversible rename; all writes atomic and confined to the
  content root.
- internal/server: GET /api/pagetypes, "meta" on /api/page(/save),
  "type" on /api/tree nodes, and the seven POST /api/page/* mutation
  endpoints with precise 400/404/409/422 mapping.
- web: tree action cluster (new/rename/delete/enable), create dialog
  offering only the types the parent allows (root flag at the root),
  live slug preview, delete confirmation naming the descendant count,
  HTML5 drag & drop for reorder and cross-section moves, and a page
  settings form generated from the page type's fields — field widgets
  shared with the element form via FieldInput.
- testdata/site: two page types (section, page) exercising the child
  constraints; content carries its type in front matter.

Closes #9

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DUR1jRbZoNqohK2qDQSZG6
All checks were successful
ci / build-and-test (pull_request) Successful in 1m18s

Pull request closed

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!28
No description provided.