Open Graph, Meta Tags, and Social Crawlers

When someone shares your URL on Slack, posts it on LinkedIn, or quotes it in a Tweet, a crawler visits your page before any human sees it. This crawler — operated by Facebook, LinkedIn, Twitter/X, Slack, or whichever platform processed the share — is a different kind of consumer than Googlebot. It doesn't build an index, doesn't follow links, and doesn't come back for a second visit. It makes one HTTP request, parses the <head>, and uses whatever it finds to construct a link preview card.

If your Open Graph tags aren't in the raw HTML of that initial response, users sharing your content see a blank card with no image, no title, and no description. The link looks like spam. Click-through rates on well-optimized Open Graph implementations run approximately 40% higher than unoptimized ones — not because the content is different, but because the preview communicates enough to warrant a click.

The Open Graph Protocol

Open Graph was introduced by Facebook in 2010 and has since become the de facto standard for link preview metadata, adopted by essentially every platform that renders link previews. The required properties for any page:

<head>
  <!-- Required Open Graph properties -->
  <meta property="og:title" content="How We Cut API Latency by 60% Using Edge Caching" />
  <meta
    property="og:description"
    content="A deep dive into our CDN-first architecture that reduced p99 latency from 800ms to 320ms across all regions."
  />
  <meta property="og:image" content="https://example.com/images/blog/edge-caching-og.jpg" />
  <meta property="og:url" content="https://example.com/blog/edge-caching-latency" />
  <meta property="og:type" content="article" />

  <!-- Recommended additions -->
  <meta property="og:image:width" content="1200" />
  <meta property="og:image:height" content="630" />
  <meta
    property="og:image:alt"
    content="Diagram showing p99 latency drop from 800ms to 320ms after CDN implementation"
  />
  <meta property="og:site_name" content="Example Engineering Blog" />
</head>

The og:image requirements are specific and frequently violated: minimum 1200×630 pixels, absolute URL using https://, and served from a publicly accessible URL (not behind authentication or IP allowlisting). Facebook and LinkedIn cache images aggressively — if you deploy a new image at the same URL, you need to explicitly flush their caches using platform-specific debugger tools.

The og:type property determines how platforms interpret the content:

  • website — default; appropriate for homepages, landing pages, and any non-article content
  • article — for blog posts, news articles, documentation pages; unlocks article-specific properties
  • product — for product pages; unlocks price and availability properties
  • video.movie, video.episode — for video content; unlocks video player embeds on some platforms

Article-Specific Metadata

When og:type is article, additional properties become available that platforms use for sorting and filtering in feeds:

<head>
  <meta property="og:type" content="article" />
  <meta property="article:published_time" content="2026-03-15T09:00:00Z" />
  <meta property="article:modified_time" content="2026-04-02T14:30:00Z" />
  <meta property="article:author" content="https://example.com/authors/jane-smith" />
  <meta property="article:section" content="Engineering" />
  <meta property="article:tag" content="performance" />
  <meta property="article:tag" content="caching" />
  <meta property="article:tag" content="CDN" />
</head>

The article:modified_time property is particularly important for technical content that gets updated. Platforms and AI systems that consume this metadata can surface "recently updated" signals in their interfaces, increasing the likelihood that a revised post gets reshared.

Twitter Cards

Twitter/X uses its own metadata system (twitter:* properties) that largely mirrors Open Graph but provides Twitter-specific preview controls:

<head>
  <!-- Twitter Card metadata -->
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:site" content="@YourBrand" />
  <meta name="twitter:creator" content="@AuthorHandle" />
  <meta name="twitter:title" content="How We Cut API Latency by 60% Using Edge Caching" />
  <meta name="twitter:description" content="A deep dive into our CDN-first architecture..." />
  <meta name="twitter:image" content="https://example.com/images/blog/edge-caching-og.jpg" />
  <meta name="twitter:image:alt" content="Diagram showing latency improvement" />
</head>

Twitter reads Open Graph tags as a fallback when twitter:* tags are absent, so if you've implemented OG fully, Twitter cards will work. The dedicated twitter:* tags let you customize the title and description specifically for Twitter — useful if your OG description is optimized for Facebook's longer preview format but Twitter's summary card cuts it short.

The twitter:card property controls the layout:

  • summary — small square image with text beside it
  • summary_large_image — full-width image above the text; best for editorial content with strong imagery
  • app — for app install cards
  • player — for embedded video/audio players

The SSR Requirement for Social Crawlers

