feat(links): universal link picker (page/element/media/external) with new-tab toggle #69

Merged
dominik.polakovics merged 1 commit from afk/65 into main 2026-07-17 11:08:49 +02:00

Closes #65

A TYPO3-style universal link picker used everywhere a link is authored, with element anchors, per-link "open in new tab", reference-aware renames, and delete warnings.

What changed

Picker (web) — new LinkPicker dialog shared by the link field widget and the RTE:

  • Internal tab: two panes — read-only page tree (navigation only, per-language) on the left; the selected page's "link to this page" row, direct child pages, and top-level block elements (schema label + summary) on the right. Single click selects and closes.
  • File tab: embeds the media picker body (library + page-bundle files); stores /media/<path> (library) or <page-url><file> (bundle) URLs.
  • External tab: free URL input (mailto:/tel: welcome).
  • "Open in new tab" toggle applies to whichever target is chosen; editing an existing link reopens pre-filled (right tab, target, toggle).
  • The link field widget is now an image-style chip (server-resolved label via GET /api/link/resolve, new-tab badge, edit/clear). The ad-hoc RTE link popover is gone; the RTE link action opens the picker, with a Remove-link action for existing links.

Storage — vanilla markdown that builds with stock Hugo:

  • Page links /pretty/url/, element links /pretty/url/#c-<uid>.
  • Markdown bodies: plain links stay [text](url); new-tab links serialize as exactly <a href="URL" target="_blank" rel="noopener">text</a>, round-tripped losslessly by the RTE markdown bridge. The body-save gate (the security boundary for unsafe = true themes) admits precisely that byte shape and keeps rejecting all other raw HTML.
  • Link params: every type: link param <name> owns an implicit reserved companion <name>Target (same mechanism as uid/disabled) — undeclarable in sidecars, accepted by validation, written as _blank only when toggled, cleared by omission, lossless through unrelated edits (elements, items, chips, and page settings).

Anchorspartials/ce/attrs.html emits id="c-<uid>" on every block element root in all builds (HTML-escaped against hand-edited uids); data-ce-* markers stay dev-preview-only. button/img/figure templates render target="_blank" rel="noopener" when the companion is set.

Reference safety:

  • New internal/linkrefs engine scans/rewrites all three link shapes — markdown destinations, inline <a href>, shortcode params/front matter (never aliases) — by longest-prefix URL pairs, preserving #c-<uid> fragments and sub-paths.
  • Page rename/move rewrites internal links site-wide in the same atomic operation (composed with alias/ladder front-matter edits, one write per file, full rollback); Hugo aliases remain the safety net. Best-effort by contract: a broken page never blocks a rename.
  • Deleting a page or element that other content links to warns with the referencing pages (GET /api/page/refs, GET /api/element/refs) — warn-and-proceed, unlike blocking library-media deletes.
  • mediaops now also matches the /media/… URL form of library refs, so file links participate in media rename rewrites and delete blocking.
  • The publish draft-link warning also scans <a href="…"> destinations.

New API: GET /api/page/elements (top-level block elements with uid/label/summary), GET /api/link/resolve (stored href → kind/label/found), GET /api/page/refs, GET /api/element/refs.

Docs: scaffold/site/AGENTS.md documents the companion param, canonical link forms, the byte-exact raw-HTML carve-out, anchors, and rename behavior (drift test extended to require the <name>Target token).

Out of scope (per issue)

Container-internal element targets, dedicated email/phone tabs, click-in-preview selection, render-hook auto-target, token-based storage, cross-page element-move rewrites.

Verification

  • Full Go suite (incl. real-Hugo theme build tests: production output carries id="c-<uid>" anchors and target="_blank" rel="noopener"; stock-Hugo compatibility) and full web suite + tsc green.
  • Byte-exact round-trip tests for the companion param (element, items, settings, chips), the markdown <a> form, rename rewrites (fragments, sub-paths, section moves, rollback), and the frozen wire shapes of the four new endpoints.
  • Live smoke: built binary serving the demo site answers the new endpoints.

🤖 Generated with Claude Code

