INTERMEDIATENEXTJSUTILITIES

Dynamic generateMetadata with Canonicals and OG Tags

A complete generateMetadata function for Next.js App Router pages with canonical URLs, OpenGraph, Twitter cards, and per-page overrides.

By Tested on Next.js 16

A complete generateMetadata function for the Next.js App Router that emits canonical URLs, OpenGraph tags, Twitter cards, and per-page overrides. This is the function I drop into every dynamic route — blog posts, product pages, user profiles. Get this right once and every page on your site has clean SEO metadata.

Tested on Next.js 16.

When to Use This

  • Per-blog-post or per-product page metadata
  • Dynamic routes where the title/description depends on content
  • Any page that needs a canonical URL different from the request URL
  • Pages with custom OG images via opengraph-image.tsx

Don't use this when the metadata is fully static (use the metadata export instead — it's simpler and faster) or when the page is a redirect (it won't render).

Code

// app/blogs/[slug]/page.tsx
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { getBlogBySlug } from '@/lib/services/blogService';
 
const SITE_URL = 'https://www.rabinarayanpatra.com';
 
interface PageProps {
  params: Promise<{ slug: string }>;
}
 
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const { slug } = await params;
  const blog = await getBlogBySlug(slug);
 
  if (!blog) {
    return { title: 'Post not found' };
  }
 
  const { frontmatter } = blog;
  const url = `${SITE_URL}/blogs/${slug}`;
  const ogImage = frontmatter.coverImage
    ? `${SITE_URL}${frontmatter.coverImage}`
    : `${SITE_URL}/og-default.png`;
 
  return {
    title: frontmatter.title,
    description: frontmatter.description,
    alternates: { canonical: url },
    openGraph: {
      title: frontmatter.title,
      description: frontmatter.description,
      url,
      type: 'article',
      publishedTime: frontmatter.publishedAt,
      modifiedTime: frontmatter.updatedAt,
      authors: ['Rabinarayan Patra'],
      tags: frontmatter.tags,
      images: [{ url: ogImage, width: 1200, height: 630, alt: frontmatter.title }],
    },
    twitter: {
      card: 'summary_large_image',
      title: frontmatter.title,
      description: frontmatter.description,
      images: [ogImage],
      creator: '@rabitalks',
    },
  };
}
 
export default async function BlogPage({ params }: PageProps) {
  const { slug } = await params;
  const blog = await getBlogBySlug(slug);
  if (!blog) notFound();
  // ... render the post ...
}

The params is a Promise in Next.js 16 — await it before reading. Returning { title: 'Post not found' } from generateMetadata for missing data avoids exceptions during the 404 render.

Usage

The matching default metadata in app/layout.tsx provides fallbacks for pages that don't override:

// app/layout.tsx
import type { Metadata } from 'next';
 
export const metadata: Metadata = {
  metadataBase: new URL('https://www.rabinarayanpatra.com'),
  title: {
    default: 'Rabinarayan Patra',
    template: '%s — Rabinarayan Patra',
  },
  description: 'SDE II at Amazon. Java, Spring Boot, AWS, Next.js.',
  openGraph: {
    siteName: 'Rabinarayan Patra',
    locale: 'en_US',
    type: 'website',
  },
  twitter: {
    card: 'summary_large_image',
    creator: '@rabitalks',
  },
};

The template: '%s — Rabinarayan Patra' automatically appends your name to every per-page title without you having to repeat it. The metadataBase lets you use relative URLs for OG images and Next.js will resolve them correctly.

Caveats

  • generateMetadata is called twice per request in dev mode. Once for metadata generation, once for the actual page render. In production it's deduplicated. Don't put expensive non-cached operations there.
  • alternates.canonical should be the absolute URL, not relative. Relative paths get resolved against metadataBase, but explicit absolute is less error-prone.
  • OG image dimensions matter. 1200x630 is the standard. Anything else may be cropped by social platforms.
  • Don't return a Promise from generateMetadata that depends on user data. It runs at request time, but the result is cached per URL. User-personalized metadata leaks across requests.
  • title.template only applies when the per-page title is a string. If you return an object { title: { absolute: '...' } }, the template is bypassed.
  • metadataBase is required for relative image URLs to resolve correctly. Skip it and OG images may not render in some validators.

Frequently Asked Questions

Why use generateMetadata instead of static metadata?

generateMetadata runs at request time and can read the route's params, search params, and cookies. Static metadata is built once at build time. Use generateMetadata for any page where the title, description, or OG image depends on dynamic content like a blog post or product.

What is a canonical URL and why does it matter?

A canonical URL tells search engines which version of a URL is the 'real' one when the same content is reachable via multiple paths (with/without query strings, with/without trailing slash, www vs non-www). Without canonicals, Google can split your ranking signal across duplicates.

X (Twitter)LinkedIn