Open Graph tags are five lines of <meta> markup that control how your page appears when someone shares it. Add them and a pasted link becomes a card with a headline, a summary and a large image; leave them out and it shares as a bare URL or a fragment of body text. The markup is trivial. The part that catches people is where it has to be β in the HTML your server sends, because link scrapers do not run JavaScript.
You need the ability to edit your page's <head>, either directly or through your CMS or framework, plus somewhere to host a 1200 Γ 630 image at a public HTTPS URL. If you are unsure what these tags do, start with What Is Open Graph?
Step 1: The Minimum Markup
These five tags cover the overwhelming majority of what platforms read. Everything else is refinement.
<head>
<meta property="og:title" content="How We Cut Build Times by 70%">
<meta property="og:description" content="A walkthrough of the caching changes that took our CI from 14 minutes to 4.">
<meta property="og:image" content="https://example.com/og/build-times.png">
<meta property="og:url" content="https://example.com/blog/build-times">
<meta name="twitter:card" content="summary_large_image">
</head>Two details in that snippet are easy to get wrong and worth stating plainly:
- Open Graph tags use
property=. Twitter Card tags usename=. They are not interchangeable, and Facebook in particular is strict about it. og:imageandog:urlmust be absolute URLs starting withhttps://. Scrapers do not resolve relative paths against the page.
<meta name="og:title"> is the single most common mistake in Open Graph markup. It looks correct, it validates as HTML, and Facebook ignores it. Every og: tag takes property.Step 2: The Full Set
Once the basics work, these are worth adding. All of them degrade gracefully if omitted.
<head>
<!-- Core -->
<meta property="og:title" content="How We Cut Build Times by 70%">
<meta property="og:description" content="A walkthrough of the caching changes that took our CI from 14 minutes to 4.">
<meta property="og:url" content="https://example.com/blog/build-times">
<meta property="og:type" content="article">
<meta property="og:site_name" content="Example">
<meta property="og:locale" content="en_US">
<!-- Image, fully described -->
<meta property="og:image" content="https://example.com/og/build-times.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="A bar chart showing build time dropping from 14 to 4 minutes">
<meta property="og:image:type" content="image/png">
<!-- X: reads these first, falls back to og: -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@example">
<!-- Article-specific, when og:type is article -->
<meta property="article:published_time" content="2026-07-29T09:00:00Z">
<meta property="article:author" content="https://example.com/authors/jane">
</head>| Tag | Purpose | Length guidance |
|---|---|---|
og:title | Card headline | Under 60 characters; 88 is where most platforms truncate |
og:description | Summary beneath the headline | 100β150 characters |
og:image | The card image | 1200 Γ 630, absolute HTTPS URL, under 1 MB |
og:url | Canonical address | Without it, UTM variants split engagement counts |
og:type | Object type | website or article covers almost everything |
For image dimensions, cropping behaviour and format limits in full, see the og:image Size Guide.
Step 3: Add Them to Your Platform
Next.js β App Router
Export a metadata object, or generateMetadata for dynamic pages. Next renders the tags server-side, which is exactly what scrapers need.
// app/blog/[slug]/page.jsx
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return {
metadataBase: new URL('https://example.com'),
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
url: `https://example.com/blog/${params.slug}`,
siteName: 'Example',
type: 'article',
locale: 'en_US',
images: [{
url: `/og/${params.slug}.png`, // resolved against metadataBase
width: 1200,
height: 630,
alt: post.title,
}],
},
twitter: { card: 'summary_large_image', site: '@example' },
};
}Setting metadataBase is what lets you write relative image paths and still emit absolute URLs. Without it, Next warns and your og:image may end up relative β which scrapers ignore.
Next.js β Pages Router
import Head from 'next/head';
export default function Post({ post }) {
const url = `https://example.com/blog/${post.slug}`;
return (
<>
<Head>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
<meta property="og:url" content={url} />
<meta property="og:type" content="article" />
<meta property="og:image" content={`https://example.com/og/${post.slug}.png`} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
</Head>
{/* ... */}
</>
);
}Make sure the page is server-rendered or statically generated. Tags added inside a useEffect, or on a page that only fetches its data client-side, will not be in the HTML the scraper receives.
WordPress
WordPress core does not output Open Graph tags. An SEO plugin β Yoast, Rank Math or SEOPress β adds them, and one of them is almost certainly already installed.
- Yoast: SEO β Social β Facebook, enable "Add Open Graph meta data". Per post, use the Social tab in the Yoast box below the editor.
- Rank Math: Titles & Meta β Social Meta, then the Social tab per post.
- Set a site-wide default image so pages without their own still get a card.
Without a plugin, add them in your theme β but only in a child theme, or a parent theme update will erase the change:
// functions.php in a CHILD theme
add_action('wp_head', function () {
if (!is_singular()) return;
global $post;
$title = get_the_title($post);
$desc = wp_trim_words(strip_tags($post->post_content), 25);
$image = get_the_post_thumbnail_url($post, 'full')
?: 'https://example.com/og/default.png';
printf('<meta property="og:title" content="%s">' . "\n", esc_attr($title));
printf('<meta property="og:description" content="%s">' . "\n", esc_attr($desc));
printf('<meta property="og:image" content="%s">' . "\n", esc_url($image));
printf('<meta property="og:url" content="%s">' . "\n", esc_url(get_permalink($post)));
echo '<meta property="og:type" content="article">' . "\n";
echo '<meta name="twitter:card" content="summary_large_image">' . "\n";
});og:image tags, and which one wins varies by scraper. If your preview is inconsistent across platforms, count the tags in the served HTML first.Shopify
Edit theme.liquid and add the tags inside <head>, branching on template type so products get their own image and price context:
<!-- layout/theme.liquid, inside <head> -->
<meta property="og:site_name" content="{{ shop.name }}">
<meta property="og:url" content="{{ canonical_url }}">
{%- if template contains 'product' -%}
<meta property="og:type" content="product">
<meta property="og:title" content="{{ product.title | escape }}">
<meta property="og:description" content="{{ product.description | strip_html | truncate: 150 | escape }}">
<meta property="og:image" content="https:{{ product.featured_image | image_url: width: 1200 }}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
{%- else -%}
<meta property="og:type" content="website">
<meta property="og:title" content="{{ page_title | escape }}">
<meta property="og:description" content="{{ page_description | default: shop.description | escape }}">
<meta property="og:image" content="https:{{ 'og-default.png' | asset_url }}">
{%- endif -%}
<meta name="twitter:card" content="summary_large_image">Note the literal https: prefix β Shopify's image filters return protocol-relative URLs beginning with //, which some scrapers will not resolve.
Static HTML sites
Paste the tags directly into each page's <head>. If you use a static site generator, put them in the base template and drive them from front matter:
<!-- Jekyll / Liquid base layout -->
<meta property="og:title" content="{{ page.title | default: site.title }}">
<meta property="og:description" content="{{ page.description | default: site.description }}">
<meta property="og:url" content="{{ page.url | absolute_url }}">
<meta property="og:image" content="{{ page.image | default: '/og/default.png' | absolute_url }}">
<meta name="twitter:card" content="summary_large_image">
<!-- Hugo -->
<meta property="og:title" content="{{ .Title }}">
<meta property="og:description" content="{{ .Description }}">
<meta property="og:url" content="{{ .Permalink }}">
<meta property="og:image" content="{{ .Params.image | default "/og/default.png" | absURL }}">The absolute_url and absURL filters matter β they are what turn /og/default.png into a full URL a scraper can fetch.
Step 4: Add a Fallback Image
Every page should end up with an image, even ones nobody thought about. Set a site-wide default in your template and override it per page:
<!-- Pattern: page-specific if present, site default otherwise -->
<meta property="og:image" content="{{ page.ogImage || 'https://example.com/og/default.png' }}">A generic branded card is much better than no card. Links without images get a fraction of the visual space in a feed.
Step 5: Verify a Crawler Sees Them
This is the step people skip, and it is the one that catches real problems. The Elements panel in DevTools shows the DOM after JavaScript has run β which is not what a scraper receives. Fetch the raw HTML instead:
# Exactly what a scraper sees β no JavaScript, no browser
curl -sL -A "facebookexternalhit/1.1" https://example.com/page \
| grep -iE '<meta[^>]+(og:|twitter:)'
# Confirm there is only ONE og:image
curl -sL https://example.com/page | grep -c 'property="og:image"'
# Confirm the image itself is reachable
curl -sI https://example.com/og/build-times.png | head -1If the first command prints nothing but the tags appear in DevTools, your tags are being injected client-side and no platform will ever see them. That is the single most common cause of a page with seemingly perfect markup sharing as a bare URL.
The Open Graph Checker does the same fetch, lists every tag with its length and status, and renders the card as four platforms will draw it.
Step 6: Force a Re-Scrape
Platforms cache scrape results for hours or days, keyed by exact URL. After adding tags, the old preview will keep appearing until you force a refresh:
- Facebook: Sharing Debugger β Scrape Again
- LinkedIn: Post Inspector
- X: the card renders fresh on new posts; the old validator is retired
- Slack and Discord: re-scrape on their own within minutes to hours
Frequently Asked Questions
Which Open Graph tags are actually required?
The protocol names og:title, og:type, og:image and og:url as required. In practice platforms are forgiving β most will build a card from og:title and og:image alone β but supplying all four plus og:description gives you full control.
Do Open Graph tags help SEO?
Not directly. Google does not use og: tags as a ranking signal and reads <title> and <meta name="description"> instead. The indirect effect is real: links that render as rich cards get shared and clicked substantially more.
Do I need Twitter Card tags as well?
Mostly no β X falls back to your og: tags. The one exception is twitter:card="summary_large_image", without which X shows a small square thumbnail rather than the full-width card.
Can I add the tags with Google Tag Manager?
No. GTM injects tags with JavaScript after the page loads, and link scrapers do not execute JavaScript. The tags must be in the HTML your server sends.
My tags are correct but the preview has not changed. Why?
Almost always a cached scrape. Platforms hold results for hours or days keyed by URL. Confirm the tags with curl, then run the URL through the platform's debugger to force a fresh fetch.
Should each page have its own og:image?
Ideally yes for content pages β a specific image outperforms a generic one. But a site-wide default on every page beats per-page images on some pages and nothing on the rest, so start with the fallback and add specific images where they matter.