The Open Graph and Twitter meta tags you actually need
You know you "need Open Graph tags." What usually goes wrong is not ignorance of the protocol — it is shipping a page that looks fine in DevTools while crawlers still see a relative image, a wrong name= attribute, two conflicting titles, or no Twitter Card at all.
This post is the minimal correct set: one HTML block you can paste, a field-by-field map of what breaks, how tags fall back to <title> / meta description / JSON-LD, a light framework note, and a self-check against stable rule ids.
For the wider workflow (preview vs official rescrape vs CI), start with What Open Graph actually solves. For blank cards, see Why your Open Graph card is blank.
Copy-paste minimum
Protocol required four from ogp.me: og:title, og:type, og:url, og:image. In production you almost always want og:description and og:site_name, plus twitter:card so X and a few other clients pick a layout.
<head>
<title>Ship notes · Acme</title>
<meta
name="description"
content="What changed in this release and why it matters."
/>
<link rel="canonical" href="https://example.com/blog/ship-notes" />
<!-- Open Graph (property=, not name=) -->
<meta property="og:title" content="Ship notes: what we fixed this week" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://example.com/blog/ship-notes" />
<meta
property="og:image"
content="https://example.com/og/ship-notes-1200x630.jpg"
/>
<meta
property="og:description"
content="What changed in this release and why it matters for teams sharing links."
/>
<meta property="og:site_name" content="Acme" />
<!-- Twitter / X -->
<meta name="twitter:card" content="summary_large_image" />
<!-- Optional if you do not want OG fallbacks:
<meta name="twitter:title" content="..." />
<meta name="twitter:description" content="..." />
<meta name="twitter:image" content="https://example.com/og/ship-notes-1200x630.jpg" />
-->
</head>
Hard rules in that block:
- Image and URL are absolute HTTPS (scheme + host + path).
og:urlmatchesrel=canonicalon purpose.- Open Graph uses
property="og:...". Twitter Cards conventionally usename="twitter:...". - One value per key. Do not let a theme, plugin, and framework each emit a different
og:title.
Field by field (and the anti-patterns)
og:title — og-title-missing
Headline of the card. Prefer a feed-readable line over a keyword-stuffed SEO string.
| Anti-pattern | What happens |
|---|---|
| Missing / empty | Platforms fall back to <title> or the bare URL |
Identical to <title> forever |
Tabs and feeds have different jobs — reuse is fine sometimes; defaulting to it wastes the chance to write for a scrolling human (og-title-matches-title, info) |
<!-- Weak: CMS slug as title -->
<meta property="og:title" content="ship-notes-q3-final-v2" />
<!-- Stronger: human headline -->
<meta property="og:title" content="Ship notes: what we fixed this week" />
og:type — og-type-missing
Tells crawlers what kind of object the URL is. Landing pages: website. Blog posts: article. Product pages: product when you mean it.
| Anti-pattern | What happens |
|---|---|
| Missing | Some rich-object handling degrades |
Invented values ("landing", "page") |
Treated as unknown; you gain nothing |
<meta property="og:type" content="website" />
<!-- or -->
<meta property="og:type" content="article" />
og:url — og-url-missing
Canonical share identity. This is how likes and unfurls group across http/https, trailing slashes, and tracking params.
| Anti-pattern | Rule / effect |
|---|---|
| Missing | Engagement fragments across variants |
Relative (/blog/post) |
og-url-relative |
http:// while the site is HTTPS |
og-url-not-https |
Differs from rel=canonical |
og-url-canonical-mismatch |
<!-- Broken -->
<meta property="og:url" content="/blog/ship-notes" />
<!-- Correct -->
<meta property="og:url" content="https://example.com/blog/ship-notes" />
og:image — og-image-missing
Without a usable image, many platforms ship a text-only row or no card chrome at all. Aim for a dedicated share asset (~1200×630, ~1.91:1), not a logo file.
| Anti-pattern | Rule / effect |
|---|---|
| Missing / empty | Blank or low-attention card |
| Relative path | og-image-relative-url — often dropped entirely |
| Not HTTPS | og-image-not-https |
Image engineering (bytes, redirects, content-type, crop risk) is a later post in this series. Get the absolute URL right first.
og:description — og-description-missing
Not in the protocol four, but a warn in practice. Without it, clients fall back to meta description or scrape body text — feed copy becomes a lottery.
Write one or two sentences for a human in a feed (roughly 70–200 characters as a starting budget; platforms truncate differently).
<!-- Empty CMS placeholder is worse than omitting carefully -->
<meta property="og:description" content="" />
og:site_name — og-site-name-missing
Publisher label on several platforms. Use the brand, not the page title.
<meta property="og:site_name" content="Acme" />
twitter:card — twitter-card-missing / twitter-card-invalid
X still keys layout off twitter:card. Valid values: summary, summary_large_image, app, player. Unknown values are treated as missing.
Most marketing and blog pages want summary_large_image.
<meta name="twitter:card" content="summary_large_image" />
Fallback reality: many clients will use og:title / og:description / og:image when the matching twitter:* tags are absent. That is convenient — and not a reason to skip twitter:card. Card type is not fully implied by OG alone.
If you need a different image or title on X only, set twitter:title, twitter:description, and twitter:image explicitly. Otherwise let OG carry the copy and still declare the card type.
Attribute and duplicate traps
Two bugs ship constantly:
namevsproperty— OG must useproperty="og:...".name="og:title"is a common paste error; major parsers ignore it (meta-attribute-wrong).- Conflicting duplicates — theme + plugin + app each emit
og:titlewith different values; crawlers pick first or last unpredictably (meta-tag-conflicting-duplicates).
<!-- Broken: wrong attribute -->
<meta name="og:title" content="Looks right in View Source until it does not" />
<!-- Broken: two winners -->
<meta property="og:title" content="From the CMS" />
<meta property="og:title" content="From the theme default" />
How tags fall back (title, description, JSON-LD)
Think of a fallback chain, not a single source of truth:
| Card field | Preferred | Common fallbacks |
|---|---|---|
| Headline | og:title |
<title>, then URL |
| Body text | og:description |
meta name="description", then scraped body text |
| Image | og:image |
Sometimes none — do not count on a logo heuristic |
| X layout | twitter:card |
Degraded / skipped without a valid type |
| X title / image | twitter:title / twitter:image |
Usually og:* when present |
JSON-LD (application/ld+json for Article, Product, etc.) helps search and rich results. It is not a reliable substitute for Open Graph on share cards. If a platform never looks at your JSON-LD for unfurl, a perfect schema block still leaves Slack with a blank image.
Practical split:
<title>+ meta description → browser tab and search snippetog:*(+twitter:card) → share surface- JSON-LD → structured data for engines that read it
Align them on facts (product name, date, URL). Do not assume one layer fills the others.
Framework notes (light touch)
Next.js Metadata
Next's metadata / generateMetadata API maps cleanly when you fill both openGraph and twitter:
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Ship notes · Acme",
description: "What changed in this release and why it matters.",
alternates: { canonical: "https://example.com/blog/ship-notes" },
openGraph: {
title: "Ship notes: what we fixed this week",
description:
"What changed in this release and why it matters for teams sharing links.",
url: "https://example.com/blog/ship-notes",
siteName: "Acme",
type: "article",
images: [
{
url: "https://example.com/og/ship-notes-1200x630.jpg",
width: 1200,
height: 630,
},
],
},
twitter: {
card: "summary_large_image",
// title / description / images fall back to openGraph when omitted
},
};
Watch for absolute image URLs (or a configured metadataBase), and verify the rendered HTML still has one value per property after layout merges.
CMS field mapping
| CMS field people fill | Emit as |
|---|---|
| SEO title / browser title | <title> |
| SEO description | meta name="description" |
| Share title (or "social title") | og:title (+ optional twitter:title) |
| Share description | og:description |
| Share image / social image | absolute og:image |
| Site / brand name (global) | og:site_name |
| Canonical URL | rel=canonical and og:url |
If the CMS only has "SEO title" and no share title, you will ship identical tab and feed headlines. That is acceptable as a default; it is not ideal for every launch post.
Self-check against rule ids
Run these mentally (or in CI) against the raw HTML a crawler would see:
| Check | Rule |
|---|---|
og:title present |
og-title-missing |
og:type present |
og-type-missing |
og:url present, absolute HTTPS |
og-url-missing, og-url-relative, og-url-not-https |
og:url ≡ canonical |
og-url-canonical-mismatch |
og:image present, absolute HTTPS |
og-image-missing, og-image-relative-url, og-image-not-https |
og:description present |
og-description-missing |
og:site_name present |
og-site-name-missing |
twitter:card present and valid |
twitter-card-missing, twitter-card-invalid |
OG uses property=, not name= |
meta-attribute-wrong |
| No conflicting duplicates | meta-tag-conflicting-duplicates |
Full catalog: rules index. Platform crop and truncation differences: platforms.
Checklist before you ship
- Protocol four + description + site name +
twitter:cardin raw HTML - Absolute HTTPS for
og:urlandog:image; image returns 200 without auth -
og:urlmatchesrel=canonical - One authoritative value per
og:*/twitter:*key - OG tags use
property=; Twitter card usesname= - Description is intentional copy, not an empty field
Scan a public URL
Paste a live page into the homepage scanner. You get findings keyed by the same rule ids above — missing required tags, relative images, wrong attributes, invalid Twitter cards — instead of a single "looks fine" mock.
Third-party previews remain approximations. Authoritative cache and fetch behavior still live in each platform's official debugger. Tags first; then preview; then rescrape when the first scrape cached a broken card.