Missing OG Tags in Next.js 16 — Social Shares Showed 'localhost:3000'
Why missing OG tags ruin social shares in Next.js.
20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.
- ✓Basic programming fundamentals (variables, functions, control flow)
- ✓No prior javascript experience needed — we start from first principles
- Static metadata in layout.tsx applies to all pages. Dynamic pages need generateMetadata() to return unique meta per route
- Missing OG tags cause social platforms to show 'localhost:3000' as the URL and a generic fallback image — or no image at all
- generateMetadata() runs on every request and can access params, searchParams, and data from fetch calls
- OG images can be generated dynamically with @vercel/og or the Satori library for auto-generated social cards per page
- Sitemaps in Next.js 16 are created with app/sitemap.ts — supporting both static and dynamic URL generation
You took a photo, framed it, and hung it on the wall — but forgot to write the caption label beneath it. That's your page without OG tags. When someone shares your page on Twitter or Slack, the platform reads the OG metadata to build the share preview. Missing tags mean a broken preview with 'localhost:3000' as the URL. OG tags are the label under your framed photo — they tell the world what it is before they click.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Your marketing team just launched a campaign. They're sharing links on Twitter, LinkedIn, and Slack. Every share shows the same thing: a generic company logo, no headline, and 'localhost:3000' as the URL. The campaign that cost $50,000 to produce looks like an internal dev link.
Next.js 16 generates metadata at build time by default. If you define a static metadata export in a layout, it applies to every page under that layout. But for dynamic pages — blog posts, product pages, user profiles — each URL needs unique OG title, description, and image. Static metadata can't do that.
This is where generateMetadata() comes in. It's an async function that receives the same params and searchParams as the page component. You fetch the data for your page and return the metadata object, including OG tags, Twitter cards, and canonical URLs. If you skip generateMetadata(), Next.js uses the parent layout's metadata — which is wrong for every dynamic page.
By the end of this guide, you'll have production-grade metadata for every page type, dynamic OG image generation, proper sitemaps, and canonical URL handling. Your social shares will show the right title, description, and image — not localhost.
Static Metadata vs. generateMetadata: When to Use Each
Static metadata is a metadata export from a layout or page. It's defined at build time and cannot change per request. Use it for the root layout (site name, global OG image, favicon) and for static pages (About, Contact, Terms) that never change content.
generateMetadata() is an async function that returns a Metadata object at request time. It receives the same params and searchParams as the page. Use it for ANY page with dynamic content — blog posts, product pages, user profiles, search results. Next.js calls generateMetadata() on every request (for dynamic routes) or at build time (for generateStaticParams routes).
The critical difference: if you don't export generateMetadata() from a dynamic page, Next.js merges the parent layout's static metadata with the page's route info. You'll get the layout's title and description for every page — metadata is duplicated, unique page info is lost.
The Metadata Object: Every Field You Need for SEO
The Metadata object in Next.js is rich. The key fields: title can be a string or an object with default and template. The template wraps the child page's title: '%s | My Blog' becomes 'My Post | My Blog'. Without a template, the child's title completely replaces the parent's — losing the site name.
description is your meta description — aim for 50-160 characters. Google shows up to 160 characters in search results. metadataBase sets the base URL for all relative URLs in metadata. THIS IS CRITICAL — if you don't set it, relative OG image URLs become relative to the page, which breaks when sharing from external platforms.
openGraph controls how your page appears on Facebook, LinkedIn, Slack, and Discord. twitter controls Twitter cards. Both need title, description, url, and images. The OG image should be 1200x630 (the standard social share aspect ratio).
Dynamic OG Image Generation with @vercel/og
Creating a unique OG image for every page manually is impossible at scale. @vercel/og (based on Satori) lets you generate OG images dynamically using JSX and CSS. The image is rendered as a PNG on the server and cached.
Create an API route or edge function that returns an image: app/og/route.tsx. Use ImageResponse from @vercel/og to return a PNG. The function receives query params (title, subtitle, image) and renders a template.
Then reference the dynamic image in generateMetadata: url: '/og?title=' + encodeURIComponent(post.title). The OG image is generated on first request and cached by Vercel's edge network or your CDN.
You can use custom fonts, gradients, background images, and multiple text sizes. The output is a 1200x630 PNG optimized for social sharing.
Sitemaps: Tell Google Every URL You Want Indexed
Next.js 16 generates sitemaps with the app/sitemap.ts convention. Export a Sitemap object or an async function that returns an array of sitemap entries. Each entry has a url, lastModified, changeFrequency, and priority.
For static pages, export a static sitemap array. For dynamic pages (blog posts, products), make the function async and fetch the list of URLs from your data source. The sitemap is generated on every request by default, but you can cache it with ISR or generate it at build time.
Google recommends sitemaps for sites with more than 500 pages, but even small sites benefit. A sitemap ensures Google discovers ALL your pages, not just the ones linked from your homepage. It also tells Google which pages changed recently via lastModified.
Robots.txt and Indexing Control
robots.txt in Next.js is generated via app/robots.ts. Export a Robots object with rules for allowed and disallowed paths, plus a sitemap reference. This tells search engines which URLs to crawl and which to ignore.
Common patterns: block /admin/, /api/, and any staging/preview paths. Allow everything else. Reference your sitemap URL so search engines find it.
For per-page indexing control, use robots in metadata: { index: true, follow: true }. Set index: false for admin pages, user dashboards, or duplicate content filters.
disallow: '/'. In production, allow everything. Read from process.env.VERCEL_ENV or similar.Canonical URLs: Stop Duplicate Content Penalties
Duplicate content confuses search engines. If your blog post is accessible at /blog/post, /blog/post?ref=twitter, and /blog/post?utm_source=twitter, Google sees three different URLs with the same content and doesn't know which to rank.
Canonical URLs tell Google which URL is the authoritative version. Set the canonical URL in generateMetadata() using the alternates.canonical field. Always use the cleanest version of the URL — no query params, no trailing slash (or always include it — pick one and stick to it).
For paginated content (blog page 2, 3, etc.), use alternates.canonical for self-referencing and add prev and next in alternates for pagination chains.
JSON-LD Structured Data: The SEO Power-Up
Structured data (JSON-LD) helps search engines understand your content beyond meta tags. For blog posts, add Article or BlogPosting schema. For products, add Product schema with price, availability, and reviews. For local businesses, add LocalBusiness.
Next.js has no built-in JSON-LD support, but you can inject it directly in the page component using <script type="application/ld+json">. Create a helper function that accepts your content and returns the schema object. Then render it in your layout or page.
Google uses structured data for rich results — the enhanced search listings with star ratings, prices, and event dates. Rich results have 20-40% higher click-through rates than plain listings.
Testing and Validating Metadata Before Deployment
Never deploy metadata changes without validation. Use these tools: Facebook Sharing Debugger (facebook.com/debug) — paste any URL and see exactly what OG tags Facebook reads. Twitter Card Validator (cards-dev.twitter.com/validator) — validates Twitter cards. Google Rich Results Test (search.google.com/test/rich-results) — validates structured data.
Automate validation by adding a CI step that crawls critical routes and verifies OG tags are present. Use a headless browser or curl to extract meta tags from rendered pages.
Also add a /debug/seo route in development that displays all metadata for the current page. This speeds up the development loop dramatically.
Environment-Specific Metadata: Dev vs. Staging vs. Production
Your root layout's metadataBase should never be 'http://localhost:3000' in production. Use environment variables: metadataBase: new URL(process.env.SITE_URL!). Set SITE_URL in your production environment and leave it undefined (or set to localhost) in development.
For staging, you might want to block search indexing entirely. Add robots: { index: false, follow: false } in your staging environment's root layout. Use environment variable gating to conditionally set this.
Also conditionally disable sitemaps in non-production environments. Export an empty array from sitemap.ts when not in production: if (process.env.VERCEL_ENV !== 'production') return [].
Complete Metadata Architecture for Production
Here's the production setup. The root layout defines the site-wide defaults: title template, description, metadataBase, global OG settings, and Twitter card type. Each dynamic page exports generateMetadata() that fetches its content and returns unique title, description, OG tags, Twitter card, and canonical URL.
sitemap.ts generates a complete sitemap with all static and dynamic pages, filtered by environment. robots.ts blocks non-production environments from indexing. JSON-LD structured data is injected per page for enhanced search results.
Finally, a CI pipeline validates OG tags on critical routes after every deployment, and the team monitors organic traffic for unexpected drops that might indicate a metadata regression.
Common Metadata Pitfalls and How to Avoid Them
Most metadata issues fall into these categories. Missing metadataBase causes relative OG URLs to break. Hardcoded localhost in metadata leaks dev URLs to staging. generateMetadata() not exported from dynamic pages means all pages share the same title. OG image URLs that are relative and don't resolve properly.
Another common issue: caching. Social platforms cache OG data aggressively. If you update a page's OG image, the old image may persist in Twitter/Facebook caches for days. Use the sharing debuggers to force a fresh scrape.
Title too long or too short — Google truncates titles to ~60 characters on desktop. Keep your primary title under 55 characters. Descriptions should be 120-160 characters. And always include a default OG image for every page type.
Production Incident: 200 Blog Posts Shared with 'localhost:3000' for 3 Weeks
- Never hardcode localhost in metadata — always use environment variable for the site URL
- Every dynamic page must have generateMetadata() — static layout metadata is for static-only pages
- Validate OG tags with the Twitter Card Validator and Facebook Sharing Debugger before launch
- Set a default OG image for all pages so every share at least shows a branded fallback
curl -I https://yoursite.com/page | grep -i og:Check metadataBase: grep -r 'metadataBase' app/ --include='*.tsx'| File | Command / Code | Purpose |
|---|---|---|
| app | export const metadata: Metadata = { | Static Metadata vs. generateMetadata |
| app | export async function generateMetadata({ | The Metadata Object |
| app | export const runtime = 'edge' | Dynamic OG Image Generation with @vercel/og |
| app | export default async function sitemap(): Promise | Sitemaps |
| app | export default function robots(): MetadataRoute.Robots { | Robots.txt and Indexing Control |
| components | interface JsonLdProps { | JSON-LD Structured Data |
| scripts | URLS=( | Testing and Validating Metadata Before Deployment |
Key takeaways
Interview Questions on This Topic
Explain the difference between static metadata and generateMetadata() in Next.js, and when to use each.
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.
That's Next.js. Mark it forged?
5 min read · try the examples if you haven't