The OG card you saw before clicking this link — the one Slack or Twitter or Facebook would have shown if you pasted this URL — wasn’t drawn by me. It was generated by dogimg, a small service I built because I tried three different ways to make Open Graph images for this site, and only one of them survived contact with actually writing posts.

This is a walk through those three approaches, in order, and why URL-driven generation won.

Stage 1: Hand-designing OG images in Figma

The first OG image for zeikar.dev was a 1200×630 PNG. I made it in Figma. I exported it. I dragged it into the repo. It looked fine.

It looked fine for one page. By the third post, I was looking at a future where every new post meant another Figma file, another export, another drag-in — and any time I changed the site’s brand color or favicon, every OG image already shipped was visually stale.

The honest problem: an OG image is a poster of metadata that already exists on the page. The <title>, the description, the theme color, the favicon. All of it is already there for browsers and crawlers to read. Hand-designing OG images means writing the same content twice — once for the page, once for the poster.

Stage 2: Param-driven generators (vercel/og-image and friends)

The next stop was @vercel/og-style services, where you call an endpoint with the content as query parameters:

https://og-generator.example/api/og?title=My+Post&description=...&theme=teal

vercel/og-image is the canonical example. The image is generated dynamically, you get a templated layout, and there are no PNGs in the repo. A real improvement over Figma.

But the responsibility didn’t actually move. Every time I wrote a post, something still had to pack the post’s metadata into a query string. Either I did it by hand, or I wrote a build-time step that read each post’s front matter, encoded title and description, and emitted a URL with the correct params. The page already knows its own title. The plugin reads it. The plugin re-encodes it. The service receives it and renders it. Three copies of the same string for one image.

The frame I landed on: URL-as-template. The URL is a template you fill in, and you do the filling.

Stage 3: A URL-driven generator (dogimg)

dogimg is what fell out of asking the next question: why is the caller packing metadata at all? The page already serves an HTML document with <title>, <meta name="description">, <meta property="og:*">, <meta name="theme-color">, and a favicon link. That’s the source of truth. Why not call that?

The API is one parameter:

https://dogimg.vercel.app/api/og?url={URL}

What it does, in three steps:

  1. Fetch the HTML at {URL}.
  2. Parse og:*, twitter:*, <title>, <meta name="theme-color">, and the favicon from the document.
  3. Render a 1200×630 PNG with @vercel/og, using the page’s theme color as a gradient accent and the favicon as the card’s icon.

The frame here is URL-as-truth. The caller doesn’t pack anything. The page already knows what it’s about, and dogimg asks the page directly. If the post’s title changes, the OG image changes — without redeploying the generator, regenerating PNGs, or even thinking about it.

In HTML it’s a single tag:

<meta property="og:image" content="https://dogimg.vercel.app/api/og?url=https://your-site.com/post" />

That’s the entire integration on the consumer side.

The payoff: one Jekyll plugin, every page covered

zeikar.dev wires this up at build time in _plugins/og_image.rb. It runs on post_read and, for every document or default-layout page that doesn’t already set image: in front matter, points it at dogimg:

encoded = CGI.escape(base + item.url)
item.data["image"] = "https://dogimg.vercel.app/api/og?url=#{encoded}"

Coverage falls out of site.documents (posts plus every collection — _projects/*.md is included for free) and site.pages filtered to layout: default. jekyll-seo-tag then emits og:image and twitter:image from page.image once each, no duplication.

Writing a post is writing a post. There is no OG step. The card you saw before clicking this link is the proof — same path as everything else on the site.