The Open Graph protocol is a set of <meta property="og:..."> tags in a page's <head> that tells social networks, chat apps and other link consumers what the page is, so they can show it as a rich preview card.
Facebook published the protocol in 2010 at ogp.me. Today most link consumers read it: LinkedIn, Slack, Discord, iMessage, WhatsApp, Mastodon and many search and AI tools. X reads its own Twitter card tags first and falls back to Open Graph. This entry belongs to the screenshot and rendering glossary.
A plain explanation
A web page is written for people. A preview card needs a small, structured summary of it: a title, a type, a canonical URL and one image. Open Graph is the agreed place to put that summary. Without it, each platform guesses from the <title> and the first image it finds, and the guess is often wrong.
| Tag | Required | What it holds |
|---|---|---|
og:title |
yes | The title for the card, without the site name suffix |
og:type |
yes | website, article, profile, video.movie and others |
og:image |
yes | Absolute URL of the preview image (see OG image) |
og:url |
yes | The canonical URL that shares and likes should count against |
og:description |
no | One or two sentences for the card body |
og:site_name |
no | The name of the whole site |
og:locale |
no | Language and territory, for example en_US |
Some properties have structured children. og:image accepts og:image:width, og:image:height, og:image:type, og:image:secure_url and og:image:alt. A type can bring its own namespace: article:published_time and article:author apply when og:type is article.
How a crawler reads it
Three facts follow from that flow. The tags must be in the server response, not added by JavaScript. The image URL must be public and absolute. And the platform keeps its copy, so a fix can take time to show unless the platform offers a re-scrape tool.
How to check and use it in Puppeteer, Playwright and RenderScreenshot
Playwright: list the tags and fail on missing ones. The script exits with code 1 when a required tag is missing, so it works as a CI check. It also flags name="og:...", a common typo.
import { chromium } from 'playwright'; const url = process.argv[2] ?? 'https://example.com'; const REQUIRED = ['og:title', 'og:type', 'og:image', 'og:url']; const browser = await chromium.launch(); try { const page = await browser.newPage(); await page.goto(url, { waitUntil: 'domcontentloaded' }); const tags = await page.$$eval('meta[property]', (metas) => metas.map((m) => [m.getAttribute('property'), m.getAttribute('content')])); const og = tags.filter(([p]) => p.startsWith('og:')); console.table(og); const missing = REQUIRED.filter((p) => !og.some(([k]) => k === p)); if (missing.length) { console.error('missing:', missing.join(', ')); process.exitCode = 1; } const misnamed = await page.$$eval('meta[name^="og:"]', (m) => m.map((x) => x.getAttribute('name'))); if (misnamed.length) console.warn('uses name= instead of property=:', misnamed.join(', ')); } finally { await browser.close(); }
Against https://example.com it printed missing: og:title, og:type, og:image, og:url, because that page has no Open Graph tags. Against our test page with a name="og:description" tag, it listed seven tags and warned about the misnamed one.
Puppeteer: find tags that only JavaScript adds. This compares the raw HTML, which is what crawlers see, with the DOM after scripts ran.
import puppeteer from 'puppeteer'; const url = process.argv[2] ?? 'https://example.com'; const ogFrom = (html) => [...html.matchAll(/<meta[^>]+property=["'](og:[^"']+)["']/gi)].map((m) => m[1]); const raw = await (await fetch(url, { headers: { 'user-agent': 'og-check/1.0' } })).text(); const browser = await puppeteer.launch(); try { const page = await browser.newPage(); await page.goto(url, { waitUntil: 'networkidle0' }); const rendered = ogFrom(await page.content()); const rawTags = ogFrom(raw); console.log('in raw HTML:', rawTags.join(', ') || 'none'); const jsOnly = rendered.filter((p) => !rawTags.includes(p)); if (jsOnly.length) console.warn('added by JavaScript, invisible to most crawlers:', jsOnly.join(', ')); } finally { await browser.close(); }
On a test page that inserts og:image with a script, it reported added by JavaScript, invisible to most crawlers: og:image.
RenderScreenshot: produce the og:image value. The protocol needs an image URL. A GET /v1/screenshot URL with preset=og_card returns a 1200 x 630 PNG, so it can be the tag's content. In public HTML, use a signed URL so the page does not expose an API key.
<meta property="og:image" content="https://api.renderscreenshot.com/v1/screenshot?url=https%3A%2F%2Fexample.com%2Fblog%2Fmy-post&preset=og_card&key_id=rs_pub_...&expires=...&signature=..."> <meta property="og:image:width" content="1200"> <meta property="og:image:height" content="630">
Tested on 2026-10-15 with Node 22.18, Playwright 1.64.1 and Puppeteer 24.43.1 (run in headless shell mode), against example.com and a local test page. The signed URL above is a pattern; build real ones with the signed URL guide.
Common mistakes
nameinstead ofproperty. Writeproperty="og:title".- Tags that differ per request. If
og:urlchanges with tracking parameters, shares are split across many URLs. Point it at the canonical URL. - Duplicate tags. Two
og:imagetags are valid (they form an array), but twoog:titletags make the result depend on the parser. - Tags in the body. Some frameworks render meta tags late. Check the first response with
curl.
Related terms
- OG image: size, format and how to render the image itself.
- Twitter card: X's tags and their fallback to Open Graph.
- Headless browser: what the checker scripts above run in.
To see what a platform will show for your URL, use the link preview checker. If the card has no image, the OG image not showing guide walks through each cause. To render card images on request, get a free API key.