feat(links): universal link picker (page/element/media/external) with new-tab toggle #69
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle!69
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/65"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
LinkPickerdialog shared by thelinkfield widget and the RTE:/media/<path>(library) or<page-url><file>(bundle) URLs.mailto:/tel:welcome).linkfield widget is now an image-style chip (server-resolved label viaGET /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:
/pretty/url/, element links/pretty/url/#c-<uid>.[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 forunsafe = truethemes) admits precisely that byte shape and keeps rejecting all other raw HTML.type: linkparam<name>owns an implicit reserved companion<name>Target(same mechanism asuid/disabled) — undeclarable in sidecars, accepted by validation, written as_blankonly when toggled, cleared by omission, lossless through unrelated edits (elements, items, chips, and page settings).Anchors —
partials/ce/attrs.htmlemitsid="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/figuretemplates rendertarget="_blank" rel="noopener"when the companion is set.Reference safety:
internal/linkrefsengine scans/rewrites all three link shapes — markdown destinations, inline<a href>, shortcode params/front matter (neveraliases) — by longest-prefix URL pairs, preserving#c-<uid>fragments and sub-paths.GET /api/page/refs,GET /api/element/refs) — warn-and-proceed, unlike blocking library-media deletes.mediaopsnow also matches the/media/…URL form of library refs, so file links participate in media rename rewrites and delete blocking.<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.mddocuments the companion param, canonical link forms, the byte-exact raw-HTML carve-out, anchors, and rename behavior (drift test extended to require the<name>Targettoken).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
id="c-<uid>"anchors andtarget="_blank" rel="noopener"; stock-Hugo compatibility) and full web suite +tscgreen.<a>form, rename rewrites (fragments, sub-paths, section moves, rollback), and the frozen wire shapes of the four new endpoints.🤖 Generated with Claude Code
Landing audit — PR #69 (
feat(links): universal link picker)Verdict: PASS
ci / build-and-test)feat(links): …)afk/65, body carries workingCloses #65mainscaffold/site/AGENTS.mdupdated + drift test extended (<name>Targettoken); enforced by green CIFocused 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 forunsafe=truethemes:canonicalAnchorOpenRE) plus a bare</a>, paired in order within a block.[^"\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.msgRawHTML.rtefields withoutlinkstill refuse the anchor.No blockers found. Awaiting explicit free-text merge confirmation.