// blog /
The Social Card That Refused to Render.
- #debugging
- #seo
- #social
- #web
For a stretch this spring, pasting tommybuilt.dev into a post on X produced a gray placeholder where the preview image should be. There was no error to chase, just a polite gray rectangle that made the site look abandoned. The fix ended up being two small commits. The diagnosis is the part worth writing down.
Start with what a social card actually is. When you paste a link, the platform’s scraper fetches your page, reads a few meta tags, and builds the preview from them. The og:image tag points at the picture. My tags were all present and valid. The page validated fine. Everything I could check from my side looked correct, which is exactly the situation that makes you start doubting things that were never broken.
The first real clue was the image format. My og:image pointed at an SVG, because the site’s whole visual identity is vector and an SVG card scaled beautifully in my browser. Scrapers don’t care what renders in a browser. X and Facebook rasterize preview images through pipelines that support formats like PNG and JPEG, and an SVG gets silently dropped. No warning in any validator I tried. The image URL returns 200, the file is a real image, and the card still comes up empty because the scraper won’t paint it.
So I rebuilt the card as a 1200 by 630 PNG, repointed the tags, deployed, and pasted the link again.
Gray rectangle.
This is where the second lesson lives, and it’s meaner than the first. X caches its link previews per URL. When its scraper first hit my homepage, the card was broken, and that broken result got stored. Fixing the image on my end changed nothing about the cached scrape on their end. I could see the correct PNG in my own browser and in curl, and the preview stayed gray because nobody at X’s cache had any reason to look again.
The proof was one of those debugging moments that feels like a magic trick. I pasted tommybuilt.dev/war-room into a post, a page on the same domain with the same meta tag structure, and the preview rendered instantly and perfectly. That URL had never been scraped during the broken period. Clean history, clean card. The homepage carried a poisoned cache entry and the war room page didn’t, and seeing the two side by side turned a mystery into a mechanism.
The escape was a rename. The image moved to a new filename, og-card-v2.png, which changed the URL the scraper fetches and sidestepped every stale copy in the chain. The commit literally says cache-bust in it. Renaming a file to defeat a cache you can’t reach is inelegant and it works, and I’ll take working.
What I keep from this one:
Validate against the consumer, and my own browser was never the consumer here. The scraper was, and the scraper has narrower taste in formats than any browser made this decade. If a preview is broken, test what the platform actually does with the URL, and test it with a URL that has no scrape history so caching can’t lie to you.
Caches you don’t control need cache-busting you do control. A version number in a filename costs nothing and gives you a guaranteed fresh fetch any time you need one. The og-card-v2 name looks silly in the repo. It also ended a multi-day problem in one deploy.
And the small stuff carries weight out of proportion to its size. Nobody hires you because your link preview renders. Plenty of people quietly discount you when it doesn’t, the same way a dead favicon or a stale copyright year reads as neglect. The gray rectangle was costing me credibility on every share, and the whole fix, once I understood it, was an image export and a filename.