Open Graph
Makes the preview image people see when your page is shared on LinkedIn, X, Slack or iMessage. You send it a headline, it sends back the image and the code that points to it. One line of setup, nothing to install.
The Problem This Solves
When someone shares your page, the platform shows a card: an image, a headline, a line of description. That card comes from a handful of <meta> tags in your page's code, called Open Graph tags. If the image is missing or wrong, you get a grey box, and a grey box gets clicked far less than a card.
Making those images by hand does not scale. Nobody opens a design tool for every blog post, so most sites either use one generic image everywhere or skip it.
This makes the image automatically from your headline. Every page gets its own, they all look consistent, and you never open a design tool.
It also hands you the meta tags to go with it, which matters more than it sounds. The usual failure is a tag pointing at an image that no longer matches, or at an address written in a way the platform cannot follow. Because both come from the same place here, they cannot disagree.
Set It Up
1. Build your URL. Start with the address below and change the text after title= and subtitle= to your own. Spaces become %20, or just let your CMS handle it.
https://cdn.jakelabate.com/open-graph/v1/og.png?title=Your%20headline%20here&subtitle=Your%20description&site=yoursite.com
2. Paste that address into your page. It goes in the <head> section, which is the block at the top of a page's code. Most CMSs have a field for this: WordPress SEO plugins call it a social image, Webflow calls it Open Graph settings.
<meta property="og:image" content="PASTE_YOUR_URL_HERE" />
3. Check it. Paste your page's address into Facebook's sharing debugger or LinkedIn's post inspector and press the rescan button. Both cache aggressively, so if you change something you must rescan or you will keep seeing the old version.
That is the whole setup. If you want the rest of the tags written for you as well, see the tags section below.
Endpoints
| Route | Returns |
|---|---|
/open-graph/v1/og.png | The rendered card. This is what og:image points at. |
/open-graph/v1/og.svg | The same card as vector, for checking layout. Not for og:image: no major crawler accepts SVG there. |
/open-graph/v1/tags | JSON containing the tag map, a ready to paste HTML block, and the absolute image URL. |
/open-graph/v1/tags.html | The meta tag block on its own, as plain text. |
/open-graph/v1/meta | Tags plus the resolved card specification, for build pipelines that want to log what was rendered. |
/open-graph/v1/embed.js | A client script configured entirely by data- attributes on its own tag. |
/open-graph/v1/themes | Available templates, themes, patterns, sizes and colour tokens. |
/open-graph/health | Liveness, and the resolved public base. |
Two Ways To Use It
Just the image. Point a tag straight at the renderer:
<meta property="og:image"
content="https://cdn.jakelabate.com/open-graph/v1/og.png?title=Hello&theme=indigo" />
The image and every tag with it. Safer, because the address it writes is complete rather than shortened, and the width and height it states always match the picture it actually made:
curl -s "https://cdn.jakelabate.com/open-graph/v1/tags.html?title=Hello&site=example.com&url=https://example.com/"
The social cards on this page are generated by this endpoint.
What The Cards Look Like
Eight layouts and nine colour schemes. Under each one is the exact text you would add to the address to get it, so you can copy any of these and swap in your own words.
Every image below is loading live from the service right now. None of them are screenshots.
author=Jake%20Labate&site=jakelabate.com&subtitle=Why%20the%20same%20markup%20serves%20classic%20search%20and%20generative%20answers&template=editorial&theme=indigo&title=Structured%20data%20is%20the%20substrate%20both%20engines%20feed%20on
author=Jake%20Labate&date=Aug%2029%2C%202026&meta=6%20min%20read&site=jakelabate.com&subtitle=And%20the%20four%20places%20every%20implementation%20quietly%20breaks&template=article&theme=mono&title=What%20Open%20Graph%20actually%20guarantees
pattern=diagonal&site=schemacdn.com&subtitle=Governed%20structured%20data%2C%20one%20deploy&template=banner&theme=forest&title=SchemaCDN%202.0
site=jakelabate.com&template=stat&theme=ink&title=Technical%20SEO%20audit%20results
author=Jake%20Labate&meta=SEO%20Consultant&template=quote&theme=sunset&title=The%20same%20markup%20feeds%20both%20engines%2C%20so%20the%20argument%20about%20which%20one%20matters%20is%20the%20wrong%20argument.
site=cdn.jakelabate.com&subtitle=One%20endpoint%20for%20the%20card%20and%20the%20markup&template=minimal&theme=paper&title=Open%20Graph%2C%20on%20demandLong headlines clamp rather than shrink, so a card is never illegible. The stat example above carries no stat parameters, which is why it renders the headline and footer alone: an absent input produces a simpler card rather than an error.
The Tags It Writes For You
Change og.png to tags.html in the address and you get the full block of meta tags instead of the picture. Paste the result into your page's <head> and you are done.
curl -s "https://cdn.jakelabate.com/open-graph/v1/tags.html?title=Structured+data+is+the+substrate&subtitle=Why+the+same+markup+serves+both+engines&site=jakelabate.com&author=Jake+Labate&url=https://jakelabate.com/"
<meta property="og:type" content="website" />
<meta property="og:title" content="Structured data is the substrate" />
<meta property="og:description" content="Why the same markup serves both engines" />
<meta property="og:url" content="https://jakelabate.com/" />
<meta property="og:site_name" content="jakelabate.com" />
<meta property="og:locale" content="en_US" />
<meta property="og:image" content="https://cdn.jakelabate.com/open-graph/v1/og.png?author=Jake+Labate&site=jakelabate.com&subtitle=Why+the+same+markup+serves+both+engines&title=Structured+data+is+the+substrate" />
<meta property="og:image:secure_url" content="https://cdn.jakelabate.com/open-graph/v1/og.png?author=Jake+Labate&site=jakelabate.com&subtitle=Why+the+same+markup+serves+both+engines&title=Structured+data+is+the+substrate" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Structured data is the substrate" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Structured data is the substrate" />
<meta name="twitter:description" content="Why the same markup serves both engines" />
<meta name="twitter:image" content="https://cdn.jakelabate.com/open-graph/v1/og.png?author=Jake+Labate&site=jakelabate.com&subtitle=Why+the+same+markup+serves+both+engines&title=Structured+data+is+the+substrate" />
<meta name="twitter:image:alt" content="Structured data is the substrate" />
Three details worth noticing. The image URL is absolute, because a relative og:image is the single most common reason a preview comes back blank. Its parameters come back sorted alphabetically rather than in the order you sent them, so two people who describe the same card differently still get the same stored image instead of drawing two. And the declared width and height always match what was rendered, including at scale=2.
Parameters
| Parameter | Notes |
|---|---|
title | Headline. Falls back to site when absent. |
subtitle | Also accepted as description. Feeds og:description. |
site, author | Footer line. site also feeds og:site_name. |
date, meta | Byline detail for the article template, attribution for the quote template. |
stat | Repeatable, Value|Label. Three are rendered. |
logo, logoWidth | An https URL to a PNG, JPEG, GIF or SVG. Dimensions are read from the file header so the aspect ratio is exact rather than guessed. |
template | editorial, article, split, banner, quote, stat, minimal, code. |
theme | indigo, ink, violet, sunset, forest, mono, cream, paper, slate. |
pattern | grid, dots, diagonal, glow, off. |
bg, bgAlt, fg, muted, accent, rule | Hex overrides applied on top of the chosen theme. |
size, w, h, scale | Named sizes, explicit dimensions, or a 2x render. |
url, type, locale, card, twitter, alt | Markup only. These never fragment the image cache. |
Every value is clamped and stripped. Unknown themes and templates fall back rather than erroring, invalid hex is ignored, control characters are collapsed, and attribute values are escaped on the way into HTML.
Why The Text Is So Large
Design detail. Useful context, but you do not need it to use the service.
Share cards are built at 1200 by 630 pixels, but nobody sees them that big. In a text message a preview is about a quarter of that size. Text that looked fine while you were designing it arrives at roughly 8 point, which is smaller than the fine print on a receipt.
Everything measurable therefore derives from that constraint rather than from what looks balanced at full resolution.
- Type is a modular scale of seven steps,
44 55 69 86 107 134 168, a 1.25 ratio applied to a base of 44. The base is the legibility floor, not a chosen body size: 44 pixels reads as 11.7 point where the card is actually seen. The smallest step on the scale is therefore the smallest readable size by construction. - Long headlines step down the scale rather than being multiplied by a factor, so every rendered size is still a step. A headline paired with a deck drops one further step, because two competing display sizes read as an argument.
- Text clamps rather than shrinks. A truncated headline someone can read beats a complete one they cannot.
- Space is an 8 point grid addressed by relationship rather than by number. A gap between two elements must exceed the leading inside them, or they read as one block.
- There are no label elements. No eyebrow, no chip. Both competed for vertical space with the only thing legible at that size.
These are enforced rather than intended. The test suite refuses to publish a change if any text would render below 11 point at preview size, if a size is not one of the seven allowed steps, or if anything overflows the edge of the card.
That last check exists because the first version passed the size floor and still clipped three cards at the bottom edge. It works by rendering each template a second time with the canvas height unset and comparing the natural content height against the card it has to fit inside. Writing it also caught a bug that had been invisible: the layout library silently ignores the instruction to cut text off after a set number of lines when the container is arranged a certain way. No clamping had been happening at all.
Speed, And What It Costs You
Nothing, and it does not slow your site. The first time a particular card is requested it is drawn, which takes a fraction of a second. After that it is stored and served instantly, so a page shared a thousand times only ever draws the card once. Crucially, share previews are only fetched when somebody actually shares a link, not on every page view, so this never touches your page speed scores.
The technical detail, if you want it: what gets stored is keyed on the things that change the picture and nothing else, sorted into a fixed order. So two pages asking for the same card in a different order share one copy, and a parameter that only affects the tags never causes the image to be drawn again. The response header x-og-cache tells you whether you got a stored copy or a fresh one.
Logo fetching is the one place the service reaches a URL someone else supplied, so it is the one place with real guards: https only, a 512 KB cap, a 3 second timeout, an image content type allowlist, private and link-local hosts blocked, and an optional host allowlist. A logo that fails any check is dropped and the card still renders. x-og-logo reports loaded, failed or none.
If you self-host this, you can optionally lock it so that only addresses you generated will produce an image. Without that, anyone who finds your endpoint can make it draw cards. It matters for a business; it rarely matters for a personal site.
Source
MIT licensed and self-hostable. The type scale, the templates and the gates that enforce both live in src/.