Skip to content
Go back

Nobody Tells You Your Link Preview Is Broken: My og:image 404'd The Whole Time 🙈

You have had the experience. You paste a link to your own site into a chat, a feed, a message — and the card comes back flat. No image. Sometimes just the domain and a line of blue text, standing there looking cheap. 🙈

What makes this bug perfect is that nothing ever told me.

There was no error. No build failure, no console warning, no line in a log. The page loaded, the tag was present, the browser had nothing to complain about — a <meta> tag pointing at a missing file is not a broken page. It’s a broken promise, and only somebody on the receiving end of a share ever sees it. Which is to say: everybody except me.

The line I should have read months ago 🕵️

I found it the most embarrassing way possible. Not with an audit tool, not with a lighthouse run. I was reading my own config file looking for something else, and this was sitting in it:

ogImage: "astropaper-og.jpg",

Fine so far. Config points at an image, a layout reads the config, a meta tag goes out. Except I had never in my life made an astropaper-og.jpg. The theme I started from shipped an example value, I never changed it, and for the entire life of this site:

$ curl -sI https://ownyourstack.dev/astropaper-og.jpg | head -1
HTTP/2 404

Still 404. I checked just now, and I’m leaving it that way — as a small monument.

The resolution logic was actively kind about it, too:

ogImage = SITE.ogImage ? `/${SITE.ogImage}` : "/og.png",

If SITE.ogImage had been empty, the whole thing would have fallen through to /og.png and worked perfectly. The safety net was right there. A wrong value stepped straight over it, because wrongness isn’t the same as absence — an empty string gets rescued, a confidently incorrect string gets shipped.

That’s the part I keep coming back to. The bug wasn’t that the code had no fallback. It was that the fallback was gated on the exact thing I’d gotten wrong.

Diagram of the og:image fallback chain. A page rendering needs exactly one og:image, and the path forks in two. Post pages take the top path: dynamicOgImage is true, an image is rendered at build time per slug, and the result is /posts/slug/index.png returning 200 image/png. Every other page takes the bottom path: it reads the single shared SITE.ogImage config value, which pointed at /astropaper-og.jpg, a file that was not on disk, so it returned 404. A strip along the bottom records the fix as one line in config.ts changing ogImage from astropaper-og.jpg to og.png, which repairs the homepage, the about page, the archives page and every tag page — while noting that posts were never broken.

Why every single post was fine 🎯

This is the detail that made it sneaky. dynamicOgImage is on, which means every post generates its own card at build time:

/posts/the-pipeline-was-the-product/index.png   200  image/png
/posts/my-ai-toolchain-graveyard/index.png      200  image/png
/posts/og-image-404/index.png                   200  image/png

Real 1200×630 renders, text and all, built by the same pipeline that draws the site’s own card at /og.png. Those never touched the broken setting — they take their own branch before the shared value is ever consulted.

So the share cards people were most likely to post — the actual articles — were perfect. What was broken was everything around them: the homepage, /about/, /archives/, every tag page. The frames, not the pictures.

Which is its own kind of lesson. I’d been sharing post links for months and seeing good cards, and concluded — without ever checking — that the whole system worked. One good sample made me confident about a set I’d only sampled one corner of.

The fix was smaller than the investigation 🧵

-  ogImage: "astropaper-og.jpg",
+  ogImage: "og.png",

One line, six characters of difference in practice, and then verification the only way that counts:

$ for p in / /about/ /archives/; do
    curl -s "https://ownyourstack.dev$p" | grep -o 'og:image[^>]*'
  done
<meta property="og:image" content="https://ownyourstack.dev/og.png" ...
<meta property="og:image" content="https://ownyourstack.dev/og.png" ...
<meta property="og:image" content="https://ownyourstack.dev/og.png" ...

No 404s left in the chain. A 1200×630 PNG, generated by the build, served from the edge.

The thing I’d actually tell you 🪞

There’s a whole category of bug like this: the ones where the failure is downstream of everything you monitor.

Your uptime check hits / and gets 200. Your build passes. Your type checker passes. Your links all resolve. Every signal you’ve wired up reads the page you are looking at — and the broken thing is the one URL only a crawler ever requests, or the one file only a crawler ever downloads.

You cannot get a warning from the surface you’re already watching. You have to go and ask the question you haven’t thought to ask.

In my case it was thirty seconds of reading a file I wrote. I’ve started doing it deliberately now: pick a tag that faces outward, paste the page somewhere it will actually be judged, and look at what comes back with someone else’s eyes. It’s not a dashboard. It’s just checking the thing the dashboard doesn’t cover.


The fix is in, the cards are rendering, and the old filename is going to sit in that repo history forever as a reminder of how long I shipped a blank square to anyone kind enough to share my work. 😅

Given that the only reason I found this was reading a file I wasn’t looking for — is “eventually read your own config” a real review strategy, or have I just been lucky enough times that it feels like one? 👇


Share this post on:

Previous Post
Netlify Builds It, Cloudflare Serves It: The Nameserver Swap That Broke Nothing 🛰️
Next Post
I Put My Own Blog Into A Knowledge Graph And It Read My Terminal History Back To Me 🔮