Skip to content

Messages: link preview cards (linkMetadata / /api/link-metadata) #33

Closed

Description

@Adron

Summary

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.

Activity

  1. Adron commented on Sep 16, 2026

    @Adron
    MemberAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Table stakesarea:messagesArea: messagesparityWeb/API feature-parity work

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions