Back to blog
·5 min read·Cell Webb

Every Link Preview Was a Square Cat Photo

Every KittyLog page used to unfurl into the same square mascot photo. How a 1200x630 card fixed it, and why social-card text is governed like copy.

Paste a link into Slack and there is a small moment of theater while the unfurl loads. For a long time, every KittyLog page resolved that moment the same way: the homepage, the docs, any blog post, any public changelog, all of them unfurled into the same square photo of a cat. No product name, no tagline, nothing to tell you what was on the other side of the link. On Twitter/X and LinkedIn it was worse, because the platforms letterboxed or cropped the square into their wide card slot and the result looked accidental.

It was accidental. The root layout pointed og:image at our 1024x1024 mascot photo, the docs page did the same, and no opengraph-image route existed anywhere in the app. Issue #1319 documented all of this, including the check anyone can run: curl -sI against the OG image route and watch the 404 come back.

Link previews are a marketing surface

I had been treating the unfurl as decoration. It is closer to an ad slot you already paid for. When someone shares a KittyLog page in a team channel, that preview is the first and possibly only impression a few dozen people get. A branded card with a legible product name does real work there. A square cat photo with no text does roughly none, and next to the cards that Linear or Vercel ship, it reads as unfinished.

The mechanics are not complicated, which is part of why it is embarrassing to get them wrong. Platforms that render large cards expect roughly 1200x630, a 1.91:1 shape. Twitter needs twitter:card set to summary_large_image or it falls back to a small thumbnail. And each page carries its own metadata, so a per-page title and description in the unfurl is just a matter of setting them. None of this is hard. It just has to be someone's job, and for a long while at KittyLog it was nobody's.

What we shipped

Commit 3b735857 replaced the square photo with a proper 1200x630 card: dark background, the cat mark, the KittyLog wordmark, the tagline, and the domain in a small pill. The first version rendered it at the edge with Next.js ImageResponse, and the implementation had two details worth passing on. Next merges metadata, so leaving the old images array in the layout would have emitted a duplicate og:image tag alongside the generated one; the fix deletes the array and says why in a comment. And satori, the layout engine underneath ImageResponse, does not support SVG clipPath, masks, or filters, so the cat mark is plain shapes and paths on purpose.

That dynamic route did not survive long. When the frontend moved to an OpenNext Worker at the end of July 2026, we rendered the card once and baked it into the bundle as a static /og-card.png, and every page's metadata pointed at the flat file. That migration is its own story; the relevant point here is that a static PNG serves the same claim with less machinery. Per-page card images, blog titles rendered onto the card and so on, are deferred. Blog posts get their own title and description in the unfurl but share the one image. The docs page kept its own explicit mascot override a little longer; in August we pointed that override at the branded card too.

The card is copy

The more useful lesson came after the pixels. Our marketing-claims checklist treats text embedded in any raster asset, screenshots, social cards, demo captures, as a public claim, audited exactly like the HTML around it. The current card carries the wordmark, the landing hero's own copy, and the domain, and deliberately no price and no tier. The moment a card names a price or a tier, it is governed by the same source-of-truth rules as the pricing page.

There is a second rule that took me longer to appreciate. If a card's copy changes materially, the new card publishes at a new versioned URL, and every metadata reference moves with it. You do not overwrite the file in place. The reason is caching: Slack, Discord, Twitter/X, and LinkedIn all cache unfurl images on their side, some for days, some until someone begs a re-scrape tool to look again. Overwrite a card that named an old price and the platforms will keep serving the retired claim under your URL, and you cannot make them stop on your schedule. A versioned path sidesteps the whole fight, because anything shared before the change keeps the card that was accurate at the time, and anything shared after picks up the replacement from a URL the caches have never seen.

We needed the rule sooner than I expected. A redesign at the end of July 2026 changed the card's copy, tagline included, so the new card shipped at /og/og-card-v2.png and every metadata reference moved in the same commit. Nobody had to rediscover why overwriting is a trap; the rule was already written down.

So, two rules we keep. Text baked into a social card is a claim, held to the same evidence standard as any sentence on the site. And a material change to that text ships at a new URL, because platform caches remember what you retire. Everything else about OG images is just dimensions.

Source records

  • Issue #1319, the missing branded OG/social card.
  • Commit 3b735857, the 1200x630 branded card (opengraph-image.tsx, twitter-image.tsx, layout.tsx).
  • Commit 725b3062, the redesigned card at the versioned path; frontend/public/og/og-card-v2.png and its references in frontend/src/app/layout.tsx, frontend/src/app/blog/[slug]/page.tsx, frontend/src/app/c/[...slug]/page.tsx.
  • frontend/src/app/docs/page.tsx, the docs-specific metadata now using the branded card.
  • .claude/skills/marketing-claims-and-pricing/SKILL.md, the screenshot and creative-claims rules.

Ready to automate your changelogs?

Try KittyLog free - no credit card required.

Get Started Free