Next.js Open Graph image not working: metadataBase and caching
Set metadataBase to the production domain, use opengraph-image, keep page openGraph objects from dropping the image, and test outside Vercel protection.
LLaunchScaler·Published ·8 min read
A Next.js Open Graph image usually fails for one of four reasons: metadataBase is missing or points at the wrong host, so the image URL is relative or unreachable; a page's own openGraph object replaces the layout's and silently drops its images; the link is tested on a Vercel preview URL that Deployment Protection hides behind a login; or the tags never reach the crawler's view of the <head>. Set metadataBase to your production domain, prefer the opengraph-image file convention, and check the served HTML with curl.
The fixes are small once you know which one applies. Start with the table, then read the section for the cause that matches.
Why is my Next.js OG image not working?
Run curl -s https://www.example.com/page | grep -io '<meta[^>]*og:image[^>]*>' against production and read the result. What you see, or do not see, points to the cause. The table maps each symptom to the Next.js behaviour behind it and the fix.
What curl shows
Cause
Fix
No og:image tag at all
The route's openGraph has no images, or a page openGraph object replaced the layout's
Spread shared images into the page, or add an file
Questions, answered
What people ask about this
01
Why is my Next.js Open Graph image not showing?
The usual causes are a missing or wrong metadataBase, so the image URL is relative or points at the wrong host; a page-level openGraph object that replaces the layout's and drops its images; testing on a Vercel preview URL behind Deployment Protection; or an image the crawler cannot fetch. Check the served head with curl to see which.
A hand-written <meta> tag or a head library; the Metadata API itself refuses relative paths without metadataBase at build time
Move the tag into the Metadata API and set metadataBase
og:image pointing at a *.vercel.app URL
metadataBase built from VERCEL_URL, a protected deployment URL
Hard-code or configure the production domain
og:image present, image URL returns 401 or a login page
Vercel Deployment Protection on that URL
Test on the production domain
og:image present, image URL returns 500
An opengraph-image.tsx route that throws
Fix the route; check its logs
Everything correct, preview still old
The platform's cached preview
Re-scrape on each platform
Do you need metadataBase for Open Graph images?
Yes, whenever an image path is relative. Next.js describes metadataBase as "a base URL prefix for metadata fields that require a fully qualified URL," composes relative paths with it, and warns that "using a relative path in a URL-based metadata field without configuring a metadataBase will cause a build error." Set it once in the root layout.
Three rules from the docs matter here. metadataBase applies to URL fields "defined in the current route segment and below," which is why the root layout is the place for it. It is ignored when a field already has an absolute URL. And trailing and duplicate slashes are normalised, so https://www.example.com/ plus /og.png resolves correctly.
Use the production domain, not a variable that changes per deployment. Vercel's documentation says that once Standard Protection is on, the generated deployment URL "becomes restricted," and for "operations requiring fully qualified URLs, such as generating OG images," it tells you to use the actual domain. A metadataBase built from VERCEL_URL produces image URLs that social crawlers cannot open.
How does the opengraph-image file convention work?
Put an opengraph-image.png (or .jpg, .jpeg, .gif) in a route segment, or an opengraph-image.tsx that generates one, and Next.js adds the og:image, og:image:type, og:image:width and og:image:height tags for that route itself. Keep metadataBase set in the root layout anyway, and note that "the more specific image will take precedence over any OG images above it in the folder structure."
The limits that break generated images, from the ImageResponse docs:
"Only flexbox and a subset of CSS properties are supported." A layout using display: grid "will not work."
A "maximum bundle size of 500KB," which "includes your JSX, CSS, fonts, images, and any other assets."
"Only ttf, otf, and woff font formats are supported." A woff2 file is not on that list.
The build enforces file size caps, failing when a twitter-image is over 5 MB or an opengraph-image is over 8 MB.
Generated images are "statically optimized" by default, built once and cached, unless they use request-time APIs or uncached data. If an image shows stale content after you change the post, check whether the route is being revalidated.
How do you set the image per page from generateMetadata?
When the image comes from data rather than a file, return it from generateMetadata. Build the openGraph object in full, because anything you leave out is dropped by the merge described below, and give the image its dimensions and alt text so crawlers can render it without downloading it first.
Two details from the Next.js docs apply here. post.coverUrl can be relative, because metadataBase in the root layout resolves it, or absolute, in which case metadataBase is ignored. And if generateMetadata uses 'use cache', its return value must be serialisable, so a metadataBase returned from that function must be a string such as url.toString() rather than a URL instance.
Why does a page lose the layout's Open Graph image?
Because Next.js merges metadata shallowly. Metadata objects from the layout and the page are "shallowly merged," and nested fields such as openGraph "defined in an earlier segment are overwritten by the last segment to define them." A page that sets only openGraph.title replaces the layout's whole openGraph object, images included.
The Next.js docs show exactly this case: a layout with openGraph: { title, description } and a page with openGraph: { title: 'Blog' } output only og:title, with "the absence of openGraph.description." Images disappear the same way.
Two fixes:
Share the image object and spread it into every page that sets openGraph:
Move the image to the file convention. An opengraph-image file is not part of the openGraph object you replace, and a more specific file overrides it where you want a different image.
The same shallow merge applies to twitter: a page that sets twitter.title alone loses the layout's twitter.card, so X can fall back to a small summary card.
Do you also need a twitter-image file?
Not for the image itself. X reads og:image when there is no twitter:image, so one opengraph-image file serves both. What X does need is a card type: without twitter:card, X can render a small summary card rather than the large image. Set it once in the root layout with twitter: { card: "summary_large_image" }, and remember that a page-level twitter object replaces that one too.
Add a twitter-image.png beside opengraph-image.png only when you want X to show a different image, for example one cropped for its 2:1 frame. Next.js outputs twitter:image, twitter:image:type, twitter:image:width and twitter:image:height for it, and a twitter-image.alt.txt file sets twitter:image:alt. The twitter-image file must stay under 5 MB or the build fails.
Why don't previews work on Vercel preview URLs?
Because they are protected. Vercel's Standard Protection "protects all domains except production domains," and with Vercel Authentication, visitors "will encounter a Vercel login redirect." Facebook's, LinkedIn's and X's crawlers have no Vercel login, so they get the login page instead of your tags and image.
This makes preview deployments a poor place to test link previews. Test on the production domain, or on a domain you have deliberately left unprotected. If you paste a *.vercel.app link into Slack or LinkedIn and see a Vercel login preview, that is Deployment Protection working as designed, not a metadata bug.
Are the Open Graph tags in the head the crawler reads?
For link-preview crawlers, Next.js puts them there. Since version 15.2, metadata from generateMetadata can stream into the <body> for browsers, but "for HTML-limited bots that can't execute JavaScript (e.g. facebookexternalhit), metadata continues to block page rendering" and lands in the <head>. Next.js detects those bots by user agent through its htmlLimitedBots setting.
Check what a crawler gets, using the command Meta publishes for its own crawler:
If you have overridden htmlLimitedBots in next.config.ts, make sure your pattern still matches the preview crawlers; the default list includes facebookexternalhit, Twitterbot, LinkedInBot, Slackbot, Discordbot and WhatsApp. Setting it to /.*/ disables streaming metadata for every visitor, which the docs say "could lead to longer response times."
How do you test the fix end to end?
Work from the served HTML outwards, on production:
curl the page with a crawler user agent and confirm an absolute og:image on your production domain.
curl -sI the image URL and confirm a 200 and content-type: image/png (or image/jpeg).
Open the image URL in a private window to confirm no login appears.
Re-scrape the URL in Facebook's Sharing Debugger and LinkedIn's Post Inspector, and paste it into the X composer.
If the image returns correctly and a platform still shows nothing, the cause is outside Next.js: a firewall, robots.txt or a platform cache. OG image not showing covers those for every platform. The same metadata files also set your favicon, which has its own rules for Google; favicon not showing in Google covers app/icon.png and app/favicon.ico. Apps generated with v0 are Next.js apps too, so everything here applies to them; v0 app SEO covers the rest of what they need.
Check the production page from the outside
Once the preview works, keep the page it depends on healthy. Run the free scan on LaunchScaler with your production URL, no account needed. It does not render social cards, but it checks conditions every preview crawler needs: markup that closes the <head> early and drops the tags below it, content that only exists after JavaScript runs, redirect chains, and an incomplete or expired certificate chain.
02
Do I need metadataBase in Next.js?
Yes, if any Open Graph or other URL-based metadata uses a relative path. Next.js composes relative paths with metadataBase into full URLs, and using a relative path without a metadataBase causes a build error. Set it in the root layout to your production domain.
03
Why does my page lose the Open Graph image set in the layout?
Next.js merges metadata shallowly. If a page sets its own openGraph object, every openGraph field from the layout is replaced, including images. Spread a shared object into the page's openGraph, or use an opengraph-image file, which the more specific route overrides.
04
Why do link previews fail on Vercel preview deployments?
With Standard Protection, Vercel protects every deployment except production domains, and visitors to a protected URL are sent to a Vercel login. A social crawler has no login, so it cannot read the page or the image. Test on the production domain.
05
How do I generate a dynamic OG image in Next.js?
Add an opengraph-image.tsx file to the route segment and return an ImageResponse from next/og, exporting size (1200 by 630) and contentType. It supports flexbox and a subset of CSS, fonts in ttf, otf or woff, and a 500 KB bundle including fonts and images.
Most missing OG images are fetch failures: a relative URL, a blocked crawler, a login wall or tags added by JavaScript. Check each, then clear the cache.
Use 1200 x 630 px (1.91:1), PNG or JPEG, under 5 MB, with width, height and alt declared. How Facebook, LinkedIn and X crop it, and where to keep text.
GDPR Article 13 lists what a SaaS privacy policy must say: who you are, each purpose and legal basis, every recipient, transfers, retention and rights.