Topic 09 / 13
Metadata, SEO & Open Graph
Server Rendering Makes SEO Real
Because Next sends complete HTML, crawlers see your content. The Metadata API handles the rest — title tags, descriptions, and social cards, all type-checked and deduplicated.
Static Metadata
// app/layout.js
export const metadata = {
title: {
default: "Coding India",
template: "%s — Coding India", // child pages slot into this
},
description: "Real production code — Django, React, FastAPI & AI.",
metadataBase: new URL("https://codingindia.in"),
};
// app/about/page.js
export const metadata = {
title: "About", // renders "About — Coding India"
};Dynamic Metadata — Per-Content Pages
// app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) return { title: "Not found" };
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
publishedTime: post.date,
images: [{ url: post.ogImage, width: 1200, height: 630 }],
},
twitter: {
card: "summary_large_image",
},
alternates: {
canonical: `/blog/${slug}`,
},
};
}The fetch inside generateMetadata is deduped with the page’s own fetch of the same data — no double queries.
Generated OG Images
Ship a branded social card per page without opening a design tool — opengraph-image.js in any route renders JSX to a PNG:
// app/blog/[slug]/opengraph-image.js
import { ImageResponse } from "next/og";
export const size = { width: 1200, height: 630 };
export default async function OGImage({ params }) {
const post = await getPost(params.slug);
return new ImageResponse(
<div style={{
display: "flex", width: "100%", height: "100%",
background: "#0A0A14", color: "#fff",
alignItems: "center", justifyContent: "center",
fontSize: 64, padding: 80,
}}>
{post.title}
</div>,
size
);
}sitemap.js & robots.js
// app/sitemap.js
export default async function sitemap() {
const posts = await getAllPosts();
return [
{ url: "https://codingindia.in", priority: 1 },
...posts.map(p => ({
url: `https://codingindia.in/blog/${p.slug}`,
lastModified: p.updatedAt,
})),
];
}
// app/robots.js
export default function robots() {
return {
rules: { userAgent: "*", allow: "/", disallow: "/admin/" },
sitemap: "https://codingindia.in/sitemap.xml",
};
}Structured Data (JSON-LD)
export default async function PostPage({ params }) {
const post = await getPost((await params).slug);
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
datePublished: post.date,
author: { "@type": "Person", name: "Digamber Jha" },
};
return (
<article>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
...
</article>
);
}Checklist per content page: unique title + description, canonical URL, OG/Twitter card, JSON-LD where a schema fits, and an entry in the sitemap. Verify with the social debuggers (Facebook/Twitter/LinkedIn each have one) before launch.