How to check og image
Read the og:image value, then fetch it and measure what came back. The tag has to hold an absolute URL. Check the status after redirects, the content type, the byte size and the real pixel dimensions, and compare those against og:image:width and og:image:height where the page declares them.
Checker offline. Follow the manual steps below, they give the same answer.
Why check this
Run this whenever the card image changes, when the asset pipeline changes its output path, and on staging sign-off for any template shipping a share card. The failure it prevents is the grey box: the tag is present, the review passes on reading the head, and nobody fetched the file behind the URL.
Four defects account for most of them. A relative URL, which has no base once the value leaves the page. A redirect, which some consumers follow and some do not. A 404 answered with an HTML error page. And declared dimensions that no longer describe the file. Reading the tag catches none of the four. Fetching the file catches all of them.
Prerequisites
- Node 22 for the fetcher and the fixture server. No packages.
- curl, for the command that shows what a consumer faces when the value is relative.
- The Open Graph protocol on
og:imageand its structured propertiesog:image:width,og:image:height,og:image:typeandog:image:alt. - A local target holding the planted defects. The server, the pages and the image generator below produced every byte count here. 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));
// makepng.js: two real PNGs, so the byte counts below are reproducible
const zlib = require('zlib'), fs = require('fs');
function crc32(buf){let c,t=[];for(let n=0;n<256;n++){c=n;for(let k=0;k<8;k++)c=c&1?0xedb88320^(c>>>1):c>>>1;t[n]=c;}
let r=0xffffffff;for(const b of buf)r=t[(r^b)&0xff]^(r>>>8);return (r^0xffffffff)>>>0;}
function chunk(type,data){const len=Buffer.alloc(4);len.writeUInt32BE(data.length);
const td=Buffer.concat([Buffer.from(type,'ascii'),data]);const crc=Buffer.alloc(4);crc.writeUInt32BE(crc32(td));
return Buffer.concat([len,td,crc]);}
function png(w,h,rgb){const ihdr=Buffer.alloc(13);ihdr.writeUInt32BE(w,0);ihdr.writeUInt32BE(h,4);
ihdr[8]=8;ihdr[9]=2;ihdr[10]=0;ihdr[11]=0;ihdr[12]=0;
const row=Buffer.alloc(1+w*3);for(let x=0;x<w;x++){row[1+x*3]=rgb[0];row[2+x*3]=rgb[1];row[3+x*3]=rgb[2];}
const raw=Buffer.concat(Array.from({length:h},()=>row));
return Buffer.concat([Buffer.from([137,80,78,71,13,10,26,10]),chunk('IHDR',ihdr),chunk('IDAT',zlib.deflateSync(raw,{level:9})),chunk('IEND',Buffer.alloc(0))]);}
fs.writeFileSync('fixtures/img/card.png', png(1200,630,[15,118,110]));
fs.writeFileSync('fixtures/img/tiny.png', png(120,63,[180,83,9]));
<!-- fixtures/social.html, the relevant lines: a relative og:image -->
<meta property="og:image" content="/img/card.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<!-- fixtures/img-redirect.html -->
<meta property="og:image" content="http://127.0.0.1:8919/img/moved.png">
<!-- fixtures/img-404.html -->
<meta property="og:image" content="http://127.0.0.1:8919/img/missing.png">
<!-- fixtures/img-tiny.html -->
<meta property="og:image" content="http://127.0.0.1:8919/img/tiny.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
// img-check.mjs
const page = process.argv[2];
const html = await (await fetch(page)).text();
const tag = /<meta[^>]*(?:property|name)=["']og:image["'][^>]*>/i.exec(html);
if (!tag) { console.log('no og:image tag'); process.exit(1); }
const raw = /content=["']([^"']*)["']/i.exec(tag[0])[1];
console.log(`og:image tag value ${raw}`);
console.log(`absolute URL? ${/^https?:\/\//.test(raw) ? 'yes' : 'NO, a relative URL. The OG protocol requires an absolute one.'}`);
const url = new URL(raw, page).href;
let hops = 0, at = url, res;
for (;;) {
res = await fetch(at, { redirect: 'manual' });
if (![301, 302, 303, 307, 308].includes(res.status)) break;
hops += 1;
console.log(`hop ${hops} ${res.status} -> ${new URL(res.headers.get('location'), at).href}`);
at = new URL(res.headers.get('location'), at).href;
}
const body = Buffer.from(await res.arrayBuffer());
console.log(`fetched ${at}`);
console.log(`status ${res.status}`);
console.log(`content-type ${res.headers.get('content-type')}`);
console.log(`bytes ${body.length}`);
const dim = (b) => {
if (b.slice(1, 4).toString() === 'PNG') return [b.readUInt32BE(16), b.readUInt32BE(20)];
if (b.slice(0, 4).toString() === 'RIFF' && b.slice(8, 12).toString() === 'WEBP') return [(b.readUInt16LE(26) & 0x3fff) + 1, (b.readUInt16LE(28) & 0x3fff) + 1];
if (b.slice(0, 3).toString('hex') === '474946') return [b.readUInt16LE(6), b.readUInt16LE(8)];
if (b[0] === 0xff && b[1] === 0xd8) {
for (let i = 2; i < b.length - 9;) {
if (b[i] !== 0xff) { i += 1; continue; }
const m = b[i + 1];
if (m >= 0xc0 && m <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(m)) return [b.readUInt16BE(i + 7), b.readUInt16BE(i + 5)];
i += 2 + b.readUInt16BE(i + 2);
}
}
return null;
};
const d = dim(body);
console.log(`real dimensions ${d ? `${d[0]} x ${d[1]}` : 'not a recognised image format'}`);
const w = /og:image:width["'][^>]*content=["'](\d+)/i.exec(html)?.[1];
const h = /og:image:height["'][^>]*content=["'](\d+)/i.exec(html)?.[1];
if (w && h && d) console.log(`declared ${w} x ${h} ${(+w !== d[0] || +h !== d[1]) ? '<-- does not match the file' : '(matches)'}`);
Steps
- Step 1.
Start the fixture server and check the page whose
og:imageis a relative path.node img-check.mjs http://127.0.0.1:8919/social.htmlog:image tag value /img/card.png absolute URL? NO, a relative URL. The OG protocol requires an absolute one. fetched http://127.0.0.1:8919/img/card.png status 200 content-type image/png bytes 3160 real dimensions 1200 x 630 declared 1200 x 630 (matches)Everything after the second line is green, and the tag is still wrong. This script knows the page URL and resolves the path against it. A consumer that reads the value out of the document has no such base.
- Step 2.
Do what a consumer does with the raw value: hand it to a fetcher on its own.
curl -sSI '/img/card.png'curl: (3) URL rejected: Malformed input to a URL functionExit code 3. No request is made, nothing is logged on your server, and the card is empty for a reason that leaves no trace anywhere you would look.
- Step 3.
Check a page whose image URL is absolute and redirects.
node img-check.mjs http://127.0.0.1:8919/img-redirect.htmlog:image tag value http://127.0.0.1:8919/img/moved.png absolute URL? yes hop 1 302 -> http://127.0.0.1:8919/img/card.png fetched http://127.0.0.1:8919/img/card.png status 200 content-type image/png bytes 3160 real dimensions 1200 x 630The file at the end of the hop is correct. Whether a consumer follows the hop is its own choice, so point the tag at the final URL and the question never comes up.
- Step 4.
Check a page whose image is gone.
node img-check.mjs http://127.0.0.1:8919/img-404.htmlog:image tag value http://127.0.0.1:8919/img/missing.png absolute URL? yes fetched http://127.0.0.1:8919/img/missing.png status 404 content-type text/html bytes 12 real dimensions not a recognised image formatThree independent signals of one defect: a 404, a
text/htmlcontent type, and twelve bytes of error page. A check that only looks for a non-empty body passes this. - Step 5.
Check a page whose declared dimensions no longer describe the file.
node img-check.mjs http://127.0.0.1:8919/img-tiny.htmlog:image tag value http://127.0.0.1:8919/img/tiny.png absolute URL? yes fetched http://127.0.0.1:8919/img/tiny.png status 200 content-type image/png bytes 166 real dimensions 120 x 63 declared 1200 x 630 <-- does not match the fileA 200, a correct content type, a real PNG, and the tag off by a factor of ten. The dimensions come from the PNG header, which is the only way to catch this.
- Step 6.
Run the same check against a page in production, to see what a healthy result reads like.
node img-check.mjs https://developer.mozilla.org/en-US/docs/Web/HTTPog:image tag value https://developer.mozilla.org/mdn-social-image.46ac2375.png absolute URL? yes fetched https://developer.mozilla.org/mdn-social-image.46ac2375.png status 200 content-type image/png bytes 5978 real dimensions 1024 x 1024 declared 1024 x 1024 (matches)Absolute, no hops, 200, an image content type, and the declared size matching the header. The filename carries a content hash, which is what keeps the declared size safe: a new image gets a new URL rather than replacing the bytes behind the old one.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| absolute URL? NO | The value cannot be fetched outside the page | Write the full URL, scheme and host included |
| hop 1 301 -> or 302 -> | Consumers split on whether they follow | Point the tag at the final URL |
| status 404 with content-type text/html | The asset is gone and an error page answered | Fix the path, or the build step that emits it |
| not a recognised image format on a 200 | The bytes are not the image the tag promises | Read the body. An HTML error page with a 200 looks like this |
| does not match the file | The declared size is stale | Correct the tag, or emit both from the same source |
| Everything matches | The file behind the tag is reachable and as described | Nothing here says a given platform will show it |
Common mistakes
What to check next
- How to check open graph tags: the tag this check starts from, and the rest of the set around it.
- How to check twitter card tags: the card that falls back to this image when it declares none of its own.
- How to check redirect chain: the hop in step 3 in full, for any URL.
- How to find broken images on a website: the same status and content type reading applied to every image on a page.
- How to validate structured data: markup that also names images, with its own way of getting them wrong.
FAQ
How do I check if the og image is working?
Fetch it and measure it, as in steps 1 to 6. Reading the tag proves only that a string is present. The status after redirects, the content type, the byte count and the dimensions from the file header tell you the image exists and is the one you meant.
How do I force a platform to update a link preview?
Change the URL. Consumers cache by the URL they fetched, so a content-hashed or versioned filename makes a re-fetch unavoidable. Some platforms also offer a manual refresh; this procedure does not test any platform's cache and cannot tell you which do.
What size should the og image be?
The Open Graph protocol sets no dimensions. Pick the size your consumers crop to, then make the tag and the file agree, which is the part step 5 checks. The two real files measured here are 1200 x 630 and 1024 x 1024.
Does og:image need og:image:alt?
The protocol recommends it whenever og:image is set. It costs one tag and is the only description of the image a consumer has.
Can I use a relative og:image if my consumer resolves it?
Some do, and the protocol does not ask them to. Step 2 shows the other case: no request leaves the client, so nothing in your access log explains the empty card.
Verified
Verified by Maks Vernynode 22.23.2curl 8.21.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
basic7 minpublished updated Maks Verny