This is where most React and Vue applications fail silently. Social crawlers are not browsers — they do not execute JavaScript. When Facebook's crawler visits your URL, it makes one HTTP GET request, parses the response body, and stops. There is no second wave of rendering, no JavaScript execution, no waiting for useEffect to run.

This means any application that injects <meta property="og:*"> tags using client-side JavaScript — including React Helmet in a traditional CSR setup, Vue Meta without SSR, or any pattern that updates document.head via JavaScript — will produce blank previews for every shared link.

The fix is server-side rendering for at minimum the <head> content. Options by framework:

Next.js App Router (recommended):

// app/blog/[slug]/page.tsx
import type { Metadata } from "next";

export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const post = await fetchPost(params.slug);

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: "article",
      url: `https://example.com/blog/${params.slug}`,
      publishedTime: post.publishedAt,
      modifiedTime: post.updatedAt,
      authors: [post.author.profileUrl],
      images: [
        {
          url: `https://example.com${post.ogImage}`,
          width: 1200,
          height: 630,
          alt: post.ogImageAlt,
        },
      ],
    },
    twitter: {
      card: "summary_large_image",
      site: "@YourBrand",
      creator: post.author.twitterHandle,
    },
  };
}

Next.js renders this metadata server-side, embedding it in the <head> before the HTML response leaves the server. Social crawlers, Googlebot, and every other consumer sees it in the initial HTML.

React with SSR (non-Next.js):

If you're using React with a custom SSR setup, libraries like react-helmet-async support server-side rendering when used with renderToString or renderToPipeableStream. The critical requirement is that helmet's renderStatic() is called server-side and the collected <head> content is injected into the HTML template before it's sent to the client:

// server.ts
import { renderToString } from "react-dom/server";
import { HelmetProvider, FilledContext } from "react-helmet-async";

const helmetContext: FilledContext = {} as FilledContext;
const appHtml = renderToString(
  <HelmetProvider context={helmetContext}>
    <App />
  </HelmetProvider>
);

const { helmet } = helmetContext;
const html = `
  <!DOCTYPE html>
  <html ${helmet.htmlAttributes.toString()}>
    <head>
      ${helmet.title.toString()}
      ${helmet.meta.toString()}
      ${helmet.link.toString()}
    </head>
    <body>
      <div id="root">${appHtml}</div>
    </body>
  </html>
`;

OG Image Generation

Static OG images require design overhead at scale — you need a unique, visually informative image for every blog post, product page, and landing page. The modern solution is server-side OG image generation using tools like @vercel/og (which uses Satori to render JSX to PNG at the edge) or sharp for Node.js-based generation:

// app/opengraph-image.tsx — Next.js App Router OG image generation
import { ImageResponse } from "next/og";

export const runtime = "edge";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";

export default async function Image({
  params,
}: {
  params: { slug: string };
}) {
  const post = await fetchPost(params.slug);

  return new ImageResponse(
    (
      <div
        style={{
          background: "linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)",
          width: "100%",
          height: "100%",
          display: "flex",
          flexDirection: "column",
          justifyContent: "flex-end",
          padding: "60px",
        }}
      >
        <div style={{ fontSize: 18, color: "#64ffda", marginBottom: 16 }}>
          {post.category}
        </div>
        <div
          style={{
            fontSize: 56,
            fontWeight: 700,
            color: "#e2e8f0",
            lineHeight: 1.2,
            maxWidth: 900,
          }}
        >
          {post.title}
        </div>
      </div>
    ),
    size
  );
}

This generates a unique, on-brand OG image for every post at build time (or on-demand at the edge), without requiring any manual design work per article.

Testing and Debugging

Each platform provides a debugger tool that fetches your URL as their crawler would and shows you the parsed metadata:

  • Facebook/LinkedIn: Facebook Sharing Debugger — enter your URL to see exactly what Facebook's crawler found, and trigger a cache refresh if you've updated the image or metadata
  • Twitter/X: Twitter Card Validator — preview how your card will render in Twitter feeds
  • LinkedIn: LinkedIn Post Inspector at linkedin.com/post-inspector/

After deploying any OG changes, run your URL through each relevant debugger. Cache invalidation is the most frequent issue — platforms cache OG images for days or weeks. The Facebook debugger's "Scrape Again" button forces a cache refresh; LinkedIn requires you to submit the URL through their inspector.

A development workflow that eliminates most OG issues: treat your <head> as a contract, verify it with curl before considering any page done, and add OG validation to your deployment checklist alongside functional testing. A URL that previews beautifully is a URL that gets shared.