You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Messages: link preview cards (linkMetadata / /api/link-metadata) #33
The web feed renders rich link preview cards (title, description, thumbnail, host) — visible on the public landing page. The app renders the bare URL.
Live verification (2026-09-15)
GET /api/link-metadata?url=https://jmap.io/ -> 200
{"link":{"url":"...","platform":"other","metadata":{"type":"link","title":"...","description":"...","thumbnail":"..."}}}
GET /api/messages/{id}/metadata -> 200 {"links":[{"url":"...","metadata":{type,title,ogType,thumbnail,description}}]}
API
linkMetadata arrives inline on each message in GET /api/messages — often no extra call is needed.
GET /api/messages/{id}/metadata — stored metadata only (lightweight).
POST /api/messages/{id}/metadata — fetch and persist metadata for a message's links.
GET /api/link-metadata?url= — ad-hoc unfurl, e.g. a compose-time preview.
Respect the user preference
GET /api/user returns showPreviews. When it is off, do not render preview cards (see the Settings epic).
Acceptance criteria
Models/Message.cs gains LinkMetadata.
Preview cards render on feed cards from inline linkMetadata, honoring showPreviews.
Thumbnails load lazily; a failed image degrades to a text-only card rather than a gap.
Compose-time preview via GET /api/link-metadata when the draft contains a URL.
Clicking a card opens the target in the OS browser.
Implemented in #158 — work continues in the PR from here.
Preview cards render from the inline linkMetadata (42 of 80 sampled rows carry it), so the feed needs no extra call; the compose-time preview uses the ad-hoc GET /api/link-metadata?url=. Thumbnails are built on first binding evaluation with DelayCreation, and DownloadFailed/DecodeFailed collapse the image so a broken thumbnail degrades to a text-only card rather than a gap. showPreviews is false on the test account, so the off path is the one real data exercised — nothing renders, on cards or in the composer.
Three things worth carrying forward: the two envelopes differ ({"link":{…}} for the ad-hoc endpoint vs {"links":[…]} inline and per-message); a failed unfurl answers 200 with fetchStatus: "failed" and no metadata at all (7 of the 42 rows), so it is not an HTTP error to catch; and platform is too coarse to display (22 of 42 said "other"), so the card shows the URL host instead. POST /api/messages/{id}/metadata is intentionally not implemented — it persists server state and nothing needs it.
Activity
Adron commentedon Sep 16, 2026
Implemented in #158 — work continues in the PR from here.
Preview cards render from the inline
linkMetadata(42 of 80 sampled rows carry it), so the feed needs no extra call; the compose-time preview uses the ad-hocGET /api/link-metadata?url=. Thumbnails are built on first binding evaluation withDelayCreation, andDownloadFailed/DecodeFailedcollapse the image so a broken thumbnail degrades to a text-only card rather than a gap.showPreviewsisfalseon the test account, so the off path is the one real data exercised — nothing renders, on cards or in the composer.Three things worth carrying forward: the two envelopes differ (
{"link":{…}}for the ad-hoc endpoint vs{"links":[…]}inline and per-message); a failed unfurl answers 200 withfetchStatus: "failed"and nometadataat all (7 of the 42 rows), so it is not an HTTP error to catch; andplatformis too coarse to display (22 of 42 said "other"), so the card shows the URL host instead.POST /api/messages/{id}/metadatais intentionally not implemented — it persists server state and nothing needs it.