DEV Community

Cover image for How to Build Rich Link Previews Like Slack and iMessage
Nico Acosta for Grabbit

Posted on Originally published at grabbit.live

How to Build Rich Link Previews Like Slack and iMessage

When you paste a link into Slack, iMessage, or Discord, it usually expands into a card with a title, a short description, and an image. That card is a rich link preview, and when yours shows up as a bare URL instead, the cause is almost always the same across every platform: the page is missing the Open Graph tags those apps read, or its og:image points at something they cannot fetch. This guide covers how the previews work, the exact tags to add, and how to give the card a real image of your page instead of a generic logo.

How a rich link preview works

Slack, iMessage, Discord, and the social platforms do not screenshot your page on demand or guess at its content. When a URL is posted, the app fetches your page's raw HTML and looks for Open Graph meta tags in the <head>. Four tags drive the entire card:

<meta property="og:title" content="Your Page Title" />
<meta property="og:description" content="A one-line summary of the page." />
<meta property="og:image" content="https://yourdomain.com/og.png" />
<meta property="og:url" content="https://yourdomain.com/page" />
Enter fullscreen mode Exit fullscreen mode

og:title and og:description become the text. og:image becomes the picture. og:url is the canonical link the card points at. If a page has no Open Graph tags at all, the app falls back to the bare URL with no card. That is what a "broken" preview almost always is: missing or unreachable tags, not a bug in Slack or iMessage.

Two details trip people up on every platform:

  • The fetcher reads the raw HTML, not the rendered page. It does not run your JavaScript. If your meta tags are injected client-side (a single-page app that sets them after load), the fetcher never sees them. The tags must be in the HTML the server sends.
  • og:image must be an absolute, public URL. A relative path like /og.png will not resolve. Use the full https://yourdomain.com/og.png, and make sure that URL is reachable without a login or a redirect the app cannot follow.

The tags every platform reads

The good news for anyone building link previews into an app: you do not write a different set of tags per platform. Open Graph is the shared standard, so one correct set of tags produces a rich card in Slack, iMessage, Discord, Facebook, and LinkedIn at once. Add one X-specific tag and you cover X (Twitter) too:

<meta property="og:title" content="Your Page Title" />
<meta property="og:description" content="A one-line summary of the page." />
<meta property="og:image" content="https://yourdomain.com/og.png" />
<meta property="og:url" content="https://yourdomain.com/page" />
<meta name="twitter:card" content="summary_large_image" />
Enter fullscreen mode Exit fullscreen mode

twitter:card set to summary_large_image tells X to render the wide card instead of a small thumbnail; X falls back to your og:image for the picture, so you do not need a separate twitter:image. That is the whole tag set. The work that remains is producing the image, and making sure the tags actually reach the fetcher.

Making the preview image a real render of your page

The og:title and og:description are just strings. The og:image is the part that makes a preview feel rich, and it is also the part most people get wrong by shipping a single generic logo for every URL. For a marketing page, a docs page, or a product page, the best preview image is often a clean shot of the page itself.

You can produce that with a screenshot API: point it at the URL, get back a hosted image, and use that image as your og:image.

curl https://api.grabbit.live/v1/grabs \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourdomain.com/og",
    "width": 1200,
    "height": 630,
    "format": "png",
    "delay_ms": 500
  }'
Enter fullscreen mode Exit fullscreen mode

The response includes a hosted image_url:

{
  "id": "grb_01jx...",
  "status": "done",
  "image_url": "https://cdn.grabbit.live/grabs/grb_01jx....png",
  "width": 1200,
  "height": 630,
  "format": "png",
  "bytes": 71240,
  "execution_ms": 1240
}
Enter fullscreen mode Exit fullscreen mode

width accepts 320 to 1920 and height 240 to 1080, so the 1200 by 630 card is comfortably in range. delay_ms (0 to 10000) gives client-rendered content or web fonts a moment to settle before the capture fires. Drop the returned image_url into your tag:

<meta property="og:image" content="https://cdn.grabbit.live/grabs/grb_01jx....png" />
Enter fullscreen mode Exit fullscreen mode

Capture once per unique page and cache the image_url alongside the page record rather than rendering on every request. A common pattern is to build a dedicated /og route with your normal components, capture that at 1200 by 630, and reuse the result everywhere. The same template-and-cache approach is covered in how to generate dynamic OG images from any URL, and the per-platform fix-it details are in how to make your Discord links unfurl.

Why 1200 by 630 covers every platform

Slack, iMessage, Discord, Facebook, X, and LinkedIn all render the same 1.91 to 1 aspect ratio as a large card, and 1200 by 630 pixels is the size that satisfies all of them. That is why one og:image fixes the preview everywhere at once: you are not producing a Slack image and an iMessage image and a Discord image, you are producing one Open Graph image that every reader parses.

Smaller images still work, but many apps fall back to a small thumbnail beside the text below roughly 300 pixels wide, which reads as a weaker preview. Stay at 1200 by 630 and the card is full-width on every platform.

Why your preview still is not showing

If the tags are present and the image URL works in a browser but a platform still shows nothing, work through these in order:

  1. The platform cached the old version. Slack, iMessage, Discord, and Facebook all cache the metadata they fetched the first time a URL was posted, so editing your tags will not update a preview that already rendered. Append a unique query string, such as ?nocache=1, and post the link again so the app treats it as a new URL and re-fetches. Facebook and X also offer manual re-scrape tools in their sharing debuggers.
  2. The image is behind auth or a redirect. If og:image requires a login, sits behind a paywall, or redirects in a way the fetcher will not follow, the card renders without an image. Test the raw image URL in a private browser window.
  3. The tags are set by JavaScript. The fetchers read static HTML and do not execute scripts. Server-render the meta tags so they are in the initial response.
  4. The image is too small. Below roughly 300 pixels wide, many apps render a small thumbnail instead of a full card. Use 1200 by 630.

Re-fetch first, because a stale cache is the single most common reason a correct tag set still shows nothing.

The short version

A rich link preview is your page's Open Graph tags rendered as a card, and every platform reads the same four tags. Add og:title, og:description, og:image, and og:url server-side, plus twitter:card for X; make og:image an absolute, public, 1200 by 630 URL; and append ?nocache=1 to force a refresh after changes. If you want the image to be a real render of the page instead of a static logo, a screenshot API turns the live URL into the hosted image you point og:image at. New to the format? Start with what is an OG image.


Originally published on the Grabbit blog.

Top comments (0)