How to make a link preview animate in iMessage, Slack, WhatsApp, Discord and Telegram

Checked November 2026. “Verified by us” means we saw it animate in that app ourselves; everything else comes from the linked docs or observations.

TL;DR

No single tag animates everywhere. Put the animated GIF itself in og:image (Slack shows it moving) and a downloadable H.264 MP4 of the same loop in og:video (iMessage downloads and plays it). Serve both as plain files at stable HTTPS URLs, with the tags in the first HTML response. WhatsApp's spec only describes a thumbnail for links, so expect it to stay still (we haven't tested it); when motion matters there, share the GIF file instead of the link. Discord and Telegram can show animated media, but which tags they use for an ordinary site isn't documented: test them.

Per app

AppCrawler user agentWhat animatesLimitsVerified by usSource
iMessageNo documented Messages user agent. An old observation shows both facebookexternalhit/1.1 and Twitterbot/1.0 (plus Facebot). Applebot is a different crawler.Documented: a direct, downloadable MP4 in og:video (or twitter:player:stream) is downloaded and played automatically. An iframe player doesn't play inline; HLS needs a tap. A GIF in og:image alone is less clearly specified.Apple's guidelines: main HTML up to 1 MB; images, icons and video combined up to 10 MB; images at least 900 px wide are recommended, under 150 px may show as an icon.Yes. macOS Messages, GIF og:image + MP4 og:video: the card animated and was labelled “Video”.Apple TN3156
UA observation
SlackSlackbot-LinkExpanding 1.0 (+https://api.slack.com/robots); images are fetched by Slack-ImgProxy.A GIF in og:image. og:video alone doesn't play: Slack only embeds video from hosts on its allowlist.No published size limit for GIF unfurls found. Unfurls are cached for about 30 minutes, and the same link isn't unfurled again in a conversation within an hour.Yes. Slack web, GIF og:image: two captures a second apart showed different frames.Slack robots
Slack help
WhatsAppWhatsApp/2.x.x.x A, I or N (Android, iOS, web), e.g. WhatsApp/2.22.20.72 A.Expected: a static thumbnail (our reading of the spec, untested). The spec reads og:title, og:description, og:url and og:image only; no documented tag makes the card animate. Send the GIF as a file instead (below).og:image under 600 KB, at least 300 px wide, aspect ratio at most 4:1; the tags must be in the first 300 KB of the HTML.No. Not tested yet.WhatsApp link previews
DiscordMozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com) (observed, not documented).Likely a GIF in og:image with twitter:card = summary_large_image. Untested: an MP4 og:video may turn the embed into a click-to-play video, so compare with and without it.No public limit for embedded GIFs found (upload limits are a different thing). Users' GIF autoplay, reduced-motion and media-preview settings affect what they see.No. Not tested yet.Discord embed object
UA observation
TelegramTelegramBot (like TwitterBot) (observed). Match it before any generic Twitterbot rule.Telegram's link-preview model has a GIF type, so animated previews exist. Which tags trigger it for an ordinary site isn't documented: test a GIF og:image, an MP4 og:video and a raw .gif link.No public limit for link previews found. Auto-download and autoplay settings can stop the motion.No. Not tested yet.Telegram webPage
UA observation

Copy-paste <head>

Use absolute HTTPS URLs and the real width and height of each file. Leave out the og:video and twitter:player:stream lines when you have no MP4.

<meta property="og:type" content="video.other">
<meta property="og:url" content="https://example.com/m/abc123">
<meta property="og:title" content="Build passed first try... suspicious">
<meta property="og:description" content="A captioned GIF">
<!-- Slack, Discord: the animated GIF itself, not a screenshot -->
<meta property="og:image" content="https://example.com/m/abc123.gif">
<meta property="og:image:secure_url" content="https://example.com/m/abc123.gif">
<meta property="og:image:type" content="image/gif">
<meta property="og:image:width" content="480">
<meta property="og:image:height" content="360">
<meta property="og:image:alt" content="Build passed first try... suspicious">
<!-- iMessage: a downloadable H.264 MP4 of the same loop -->
<meta property="og:video" content="https://example.com/m/abc123.mp4">
<meta property="og:video:secure_url" content="https://example.com/m/abc123.mp4">
<meta property="og:video:type" content="video/mp4">
<meta property="og:video:width" content="480">
<meta property="og:video:height" content="360">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://example.com/m/abc123.gif">
<meta name="twitter:player:stream" content="https://example.com/m/abc123.mp4">
<meta name="twitter:player:stream:content_type" content="video/mp4">

Serve the media from their own URLs, the same bytes to every client:

WhatsApp: share the file, not the link

WhatsApp's spec describes the link card as a thumbnail, so plan for a pasted link to stay still (not tested by us). The Web Share API can hand the GIF to the share sheet as a file instead, so it goes out as media rather than a link card. We haven't recorded the result in WhatsApp yet. Fetch the file before the tap: browsers only allow navigator.share() shortly after a user gesture.

// Fetch the GIF on page load, so the tap can call share() at once.
let file = null;
fetch("/m/abc123.gif")
  .then((r) => r.blob())
  .then((b) => {
    const f = new File([b], "abc123.gif", { type: "image/gif" });
    if (navigator.canShare?.({ files: [f] })) file = f;
  });

button.onclick = async () => {
  if (file) return navigator.share({ files: [file], text: "Remix it: https://example.com/m/abc123" });
  if (navigator.share) return navigator.share({ url: "https://example.com/m/abc123" });
  await navigator.clipboard.writeText("https://example.com/m/abc123");
};

Desktop browsers without file sharing fall back to sharing or copying the link.

Making the MP4 in the browser (WebCodecs)

You don't need a server-side ffmpeg. We draw each GIF frame to a canvas, encode it with VideoEncoder as H.264 Constrained Baseline (avc1.42001f, about 1 Mbps, a keyframe every 2 seconds) and mux it with mp4-muxer using fast start. Check VideoEncoder.isConfigSupported() first; H.264 needs even width and height, so crop a pixel if needed. If the browser can't encode H.264, the file is over 4 MB, or encoding takes more than 20 seconds, we publish without an MP4 and the page simply has no og:video.

How to test

# What the crawler sees: the tags must be in the first HTML response, no JavaScript
curl -s -A "Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)" https://example.com/m/abc123 | grep -E 'og:|twitter:'
curl -s -A "WhatsApp/2.22.20.72 A" https://example.com/m/abc123 | grep 'og:image'

# The media URLs: right type, real bytes, and a 206 for a Range request
curl -sI https://example.com/m/abc123.gif
curl -s -o /dev/null -w '%{http_code} %{content_type}\n' -H 'Range: bytes=0-1023' https://example.com/m/abc123.mp4

We learned this building vibe gifs, a GIF caption/remix app: gifs.agentswarm.space. Live example share page: /m/D5HcmZ8.

All templates · Guide · Copyright & takedowns · Credits