Next.js Metadata & OG Done Right: Clean SEO Without Hardcoding
By Arash Latifi
Practical guide to Next.js Metadata API — title, description, Open Graph, Twitter cards, canonical and dynamic sitemap with generateMetadata.
TL;DR: Stop hardcoding SEO — use
generateMetadataandsitemap.tsso each page gets its own title, description and OG image that renders perfectly on Telegram and WhatsApp.
Why the Metadata API matters
Manually writing Head per page and hardcoding og:image does not scale. Since Next.js 13, the metadata object centralizes title, description, canonical, openGraph and twitter with full types. Result: consistent SEO, correct previews on Telegram/WhatsApp, and no Head duplication.
Kept on the server, metadata never ships to the client bundle. To keep the boundary clean, see RSC poisoning and use-client boundaries and for rendering and caching, Next.js 15 cache and PPR deep dive.
Clean structure: layout plus generateMetadata
Set base metadata in app/layout.tsx, override per route with generateMetadata:
metadataBase must be an absolute URL so relative OG links resolve correctly.
Dynamic OG images with ImageResponse
Add an opengraph-image.tsx next to each route for server-rendered 1200x630 images:
Served automatically at /blog/[slug]/opengraph-image, no manual openGraph.images needed if you keep the convention.
Dynamic sitemap and robots
Instead of a static sitemap.xml:
Make sure sitemap.ts covers the final URLs — with or without /en or /fa — depending on your i18n setup.
| Common mistake | What happens | Fix |
| --- | --- | --- |
| One hardcoded og:image for all pages | Same preview everywhere on Telegram | Dynamic generateMetadata or opengraph-image.tsx |
| Missing metadataBase | Relative OG URLs stay broken | metadataBase: new URL("https://...") |
| title without template | Bloated repetitive titles | template: "%s | Brand" in layout |
| Manual stale sitemap.xml | New pages not indexed | Dynamic sitemap.ts from DB/MDX |
Metadata SEO checklist
- [x] Set
metadataBaseandalternates.canonicalin layout - [x] Added
generateMetadatafor dynamic routes withnotFoundhandling - [ ] Added
opengraph-image.tsxat 1200x630 and tested on Telegram - [ ] Made
sitemap.tsandrobots.tsdynamic - [ ] Kept titles under 60 chars and descriptions under 155
- [ ] Set
twitter.cardtosummary_large_image
Mini-case: a shop with 80 products used one static OG image — every share looked identical. After adding generateMetadata and per-product opengraph-image.tsx, click-through from Telegram rose 22% and Missing og:image errors in Search Console dropped to zero. Input validation was handled with Next.js form validation with Zod so metadata was built from clean data.
FAQ
Q: Can I set metadata inside a Client Component?
No. metadata and generateMetadata work only in Server Components. If your page is use client, move metadata to the parent page.tsx.
Q: Where should OG images be served from?
Easiest is opengraph-image.tsx next to page.tsx. If you use a CDN, provide an absolute URL with explicit width and height so crawlers crop correctly.
Q: How to handle canonical for fa and en?
Set a separate canonical per locale pointing to that locale URL and link the other locale via alternates.languages. Do not share one canonical across locales.