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

// 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

  1. 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.html
    
    property 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 URL

    Three 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.

  2. 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/HTTP
    
    200 text/html 221672
  3. Step 3.

    Count the Open Graph tags the way a parser built on the protocol counts them.

    grep -c 'property="og:' mdn.html
    
    0

    Zero. Stopping here puts "no Open Graph tags" in the report, and that is wrong.

  4. Step 4.

    Read every tag regardless of which attribute carries it.

    node og-check.mjs mdn.html
    
    name     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=, on developer.mozilla.org as fetched on 2026-09-11. The protocol is built on RDFa and its own examples write property. A lenient extractor reads these, one that follows the protocol reads none, and the page then previews differently depending on where it is pasted. The og:description value also carries a raw newline, which is why it wraps above.

  5. Step 5.

    Check the required four on the same saved file.

    node og-check.mjs mdn.html | tail -2
    
    required by the OG protocol: og:title yes, og:type MISSING, og:image yes, og:url yes
    og:image absolute

    Three of the four are present and og:type is 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.

  6. 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.html
    
    property og:title               How to check if a redirect is 301 or 302
    property og:description         Run curl -sI &lt;url&gt; 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 &lt;url&gt; 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 MISSING

    Three tags on the attribute the protocol names, and two of the required four missing. A page with no og:image has no card image, and one with no og:url gives 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 &lt;url&gt; 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

Sign: A checker reports no Open Graph tags on a page whose head visibly contains ten of them.Cause: The tags sit on name= instead of property=. The Open Graph protocol is an RDFa vocabulary and names the property attribute throughout, so an extractor built to the protocol matches nothing. Steps 3 and 4 show it on developer.mozilla.org: grep for property= returns 0, a lenient read returns 10.
Sign: Two og:description tags with different text, and the preview shows the one that is not in the template you edited.Cause: The Open Graph protocol gives preference to the first tag from top to bottom during conflicts. A layout that emits a default and a template that appends an override produce exactly this, with the default winning. Delete the loser rather than reordering.
Sign: og:image:width and og:image:height describe a different image than the one that appears.Cause: Structured properties attach to the og:image root tag that precedes them. A second og:image emitted between the root and its dimensions starts a new group, so the width and height land on the wrong image. Keep each image and its properties adjacent and in order.

What to check next

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.

basic6 minpublished updated Maks Verny