Dev.to WebDev 🛠 Dev 👁 1 📖 2 min read

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

My entry for DEV's Big Summer Bug Smash, Clear the Lineup track: a silent, reproducible production bug and the exact fix that clears it off the stage. The bug (a silent one) On my static blog (React + vite-re

My entry for DEV's Big Summer Bug Smash, Clear the Lineup track: a silent, reproducible production bug and the exact fix that clears it off the stage.

The bug (a silent one)

On my static blog (React + vite-react-ssg), every article's og:image was a .webp. The page looks perfect. But Medium's "Import a story" and the LinkedIn and Twitter link-preview scrapers cannot read WebP or AVIF, so importing an article pulled no hero image, and shared links showed no preview. At a handful of articles it is annoying. At hundreds, re-adding the hero by hand on every import is a tax you pay forever.

There is a sneakier second half: even after I fixed the og:image, Medium still dropped the hero, because Medium imports the image from the article body, not the og:image meta tag. The body <img> was still 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. Nothing errors; the image just vanishes from the import and the preview.

The fix (automatic, for every current and future article)

Keep WebP for the live page render (performance), but serve a JPG wherever a scraper or importer looks. Three small changes.

1) A prebuild step that 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]);
}

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')}

3) Commit the generated JPGs so the deploy does not depend on the CI having ffmpeg. 37 heroes converted; new articles are covered automatically because the script runs on every build.

Before / after

Before: WebP hero and og:image, Medium import drops the hero, LinkedIn preview shows no image. After: a JPG twin served to importers and scrapers, so the hero imports automatically and every link preview renders, while browsers still get WebP performance on the live page.

The boring, permanent kind of fix. The bug is off the stage for good.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.