Next.js Link Preview Metadata
Configure OpenGraph and Twitter Card metadata in Next.js so shared links render preview cards with absolute URLs, titles, descriptions, and images.
Overview
The problem applies to any Next.js application (App Router or Pages Router) whose shared links render without a title, description, or thumbnail when pasted into Slack, WhatsApp, LinkedIn, X, iMessage, or similar platforms.
After following this article you will be able to:
What you'll learn
Confirm whether OpenGraph and Twitter Card metadata is present and correctly formed
Configure metadataBase so relative metadata URLs resolve to absolute URLs
Add the metadata exports for a static site generation at build time
Generate sitemap.xml and robots.txt during the build
Not covered
This article does not cover dynamic metadata for server-rendered pages, client-side share buttons, or platform-specific caching behavior beyond the basics needed to diagnose a missing preview card.
Key concepts
Link previews and crawlers
When you paste a link into a messaging or social platform, that platform runs a crawler against the URL. The crawler reads the page's HTML head, extracts standardized metadata, and builds the preview card. If the required tags are missing, the platform falls back to showing the bare URL.
The two metadata standards that matter are:
| Standard | Purpose | Key tags |
|---|---|---|
| OpenGraph | Used by Facebook, LinkedIn, WhatsApp, Slack, and most others | og:title, og:description, og:image, og:url, og:type |
| Twitter Cards | Used by X (formerly Twitter) | twitter:card, twitter:title, twitter:description, twitter:image |
metadataBase
Setting metadataBase to your canonical production origin makes all relative metadata URLs resolve to absolute URLs consistently, regardless of how the page is requested.
Static metadata vs. dynamic metadata
Prerequisites
Before following this article, you will need:
- A Next.js application using the App Router (
app/directory) - Access to the root layout file (
app/layout.tsxorapp/layout.js) - A deployable production URL you control
- An image file for the preview card (recommended 1200 × 630 px) with the file committed to your repository or hosted on your domain
Diagnosis
Check what the crawler sees
Use a link preview debugger to fetch the URL as a crawler would:
- Facebook Sharing Debugger
- LinkedIn Post Inspector
- X Card Validator
- Slack: paste the URL into any channel and observe the preview
Alternatively, fetch the page's HTML directly:
curl -s <https://your-domain.example/path> | grep -o '<meta[^>]*>' | head -50
Procedure
The steps below configure metadata in the root layout so it applies to every page in the application.
Step 1 — Configure metadataBase
-
1
Configure metadataBase
Open the root layout file,
app/layout.tsx. AddmetadataBaseto the metadata export: -
2
Add social metadata
Extend the metadata export with the social metadata fields:
-
3
Add per-page metadata
The root layout metadata becomes the default for all routes. For pages that need a distinct title or description — for example article pages or product pages — export metadata from that page's
page.tsx: -
4
Generate sitemap and robots
Create
app/sitemap.ts: -
5
Rebuild and deploy
Run a production build:
-
6
Verify result
Re-run the curl check from Diagnosis and confirm the
og:andtwitter:tags are now present with absolute URLs:
import type { Metadata } from 'next';
export const metadata: Metadata = {
metadataBase: new URL('<https://your-domain.example>'),
// other metadata fields go here
};
Replace https://your-domain.example with your canonical production origin. Do not include a trailing path — only the origin.
Step 2 — Add OpenGraph and Twitter metadata
import type { Metadata } from 'next';
export const metadata: Metadata = {
metadataBase: new URL('<https://your-domain.example>'),
title: {
default: '<Your Page Title>',
template: '%s | <Your Site Name>',
},
description: '<Your site or page description, one or two sentences.>',
openGraph: {
title: '<Your Page Title>',
description: '<Your site or page description, one or two sentences.>',
url: '/',
siteName: '<Your Site Name>',
images: [
{
url: '/og-image.png',
width: 1200,
height: 630,
alt: '<Description of the image>',
},
],
locale: 'en_US',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: '<Your Page Title>',
description: '<Your site or page description, one or two sentences.>',
images: ['/og-image.png'],
},
};
Notes on the fields:
openGraph.urlandimages[].urlare relative paths here. Next.js resolves them againstmetadataBasefrom Step 1.twitter.card: 'summary_large_image'produces a large image preview.'summary'produces a small thumbnail.localeshould match your content language, e.g.en_US,de_DE,fr_FR.- The image path in
openGraph.imagesandtwitter.imagescan be the same file.
Step 3 — Add per-page metadata where needed
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '<Specific Page Title>',
description: '<Specific page description>',
};
Step 4 — Generate sitemap.xml and robots.txt at build
Next.js generates these files automatically when you export a sitemap.ts and a robots.ts at the app/ root. They also use metadataBase to resolve absolute URLs.
import type { MetadataRoute } from 'next';
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: '<https://your-domain.example>',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 1,
},
{
url: '<https://your-domain.example/about>',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
// Add the remaining public routes
];
}
For a site with many routes, build this list from your data source or content collection rather than maintaining it by hand:
import type { MetadataRoute } from 'next';
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = '<https://your-domain.example>';
const posts = await getAllPosts(); // your data-fetching function
const postRoutes = posts.map((post) => ({
url: `${baseUrl}/posts/${post.slug}`,
lastModified: post.updatedAt,
}));
return [
{ url: baseUrl, lastModified: new Date() },
...postRoutes,
];
}
Create app/robots.ts:
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/admin', '/private'],
},
sitemap: '<https://your-domain.example/sitemap.xml>',
};
}
Replace /admin and /private with routes you do not want indexed. If all routes are public, use:
rules: {
userAgent: '*',
allow: '/',
}
Step 5 — Rebuild and deploy
npm run build
Confirm the build output includes /sitemap.xml and /robots.txt, then deploy. Link preview crawlers will only see the new metadata once the deployed site serves it.
Step 6 — Verify the result
curl -s <https://your-domain.example> | grep -o '<meta[^>]*property="og:[^"]*"[^>]*>'
Then re-test the URL in the link preview debugger for the platform where the preview was missing. If the platform cached the old page, force a refresh in the debugger (Facebook and X both offer a "Scrape again" or similar option) before rechecking.
Expected behavior
After the fix:
- The page HTML includes
og:title,og:description,og:image,og:url, andtwitter:cardmeta tags - All image and URL values are absolute, prefixed with your production origin
https://your-domain.example/sitemap.xmlreturns a valid XML sitemaphttps://your-domain.example/robots.txtreturns the rules and references the sitemap- Pasting the link into a supported platform produces a card with title, description, and image
Troubleshooting
The preview still shows a bare URL after deploying
Bare URL
Likely cause: The platform cached the old version of the page before the metadata existed.
Missing image
Likely cause: The og:image URL is inaccessible to the crawler, or the image file is too large or has an unsupported format.
Relative tags
Check: Inspect the rendered HTML and look at the content attribute of the og:image tag. If it is /og-image.png instead of https://your-domain.example/og-image.png, the metadataBase is not applied.
Platform differences
Note: Slack and WhatsApp use different crawlers with different caching policies and image requirements. A card that works in one may be delayed or rejected by another. Verify the tags are standard and the image meets the most restrictive requirements, then allow time for the cache to refresh.
Check: Fetch the live page and confirm the meta tags are actually present in the response.
Resolution: Force a refresh in the platform's debugger tool — Facebook Sharing Debugger and X Card Validator both provide a scrape/refresh action. Platforms can cache previews for hours or days, so re-test after a refresh. Also confirm the deployed domain matches metadataBase exactly.
The image does not appear in the preview
Check: Open the og:image URL directly in a browser. Confirm the server responds with 200 and a Content-Type such as image/png or image/jpeg. Verify the image is at least 600 × 315 px (1200 × 630 px is recommended) and under roughly 8 MB.
Resolution: Correct the image path or replace the file. Ensure the image is served from your own domain and is not blocked by authentication or a robots.txt disallow rule.
Metadata tags are present but relative
Resolution: Confirm metadataBase is set on the metadata object in the root layout with the full origin (including https://) and no trailing slash. Rebuild and redeploy after the change.
Scope and limitations
- This configuration covers statically generated pages. For server-rendered or dynamically generated routes, use
generateMetadatainstead of a staticmetadataexport. - Some platforms generate previews from the JSON-LD structured data instead of OpenGraph tags. This article only covers OpenGraph and Twitter Cards; if a specific platform ignores these tags, consult that platform's documentation.
- Preview caches are controlled by the platform, not by your server. You cannot force immediate refresh on all platforms.
metadataBaseonly controls metadata URL resolution. It does not change canonical URLs, redirects, or other SEO behavior unless you configure those separately.
Related
- Next.js metadata documentation — OpenGraph and Twitter Card field reference
- Next.js sitemap and robots file generation
- Dynamic metadata with
generateMetadatafor route-specific pages
Was this article helpful? Thanks for your feedback.
Ready to build your first automation?
Get started with Octacer and transform how your team works.
Schedule Consultation