Next.js Intermediate

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.

Next.js Intermediate Updated August 26, 2026

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

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.tsx or app/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:

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. 1

    Configure metadataBase

    Open the root layout file, app/layout.tsx. Add metadataBase to the metadata export:

  2. 2

    Add social metadata

    Extend the metadata export with the social metadata fields:

  3. 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. 4

    Generate sitemap and robots

    Create app/sitemap.ts:

  5. 5

    Rebuild and deploy

    Run a production build:

  6. 6

    Verify result

    Re-run the curl check from Diagnosis and confirm the og: and twitter: 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.url and images[].url are relative paths here. Next.js resolves them against metadataBase from Step 1.
  • twitter.card: 'summary_large_image' produces a large image preview. 'summary' produces a small thumbnail.
  • locale should match your content language, e.g. en_US, de_DE, fr_FR.
  • The image path in openGraph.images and twitter.images can 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, and twitter:card meta tags
  • All image and URL values are absolute, prefixed with your production origin
  • https://your-domain.example/sitemap.xml returns a valid XML sitemap
  • https://your-domain.example/robots.txt returns 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 generateMetadata instead of a static metadata export.
  • 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.
  • metadataBase only controls metadata URL resolution. It does not change canonical URLs, redirects, or other SEO behavior unless you configure those separately.
  • Next.js metadata documentation — OpenGraph and Twitter Card field reference
  • Next.js sitemap and robots file generation
  • Dynamic metadata with generateMetadata for 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