Skip to main content
Ssnap Docs
Use cases

Open Graph images

Generate social preview images from a real HTML template, and serve them without burning quota.

Social preview images are a layout problem pretending to be an image problem. Drawing them with a canvas library means re-implementing text wrapping, web fonts and ellipsis by hand. Rendering an HTML template you already know how to style skips all of it.

The shape is always the same: a route that renders the card as a web page, and a capture of that route at 1200×630.

The template route

Build a normal page on your own site that takes the dynamic parts as query parameters:

https://your-app.com/og?title=Ship+it&author=Ada

Style it at exactly the output size, and make it self-contained — no cookie banner, no navigation, no analytics:

body {
  width: 1200px;
  height: 630px;
  margin: 0;
  display: grid;
  place-content: center;
}

The capture

curl -G https://ssnap.cc/api/v1/screenshot \
  -H "Authorization: Bearer $SSNAP_API_KEY" \
  --data-urlencode "url=https://your-app.com/og?title=Ship+it" \
  --data-urlencode "width=1200" \
  --data-urlencode "height=630" \
  --data-urlencode "format=png" \
  --data-urlencode "cache_ttl=2592000" \
  --data-urlencode "response=url"

Four things matter here.

Both width and height. A lone width is ignored — the viewport override only applies when both are present. See Capture parameters.

No full_page. The template is already exactly 1200×630. full_page would follow the document's scroll height instead, and any stray margin would change the aspect ratio.

format=png. Crisp text on flat colour, and quality does nothing for PNG anyway. Use webp if your consumers accept it and the card is photographic.

A long cache_ttl. This is the part that decides whether the feature is affordable.

Caching is the whole trick

An OG image endpoint is hit by every crawler, every chat unfurl and every share preview — the same card, over and over. Without cache_ttl each of those is a fresh browser launch and a fresh capture against your monthly allowance.

A cache hit does not consume a capture slot at all. Because the cache key is a hash of the url plus the output-affecting parameters, and your title travels in the url, each distinct card renders once and is then free:

cache_ttl=2592000   # 30 days, the maximum

Check X-Screenshot-Cached (binary) or "cached" (with response=url) to confirm you are actually hitting it. Full rules in Screenshot caching.

Rate limiting still applies to cache hits — they cost a request even though they cost no quota. An OG endpoint fronted directly by a crawler stampede should sit behind your own CDN cache as well.

Serving the result

Two workable patterns.

Generate at publish time. When a post is saved, capture it, store the bytes on your own storage, and put that permanent url in the og:image tag. Predictable, no runtime dependency, and crawlers never wait on a render.

Generate on demand with a signed url. Capture with response=url and hand back the signed link. It is valid for 24 hours and needs no API key, which is fine for a preview and wrong for a permanent tag — crawlers re-fetch og:image long after that. If you go this route, cache the bytes yourself on first use.

Whichever you pick, do not call the API from the browser. The key would be public.

Fonts and timing

Web fonts are the usual cause of a card that renders in a fallback face. The capture happens when the page is ready, not when every font file has painted:

  --data-urlencode "network_idle=true" \
  --data-urlencode "delay=300"

network_idle waits for network activity to settle; delay adds a fixed pause after that. Together they cover fonts and any entry animation. Better still, inline the font as a data url in the template so there is no request to wait for.

Checklist

  • Template route renders standalone at 1200×630, no chrome.
  • width and height both set; no full_page.
  • cache_ttl at or near the 30-day maximum.
  • network_idle=true plus a small delay if the card uses web fonts.
  • Bytes stored on your own storage before they go into a permanent og:image tag.
  • API key server-side only.

Next