How to Add Open Graph Tags to Your Site

Five tags turn a bare URL into a card with a headline, a summary and an image. This guide covers the markup, how to add it in the major platforms, and how to confirm a crawler actually sees it.


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.

Prerequisites

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.

html
<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 use name=. They are not interchangeable, and Facebook in particular is strict about it.
  • og:image and og:url must be absolute URLs starting with https://. Scrapers do not resolve relative paths against the page.
property, not name<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.

html
<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>
TagPurposeLength guidance
og:titleCard headlineUnder 60 characters; 88 is where most platforms truncate
og:descriptionSummary beneath the headline100–150 characters
og:imageThe card image1200 Γ— 630, absolute HTTPS URL, under 1 MB
og:urlCanonical addressWithout it, UTM variants split engagement counts
og:typeObject typewebsite 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.

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

Command
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:

php
// 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"; });
Two plugins means duplicate tagsRunning an SEO plugin alongside a theme that also emits Open Graph tags produces two 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:

html
<!-- 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:

html
<!-- 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:

html
<!-- 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:

bash
# 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 -1

If 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
Verify the source first, then the cacheConfirm the tags are correct in the served HTML before forcing a re-scrape. A cached bad preview and a genuinely broken tag look identical, and debugging in the wrong order wastes a lot of time.

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.

Related