OGKit

OG image engineering: size, bytes, format, redirects

  • open-graph
  • og-image
  • image-engineering

Someone pastes your launch URL into WhatsApp. Slack shows a crisp card. WhatsApp shows nothing. The meta tags look perfect in View Source. The debugger for another platform still works. So you re-export a “sharper” PNG at 1200×630, push again — and WhatsApp is still blank.

That is not a branding problem. That is image engineering: the bytes, MIME type, hop chain, and auth model of og:image under real crawlers.

Fault tree: four different “no image” stories

Treat symptoms as separate branches. Mixing them wastes days.

Symptom Likely branch First check
No image on every platform Tag / URL / reachability Raw HTML has absolute https://… og:image; curl -sI returns 200
Old image after you replaced the file Cache / cache key Same URL → platforms keep the previous scrape (rescrape; series post on cache)
Broken / soft / cropped art Dimensions / aspect / declared size Real pixels ≥ ~1200×630, ~1.91:1, safe zone inset
Only one client fails (often WhatsApp / chat) Bytes / format / hops Payload size, JPEG vs giant PNG, redirect depth, Content-Type vs magic

The blank-card checklist covers relative URLs, JS-only tags, and crawler blocks. This post stays on the image resource itself once the tag is present.

Size and aspect ratio (necessary, not sufficient)

Industry default for large link previews is still roughly 1200×630 (~1.91:1). Facebook / Meta, LinkedIn, and X publish recommended heights in that band (630 / 627 / 628). For engineering purposes, treat them as one banner slot: design on 1.91:1, put critical faces and copy inside a ~10% inset, and accept center-crop.

<meta property="og:image" content="https://cdn.example.com/share/launch.jpg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Product dashboard on a dark background" />

Rules of thumb that match how scanners grade you:

If your only export is a 400×400 logo, no amount of JPEG quality will make a large banner card look intentional.

Bytes and format (where IM clients go silent)

Pixels tell designers a story. Bytes tell chat apps whether to bother.

@vercel/og, Satori, and many “dynamic OG” paths default to PNG. At 1200×630 a PNG is often hundreds of KB to multi-MB. That is fine for a marketing site hero; it is hostile for some unfurl paths.

Format When it helps When it hurts
JPEG Photos, gradients, product shots; best “default share” format Text + flat UI can look muddy if quality is too low
WebP Smaller than JPEG at similar quality where supported Not every client treats it equally; keep a JPEG path if you must maximize compatibility
PNG Sharp UI chrome, logos, screenshots with hard edges Payload explodes; chat previews often drop it first
GIF Rare motion / legacy Large, limited; not a default for product launches

Byte budgets are platform-specific and often unofficial. In OGKit’s platform profiles, WhatsApp is modeled with a ~300KB hard / ~200KB soft budget, source marked measured / community consensus — not an official guarantee. Other profiles allow multi-megabyte limits (Meta/Facebook-class order of magnitude ~8MB, several others ~5MB) with preferred formats listed per platform. Treat every hard number as operational guidance, not a contract. Re-measure if a client changes behavior.

Practical target for a share card that must work in chat apps:

  1. Export 1200×630.
  2. Encode JPEG quality ~80 (or WebP if you control fallbacks).
  3. Aim for ~100–300KB total when WhatsApp / IM matters.
  4. If you generate PNG in edge functions, transcode before you put the URL in og:image.

The rule that surfaces this in scans is og-image-bytes-exceeds-platform-budget.

Content-Type must match the bytes

Crawlers are picky about lying headers. If the file is JPEG magic but Content-Type: image/png (or vice versa), some clients reject the image even though your browser displays it from <img>.

# Headers the crawler sees
curl -sI "https://cdn.example.com/share/launch.jpg" | rg -i 'HTTP/|content-type|content-length|location'

# Optional: confirm magic vs extension (install file(1) or use a hex dump of the first bytes)
curl -sL "https://cdn.example.com/share/launch.jpg" | head -c 16 | xxd

