Home/Learn/Next.js/Metadata and SEO

Metadata and SEO

Intermediate
UX & Assets

Export an object or a function and Next renders the head tags. For a content site this is the feature that decides whether the pages exist as far as Google is concerned.

Overview

Server rendering only helps SEO if the metadata is right, and Next makes that a matter of exporting a value rather than manipulating a head element. Static pages export a `metadata` object; dynamic ones export `generateMetadata`, which can await the same data the page does — and thanks to request memoization, that costs nothing extra. The parts people get wrong are the ones with no visible symptom: a missing canonical, a relative Open Graph image, or a title template that reads backwards in a search result.

Static and Generated

The two exports, and the template that saves repeating the brand.

metadata, generateMetadata, and a title template
// A static page
import type { Metadata } from 'next'
export const metadata: Metadata = {
  title: 'Problems',
  description: 'Practice DSA problems with Java and Python solutions.',
}

// The root layout sets the template and the defaults
export const metadata: Metadata = {
  metadataBase: new URL('https://aicancode.org'),    // makes relative URLs work
  title: {
    default: 'AiCanCode — Learn with Aria',
    template: '%s | AiCanCode',                       // child pages fill %s
  },
  description: '…',
}
// -> "Problems | AiCanCode". Front-load the specific part: a search
// result truncates the end, not the beginning.

// A dynamic page
export async function generateMetadata({ params }): Promise<Metadata> {
  const problem = await getProblem((await params).slug)   // memoized —
  if (!problem) return { title: 'Not found' }             // the page's own
                                                          // await is free
  return {
    title: problem.title,
    description: problem.summary.slice(0, 155),
    alternates: { canonical: `/problems/${problem.slug}` },
    openGraph: {
      title: problem.title,
      description: problem.summary,
      images: [{ url: `/problems/${problem.slug}/opengraph-image` }],
      type: 'article',
    },
  }
}

Canonicals, Robots and Sitemaps

The three that decide what gets indexed.

canonical, robots, generated sitemap
// A canonical tells Google which URL is authoritative when the same
// content is reachable several ways — with and without a trailing
// slash, with tracking params, on www and apex.
alternates: { canonical: '/problems/two-sum' }
// metadataBase makes that relative path absolute. Without it, the
// canonical and the og:image are relative and effectively ignored.

// Keep filtered and paginated views out of the index:
export const metadata = { robots: { index: false, follow: true } }

// app/sitemap.ts — generated, not hand-maintained
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const problems = await getAllProblems()
  return [
    { url: 'https://aicancode.org', changeFrequency: 'daily', priority: 1 },
    ...problems.map((p) => ({
      url: `https://aicancode.org/problems/${p.slug}`,
      lastModified: p.updatedAt,
    })),
  ]
}
// Over 50,000 URLs needs a sitemap index — generateSitemaps splits it.

// app/robots.ts
export default function robots(): MetadataRoute.Robots {
  return {
    rules: { userAgent: '*', allow: '/', disallow: ['/admin', '/api'] },
    sitemap: 'https://aicancode.org/sitemap.xml',
  }
}

Structured Data and OG Images

What makes a result look different in the SERP and in a shared link.

JSON-LD, and an ImageResponse per page
// JSON-LD — rich results. Render it as a script tag in the page:
export default async function Page({ params }) {
  const problem = await getProblem((await params).slug)
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'TechArticle',
    headline: problem.title,
    datePublished: problem.publishedAt,
    author: { '@type': 'Organization', name: 'AiCanCode' },
  }
  return (
    <>
      <script type="application/ld+json"
              dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
      …
    </>
  )
}
// Validate with Google's Rich Results Test — a malformed block is
// silently ignored rather than reported.

// Generated OG images — a real image per page, no design tool needed
// app/problems/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'

export default async function Image({ params }) {
  const problem = await getProblem((await params).slug)
  return new ImageResponse(
    <div style={{ display: 'flex', fontSize: 64, background: '#0b0f14',
                  color: 'white', width: '100%', height: '100%', padding: 80 }}>
      {problem.title}
    </div>,
    { ...size },
  )
}
// Only a subset of CSS is supported — flexbox yes, grid no. Every
// element needs an explicit display.

Key Points to Remember

  • 1Export metadata for static pages and generateMetadata for dynamic ones — its data fetch is memoized with the page's
  • 2metadataBase is what makes canonical and Open Graph URLs absolute; without it they are effectively ignored
  • 3A title template keeps branding consistent, and the specific part belongs first because results truncate the end
  • 4Generate sitemap.ts and robots.ts from data rather than maintaining files by hand
  • 5JSON-LD drives rich results and ImageResponse generates a real OG image per page, with a limited CSS subset

Interview Questions

Sign in to ask Aria
1

What is the difference between metadata and generateMetadata?

Easy
2

What does metadataBase affect?

Medium
3

Why does fetching data in generateMetadata not double your queries?

Hard

Ask Aria about Metadata and SEO

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…