Closes #65 A TYPO3-style universal link picker used everywhere a link is authored, with element anchors, per-link "open in new tab", reference-aware renames, and delete warnings. ## What changed **Picker (web)** — new `LinkPicker` dialog shared by the `link` field widget and the RTE: - **Internal** tab: two panes — read-only page tree (navigation only, per-language) on the left; the selected page's "link to this page" row, direct child pages, and top-level block elements (schema label + summary) on the right. Single click selects and closes. - **File** tab: embeds the media picker body (library + page-bundle files); stores `/media/<path>` (library) or `<page-url><file>` (bundle) URLs. - **External** tab: free URL input (`mailto:`/`tel:` welcome). - "Open in new tab" toggle applies to whichever target is chosen; editing an existing link reopens pre-filled (right tab, target, toggle). - The `link` field widget is now an image-style chip (server-resolved label via `GET /api/link/resolve`, new-tab badge, edit/clear). The ad-hoc RTE link popover is gone; the RTE link action opens the picker, with a Remove-link action for existing links. **Storage** — vanilla markdown that builds with stock Hugo: - Page links `/pretty/url/`, element links `/pretty/url/#c-<uid>`. - Markdown bodies: plain links stay `[text](url)`; new-tab links serialize as exactly `<a href="URL" target="_blank" rel="noopener">text</a>`, round-tripped losslessly by the RTE markdown bridge. The body-save gate (the security boundary for `unsafe = true` themes) admits precisely that byte shape and keeps rejecting all other raw HTML. - Link params: every `type: link` param `<name>` owns an implicit reserved companion `<name>Target` (same mechanism as `uid`/`disabled`) — undeclarable in sidecars, accepted by validation, written as `_blank` only when toggled, cleared by omission, lossless through unrelated edits (elements, items, chips, and page settings). **Anchors** — `partials/ce/attrs.html` emits `id="c-<uid>"` on every block element root in all builds (HTML-escaped against hand-edited uids); `data-ce-*` markers stay dev-preview-only. `button`/`img`/`figure` templates render `target="_blank" rel="noopener"` when the companion is set. **Reference safety**: - New `internal/linkrefs` engine scans/rewrites all three link shapes — markdown destinations, inline `<a href>`, shortcode params/front matter (never `aliases`) — by longest-prefix URL pairs, preserving `#c-<uid>` fragments and sub-paths. - Page rename/move rewrites internal links site-wide in the same atomic operation (composed with alias/ladder front-matter edits, one write per file, full rollback); Hugo aliases remain the safety net. Best-effort by contract: a broken page never blocks a rename. - Deleting a page or element that other content links to warns with the referencing pages (`GET /api/page/refs`, `GET /api/element/refs`) — warn-and-proceed, unlike blocking library-media deletes. - `mediaops` now also matches the `/media/…` URL form of library refs, so file links participate in media rename rewrites and delete blocking. - The publish draft-link warning also scans `<a href="…">` destinations. **New API**: `GET /api/page/elements` (top-level block elements with uid/label/summary), `GET /api/link/resolve` (stored href → kind/label/found), `GET /api/page/refs`, `GET /api/element/refs`. **Docs**: `scaffold/site/AGENTS.md` documents the companion param, canonical link forms, the byte-exact raw-HTML carve-out, anchors, and rename behavior (drift test extended to require the `<name>Target` token). ## Out of scope (per issue) Container-internal element targets, dedicated email/phone tabs, click-in-preview selection, render-hook auto-`target`, token-based storage, cross-page element-move rewrites. ## Verification - Full Go suite (incl. real-Hugo theme build tests: production output carries `id="c-<uid>"` anchors and `target="_blank" rel="noopener"`; stock-Hugo compatibility) and full web suite + `tsc` green. - Byte-exact round-trip tests for the companion param (element, items, settings, chips), the markdown `<a>` form, rename rewrites (fragments, sub-paths, section moves, rollback), and the frozen wire shapes of the four new endpoints. - Live smoke: built binary serving the demo site answers the new endpoints. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(links): universal link picker with element anchors, new-tab links, and reference-aware renames
All checks were successful
ci / build-and-test (pull_request) Successful in 5m3s
dbce9fe106
One shared TYPO3-style picker (internal page / element-on-page / media file /
external, with an open-in-new-tab toggle) now serves both link-typed params
and the rich-text editor, replacing the ad-hoc RTE popover.

