Why Open Graph Tags Don’t Update When Shared

You set per-route Open Graph tags in your single-page app, but when someone shares the link the preview shows the wrong title and image — or nothing. The reason is simple and absolute: social scrapers fetch the raw HTML and do not run JavaScript, so any og: tag your client injects after load does not exist for them. They see only the shell’s default tags. Fixing this means putting Open Graph data in the HTTP response, which is the scraper-facing half of dynamic Open Graph and Twitter card injection.

Why the browser and the scraper see different tags The browser runs JavaScript and sees the updated Open Graph tags, but the social scraper skips JavaScript and reads only the shell defaults, then caches that wrong preview. Same URL, two readers — only one runs your JavaScript Browser (the user) Loads shell JS executes og: tags correct preview would be right ✓ Scraper (the share bot) Fetch raw HTML no JavaScript JS skipped step never runs Reads shell defaults wrong / blank preview ✗ The platform caches the scrape — the wrong preview persists until you force a re-scrape
The scraper's lane never reaches your client-side update, so it captures the shell defaults — and caches them until you re-scrape.

Step-by-step fix

  1. Confirm scrapers see no OG tags. Fetch the URL the way a scraper does — without JavaScript.

    curl -sL https://example.com/blog/post | grep 'og:'
    # ❌ Returns only the shell defaults → client-injected tags are invisible to scrapers
  2. Render per-route OG tags server-side or in prerendered HTML. The tags must be in the response body, not added later by the client.

    <!-- ✅ In the server response for /blog/post -->
    <meta property="og:title" content="The Real Post Title">
    <meta property="og:description" content="A specific summary of this post.">
    <meta property="og:image" content="https://example.com/og/post.png">
    <meta property="og:url" content="https://example.com/blog/post">
What the scraper receives before and after the fix Before the fix the raw HTML response carries only generic shell defaults; after the fix the response carries per-route Open Graph tags the scraper can read. What arrives in the raw HTML response Before — client injects OG og:title = "My App" og:image = /default-cover.png generic shell defaults ✗ After — server-rendered OG og:title = "The Real Post Title" og:image = /og/post.png per-route tags the scraper reads ✓
Move the tags from client JavaScript into the response body and the scraper finally sees the real title and image.
  1. Generate dynamic preview images at request or build time so each route has a real image — see dynamic OG image generation for blog SPAs.

  2. Re-scrape to refresh the cache. Platforms cache previews; use their debugger to force a fresh scrape after deploying.

    ❌ Share the link again and hope → platform serves the stale cached preview
    ✅ Run the platform's sharing debugger on the URL → re-scrapes and refreshes

Validation

  • curl … | grep 'og:' returns the route’s real OG tags without JavaScript.
  • Platform sharing debuggers show the correct title, description, and image.
  • A fresh share in a private channel renders the right preview.
  • GSC URL Inspection rendered HTML matches the OG values for consistency.
Why the preview stays stale until you force a re-scrape A cached stale preview stays stale after you deploy correct tags; only running the platform's sharing debugger to re-scrape refreshes it to the correct preview. Deploying the fix is not enough — the cache must be refreshed Cached stale preview wrong title / image Still stale cache unchanged Fresh preview ✓ correct title / image deploy fixed tags run re-scrape Just re-sharing the link keeps serving the cached copy ✗
Correct tags in the response fix new scrapes, but existing shares stay stale until the platform's debugger forces a fresh scrape.

Reference

<!-- Per-route OG + Twitter tags, server-rendered so scrapers see them -->
<meta property="og:type" content="article">
<meta property="og:title" content="{{ post.title }}">
<meta property="og:description" content="{{ post.summary }}">
<meta property="og:image" content="{{ post.ogImage }}">
<meta property="og:url" content="{{ post.url }}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ post.title }}">
<meta name="twitter:image" content="{{ post.ogImage }}">
The server-rendered tag set every shared route needs Open Graph supplies type, title, description, image, and url, while the Twitter card block adds card type, title, and image; all must sit in the response body for scrapers. A complete per-route tag set in the response body Open Graph og:type = article og:title = the real post title og:description = specific summary og:image = /og/post.png og:url = the canonical route Twitter Card twitter:card = summary_large_image twitter:title = the real post title twitter:image = /og/post.png mirrors the Open Graph image rendered, not client-injected all eight tags live in the HTTP response so scrapers read them without JavaScript
The reference set — five Open Graph tags and three Twitter tags — must ship in the response body so no-JavaScript scrapers can read every value.

Frequently Asked Questions

Why is my social share preview blank or wrong? Social scrapers like Facebook’s and Slack’s fetch the raw HTML and do not execute JavaScript, so Open Graph tags injected by your SPA after load are invisible to them. They see only the shell’s default tags. The fix is to render og: tags server-side or in prerendered HTML.

Why does the preview stay wrong after I fix the tags? Platforms cache the scraped preview. After deploying correct server-rendered tags, use the platform’s sharing debugger to re-scrape the URL and refresh its cached image, title, and description.

← Back to Dynamic Open Graph and Twitter Card Injection