Skip to content

[Tracking] Inline link icons through a hyperlink mark view #512

Open

Description

@HMarzban

Summary

Every link in a docs.plus pad will show a small icon before its text:

  • docs.plus links get the docs.plus logo;
  • mailto: and tel: links get a glyph;
  • other http(s) links get the site favicon.

The favicon comes from our own REST route, never from Google or the linked site. A link with no icon looks like a plain link.

The mechanism ships in @docs.plus/extension-hyperlink 2.2.0 as a public option, so any Tiptap host can use it:

  • linkIcon, a sync resolver that returns an element or null;
  • a mark view that draws that element at the start of the <a>, outside contentDOM;
  • two helpers: linkIconMap, an offline domain table, and linkIconFetcher, which builds one <img> per link from an icon URL per origin.

The webapp wires the chain, and the REST server adds GET /api/metadata/icon. Four side issues clean up related code.

The maintainer rejected the Decoration.widget design for its per-keystroke cost and chose a mark view. Keystroke dispatch cost, median, from one local bench (laptop, jsdom view, no layout):

Link blocks No icon Widget design Mark view
2000 0.108 ms 1.150 ms 0.121 ms
5000 0.239 ms 6.433 ms 0.250 ms

Real-browser check (2026-10-18). A throwaway build of this design ran in Chromium 151 and WebKit 26.5 through Playwright, with layout on. It used prosemirror-view 1.42.3, the 150-origin icon stub and 500 / 2000 / 5000 link blocks. Firefox was not run. Every gate in #516 Step 9 passed:

At 2000 link blocks, icons on minus off Chromium WebKit
Keystroke dispatch, median (plain, in a link, at a block-start link) −0.1 ms 0 ms (1 ms clock)
Cold mount, sync / to the next frame +10.4 / +22.8 ms +22 / +23 ms
Mark views, resolver calls, images and requests per local or remote keystroke 0 0
Icon requests on a cold open at most 1 per URL (149 URLs) at most 1 per URL

Maintainer rulings

Quoted words are the maintainer's own. Where no quote was kept, the ruling is stated as recorded. These rulings are also posted as one Maintainer ruling, 2026-10-17 comment on this tracker.

2026-10-16

  1. Favicons, from our own route. "for webapp we have to work on own fetcher also and put it into the hocuspocus". Here "fetcher" means the server side: the icon route in hocuspocus. The client helper is linkIconFetcher.

2026-10-17, on the plan

  1. Parity exception. Every link in the pad shows its icon automatically, with no opt-in. This goes against Google Docs, Word and Notion (3 of 3) on purpose, like Revise the heading size scale to follow a documented typographic convention #369. Do not "fix" it back. Show a link icon before every pad link in the webapp #516 records it in AGENTS.md beside Revise the heading size scale to follow a documented typographic convention #369.
  2. Webapp map content and glyph freedom. "for the webapp just hold our docsplus brand icon that customize for the hyperlink ... let user to use diffrent glyphs not only from lucide ... the rest of logos come from the fetcher"
  3. Offline map has priority. A link that matches the map takes the map's icon, and the fetcher helper never runs for it. A host may use the map alone. (Recorded; no quote kept.)
  4. Light and dark icons, and the tile, are developer choices. A map entry may give one icon or a { light, dark } pair. The fetcher helper takes a tile option. (Recorded; no quote kept.)
  5. Webapp tile off. "no we do not need it"
  6. Sync, one function. "the A is the best". The resolver returns an element at once, and the element does its own fetching.
  7. Where the code lives. The mechanism goes into @docs.plus/extension-hyperlink. The webapp uses it and is where it is tested. The icon route lives in hocuspocus. (Recorded; no quote kept.)

2026-10-17, on the architecture review

  1. Mark view, not a decoration. "the alternative the node view, decoration for this cost sooo much."
  2. Byte cap is configurable, default 16 KiB. "let developer chose,but set 16 kib as default." The developer is the server operator.
  3. Privacy page. "yeas, mention this feature in privacy page."
  4. Print hides link icons. "agrre"
  5. Icon box 1em, gap 0.25em, align by measurement, one design-system row. "agrre"
  6. docs.plus logo: apps/webapp/public/icons/logo.svg as is. "true"
  7. Tests. "true. also we have to test the offline map and other side that related to the extension in the extension sides." The ruled set:
    • webapp: one Cypress spec for the Edit-form prefill;
    • extension: clean-room Cypress, plus unit tests for the mark view, linkIconMap, linkIconFetcher, pairs and the tile;
    • server: one unit test for the byte check.
    • The byte check has two parts: the cap in readCappedBody, and the sniff. Test files mirror source names, so Add an icon route to link-metadata with an operator-set byte cap #515 tests these two parts in two files.
  8. Legacy link marks: no production count, README caveat only. "true"
  9. File four side issues, adjusted to the new plan. "yep, base on the new need and plan."