- link params gain an implicit reserved <name>Target companion (uid-style):
  accepted undeclared, written as _blank only when toggled, lossless through
  element, items, chip, and page-settings round-trips
- markdown bodies keep plain links as [text](url); new-tab links serialize
  as exactly <a href="URL" target="_blank" rel="noopener">text</a>, admitted
  byte-exactly by the body-save gate and round-tripped by the RTE bridge
- the shared ce/attrs partial emits id="c-<uid>" on block elements in every
  build so element links work on the published site; button/img/figure
  render target="_blank" rel="noopener" from the companion
- new internal/linkrefs engine rewrites markdown destinations, inline
  <a href>, and param/front-matter links on page rename/move (fragments and
  sub-paths preserved, aliases still recorded, atomic with rollback), and
  lists referencing pages for warn-and-proceed page/element deletes
- mediaops also matches the /media/… URL form of library refs; the publish
  draft-link scan covers <a href> destinations
- new endpoints: /api/page/elements, /api/link/resolve, /api/page/refs,
  /api/element/refs; scaffold/site/AGENTS.md documents the new vocabulary

Closes #65

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

This was generated by AI while landing a PR.

Verdict: PASS

Check Result
State open, not draft
CI (ci / build-and-test) success (5m3s) — relied on as the vouching signal, not re-run
Title convention Conventional Commits (feat(links): …)
AFK contract head afk/65, body carries working Closes #65
Mergeable vs main clean merge, no conflicts
AGENTS.md drift scaffold/site/AGENTS.md updated + drift test extended (<name>Target token); enforced by green CI

Focused human review — read the linked issue #65 for intent and reviewed the highest-risk surface, the raw-HTML body-save gate (internal/server/body.go), since it is the load-bearing defense for unsafe=true themes:

  • The new-tab carve-out admits exactly one byte-exact tag shape (canonicalAnchorOpenRE) plus a bare </a>, paired in order within a block.
  • The href value is [^"\n]*, so a value cannot contain a quote or newline — an attacker cannot break out of the attribute to inject extra attributes or tags. HTML blocks are rejected unconditionally; the carve-out is inline-only.
  • Opener/closer pairing rejects nested anchors, unpaired tags, and dangling openers at block boundaries; every other raw HTML remains msgRawHTML. rte fields without link still refuse the anchor.

No blockers found. Awaiting explicit free-text merge confirmation.

> *This was generated by AI while landing a PR.* ## Landing audit — PR #69 (`feat(links): universal link picker`) **Verdict: PASS** | Check | Result | | --- | --- | | State | open, not draft | | CI (`ci / build-and-test`) | ✅ success (5m3s) — **relied on as the vouching signal, not re-run** | | Title convention | ✅ Conventional Commits (`feat(links): …`) | | AFK contract | ✅ head `afk/65`, body carries working `Closes #65` | | Mergeable vs `main` | ✅ clean merge, no conflicts | | AGENTS.md drift | ✅ `scaffold/site/AGENTS.md` updated + drift test extended (`<name>Target` token); enforced by green CI | **Focused human review** — read the linked issue #65 for intent and reviewed the highest-risk surface, the raw-HTML body-save gate (`internal/server/body.go`), since it is the load-bearing defense for `unsafe=true` themes: - The new-tab carve-out admits exactly one byte-exact tag shape (`canonicalAnchorOpenRE`) plus a bare `</a>`, paired in order within a block. - The href value is `[^"\n]*`, so a value cannot contain a quote or newline — an attacker cannot break out of the attribute to inject extra attributes or tags. HTML blocks are rejected unconditionally; the carve-out is inline-only. - Opener/closer pairing rejects nested anchors, unpaired tags, and dangling openers at block boundaries; every other raw HTML remains `msgRawHTML`. `rte` fields without `link` still refuse the anchor. No blockers found. Awaiting explicit 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!69
No description provided.