Back to blog
SEO for Devs
11 min read
Next.js
SEO

How to SEO Your Next.js App: A Developer's Guide

A complete SEO guide for Next.js developers — covering generateMetadata, dynamic OG images, sitemaps, structured data, Core Web Vitals, semantic HTML, and internal linking strategies.

SB

SERP Blocks Team

Product

How to SEO Your Next.js App: A Developer's Guide cover

Next.js gives you the technical foundation for excellent SEO out of the box — server-side rendering, streaming, edge capabilities, and a file-system router that maps cleanly to URL structure. But the framework does not make SEO decisions for you. Metadata, structured data, sitemaps, performance optimization, and semantic HTML all require deliberate implementation.

This guide walks through every SEO capability available in the Next.js App Router, from basic meta tags to advanced structured data and Core Web Vitals tuning. The goal is to give you a single reference for making your Next.js application rank.

Metadata with generateMetadata

The generateMetadata function is the primary way to set page-level metadata in the Next.js App Router. It replaces the old Head component from the Pages Router and supports both static and dynamic metadata generation.

Static Metadata

For pages where the metadata does not depend on runtime data, export a metadata object directly from the layout or page file:

// app/about/page.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "About Us — Your Company",
  description:
    "Learn about our mission, team, and the story behind our product.",
  openGraph: {
    title: "About Us — Your Company",
    description:
      "Learn about our mission, team, and the story behind our product.",
    url: "https://yoursite.com/about",
    siteName: "Your Company",
    type: "website",
  },
  twitter: {
    card: "summary_large_image",
    title: "About Us — Your Company",
    description:
      "Learn about our mission, team, and the story behind our product.",
  },
};

Dynamic Metadata

For pages where metadata depends on URL params, database lookups, or other runtime data, use the generateMetadata function:

// app/blog/[slug]/page.tsx
import type { Metadata } from "next";

interface Props {
  params: Promise<{ slug: string }>;
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);

  return {
    title: `${post.title} — Your Blog`,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      url: `https://yoursite.com/blog/${slug}`,
      type: "article",
      publishedTime: post.publishDate,
      authors: [post.author.name],
      images: [
        {
          url: post.heroImage,
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
  };
}

Metadata Template

Use the title.template property in your root layout to create a consistent title pattern across your site without repeating the site name in every page:

// app/layout.tsx
export const metadata: Metadata = {
  title: {
    default: "Your Company — Build Faster",
    template: "%s — Your Company",
  },
};

Now any child page that sets title: "About Us" will render as "About Us — Your Company" in the browser tab and search results.

Dynamic OG Images with next/og

Social sharing images are critical for click-through rates from social media and messaging platforms. Next.js provides the ImageResponse API (built on Vercel's @vercel/og package) for generating OG images dynamically at the edge.

Basic OG Image Route

Create an opengraph-image.tsx file in any route directory to generate an OG image for that route:

// app/blog/[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 OGImage({
  params,
}: {
  params: { slug: string };
}) {
  const post = await getPost(params.slug);

  return new ImageResponse(
    (
      <div
        style={{
          display: "flex",
          flexDirection: "column",
          justifyContent: "center",
          alignItems: "flex-start",
          width: "100%",
          height: "100%",
          backgroundColor: "#09090b",
          color: "#fafafa",
          padding: "60px 80px",
          fontFamily: "Inter, sans-serif",
        }}
      >
        <div style={{ fontSize: 28, color: "#a1a1aa", marginBottom: 16 }}>
          Your Blog
        </div>
        <div style={{ fontSize: 56, fontWeight: 700, lineHeight: 1.2 }}>
          {post.title}
        </div>
      </div>
    ),
    { ...size }
  );
}

Next.js automatically adds the appropriate og:image meta tag pointing to this generated image. Every blog post, product page, or landing page gets a unique, branded social image without you creating them manually in Figma.

Custom Fonts in OG Images

Load custom fonts by fetching the font file and passing it to the ImageResponse options:

const interBold = fetch(
  new URL("../../assets/fonts/Inter-Bold.ttf", import.meta.url)
).then((res) => res.arrayBuffer());

return new ImageResponse(/* JSX */, {
  ...size,
  fonts: [
    {
      name: "Inter",
      data: await interBold,
      style: "normal",
      weight: 700,
    },
  ],
});

Sitemap Generation

A sitemap tells search engines which pages exist on your site and how often they change. Next.js supports programmatic sitemap generation through the sitemap.ts convention.

Static Sitemap

// app/sitemap.ts
import type { MetadataRoute } from "next";

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: "https://yoursite.com",
      lastModified: new Date(),
      changeFrequency: "weekly",
      priority: 1,
    },
    {
      url: "https://yoursite.com/about",
      lastModified: new Date(),
      changeFrequency: "monthly",
      priority: 0.8,
    },
    {
      url: "https://yoursite.com/pricing",
      lastModified: new Date(),
      changeFrequency: "weekly",
      priority: 0.9,
    },
  ];
}

Dynamic Sitemap with Database Content

For sites with dynamic content — blog posts, product pages, documentation — generate the sitemap from your data source:

// app/sitemap.ts
import type { MetadataRoute } from "next";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts();
  const products = await getAllProducts();

  const postEntries = posts.map((post) => ({
    url: `https://yoursite.com/blog/${post.slug}`,
    lastModified: new Date(post.updatedAt),
    changeFrequency: "monthly" as const,
    priority: 0.7,
  }));

  const productEntries = products.map((product) => ({
    url: `https://yoursite.com/products/${product.slug}`,
    lastModified: new Date(product.updatedAt),
    changeFrequency: "weekly" as const,
    priority: 0.8,
  }));

  return [
    {
      url: "https://yoursite.com",
      lastModified: new Date(),
      changeFrequency: "weekly",
      priority: 1,
    },
    ...postEntries,
    ...productEntries,
  ];
}

For large sites with more than 50,000 URLs, use generateSitemaps to split the sitemap into multiple files. Search engines handle sitemap index files natively.

robots.txt

Control which pages search engines crawl with a robots.ts file:

// app/robots.ts
import type { MetadataRoute } from "next";

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      {
        userAgent: "*",
        allow: "/",
        disallow: ["/dashboard/", "/api/", "/admin/"],
      },
    ],
    sitemap: "https://yoursite.com/sitemap.xml",
  };
}

Key decisions for robots.txt:

  • Block dashboard and admin routes — These pages have no search value and waste crawl budget.

  • Block API routes — Prevent search engines from indexing your API endpoints.

  • Allow marketing pages — Your homepage, blog, product pages, pricing, and about page should always be crawlable.

  • Reference the sitemap — The sitemap URL in robots.txt helps search engines discover your sitemap without relying on Google Search Console submission alone.

Structured Data with JSON-LD

Structured data helps search engines understand the content and context of your pages. It enables rich results — star ratings, FAQ dropdowns, breadcrumbs, product prices, and article metadata directly in search results.

Adding JSON-LD to Pages

The cleanest approach in Next.js is rendering a <script> tag with type="application/ld+json" directly in your page component:

// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: Props) {
  const { slug } = await params;
  const post = await getPost(slug);

  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "Article",
    headline: post.title,
    description: post.excerpt,
    image: `https://yoursite.com${post.heroImage}`,
    datePublished: post.publishDate,
    dateModified: post.updatedAt,
    author: {
      "@type": "Person",
      name: post.author.name,
    },
    publisher: {
      "@type": "Organization",
      name: "Your Company",
      logo: {
        "@type": "ImageObject",
        url: "https://yoursite.com/logo.png",
      },
    },
  };

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <article>{/* post content */}</article>
    </>
  );
}

Common Schema Types

Organization — Add to your root layout. Defines your brand, logo, social profiles, and contact information.

Article — Add to blog posts. Enables article rich results with headline, author, and publish date.

Product — Add to product pages. Enables price, availability, and review rich results.

FAQ — Add to FAQ pages or sections. Enables expandable FAQ results directly in Google search. This pairs well with FAQ section blocks — our 21 FAQ designs use proper heading hierarchy and semantic markup that search engines can parse.

BreadcrumbList — Add to any page with breadcrumb navigation. Enables breadcrumb display in search results, improving click-through rates.

LocalBusiness — Add to business landing pages. Enables Google Maps integration, hours, and contact info in search results.

Validate Your Structured Data

Use Google's Rich Results Test (search.google.com/test/rich-results) to validate your JSON-LD before deploying. Invalid structured data is silently ignored by search engines — you will not get an error, you just will not get rich results.

Core Web Vitals Optimization

Core Web Vitals are a confirmed Google ranking factor. The three metrics — Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS) — measure loading performance, interactivity, and visual stability.

LCP: Largest Contentful Paint

LCP measures how long it takes for the largest visible element (usually a hero image or heading) to render. Target: under 2.5 seconds.

Optimize images. Use the Next.js <Image> component, which automatically serves WebP/AVIF, lazy-loads below-the-fold images, and generates responsive srcset attributes. For hero images, set priority to disable lazy loading:

<Image src="/hero.jpg" alt="Hero" width={1200} height={600} priority />

Minimize server response time. Use Next.js caching strategies — revalidate for ISR, force-cache for static data, and edge runtime for latency-sensitive pages.

Reduce render-blocking resources. Next.js automatically code-splits and lazy-loads route-specific JavaScript. Avoid importing large libraries in your root layout. Use next/dynamic for heavy components that are not needed on initial render.

INP: Interaction to Next Paint

INP measures responsiveness — how quickly the page responds to user interactions. Target: under 200 milliseconds.

Avoid heavy JavaScript on the main thread. Use Server Components (the default in the App Router) for everything that does not need interactivity. Only add "use client" to components that handle user input, state, or browser APIs.

Debounce expensive handlers. Search inputs, filter controls, and real-time calculations should debounce user input to avoid blocking the main thread on every keystroke.

Use startTransition for non-urgent updates. React's startTransition lets you mark state updates as non-urgent, preventing them from blocking user interactions.

CLS: Cumulative Layout Shift

CLS measures visual stability — how much the page layout shifts during loading. Target: under 0.1.

