Guide
How to add Open Graph tags in Next.js, Astro, Hugo, and WordPress
Every framework has its own place for share tags, and each one makes a different mistake easy: a relative image path, a child page that wipes the parent's image, or two plugins printing competing tags. Here is the setup per stack, and the one test that tells you it worked.
The tags you are aiming for
Whatever generates them, the server HTML for each page should end up with this in <head>:
<meta property="og:title" content="Page title">
<meta property="og:description" content="One or two sentences.">
<meta property="og:url" content="https://example.com/blog/post">
<meta property="og:type" content="article">
<meta property="og:image" content="https://example.com/og/post.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="What the image shows">
<meta name="twitter:card" content="summary_large_image">
On a plain HTML site, paste that block and you are done. Note the absolute https:// URLs; see absolute OG image URL for why relative paths fail.
Next.js (App Router)
Export metadata from a server layout or page. Set metadataBase once in the root layout so relative url and image values come out absolute.
// app/layout.tsx
export const metadata = {
metadataBase: new URL("https://example.com"),
openGraph: { siteName: "Example", type: "website" },
twitter: { card: "summary_large_image" },
};
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.summary,
openGraph: {
title: post.title,
description: post.summary,
url: `/blog/${slug}`,
type: "article",
images: [{ url: `/og/${slug}.png`, width: 1200, height: 630, alt: post.title }],
},
};
}
- Child pages replace, not merge. A page's
openGraphobject replaces the layout'sopenGraphobject completely. If the page sets a title but noimages, the image from the layout is gone. Repeat it, or spread a shared base object. - File convention. An
opengraph-image.png(or.jpg) in a route folder becomes that route'sog:imagewith width and height filled in. Anopengraph-image.tsxcan generate one withImageResponse. - Client components cannot export
metadata. Keep it in a server file, or the tags never reach the HTML.
Next.js (Pages Router)
Use next/head in the page, and fill it from getStaticProps or getServerSideProps so the values are in the first HTML response:
import Head from "next/head";
export default function Post({ post }) {
const url = `https://example.com/blog/${post.slug}`;
return (
<>
<Head>
<meta property="og:title" content={post.title} key="og:title" />
<meta property="og:description" content={post.summary} key="og:description" />
<meta property="og:url" content={url} key="og:url" />
<meta property="og:image" content={`https://example.com/og/${post.slug}.png`} key="og:image" />
<meta name="twitter:card" content="summary_large_image" key="twitter:card" />
</Head>
{/* page */}
</>
);
}
The key props stop a default in _app and the page's tag from both rendering. Data fetched in useEffect arrives too late for crawlers; see JavaScript / SPA previews.
Astro
Set site in astro.config.mjs (for example site: "https://example.com"), then build absolute URLs from it in your layout:
---
// src/layouts/Base.astro
const { title, description, image = "/og.png" } = Astro.props;
const pageUrl = new URL(Astro.url.pathname, Astro.site);
const imageUrl = new URL(image, Astro.site);
---
<head>
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={pageUrl} />
<meta property="og:image" content={imageUrl} />
<meta name="twitter:card" content="summary_large_image" />
</head>
Without site, Astro.site is undefined and you are back to relative paths. Pass title, description, and image from each page or from content collection front matter.
Hugo
Hugo ships Open Graph and X card templates. Call them from your head partial:
{{ partial "opengraph.html" . }}
{{ partial "twitter_cards.html" . }}
Older Hugo versions use {{ template "_internal/opengraph.html" . }} and {{ template "_internal/twitter_cards.html" . }} instead. The image comes from the page's images front matter, then a page resource named like *feature*, *cover*, or *thumbnail*, then the first entry in params.images in your site config. Set baseURL to your real https:// domain, or every absolute URL points at the wrong host.
WordPress
An SEO plugin such as Yoast SEO or Rank Math prints Open Graph and X card tags for every post. Set a social image per post in the plugin's panel, and a site-wide default for pages without one.
- Keep one source. If the theme, an SEO plugin, and a sharing plugin all print tags, crawlers see two
og:imagevalues and may pick the wrong one. Turn Open Graph off everywhere but one place. - No plugin? Print the block from the top of this page in your theme's
wp_headoutput, filled from the post's title, excerpt, permalink, and featured image URL. - Caching plugins can keep serving old HTML after you change tags. Purge the page cache, then refresh platform caches with clear Open Graph cache.
Confirm the tags are in the server HTML
Your browser's inspector shows the DOM after JavaScript runs, which is not what crawlers read. Check the raw response instead:
curl -sL https://example.com/blog/post | grep -iE 'og:|twitter:'
You want exactly one of each tag, absolute https:// URLs, and the page's own values rather than the site default. If the output is empty but the tags show in the browser, they are being added client-side. If curl gets a challenge page, see crawler blocked.
Checklist before you ship
- Set your real domain once:
metadataBasein Next.js,sitein Astro,baseURLin Hugo, Site Address in WordPress. - Give each page its own title, description,
og:url, and image; check that a page-level override didn't drop the image. - Use one generator of share tags, so there's one
og:imagein the HTML. - Use a 1200×630 PNG or JPEG; see OG image size and OG image format.
- Run the
curlcheck above on a real deployed URL, not localhost. - Verify with CardScope to see the reconstructed card per client and what to fix, then refresh caches.
FAQ
How do I add Open Graph tags in the Next.js App Router?
Export a metadata object (or generateMetadata for dynamic routes) from a server layout or page, with an openGraph block and a twitter block. Set metadataBase in the root layout so relative image and url values become absolute. You can also drop an opengraph-image.png file into a route folder.
Why did my Next.js page lose its og:image?
Next.js merges metadata shallowly. If a page defines its own openGraph object, it replaces the parent layout's openGraph entirely, including images. Repeat the image in the page's block, or share a base object and spread it.
How do I add Open Graph tags in Hugo?
Call {{ partial "opengraph.html" . }} and {{ partial "twitter_cards.html" . }} in your head partial (older versions use the _internal/ template form). Set images in front matter or params.images in your site config, and make sure baseURL is your real https:// domain.
Why does my WordPress site have two sets of og tags?
More than one plugin or the theme is printing them, for example an SEO plugin plus a social or sharing plugin. Keep one source of Open Graph tags and turn the feature off in the others, then view the page source to confirm one og:title and one og:image.
What does a CardScope check cost?
Link-card previews and the fix checklist are Pro: $19 once via Stripe Checkout. No account. We parse HTML in memory and discard it — we do not store the page.
Check a URL on CardScope
Paste a deployed page. CardScope reads the tags crawlers see in the server HTML, fetches the og:image, and shows a reconstructed card for X, LinkedIn, Slack, Discord, and iMessage with a fix checklist. Previews and the fix checklist unlock with Pro ($19 once) via Stripe Checkout. No account. We parse HTML in memory and discard it — we do not store the page.