Universal link picker (TYPO3-style): page, element, media file, external targets with per-link new-tab toggle #65

Closed
opened 2026-07-17 02:43:48 +02:00 by dominik.polakovics · 1 comment

This was generated by AI during triage.

Motivation

Authoring links in deckle is currently bare-bones: link-typed element params render as a plain text input, and the RTE has an ad-hoc popover with a URL field and a flat page list. There is no way to link to a specific content element, no media-file linking, and no way to open a link in a new tab. This issue introduces a TYPO3-style universal link picker used everywhere a link is authored.

Agreed design (from grilling session)

Storage. Internal links are stored as resolved pretty URLs in vanilla markdown (/blog/post/, element targets as /services/#c-<uid>). No tokens, no render-time resolution — content files keep building with stock Hugo. On page rename/move, a media-ops-style scan rewrites all stored links to the new canonical URL; Hugo aliases remain as the redirect safety net.

Link kinds. Four tabs in the picker: internal page, element-on-page, media file (reusing the existing media picker), external URL. No dedicated email/phone tabs — mailto:/tel: can be typed into the external tab.

target="_blank". A per-link "open in new tab" toggle:

  • Markdown bodies: plain links stay plain markdown; only new-tab links serialize as inline <a href="…" target="_blank" rel="noopener"> HTML (the theme already renders with unsafe=true). The RTE markdown bridge must round-trip inline <a> HTML back into a link mark with a target attribute.
  • Link params: every type: link param gets an implicit reserved companion param <name>Target (same mechanism as the reserved uid param) — written only when set, accepted by schema validation automatically, rendered by the scaffold shortcode templates.

Element anchors. The shared content-element attrs partial emits id="c-<uid>" on every block-level element in all builds (not just dev preview), so any element is always linkable.

Picker UX (internal tab). Two-pane dialog: page tree on the left, pure navigation only. The larger right pane shows the selected page's direct child pages and direct child block elements (labeled with element type + schema-declared summary), plus a "link to this page" header row. Single click on any right-pane item immediately takes it and closes the dialog — no confirm step. Top-level elements only; container internals are out of scope for v1 (anchors exist, so it can be added later without a format change).

Delete protection. The reference scan extends to page URLs and element anchors: deleting a linked page or element shows the media-style warning listing every referencing page, but the author may proceed.

RTE unification. The ad-hoc link popover is removed; the same picker dialog serves inline links, pre-filled when editing an existing link.

Widget. The link field widget mirrors the image field: a friendly chip (page title / element summary / filename / URL, plus a new-tab indicator) with edit and clear actions, instead of a raw text input. The picker's tree shows the language currently being edited.

> *This was generated by AI during triage.* ## Motivation Authoring links in deckle is currently bare-bones: `link`-typed element params render as a plain text input, and the RTE has an ad-hoc popover with a URL field and a flat page list. There is no way to link to a specific content element, no media-file linking, and no way to open a link in a new tab. This issue introduces a TYPO3-style universal link picker used everywhere a link is authored. ## Agreed design (from grilling session) **Storage.** Internal links are stored as resolved pretty URLs in vanilla markdown (`/blog/post/`, element targets as `/services/#c-<uid>`). No tokens, no render-time resolution — content files keep building with stock Hugo. On page rename/move, a media-ops-style scan rewrites all stored links to the new canonical URL; Hugo aliases remain as the redirect safety net. **Link kinds.** Four tabs in the picker: internal page, element-on-page, media file (reusing the existing media picker), external URL. No dedicated email/phone tabs — `mailto:`/`tel:` can be typed into the external tab. **target="_blank".** A per-link "open in new tab" toggle: - *Markdown bodies:* plain links stay plain markdown; only new-tab links serialize as inline `<a href="…" target="_blank" rel="noopener">` HTML (the theme already renders with `unsafe=true`). The RTE markdown bridge must round-trip inline `<a>` HTML back into a link mark with a target attribute. - *Link params:* every `type: link` param gets an implicit reserved companion param `<name>Target` (same mechanism as the reserved `uid` param) — written only when set, accepted by schema validation automatically, rendered by the scaffold shortcode templates. **Element anchors.** The shared content-element attrs partial emits `id="c-<uid>"` on every block-level element in all builds (not just dev preview), so any element is always linkable. **Picker UX (internal tab).** Two-pane dialog: page tree on the left, pure navigation only. The larger right pane shows the selected page's direct child pages and direct child block elements (labeled with element type + schema-declared summary), plus a "link to this page" header row. Single click on any right-pane item immediately takes it and closes the dialog — no confirm step. Top-level elements only; container internals are out of scope for v1 (anchors exist, so it can be added later without a format change). **Delete protection.** The reference scan extends to page URLs and element anchors: deleting a linked page or element shows the media-style warning listing every referencing page, but the author may proceed. **RTE unification.** The ad-hoc link popover is removed; the same picker dialog serves inline links, pre-filled when editing an existing link. **Widget.** The `link` field widget mirrors the image field: a friendly chip (page title / element summary / filename / URL, plus a new-tab indicator) with edit and clear actions, instead of a raw text input. The picker's tree shows the language currently being edited.
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: enhancement
Summary: Replace deckle's bare link inputs with a universal TYPO3-style link picker (page / element-on-page / media file / external) with a per-link "open in new tab" toggle, plus the supporting anchor emission, schema companion param, reference warnings, and rename-rewrite.

Current behavior:

  • link is already a first-class field type in the element/page-type schema vocabulary, but its editor widget is the same plain text input as text.
  • The RTE (TipTap-based rich text bodies) has an ad-hoc link popover with a URL input and a flat page list; no element targets, no target attribute, no file linking.
  • Internal links are stored as site-absolute pretty URLs. Page renames record a Hugo alias but stored links are never rewritten.
  • Production HTML emits no per-element id; the data-ce-* markers exist only under the dev preview server.
  • Media has reference scanning (rename/move rewrites refs across pages; deletes of referenced library files are blocked) — links have only a publish-time draft-link warning.

Desired behavior:

Picker dialog (one shared component used by both the link field widget and the RTE):

  • Tabs: Internal, File, External.
  • Internal tab: two-pane layout. Left: the page tree for the language currently being edited — clicking only navigates, never selects. Right (larger): the selected page's direct child pages and direct child block elements (each labeled with element type + its schema-declared summary value), plus a "link to this page" header row. A single click on any right-pane row immediately chooses that target and closes the dialog — no confirm button. Only top-level elements are listed (no container internals).
  • File tab: reuses the existing media picker (library + page-bundle files); choosing a file stores its public URL/path so media reference scanning covers it.
  • External tab: free URL input (any scheme, so mailto:/tel: work here too).
  • An "open in new tab" toggle applies to whichever target is chosen.
  • Editing an existing link reopens the dialog pre-filled (correct tab, target, and toggle state).

Storage format:

  • Page link: /pretty/url/. Element link: /pretty/url/#c-<uid> where <uid> is the element's reserved uid param value. Content files remain vanilla markdown that builds with stock Hugo.
  • Markdown bodies: links without the toggle serialize as plain markdown [text](url); links with the toggle serialize as inline HTML <a href="…" target="_blank" rel="noopener">text</a>. The RTE markdown bridge must round-trip that inline <a> form back into a link mark carrying a target attribute (lossless: plain links must never gain the HTML form).
  • Link params: a type: link param named e.g. href gets an implicit reserved companion hrefTarget, written only when the toggle is on. Schema validation must accept the companion without sidecar declaration (same mechanism as the reserved uid param), and it must round-trip losslessly through the content engine. Scaffold shortcode templates that render link params (e.g. the button element) render target + rel="noopener" when the companion is present.

Anchor emission:

  • The shared theme partial that decorates block-level content elements emits id="c-<uid>" on every block element in all builds (production included), not only under the dev preview.

Reference safety:

  • Page rename/move: the existing scan/rewrite machinery (as used by media relocate) also rewrites internal link targets — in markdown link syntax, in inline <a href> HTML, and in link-typed params — to the new canonical URL, preserving any #c-<uid> fragment. Aliases stay as the fallback for anything missed.
  • Deleting a page or a content element that other content links to shows a confirmation listing every referencing page (media-delete style warning), but the author may proceed.

Widget:

  • The link field widget becomes a chip-style control mirroring the image field: shows a friendly label (page title, element summary, filename, or the external URL) plus a new-tab indicator, with edit (reopens picker) and clear actions.

Key interfaces:

  • The FieldType vocabulary already contains link — no new type constant; the change is widget + implicit companion param.
  • The reserved-param mechanism used for the element uid — extend the same concept so <linkParamName>Target is accepted/preserved for every type: link param.
  • The schema Summary declaration — the source of element row labels in the picker's right pane.
  • The page tree API and the page document/element listing API — the internal tab's data sources (an endpoint listing a page's top-level elements with uid, type, and summary may need to be added or derived from the existing page document response).
  • The media scan/rewrite engine pattern — model for the link rewrite on page rename/move and for the read-side reference listing on delete.
  • The TipTap link mark — needs a target attribute and serializer/parser support in the markdown bridge.

