OGKit

The Open Graph and Twitter meta tags you actually need

  • open-graph
  • meta-tags
  • twitter-cards

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:url matches rel=canonical on purpose.
  • Open Graph uses property="og:...". Twitter Cards conventionally use name="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:titleog-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:typeog-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:urlog-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:imageog-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:descriptionog-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_nameog-site-name-missing

Publisher label on several platforms. Use the brand, not the page title.

<meta property="og:site_name" content="Acme" />

twitter:cardtwitter-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:

  1. name vs property — OG must use property="og:...". name="og:title" is a common paste error; major parsers ignore it (meta-attribute-wrong).
  2. Conflicting duplicates — theme + plugin + app each emit og:title with 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 snippet
  • og:* (+ 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:card in raw HTML
  • Absolute HTTPS for og:url and og:image; image returns 200 without auth
  • og:url matches rel=canonical
  • One authoritative value per og:* / twitter:* key
  • OG tags use property=; Twitter card uses name=
  • 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.