I Built Dynamic Social Cards by Talking to an Agent
Every time I shared my site, the preview was the same selfie. I fixed it end-to-end from a chat — a next/og route, wired into every page — and then learned the hard way that the real boss fight isn't the code, it's the caches.
For the longest time, every link I shared from this site showed the same thing: my profile photo. Send the homepage to someone on WhatsApp, share a blog post on Telegram — same selfie, every time, regardless of what the page was actually about. It looked lazy, because it was.
I decided to fix it the way I fix most things now: by describing what I wanted to an agent and reviewing what came back. The whole feature — design, code, wiring — got built while I was mostly watching. Here's the build log, including the part that actually cost me time (spoiler: not the code).
The TODO that was waiting for me
The first thing the agent turned up was that past-me had already started this. Buried in the metadata helper was a function called buildOgImageUrl — commented out, with a note: "Disabled — next/og requires Next 14+, project is on 13.5. Re-enable once we upgrade."
We'd upgraded to Next 15 ages ago. The blocker was gone; nobody had gone back to flip the switch. That's the most relatable kind of tech debt: not a hard problem, just a thing that fell off the list once the reason to defer it expired.
So the task wasn't "research how to do dynamic OG images." It was "finish what you started, now that the stack can handle it."
What I asked for
The message was short: "every page shares the same static photo as its social preview — build a dynamic OG image so each page gets its own branded card with the title and description, and wire it in everywhere."
That's the whole spec. No file names, no API details. The agent's job was to turn that into a plan and a diff.
How it got built
It came back with a clear shape:
- A new route at
app/api/ogusing Next'snext/ogImageResponse— the thing that renders an image from JSX on the edge. It takes a title and subtitle as query params and draws a 1200×630 card: dark background, a subtle gradient, the page title, the description, my name, and a little "MF" monogram in the corner. - The old
buildOgImageUrlhelper, re-enabled and pointed at that route. - The metadata builder changed so that every page falls back to a generated card by default — homepage, projects, each blog post — unless it explicitly passes its own image.
The nice part of wiring it at the metadata layer: I didn't have to touch a single page. One change, and the homepage, the projects page, and every blog post past and future all got their own card automatically. I reviewed the diff, rendered the image once to eyeball it, and shipped.
If I'd done this by hand, the next/og part is exactly the kind of thing I'd have spent an hour relearning — the JSX-to-image constraints, the edge runtime, the font handling. I didn't. I described the outcome and checked the result.
The boss fight: caches
Here's where the honesty comes in. The code worked on the first real review. The thing that ate my afternoon was everything downstream of the code.
I shipped it, opened WhatsApp, shared my homepage to confirm the new card… and got the old selfie. Shared it on Telegram — same. For a few minutes I was convinced the deploy hadn't gone out, or the route was broken.
It wasn't. Production was serving the correct new card — I checked the actual meta tag and hit the image route directly; both were fine. The problem was that WhatsApp and Telegram cache link previews aggressively, keyed by URL. Anything I'd shared before the change still showed its cached preview — the old photo — and would keep showing it until the cache expired or got refreshed.
The tell was beautiful: the Indonesian version of the homepage previewed correctly, while the English one and the bare domain still showed the photo. Same code, same meta tags. The /id URL just happened to have never been shared before, so it got scraped fresh. The ones I'd shared in the past were stuck on stale cache.
The fixes, for anyone hitting this:
- Telegram: send the URL to
@WebpageBot. It re-scrapes and clears the cached preview. Instant. - WhatsApp: no official refresh tool. Share with a throwaway query param (
?v=2) so it's treated as a new URL, or wait for the cache to age out. - Facebook / LinkedIn: their sharing debuggers have a "scrape again" button.
None of this is in the code. All of it is the actual job.
What this kind of build feels like
The whole feature — discovering the dead TODO, building the route, wiring it in, shipping — was something I drove from a chat thread, mostly by saying "yes" and "show me." The agent did the parts that are tedious-but-known. I did the parts that need a human: deciding it was worth doing, judging whether the card looked good, and recognizing that "it shows the old image" was a caching problem and not a code one.
That last bit is the pattern I keep noticing. The agent is great at building the thing. It's still on me to understand the system well enough to know where the thing will actually break — which, more often than not, is somewhere the code isn't.