Set explicit dimensions on images and videos. The Next.js <Image> component handles this automatically when you provide width and height. For videos and iframes, use the aspect-ratio CSS property.

Reserve space for dynamic content. If a section loads data asynchronously (testimonials, pricing, product listings), reserve the expected height with a skeleton or placeholder. SERP Blocks components like hero sections, feature blocks, and card layouts are built with fixed layout structures that prevent CLS by design.

Avoid injecting content above existing content. Banners, cookie notices, and notification bars that push content down cause CLS. Use fixed or sticky positioning for elements that appear after initial render.

Semantic HTML and Heading Hierarchy

Search engines rely on HTML semantics to understand page structure. Proper use of headings, landmarks, and semantic elements improves both SEO and accessibility.

Heading Hierarchy

Every page should have exactly one <h1> that describes the primary topic. Subheadings should follow a logical hierarchy: <h2> for major sections, <h3> for subsections within those, and so on. Never skip heading levels (e.g., jumping from <h1> to <h3>).

<h1>Product Name</h1>
  <h2>Features</h2>
    <h3>Feature One</h3>
    <h3>Feature Two</h3>
  <h2>Pricing</h2>
  <h2>Testimonials</h2>

This hierarchy helps search engines identify the main topic, subtopics, and relationships between sections. It also improves screen reader navigation.

Semantic Landmarks

Use HTML5 semantic elements to define page regions:

  • <header> — Site header and navigation

  • <nav> — Navigation menus

  • <main> — Primary page content (one per page)

  • <article> — Self-contained content (blog posts, product cards)

  • <section> — Thematic groupings within the page

  • <aside> — Supplementary content (sidebars, related articles)

  • <footer> — Site footer

These landmarks are used by search engines to identify and weight content. Content inside <main> carries more weight than content inside <footer>. Content inside <article> is treated as a self-contained unit that can be independently indexed.

When building landing pages from block components, each section — navbar, hero, feature, testimonial, pricing, footer — should use appropriate semantic elements. The SERP Blocks library uses semantic HTML throughout, with proper <section>, <article>, and heading tags in all 1,200+ blocks.

Internal Linking Strategy

Internal links distribute page authority across your site and help search engines discover and understand the relationships between pages. A strong internal linking structure is one of the most underused SEO techniques.

Link from High-Authority Pages

Your homepage and top-level navigation pages have the most authority. Every page linked from these high-authority pages inherits some of that authority. Ensure your most important pages (product pages, pricing, key blog posts) are reachable within 2-3 clicks from the homepage.

Use Descriptive Anchor Text

Search engines use anchor text to understand what the linked page is about. "Click here" tells search engines nothing. "Browse our 132 feature section designs" tells search engines exactly what to expect on the linked page.

Blog-to-Product Internal Links

If you maintain a blog, link from blog posts to your product pages, pricing page, and relevant feature pages. A blog post about "building a SaaS landing page" should link to your hero sections, pricing blocks, and CTA components. This creates topical clusters that search engines use to establish your authority on a subject.

Footer and Navigation Links

Your footer is a site-wide internal linking opportunity. Include links to your most important pages — product categories, documentation, blog, pricing, and about page. The navbar serves a similar function, but with fewer links. Prioritize your highest-value pages in the navbar and use the footer for a broader set.

Putting It All Together: An SEO Checklist for Next.js

Here is a practical checklist for shipping an SEO-ready Next.js application:

Metadata

  • generateMetadata or static metadata export on every page

  • Unique title and description for every page

  • Title template configured in root layout

  • Open Graph and Twitter Card meta tags on all shareable pages

OG Images

  • Dynamic OG images for content pages (blog, products)

  • Static OG image for marketing pages (homepage, pricing, about)

  • 1200x630px dimensions, readable text, brand consistency

Sitemap and Robots

  • sitemap.ts with all public URLs

  • robots.ts blocking dashboard, API, and admin routes

  • Sitemap URL referenced in robots.txt

Structured Data

  • Organization schema on root layout

  • Article schema on blog posts

  • Product schema on product pages

  • FAQ schema on FAQ sections

  • BreadcrumbList schema where applicable

  • Validated with Google Rich Results Test

Performance

  • LCP under 2.5s (Next.js <Image> with priority, caching, code splitting)

  • INP under 200ms (Server Components, debouncing, startTransition)

  • CLS under 0.1 (explicit image dimensions, skeleton placeholders, no above-the-fold content injection)

Semantic HTML

  • Single <h1> per page

  • Logical heading hierarchy (no skipped levels)

  • Semantic landmarks (<header>, <main>, <article>, <section>, <footer>)

Internal Linking

  • Important pages reachable within 2-3 clicks from homepage

  • Descriptive anchor text on all internal links

  • Blog posts link to product and feature pages

  • Footer includes links to key pages

Getting all of this right is the difference between a Next.js app that looks good and one that ranks well. The technical foundation is there in the framework. Your job is to use it deliberately.

If you are building with SERP Blocks, the component library already handles semantic HTML, proper heading hierarchy, and accessible markup across all 1,200+ blocks in 50+ categories. That gives you a head start on the markup side so you can focus on metadata, structured data, and performance — the parts that require application-level decisions.