Acceptance criteria:

  • The link field widget opens the picker; chosen page/element/file/external targets are stored as described and re-open pre-filled for editing.
  • The RTE link action opens the same picker; the old popover is gone; existing inline links are editable through it.
  • Internal tab: left tree navigates only; right pane lists direct child pages + top-level elements with type + summary labels and a "link to this page" row; a single click selects and closes the dialog.
  • An element link is stored as /page/url/#c-<uid> and the published site's HTML contains a matching id="c-<uid>" on that element, so the browser scrolls to it.
  • A markdown body link with the new-tab toggle round-trips: save → file contains the inline <a … target="_blank" rel="noopener"> form → reopen editor → link mark shows toggle on; toggling off returns it to plain markdown syntax.
  • A link param with the toggle produces <name>Target="_blank" in the shortcode call without any sidecar change, passes validation, survives an unrelated edit of the same element (lossless round-trip), and the scaffold button element renders target="_blank" rel="noopener".
  • Renaming or moving a page rewrites all three link shapes (markdown links, inline <a> hrefs, link params) across the site to the new URL, preserving #c-<uid> fragments; the alias is still written.
  • Deleting a page or element that is linked from other content shows a warning listing the referencing pages; confirming proceeds with the delete.
  • A site built with stock Hugo (harness removed) renders all stored link forms correctly, including element anchors.
  • Go and web test suites pass; new behavior is covered by tests (companion-param validation + round-trip, markdown <a> round-trip, rewrite-on-rename, reference listing).

