Topic 09 / 13

Metadata, SEO & Open Graph

~9 min read  //  Next.js Series  //  Coding India

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.