How to check open graph tags
Save the page and read every og: tag out of the HTML. The Open Graph protocol carries the tag name in a property attribute, not in name, and requires og:title, og:type, og:image and og:url. One command counts them, names the attribute each sits on, and flags duplicates.
Checker offline. Follow the manual steps below, they give the same answer.
Why check this
Run this before release on any shared page template, and after a CMS or framework upgrade that rewrites the head. The failure it prevents is the empty preview: a page that looks complete in a browser and pastes into a chat window as a bare URL, because the consumer reads a different tag than the template wrote.
The check answers four things: which og: tags exist, which attribute carries each, whether the four required properties are present, and whether the image URL is absolute. It does not tell you what a platform renders, which depends on that platform's extractor, its cache and its size rules.
Prerequisites
- curl 7.x or later. See the curl manual.
- Node 22 for the extractor. No packages.
- The Open Graph protocol, which names the four required properties and states that where a tag repeats, the first one from top to bottom wins.
- A local target with the planted defects, so no third party is used as an example of broken markup. Stop it by process id when you are done, never by image name.
// fixtures.js
const http = require('http'), fs = require('fs'), path = require('path');
const TYPES = { '.html': 'text/html; charset=utf-8', '.png': 'image/png' };
http.createServer((req, res) => {
const url = req.url.split('?')[0];
if (url === '/img/moved.png') return res.writeHead(302, { location: '/img/card.png' }).end();
if (url === '/img/missing.png') return res.writeHead(404, { 'content-type': 'text/html' }).end('<h1>404</h1>');
const file = path.join(__dirname, 'fixtures', path.normalize(url).replace(/^[\\/]+/, ''));
if (!file.startsWith(path.join(__dirname, 'fixtures')) || !fs.existsSync(file)) {
return res.writeHead(404).end('not found');
}
const body = fs.readFileSync(file);
res.writeHead(200, { 'content-type': TYPES[path.extname(file)] || 'application/octet-stream',
'content-length': body.length });
res.end(body);
}).listen(8919, '127.0.0.1', () => console.log('fixtures on http://127.0.0.1:8919, pid ' + process.pid));
<!-- fixtures/social.html -->
<!doctype html>
<html lang="en"><head><meta charset="utf-8">
<title>Release notes 4.2 | Fixture</title>
<meta name="description" content="What changed in Fixture 4.2.">
<meta property="og:title" content="Release notes 4.2">
<meta property="og:description" content="What changed in Fixture 4.2.">
<meta property="og:description" content="Fixture 4.2 changelog.">
<meta property="og:image" content="/img/card.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Fixture 4.2 is out">
<meta name="twitter:site" content="fixture">
</head><body><h1>Release notes 4.2</h1></body></html>
// og-check.mjs
import { readFile } from 'node:fs/promises';
const src = process.argv[2];
const html = /^https?:/.test(src) ? await (await fetch(src)).text() : await readFile(src, 'utf8');
const attr = (tag, n) => (new RegExp(`${n}\\s*=\\s*["']([^"']*)["']`, 'i').exec(tag) || [])[1];
const tags = [];
for (const m of html.matchAll(/<meta[^>]*>/gi)) {
const key = attr(m[0], 'property') ?? attr(m[0], 'name');
if (!key || !/^(og|twitter|fb):/.test(key)) continue;
tags.push({ key, via: attr(m[0], 'property') !== undefined ? 'property' : 'name', value: attr(m[0], 'content') ?? '' });
}
const seen = new Map();
for (const t of tags) {
const dup = seen.get(t.key);
seen.set(t.key, (dup ?? 0) + 1);
console.log(`${t.via.padEnd(8)} ${t.key.padEnd(22)} ${t.value}${dup ? ` <-- duplicate #${dup + 1}` : ''}`);
}
const has = (k) => tags.some((t) => t.key === k);
const viaProp = tags.filter((t) => t.key.startsWith('og:') && t.via === 'property').length;
const viaName = tags.filter((t) => t.key.startsWith('og:') && t.via === 'name').length;
console.log(`\nog: tags ${viaProp} on property=, ${viaName} on name=`);
console.log(`required by the OG protocol: ${['og:title', 'og:type', 'og:image', 'og:url'].map((k) => `${k} ${has(k) ? 'yes' : 'MISSING'}`).join(', ')}`);
const img = tags.find((t) => t.key === 'og:image')?.value;
if (img) console.log(`og:image ${/^https?:\/\//.test(img) ? 'absolute' : `RELATIVE (${img}), the protocol requires an absolute URL`}`);
Steps
- Step 1.
Start the fixture server and read the tags off the page with the planted defects.
node og-check.mjs http://127.0.0.1:8919/social.htmlproperty og:title Release notes 4.2 property og:description What changed in Fixture 4.2. property og:description Fixture 4.2 changelog. <-- duplicate #2 property og:image /img/card.png property og:image:width 1200 property og:image:height 630 name twitter:card summary_large_image name twitter:title Fixture 4.2 is out name twitter:site fixture og: tags 6 on property=, 0 on name= required by the OG protocol: og:title yes, og:type MISSING, og:image yes, og:url MISSING og:image RELATIVE (/img/card.png), the protocol requires an absolute URLThree findings. Two required properties are absent, the description is declared twice with different text, and the image path is relative. The protocol resolves the duplicate by taking the first, so the second line is dead text that a reviewer reading top to bottom will take for the live value.
- Step 2.
Save a real page once, so the rest of the check costs no further requests.
curl -sS -o mdn.html -w '%{http_code} %{content_type} %{size_download}\n' https://developer.mozilla.org/en-US/docs/Web/HTTP200 text/html 221672 - Step 3.
Count the Open Graph tags the way a parser built on the protocol counts them.
grep -c 'property="og:' mdn.html0Zero. Stopping here puts "no Open Graph tags" in the report, and that is wrong.
- Step 4.
Read every tag regardless of which attribute carries it.
node og-check.mjs mdn.htmlname og:url https://developer.mozilla.org/en-US/docs/Web/HTTP name og:title HTTP: Hypertext Transfer Protocol | MDN name og:locale en_US name og:description HTTP is an application-layer protocol for transmitting hypermedia documents, such as HTML. It was designed for communication between web browsers and web servers, but it can also be used for other purposes, such as machine-to-machine communication, programmatic access to APIs, and more. name og:image https://developer.mozilla.org/mdn-social-image.46ac2375.png name og:image:type image/png name og:image:height 1024 name og:image:width 1024 name og:image:alt The MDN logo name og:site_name MDN Web Docs name twitter:card summary name twitter:creator MozDevNet og: tags 0 on property=, 10 on name=Ten Open Graph tags, every one on
name=, ondeveloper.mozilla.orgas fetched on 2026-09-11. The protocol is built on RDFa and its own examples writeproperty. A lenient extractor reads these, one that follows the protocol reads none, and the page then previews differently depending on where it is pasted. Theog:descriptionvalue also carries a raw newline, which is why it wraps above. - Step 5.
Check the required four on the same saved file.
node og-check.mjs mdn.html | tail -2required by the OG protocol: og:title yes, og:type MISSING, og:image yes, og:url yes og:image absoluteThree of the four are present and
og:typeis not. Read that line together with step 3: this script counts a tag as present whichever attribute carries it, so three-of-four is the lenient reading. On the attribute the protocol names, the count is zero of four. Record both numbers, because consumers split the same way. - Step 6.
Run the same extractor against a page you control, and read what is absent rather than what is present.
node og-check.mjs out/check/check-301-vs-302-redirect/index.htmlproperty og:title How to check if a redirect is 301 or 302 property og:description Run curl -sI <url> without -L and read the status line. HTTP/2 301 is permanent and browsers cache it, HTTP/2 302 is temporary and they ask again. For a on property og:type article name twitter:card summary name twitter:title How to check if a redirect is 301 or 302 name twitter:description Run curl -sI <url> without -L and read the status line. HTTP/2 301 is permanent and browsers cache it, HTTP/2 302 is temporary and they ask again. For a on og: tags 3 on property=, 0 on name= required by the OG protocol: og:title yes, og:type yes, og:image MISSING, og:url MISSINGThree tags on the attribute the protocol names, and two of the required four missing. A page with no
og:imagehas no card image, and one with noog:urlgives a consumer no canonical identity to deduplicate against. Two more things show in the raw values. The description stops mid-sentence at "For a on" because the template cuts it to a fixed length, and<url>is an HTML entity this extractor prints without decoding.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| property og:title ... | The tag is where the protocol says it is | Nothing |
| name og:title ... | A lenient consumer reads it, a strict one does not | Change name to property on every og: tag |
| og:type MISSING | One of the four required properties is absent | Add it. website or article covers most pages |
| og:url MISSING | Consumers have no canonical identity for the page | Add the absolute canonical URL |
| <-- duplicate #2 | Two values for one property | The first wins, so delete the later one rather than reordering by eye |
| og:image RELATIVE | A consumer fetching the raw value has no base to resolve it | Write the absolute URL, scheme and host included |
Common mistakes
What to check next
- How to check og image: the tag names a URL, and the file behind it is a separate check.
- How to check twitter card tags: the tags that sit beside these and fall back to them.
- How to validate structured data: the other machine-readable description of the same page.
- How to check canonical tag:
og:urland the canonical should name the same URL. - How to check json ld: another head element that fails without reporting anything.
FAQ
How do I check og tags without a third-party site?
Save the page with curl -o and read it locally, as in steps 2 to 5. Nothing here needs an external validator, and a local read gives the raw attributes rather than one tool's reading of them.
Some open graph meta tags are missing. Which ones matter?
og:title, og:type, og:image and og:url are the four the protocol marks required. Anything past that is optional, with og:image:alt recommended by the protocol whenever og:image is set.
Is name= instead of property= actually broken?
It is outside the protocol. Some consumers accept it, which is why pages ship that way for years unnoticed. The correction costs one attribute per tag, so write property.
Why does the checker read my page but the preview stays empty?
The consumer fetched an older copy, or fetched a different URL, or never reached the page. Check the tag on the exact URL being shared, scheme and trailing slash included, before looking at the consumer.
Do og tags need to be in the head?
Put them there. An extractor that stops reading at </head> is common, and tags emitted into the body by a script are not in the served HTML at all.
Verified
Verified by Maks Vernycurl 8.21.0node 22.23.2grep 3.0
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
- Checker: meta-tags title, description, canonical, robots meta, hreflang, Open Graph, Twitter card, viewport
- All crawlability and indexing checks
basic6 minpublished updated Maks Verny