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.
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? 👇