Stable rule: og-image-content-type-mismatch. Fix the origin or CDN mapping — not the meta tag alone.

Redirects, signed URLs, and 403 after “it worked in the browser”

Your browser follows cookies, multi-hop 302s, and marketing short links. Social image fetchers are logged-out, hop-budgeted clients.

Common failure patterns:

  1. Short link → auth wall → CDN
    og:image points at https://link.example/x which 302s through a tracker to a signed object URL. By the time a slow crawler arrives, the signature expired → 403, blank card. Rule: og-image-forbidden.

  2. Too many hops
    Some crawlers follow zero or few redirects on image URLs even if they follow redirects on the HTML page. Rule: og-image-redirects.

  3. Cookie / IP allowlists
    Staging CDNs that “work for the team VPN” fail for every public unfurl.

Fix: put the final public CDN object URL (HTTPS, 200, no cookies, long-lived or immutable) directly in og:image. Do not make crawlers finish your redirect graph.

# See hop chain without following forever
curl -sI -L --max-redirs 5 -A "facebookexternalhit/1.1" \
  "https://example.com/og/launch.png"

If the final URL differs from the meta value, update the meta. If any hop requires auth, stop using that chain for share images.

Also keep HTTPS absolute URLs: og-image-relative-url, og-image-not-https.

Executable triage order (do this in order)

When a card “sometimes” loses its image, run this sequence — stop when you find a hard fail.

  1. Tag present in raw HTML?
    curl -sL URL | rg -i 'og:image'
    No → SSR / injection problem (other posts in the series).

  2. Absolute HTTPS?
    Relative or http:// → fix URL before touching design.

  3. Final image GET as a crawler
    curl -sI -A "facebookexternalhit/1.1" IMAGE_URL
    Expect 200, correct Content-Type, no auth.

  4. Redirect budget
    Count Location hops. Prefer zero on the image URL.

  5. Pixels
    Decode real width×height; compare to 1200×630 and 1.91:1; fix declared meta if present.

  6. Bytes
    Compare Content-Length (or actual size) to the strictest client you care about. For chat-heavy launches, re-encode under a few hundred KB rather than debating theoretical multi-MB caps.

  7. Format honesty
    Magic vs Content-Type vs extension must agree.

  8. Platform-only stale card
    If steps 1–7 are green but one network still shows an old asset, that is cache, not image engineering — use official rescrape paths.

What scanners already encode as rules

Rule id Severity (typical) Catches
og-image-missing error No image field
og-image-relative-url error Relative path
og-image-not-https error Non-HTTPS
og-image-forbidden error 401 / 403 / auth
og-image-content-type-mismatch error MIME ≠ magic
og-image-redirects warn Redirecting image URL
og-image-dimensions-too-small warn Below ~1200×630
og-image-aspect-ratio-crop-risk warn Far from 1.91:1
og-image-declared-size-mismatch warn Meta size ≠ pixels
og-image-bytes-exceeds-platform-budget warn Over platform byte budget
og-image-alt-missing warn No alt

Full catalogue: rule reference. Per-platform notes and preferred formats: platform profiles.

Ship checklist for a share image

  • Dedicated export ≥ 1200×630, ~1.91:1, critical content in the safe zone
  • JPEG (or carefully tested WebP) under the strictest relevant byte budget; avoid multi-MB PNG for chat
  • og:image is the final HTTPS CDN URL that returns 200 anonymously
  • Content-Type matches file magic; og:image:width / height match reality if you set them
  • og:image:alt is a short plain-language description
  • After swapping art, rescrape platforms that still show the old card

Try it on a live URL

Paste a production page into the homepage scan. Image findings show up as named rules — dimensions, budgets, redirects, forbidden responses — not a single “looks fine” mock card. Use that report to drive the triage order above, then fix the origin once instead of re-exporting PNGs forever.

Next in the series: cache TTL and official rescrape (why a correct image URL still shows last week’s campaign), then dynamic OG generation paths where PNG defaults come back to haunt you.