Child issues, in order

Close this tracker when every child is closed and 2.2.0 is on npm.

Shared rules

Children point here instead of repeating these.

API contract

// @docs.plus/extension-hyperlink 2.2.0
interface LinkIconContext { href: string } // exported type; new fields are additive

interface HyperlinkOptions {
  /** Sync. A fresh element with no text, or null for a plain link. A throw counts as null and logs once. */
  linkIcon?: (ctx: LinkIconContext) => HTMLElement | null
}

function linkIconMap(entries: Record<string, string | { light: string; dark: string } | (() => HTMLElement)>):
  (ctx: LinkIconContext) => HTMLElement | null
function linkIconFetcher(options: { src: (origin: string) => string; tile?: boolean }):
  (ctx: LinkIconContext) => HTMLElement | null

Resolver input. The resolver sees only hrefs that pass isSafeHref (validateURL.ts#L18).

DOM shape. An iconed link is <a …><ICON class="hyperlink-icon" contenteditable="false" aria-hidden="true"><span>text</span></a>. ICON is whatever element the resolver returns: an <img>, a <span> pair, or a host element.

  • The resolver returns a bare element with no text. It must not be in the document yet.
  • The mark view adds the hyperlink-icon class, contenteditable="false" and aria-hidden="true". On an <img>, it also sets draggable=false.
  • The mark view wraps the link text in a new <span>, which is contentDOM.
  • A plain link keeps today's <a>text</a>.
  • getHTML(), Markdown, copy that ProseMirror handles, and drag never contain an icon. They use the schema serializer, not mark views.

When a link has no icon. There are two cases, and both look like a plain link:

  • The resolver returns null, or throws. The DOM is today's <a>text</a>.
  • The resolver returns an <img> that later fails to load. The img stays hidden (display: none), so it has no box and no gap. The DOM keeps the iconed shape until the view is rebuilt.
  • A pair has no hidden-until-load step. A bad pair URL shows a blank 1em box. This goes in the README Caveats.

Requests and logging.

  • "One <img> per link" is literal: each call returns a fresh element. "One request per origin" is the HTTP cache's job, because every link on one origin uses the same image URL.
  • A throwing or invalid resolver logs once per resolver function per page. The pad, history and /editor share one flag, because they share padLinkIcon.

Map matching (linkIconMap):

  • Keys are bare hostnames. A key with /, :, ?, #, @, *, % or a space throws TypeError. So a key holds no scheme, port or path, and scheme keys do not exist.
  • IDN keys become punycode, like hrefs.
  • www. and a trailing dot are ignored on both sides.
  • Subdomains match their parent key, and the longest key wins.
  • Only http: and https: hrefs match.
  • When two keys name one host, the later key wins; no throw.

Pairs. A pair is a <span> with an inline background-image: light-dark(url(light), url(dark)), set through CSSOM. light-dark() follows the app's color-scheme, which the theme sets per data-theme (globals.scss#L15 light, #L70 dark). So pairs follow the app theme with no listener and no theme attribute. Engines without the image form should keep the earlier url(light) value (UNVERIFIED in a browser).

Exports.

  • New barrel lines in src/index.ts: linkIconMap, linkIconFetcher and type LinkIconContext. Nothing else.
  • src/index.ts has export * from './hyperlink' (index.ts#L28). So hyperlink.ts must not export any link-icon symbol. It imports the mark view as a value and the types with import type.
  • The helpers live in src/link-icons/, not src/utils/. So the utils/index.ts named-export rule does not apply. The barrel lines are explicit instead.
  • This is a minor bump under lockstep: all five packages go to 2.2.0.

prosemirror-view floor.

Webapp surfaces and the chain

  • Editors with icons. Every editor built from editorConfig in TipTap.tsx (TipTap.tsx#L203-L212): the pad, /editor, and history (useHistoryEditor.ts#L16-L19), including compare mode.
  • No icons. The chat composer passes no linkIcon (useTiptapEditor.ts#L180-L188). Chat messages are sanitized HTML with no editor (HTMLBody.tsx#L29), so no mark view runs there.
  • Which links count as docs.plus links. The webapp map has two keys, both mapped to /icons/logo.svg (logo.svg#L1):
    • docs.plus. Through map matching, it also covers www.docs.plus and new.docs.plus.
    • new URL(HOME_SITE_URL).hostname. HOME_SITE_URL is NEXT_PUBLIC_APP_URL, or https://docs.plus when unset (homeMetadata.ts#L1, #L10). So a self-hosted origin gets the logo. In development this key is localhost, so every localhost link gets the logo, on any port.
  • Mail and phone glyphs. mailto: and tel: links cannot get a favicon. Ruling 2 says every pad link shows an icon, so the chain adds a glyph step. It uses Icons.mail and Icons.phone from the webapp icon registry, through lucideSvgString. Ruling 3 lets a host use any glyph; the webapp uses its own registry.
  • Links that stay plain. Other schemes, such as ftp:, and favicon misses.

Names

Use one name per thing in code, issues and docs:

  • link icon — the element before the link text. Its class is hyperlink-icon.
  • docs.plus logo — apps/webapp/public/icons/logo.svg. Never call it a mark, because a mark is the ProseMirror hyperlink mark.
  • favicon — a website's own icon image.
  • glyph — the Lucide mail and phone SVGs only.
  • resolver — the function passed as linkIcon.
  • chain — the webapp resolver padLinkIcon: docsplusMap(ctx) ?? appScheme(ctx) ?? fetchIcon(ctx). The first non-null result wins.
  • appScheme — the chain step that returns the mail or phone glyph for a mailto: or tel: href.
  • mark view — the hyperlink mark's view in linkIconMarkView.ts. Not "widget" and not "decoration".
  • hole — the space inside the <a> but outside contentDOM.
  • sweep — moving stray nodes from the hole into contentDOM.
  • pair — a { light, dark } map entry.
  • tile — the light square behind fetched icons on a dark scheme.
  • icon route — GET /api/metadata/icon?origin=<origin>. It takes an origin only, never a full href.
  • fetcher helper — linkIconFetcher, the client function. Never say "fetcher" alone; it is ambiguous with the icon route.
  • failed-URL set — the per-helper set of image URLs that errored once.

Links and numbers

  • Every code permalink is pinned to 69ca1ef1649540f7c8989dfd74ad901c741a30b1. Markdown files use ?plain=1#L….
  • Installed-package lines name the version and the node_modules/.bun/ path, because they have no permalink. They cite the versions at the pinned commit: @tiptap/core 3.31.3 and prosemirror-view 1.42.3. After Bump TipTap packages to 3.32.0 for the remote-cursor fix #473, re-check each cited line against 3.32.0 and 1.42.6, and re-cite it. Do not stop on the version change alone.
  • Cite a maintainer ruling as Ruling N (1 to 17). Only this tracker quotes the maintainer's words.
  • Every number carries one of these labels:
    • "laptop, jsdom view, no layout" for a full view.dispatch in jsdom 26.1.0 with bun 1.4.3;
    • "laptop, plugin only, no view" for a bun 1.4.3 script with no editor view;
    • "laptop, Chromium headless or WebKit headless, layout on" for the 2026-10-18 real-browser check;
    • "laptop, Chrome , dev build" for the Show a link icon before every pad link in the webapp #516 gates;
    • "read from code, not measured" for anything else.
  • The design note and the probe scripts are local files. Children cite them only as the design review, with no file name or section number.

Test policy

  • Write only the ruled tests (ruling 15). No DOM snapshots, no "renders without crashing" tests.
  • Prove each new test by sabotage: break the named code, see the test fail, then restore it.
  • Extension unit tests that need a DOM wait for Add a linkIcon option and a hyperlink mark view that draws an icon before each link #513 Decision 6 (jsdom). That Decision names the tests that a "no" drops. Today the unit layer is headless on purpose (commands.test.ts#L1-L3). jsdom@26.1.0 is in bun.lock only through the root jest-environment-jsdom. It does not resolve from the extension. If the Decision is yes, use a per-file setup, not a global preload.
  • The clean-room playground keeps its default behaviour. Icons turn on only behind new query flags, such as ?icons=element|map|fetcher|both|throw and ?tile=on. So existing specs and the README gallery assets stay as they are.

Performance gates

#516 Step 9 holds the gates, their method and their limits. They are engineering defaults from the review.

Checks every child runs where it applies

Each line names the check and its owner. The pass detail lives only in the owner child.

Rules every child keeps

Local stack

  • Run make dev-local. The webapp serves port 3000, or the next free port. Read the port from the WEBAPP log (scripts/dev-local-preflight.ts:318). REST serves port 4000.
  • Run webapp specs as (cd apps/webapp && bunx cypress run --browser electron --config baseUrl=http://localhost:<port> --spec <spec>).
  • bun run --filter @docs.plus/hocuspocus test needs make dev-local up, because some suites need Postgres and Redis.

Tests and benchmarks

The design was measured before these issues were filed. These numbers are evidence, not gates. The gates live in #516 Step 9 and in each child's Verification. Every child re-measures on the code it ships.

How it was measured

  1. jsdom bench. ProseMirror 1.42.3 in jsdom with no layout. It compared no icon, a Decoration.widget icon and a mark-view icon. This bench made the maintainer choose the mark view (Ruling 9).
  2. Real-browser check, 2026-10-18. A throwaway build of the planned code: the mark view, linkIcon, linkIconMap, linkIconFetcher with its failed-URL set, and the webapp chain. It used the real @tiptap/core 3.31.3 Editor and the repo's Hyperlink extension from source.
    • Browsers: Chromium 151 headless and WebKit 26.5 headless, driven by Playwright, with layout and paint on. Firefox was not run.
    • Documents: 500, 2000 and 5000 top-level paragraphs, one link each, cycling about 150 origins. A local stub served the icons with the planned route headers, and 10 origins answered 404.
    • Method: the same document with linkIcon unset and then set, 5 runs each, median and p95. Times are sync dispatch, plus a number to the next frame (double requestAnimationFrame). Remote edits used two editors joined by y-tiptap in one page.
    • The harness lived outside the repo and is not committed. Show a link icon before every pad link in the webapp #516 Step 9 repeats the key measurements on a real pad.

Results

Icons off / on, median, ms (laptop, Chromium headless, layout on):

Link blocks Mount, sync Mount, to next frame Keystroke in plain text Keystroke in a link Paste of 500 links
500 15.6 / 19.3 21.3 / 29.6 0.3 / 0.3 0.4 / 0.4 24.8 / 32.3
2000 35.0 / 45.4 53.8 / 76.6 0.5 / 0.4 0.6 / 0.5 26.8 / 34.9
5000 71.5 / 101.4 124.3 / 187.3 0.6 / 0.4 0.6 / 0.5 27.2 / 32.9
  • WebKit (1 ms clock): keystrokes 1 to 3 ms in both modes; mount 22 / 44 ms sync at 2000.
  • 4x CPU throttle, Chromium at 2000: mount 148.1 / 195.7 ms sync; keystrokes 0.1 to 0.5 ms in both modes.
  • Remote edits, Chromium at 2000, icons off / on:
y-tiptap Remote keystroke Remote insert at the top Remote Enter Remote 10-block paste
3.0.9 (installed today) 4.9 / 4.9 30.9 / 53.4 32.1 / 49.8 33.5 / 50.9
3.0.10 (#473) 1.6 / 1.5 1.8 / 2.4 2.3 / 3.0 2.2 / 3.0

Counts

Scenario Mark views built Icon requests
Cold open one per link at most 1 per URL (149 URLs)
Typing, 600 local keystrokes 0 0
200 remote keystrokes 0 0
Scroll through the whole document 0 0
Paste of 500 links 500 0 (cached origins; known misses get no src)
Remote insert at the top, y-tiptap 3.0.9 every link 0
Remote insert at the top, y-tiptap 3.0.10 1 0

Memory (Chromium, through CDP)

  • Pad switches: 30 editor create-and-destroy cycles at 2000 links, with garbage collection after each. They left 0 Editors, 0 EditorViews and 0 icon images. DOM nodes and listeners stayed flat. The heap slope was the same with icons on and off, about 9 KiB per cycle.
  • Long session: 2000 remote edits at 2000 links. The heap grew the same amount with icons on and off. The growth is Yjs history.
  • Cost per link: about +350 to +480 bytes of heap and +2.4 DOM nodes (information only).

Proof that the harness can fail

  • A mark view that rebuilt its image on every update failed the request gates: 492 requests on a cold open, and about 494 on a remote insert.
  • A window listener that never detaches failed the leak loop: +5,706 listeners and +2.2 MiB per cycle.
  • The clean runs above come from the restored code.

Limits

What only the maintainer can do

Relation to #174

After #331, #174 internal kinds join the front of the chain: internalKind(ctx) ?? docsplusMap(ctx) ?? appScheme(ctx) ?? fetchIcon(ctx). The plan-update comment on #174 lists the #174 build notes that this plan replaces. #174 executors read them there.

Rejected options

  • Decoration.widget with host keys. Its forChild cost grows with the doc size (Summary table). Ruling 9.
  • Tiptap addDecorations. It warns on shared widget keys (node_modules/.bun/@tiptap+core@3.31.3+ef29727eaaf819d3/node_modules/@tiptap/core/src/decorations/DecorationManager.ts:378-382). It gains no speed under the y-tiptap whole-doc replace.
  • The { key, render } resolver result. A mark view reuses its DOM through mark.eq and calls the resolver once. A key and a thunk add nothing.
  • Icons or favicon URLs in mark attrs, doc attrs or Y.Map. Every peer gets them, and they land in version rows. Any editor could plant a tracker, and they never expire.
  • Caches in Tiptap storage or plugin state. They die on reload and pad switch.
  • localStorage, IndexedDB or Workbox icon caches. They repeat the HTTP cache and break the Workbox cross-origin rule.
  • Google s2, a direct origin/favicon.ico load, DuckDuckGo, icon.horse, Favicone and logo.dev. Each sends the reader's IP and the link's site to a third party.
  • Redis icon bytes. Callers could mint keys on an unauthenticated route and evict store-doc-state keys.
  • A batch icon endpoint. It breaks the per-URL HTTP cache.
  • A fixed 64 KiB cap. Ruled configurable instead, with a 16 KiB default (ruling 10).
  • A CSS ::before icon. It cannot hold an arbitrary element, so it overturns ruling 3. It stays the only known fallback if the IME gate fails.
  • <picture>, prefers-color-scheme or two images toggled by CSS for pairs. They follow the OS or need a theme selector.
  • A JS color-scheme listener. Nothing could unsubscribe it, and a redraw would cost a whole-doc Yjs pass.
  • Tiptap's MarkView class. Its dom getter returns the editor root (node_modules/.bun/@tiptap+core@3.31.3+ef29727eaaf819d3/node_modules/@tiptap/core/src/MarkView.ts:77-79).
  • A deferred microtask insert to skip image-only links. It adds timing state. Add a linkIcon option and a hyperlink mark view that draws an icon before each link #513 Decision 1 handles that case.
  • A module-level pending set for sweeps. It would mix the pad, history and /editor queues.
  • A compositionend listener on each <a>. Composition events target the editing host.
  • Ignoring hole records or hole selection during composition. Both failed in jsdom.
  • Typed URL-pattern glyphs (GitHub pull requests and issues). Not ruled.
  • A U+2060 joiner in the icon. It would leak into Yjs through innerText prefills.

Activity

  1. HMarzban commented on Oct 18, 2026

    @HMarzban
    CollaboratorAuthor

    Maintainer ruling, 2026-10-17

    This comment records the maintainer's rulings for this tracker, so they do not live only in the body. Quoted words are the maintainer's own. Ruling 1 was given on 2026-10-16; the rest on 2026-10-17.

    2026-10-16

    1. Favicons, from our own route. "for webapp we have to work on own fetcher also and put it into the hocuspocus". Here "fetcher" means the server side: the icon route in hocuspocus. The client helper is linkIconFetcher.

    2026-10-17, on the plan

    1. Parity exception. Every link in the pad shows its icon automatically, with no opt-in. This goes against Google Docs, Word and Notion (3 of 3) on purpose, like Revise the heading size scale to follow a documented typographic convention #369. Do not "fix" it back. Show a link icon before every pad link in the webapp #516 records it in AGENTS.md beside Revise the heading size scale to follow a documented typographic convention #369.
    2. Webapp map content and glyph freedom. "for the webapp just hold our docsplus brand icon that customize for the hyperlink ... let user to use diffrent glyphs not only from lucide ... the rest of logos come from the fetcher"
    3. Offline map has priority. A link that matches the map takes the map's icon, and the fetcher helper never runs for it. A host may use the map alone. (Recorded; no quote kept.)
    4. Light and dark icons, and the tile, are developer choices. A map entry may give one icon or a { light, dark } pair. The fetcher helper takes a tile option. (Recorded; no quote kept.)
    5. Webapp tile off. "no we do not need it"
    6. Sync, one function. "the A is the best". The resolver returns an element at once, and the element does its own fetching.
    7. Where the code lives. The mechanism goes into @docs.plus/extension-hyperlink. The webapp uses it and is where it is tested. The icon route lives in hocuspocus. (Recorded; no quote kept.)

    2026-10-17, on the architecture review

    1. Mark view, not a decoration. "the alternative the node view, decoration for this cost sooo much."
    2. Byte cap is configurable, default 16 KiB. "let developer chose,but set 16 kib as default." The developer is the server operator.
    3. Privacy page. "yeas, mention this feature in privacy page."
    4. Print hides link icons. "agrre"
    5. Icon box 1em, gap 0.25em, align by measurement, one design-system row. "agrre"
    6. docs.plus mark: apps/webapp/public/icons/logo.svg as is. "true"
    7. Tests. "true. also we have to test the offline map and other side that related to the extension in the extension sides." The ruled set:
      • webapp: one Cypress spec for the Edit-form prefill;
      • extension: clean-room Cypress, plus unit tests for the mark view, linkIconMap, linkIconFetcher, pairs and the tile;
      • server: one unit test for the byte check.
      • The byte check has two parts: the cap in readCappedBody, and the sniff. Test files mirror source names, so Add an icon route to link-metadata with an operator-set byte cap #515 tests these two parts in two files.
    8. Legacy link marks: no production count, README caveat only. "true"
    9. File four side issues, adjusted to the new plan. "yep, base on the new need and plan."

    Still open

    The 12 Decisions on the card under "What only the maintainer can do" are not ruled yet. Each child issue holds its own Decisions, with the recommended answer first and a stop gate in the step that needs it. A later comment headed Maintainer ruling, <date> records each answer.

  2. HMarzban commented on Oct 18, 2026

    @HMarzban
    CollaboratorAuthor

    Maintainer ruling, 2026-10-18

    The maintainer answered these Decisions through a multiple-choice prompt in the agent session. Each answer below is the option the maintainer picked; the quoted label is the option text.

    All 12 Decisions on the card under "What only the maintainer can do" are ruled. Every answer is the recommended one. Each owning issue has its own ruling comment, which is what its stop gates check.

    1. Take y-tiptap 3.0.10 so a remote change stops rebuilding the paragraphs below it #520 Decision 1: yes. Mark Bump TipTap packages to 3.32.0 for the remote-cursor fix #473 ready and merge it before Add a linkIcon option and a hyperlink mark view that draws an icon before each link #513 and Take y-tiptap 3.0.10 so a remote change stops rebuilding the paragraphs below it #520.
    2. Add a linkIcon option and a hyperlink mark view that draws an icon before each link #513 Decision 2: answered by item 1. Bump to 1.42.6 in Add a linkIcon option and a hyperlink mark view that draws an icon before each link #513 only if Bump TipTap packages to 3.32.0 for the remote-cursor fix #473 closes unmerged.
    3. Add a linkIcon option and a hyperlink mark view that draws an icon before each link #513 Decision 1: yes. The icon goes at the start of every iconed link.
    4. Add a linkIcon option and a hyperlink mark view that draws an icon before each link #513 Decision 3: yes. Unset linkIcon means the plain mark view and no composition plugin.
    5. Add a linkIcon option and a hyperlink mark view that draws an icon before each link #513 Decision 4: yes, with no upstream post. The block-start caret jump is a recorded parity exception.
    6. Add a linkIcon option and a hyperlink mark view that draws an icon before each link #513 Decision 5: yes. The maintainer runs the real-device legs, and the merge waits for them.
    7. Add a linkIcon option and a hyperlink mark view that draws an icon before each link #513 Decision 6: yes. Add jsdom and @types/jsdom to the root catalog.
    8. Add an icon route to link-metadata with an operator-set byte cap #515 Decision 1: yes. Drop Traefik log query strings; restart on v3.6.18 or later.
    9. Show a link icon before every pad link in the webapp #516 Decision 1: yes. Ship Load profile and preview favicons from our route, not from Google #517 with the privacy paragraph.
    10. Show a link icon before every pad link in the webapp #516 Decision 2: yes. One shared LEGAL_UPDATED, set to the ship date.
    11. Load profile and preview favicons from our route, not from Google #517 Decision 1: yes. The fallback favicon points at our icon route.
    12. Stop link preview metadata writes from reaching every peer #519 Decision 1: the recommended route. Read-only previews; the fetched tooltip goes away.

    Next: merge #473, then start #513. #515 can start at once.

  3. added this to the v2.3.0 milestone on Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions