Search engines and social platforms read your metadata before they read your design. This tutorial wires production-ready SEO metadata in the Next.js App Router using the Metadata API.
What you will build
- Root `metadata` and `generateMetadata` for dynamic pages
- Open Graph and Twitter card tags
- Absolute canonical URLs from a shared site config
- A reusable helper so blog and marketing pages stay consistent
Prerequisites
- Next.js 14+ App Router project
- A public site URL (e.g. `https://kabimtech.com`)
- Basic TypeScript
Step-by-step
- Add `lib/seo/site-url.ts` that returns your absolute origin from `NEXT_PUBLIC_SITE_URL`.
- In `app/layout.tsx`, export a base `metadata` object with `metadataBase`, default `title.template`, and site-wide `description`.
- For listing pages (`/blog`, `/services`), export static `metadata` with a unique title and description.
- For dynamic routes (`/blog/[slug]`), export `generateMetadata` that loads the post and returns title, description, `openGraph.images`, and `alternates.canonical`.
- Always pass absolute image URLs (or rely on `metadataBase` so relative paths resolve).
- Verify with View Source and the Facebook Sharing Debugger / X Card Validator.
Root metadata
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL!),
title: {
default: "Kabim Technologies",
template: "%s | Kabim Tech",
},
description:
"Product engineering, developer rent, and dedicated teams from Kathmandu, Nepal.",
openGraph: {
type: "website",
locale: "en_US",
siteName: "Kabim Technologies",
},
twitter: {
card: "summary_large_image",
},
};Dynamic blog metadata
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) return { title: "Post not found" };
return {
title: post.title,
description: post.excerpt,
alternates: { canonical: `/blog/${post.slug}` },
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
publishedTime: post.publishedAt,
images: [{ url: post.coverImage, alt: post.coverImageAlt }],
},
};
}Checklist
- Unique title and description per indexable URL
- Canonical points to the preferred URL (no query-string duplicates)
- OG image at least 1200×630
- No `noindex` on pages you want ranked
Next steps
Add JSON-LD (`Organization`, `BlogPosting`) in a server component so rich results can pick up structured data alongside your meta tags.


