DEV Community

Cover image for Fix: WebP og:images silently break Medium imports and social previews
DEVALAND
DEVALAND

Posted on

Fix: WebP og:images silently break Medium imports and social previews

Summer Bug Smash: Clear the Lineup 🐛🛹

This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.

Project Overview

My site (devaland.com) is a static blog built with React and vite-react-ssg, around 70 articles. Every article has a hero image, served as WebP/AVIF for performance. Those articles are also imported into Medium and shared on LinkedIn and X.

Bug Fix or Performance Improvement

A silent bug: WebP/AVIF hero images and og:image tags do not import into Medium's "Import a story" and do not render link previews on LinkedIn or X. The page looks perfect, but importing an article pulled no hero image, and shared links showed no preview. Nothing errors; the image just vanishes.

There is a sneaky second half: even after fixing the og:image, Medium still dropped the hero, because Medium imports the image from the article body <img>, which was also WebP. Two layers, one root cause.

Repro: point a page's hero <img> and og:image at a .webp, then run the URL through Medium's importer or the LinkedIn Post Inspector. No image. Swap to .jpg, it appears.

Code

The repo is private, so here are the exact changes as code snippets.

1) A prebuild step that auto-generates a .jpg twin of every local hero, skip-if-exists:

// scripts/gen-og-jpg.mjs (runs in `prebuild`)
import { blogPosts } from "../src/data/mock.mjs";
import { execFileSync } from "node:child_process";
import fs from "node:fs"; import path from "node:path";
const isLocalRaster = i => i && !/^https?:/i.test(i) && /\.(webp|avif)$/i.test(i);
for (const rel of [...new Set(blogPosts.map(p => p.image).filter(isLocalRaster))]) {
  const src = path.join("public", rel), jpg = src.replace(/\.(webp|avif)$/i, ".jpg");
  if (fs.existsSync(jpg) || !fs.existsSync(src)) continue;
  execFileSync("ffmpeg", ["-y", "-loglevel", "error", "-i", src, "-q:v", "3", jpg]);
}
Enter fullscreen mode Exit fullscreen mode

2) Point both the og:image and the visible body hero <img> at the JPG twin:

ogImage={post.image?.replace(/^(\/.+)\.(webp|avif)$/i, '$1.jpg')}
src={post.image?.replace(/^(\/.+)\.(webp|avif)$/i, '$1.jpg')}
Enter fullscreen mode Exit fullscreen mode

My Improvements

The trick is to keep WebP for the live page (performance) but serve a JPG wherever a scraper or importer looks. The prebuild script runs on every build, so new articles are covered automatically with zero manual work, and the generated JPGs are committed so the deploy does not depend on the CI having ffmpeg. 37 heroes converted. Result: the hero now imports into Medium automatically and every link preview renders, while browsers still get WebP on the page. The boring, permanent kind of fix that clears the bug off the stage for good.

Top comments (0)