Out of scope:

  • Linking to elements nested inside containers (anchors exist; picker support can come later without a format change).
  • Dedicated email/phone tabs (use the external tab).
  • Click-in-preview element selection.
  • Auto-target=_blank for external links via a render hook, and any layouts/_default/_markup/render-link.html.
  • Token-based link storage (page://…) or blocking deletes of referenced pages/elements.
  • Rewriting links when an element is moved across pages (publish-time draft-link warnings remain the only guard there).
> *This was generated by AI during triage.* ## Agent Brief **Category:** enhancement **Summary:** Replace deckle's bare link inputs with a universal TYPO3-style link picker (page / element-on-page / media file / external) with a per-link "open in new tab" toggle, plus the supporting anchor emission, schema companion param, reference warnings, and rename-rewrite. **Current behavior:** - `link` is already a first-class field type in the element/page-type schema vocabulary, but its editor widget is the same plain text input as `text`. - The RTE (TipTap-based rich text bodies) has an ad-hoc link popover with a URL input and a flat page list; no element targets, no target attribute, no file linking. - Internal links are stored as site-absolute pretty URLs. Page renames record a Hugo alias but stored links are never rewritten. - Production HTML emits no per-element `id`; the `data-ce-*` markers exist only under the dev preview server. - Media has reference scanning (rename/move rewrites refs across pages; deletes of referenced library files are blocked) — links have only a publish-time draft-link warning. **Desired behavior:** *Picker dialog* (one shared component used by both the `link` field widget and the RTE): - Tabs: **Internal**, **File**, **External**. - **Internal tab:** two-pane layout. Left: the page tree for the language currently being edited — clicking only navigates, never selects. Right (larger): the selected page's direct child pages and direct child block elements (each labeled with element type + its schema-declared summary value), plus a "link to this page" header row. A single click on any right-pane row immediately chooses that target and closes the dialog — no confirm button. Only top-level elements are listed (no container internals). - **File tab:** reuses the existing media picker (library + page-bundle files); choosing a file stores its public URL/path so media reference scanning covers it. - **External tab:** free URL input (any scheme, so `mailto:`/`tel:` work here too). - An **"open in new tab"** toggle applies to whichever target is chosen. - Editing an existing link reopens the dialog pre-filled (correct tab, target, and toggle state). *Storage format:* - Page link: `/pretty/url/`. Element link: `/pretty/url/#c-<uid>` where `<uid>` is the element's reserved uid param value. Content files remain vanilla markdown that builds with stock Hugo. - Markdown bodies: links without the toggle serialize as plain markdown `[text](url)`; links with the toggle serialize as inline HTML `<a href="…" target="_blank" rel="noopener">text</a>`. The RTE markdown bridge must round-trip that inline `<a>` form back into a link mark carrying a target attribute (lossless: plain links must never gain the HTML form). - Link params: a `type: link` param named e.g. `href` gets an implicit reserved companion `hrefTarget`, written only when the toggle is on. Schema validation must accept the companion without sidecar declaration (same mechanism as the reserved `uid` param), and it must round-trip losslessly through the content engine. Scaffold shortcode templates that render link params (e.g. the button element) render `target` + `rel="noopener"` when the companion is present. *Anchor emission:* - The shared theme partial that decorates block-level content elements emits `id="c-<uid>"` on every block element in **all** builds (production included), not only under the dev preview. *Reference safety:* - Page rename/move: the existing scan/rewrite machinery (as used by media relocate) also rewrites internal link targets — in markdown link syntax, in inline `<a href>` HTML, and in `link`-typed params — to the new canonical URL, preserving any `#c-<uid>` fragment. Aliases stay as the fallback for anything missed. - Deleting a page or a content element that other content links to shows a confirmation listing every referencing page (media-delete style warning), but the author may proceed. *Widget:* - The `link` field widget becomes a chip-style control mirroring the image field: shows a friendly label (page title, element summary, filename, or the external URL) plus a new-tab indicator, with edit (reopens picker) and clear actions. **Key interfaces:** - The `FieldType` vocabulary already contains `link` — no new type constant; the change is widget + implicit companion param. - The reserved-param mechanism used for the element `uid` — extend the same concept so `<linkParamName>Target` is accepted/preserved for every `type: link` param. - The schema `Summary` declaration — the source of element row labels in the picker's right pane. - The page tree API and the page document/element listing API — the internal tab's data sources (an endpoint listing a page's top-level elements with uid, type, and summary may need to be added or derived from the existing page document response). - The media scan/rewrite engine pattern — model for the link rewrite on page rename/move and for the read-side reference listing on delete. - The TipTap link mark — needs a `target` attribute and serializer/parser support in the markdown bridge. **Acceptance criteria:** - [ ] The `link` field widget opens the picker; chosen page/element/file/external targets are stored as described and re-open pre-filled for editing. - [ ] The RTE link action opens the same picker; the old popover is gone; existing inline links are editable through it. - [ ] Internal tab: left tree navigates only; right pane lists direct child pages + top-level elements with type + summary labels and a "link to this page" row; a single click selects and closes the dialog. - [ ] An element link is stored as `/page/url/#c-<uid>` and the published site's HTML contains a matching `id="c-<uid>"` on that element, so the browser scrolls to it. - [ ] A markdown body link with the new-tab toggle round-trips: save → file contains the inline `<a … target="_blank" rel="noopener">` form → reopen editor → link mark shows toggle on; toggling off returns it to plain markdown syntax. - [ ] A link param with the toggle produces `<name>Target="_blank"` in the shortcode call without any sidecar change, passes validation, survives an unrelated edit of the same element (lossless round-trip), and the scaffold button element renders `target="_blank" rel="noopener"`. - [ ] Renaming or moving a page rewrites all three link shapes (markdown links, inline `<a>` hrefs, link params) across the site to the new URL, preserving `#c-<uid>` fragments; the alias is still written. - [ ] Deleting a page or element that is linked from other content shows a warning listing the referencing pages; confirming proceeds with the delete. - [ ] A site built with stock Hugo (harness removed) renders all stored link forms correctly, including element anchors. - [ ] Go and web test suites pass; new behavior is covered by tests (companion-param validation + round-trip, markdown `<a>` round-trip, rewrite-on-rename, reference listing). **Out of scope:** - Linking to elements nested inside containers (anchors exist; picker support can come later without a format change). - Dedicated email/phone tabs (use the external tab). - Click-in-preview element selection. - Auto-`target=_blank` for external links via a render hook, and any `layouts/_default/_markup/render-link.html`. - Token-based link storage (`page://…`) or blocking deletes of referenced pages/elements. - Rewriting links when an element is *moved across pages* (publish-time draft-link warnings remain the only guard there).
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#65
No description provided.