Upgrade Your Website with Animated Previews

bobbyg603

Most link previews are a still image. Send a link to workingdevshero.com in iMessage now and the bubble plays the video at the top of this post instead.

That’s the Midnight Rooftop Flow loop from our Heroic Lofi album, with the site headline laid over the empty sky. We didn’t make anything new for it: the album had already left us twenty animated scenes, each cut into a clean loop. A handful of meta tags asks for it. Getting it to actually play took one more fix.

The Tags 🏷️

Open Graph has had an og:video property for years. Apple’s link previews in Messages will download an MP4 from it and play it muted, on a loop, right in the conversation. Here’s what we serve on the home page:

<meta property="og:image" content="https://workingdevshero.com/og-image.jpg" />
<meta property="og:video" content="https://workingdevshero.com/og-video.mp4" />
<meta property="og:video:secure_url" content="https://workingdevshero.com/og-video.mp4" />
<meta property="og:video:type" content="video/mp4" />
<meta property="og:video:width" content="1200" />
<meta property="og:video:height" content="630" />

A few things we settled on along the way:

  • The image is the video’s first frame. Previews show the image first, so starting the video on that exact frame means nothing jumps when it begins to play.
  • The video is silent and small. 6 seconds, 1200×630 H.264, 237 KB. Apple caps a preview’s images and video at 10 MB total, and smaller files start playing sooner.
  • The text is baked in with ffmpeg. We lay out the headline as an HTML page, screenshot it with a transparent background in headless Chrome, and overlay it on every frame. One script rebuilds both files.
  • Only the default image gets the video. Blog posts have their own hero images, so they keep a plain image preview instead of borrowing the rooftop, unless a post brings a loop of its own (this one uses the site’s).

Why It Only Showed the Picture 🔍

We shipped it, pasted the link into iMessage, and got the new image but no video.

The tags were right and the file was right. To see what Apple’s side was doing, we ran the macOS LinkPresentation framework (LPMetadataProvider, the API apps use to build these previews) against the site. It found the image and returned no video at all.

So we served the same page and video from two local servers. The one that supports HTTP range requests (Range: bytes=0-1 returns 206 Partial Content) got a video. The one that doesn’t got nothing. Production was the second kind: our site runs on Cloudflare Workers static assets, which ignores Range and always sends the whole file with a 200. Safari has the same expectation, which is why some MP4s show a crossed-out play button there.

The Fix 🔧

We added a Worker that only runs for video paths and turns a full response into the requested slice:

// wrangler.jsonc
"main": "./worker/index.ts",
"assets": {
  "directory": "./dist",
  "binding": "ASSETS",
  "run_worker_first": ["/og-video.mp4", "/videos/*"]
}

The Worker fetches the file through the ASSETS binding, parses the Range header, and answers 206 with a Content-Range, or 416 when the range starts past the end. Every other path still goes straight to static assets with no Worker in the way. With it deployed locally, the same LinkPresentation check downloaded the full video.

Then the Home Page 🌙

Once the loop was in previews, the home page’s still hero looked a little flat next to it. The hero was already this scene, so it’s now the loop too: a 16:9 version on desktop and a 9:16 one on phones, about 220 KB each. Both are set to preload="none", and a few lines of script play only the one on screen, so phones never download the desktop file. Anyone with reduced motion turned on gets the first frame as a still.

Copy It With Your Agent 🤖

This site’s code is public, so you don’t have to rebuild any of this from the snippets above. Point your coding agent at this post and tell it:

Copy the animated link preview from workingdevshero.com/blog/animated-link-previews into this site, using our own logo and a short loop of our own.

The hard part is the loop itself, not the code. If you don’t have one yet, the Loops section of our album post covers how we got clean ones out of an image-to-video model: what breaks the seam (slow camera zooms, waving capes, turning heads) and what holds.

These are the files it needs:

If you want to try it yourself, send workingdevshero.com to a friend in iMessage. And if the rooftop looks familiar, the whole album is on YouTube.