Open Graph Image Stale After Vercel Deploy: Cache Fix

Open Graph image showing the old version after deploy on Vercel? Here is why Facebook, Slack, and LinkedIn keep the stale image and the cache-bust that works.
PerformanceSEONext.js
June 12, 20266 min read1085 words

The Problem

A client redesigned every Open Graph card across their blog. New typography, accent colour, background. We shipped on a Friday. On Monday, every link pasted into Slack or LinkedIn still previewed the old card. Facebook's debugger refused to scrape anything new. X kept the previous image. The page source was correct, the OG image URL responded with the right pixels, and every social platform still served the old card.

If you have shipped a new OG image and the world keeps showing the old one, this is the trap. Refreshing one platform does not fix the others. There are three caches in the path and each needs its own invalidation.

For a Next.js 16 dynamic OG route, the metadata typically looks like this:

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: Props) {
  const { slug } = await params
  return {
    openGraph: {
      images: [`https://qasimcode.com/blog/${slug}/opengraph-image`],
    },
  }
}

That URL is what the platforms scrape. And once they have a copy, they hold onto it.

Why It Happens

The OG image has three caches in front of it, and each one needs its own answer.

The first is the Vercel edge cache. A dynamic OG route from opengraph-image.tsx is treated as static after the first hit and held at the edge with a long TTL. The file path does not change on redeploy, so the cached object stays unless you bust it.

The second is the social platform's scraper cache. Facebook holds an OG image for up to 30 days, LinkedIn for 7, Slack for 24 hours through its own image proxy. Even if Vercel serves a fresh image, the platform will not re-scrape unless you ask.

The third is the messaging clients themselves. Slack, X, and LinkedIn each cache the bytes their scraper downloaded on their own CDN. Re-trigger a scrape and old clients can still show the previous thumbnail until that entry expires.

Most posts about this only fix one layer, which is why teams keep filing the same ticket every six months.

The Fix

Step 1: Version the OG image URL. The easiest, most durable fix is to change the URL whenever the image changes. Add a content hash or a version query string to the metadata:

// app/blog/[slug]/page.tsx
import { revisionFor } from '@/lib/og-version'

export async function generateMetadata({ params }: Props) {
  const { slug } = await params
  const v = await revisionFor(slug)
  return {
    openGraph: {
      images: [`https://qasimcode.com/blog/${slug}/opengraph-image?v=${v}`],
    },
  }
}

revisionFor() should return a hash that changes when the underlying post changes. For most blogs that is the post's updatedAt epoch or the content's SHA-1 truncated to 8 characters:

// lib/og-version.ts
import { createHash } from 'crypto'
import { getPostMeta } from '@/lib/posts'

export async function revisionFor(slug: string) {
  const post = await getPostMeta(slug)
  return createHash('sha1').update(post.updatedAt + post.title).digest('hex').slice(0, 8)
}

A new query string means the platform scraper sees a new URL, which means a new cache entry on its side and a fresh request through Vercel. The edge cache miss does the rest.

Step 2: Set explicit cache headers on the OG route. Even with versioned URLs, you want the route itself to declare its caching behaviour so unversioned old links eventually fall out:

// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'

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

export default async function Image({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = await getPost(slug)

  return new ImageResponse(<OgCard title={post.title} />, {
    ...size,
    headers: {
      'cache-control': 'public, max-age=300, s-maxage=86400, stale-while-revalidate=86400',
    },
  })
}

s-maxage=86400 lets the Vercel edge hold the image for a day, stale-while-revalidate keeps the route responsive while a background refresh runs, and max-age=300 on the browser stops old tabs from holding a fresh copy too long. The defaults from ImageResponse are more aggressive than this, which is part of why edge purging matters.

Step 3: Force a re-scrape on the platforms that need it. Once the new URL is live, ask each platform to refresh:

# Facebook / Meta: POST to the Sharing Debugger API
curl -X POST "https://graph.facebook.com/v23.0/?id=https%3A%2F%2Fqasimcode.com%2Fblog%2Fmy-post&scrape=true&access_token=$FB_TOKEN"

# LinkedIn: open the Post Inspector and re-submit
open "https://www.linkedin.com/post-inspector/inspect/https%3A%2F%2Fqasimcode.com%2Fblog%2Fmy-post"

X reads cards on demand from a fresh URL, so a versioned query string covers it. Slack honours og:image from the page on the first unfurl after the cached entry expires, which is 24 hours.

The Facebook scrape call is the one to automate. Run it from a Vercel deployment hook so every production deploy that touches an OG route also kicks the scraper. Their full debugger flow is documented in the Open Graph debugger.

Verify the fix. Hit the OG URL twice and check the cache header reports a hit on the second pass:

curl -I "https://qasimcode.com/blog/my-post/opengraph-image?v=$(date +%s)"
curl -I "https://qasimcode.com/blog/my-post/opengraph-image?v=fixed"
curl -I "https://qasimcode.com/blog/my-post/opengraph-image?v=fixed"

The first call with a unique version should report x-vercel-cache: MISS. The third (same version repeated) should report HIT. That confirms each variant is isolated and the edge caches repeat reads, which is the behaviour the social scrapers depend on.

The Lesson

A stale OG image after deploy is three caches stacked on top of each other, not a Next.js bug. Version the URL so scrapers see a new asset, set deliberate cache headers so the edge holds the version it has, and trigger a fresh scrape on the platforms that hold previews the longest. Skip any one of those and you will redo this fix on the next redesign.

If your OG cards are out of sync with your site and you want it fixed across Vercel, the platforms, and the messaging clients, that is the work I do. See my services. For a related production issue, read Next.js 16 dynamic OG image 404 in production.

Need OG previews that update the moment you ship? Get in touch.

Back to blogStart a project