Web Development
Next.jsSEOTechnical SEOApp RouterPerformanceCore Web VitalsStructured Data

Next.js Technical SEO Audit Checklist (App Router): Indexing, Metadata, Core Web Vitals, and Structured Data

AO
Adrijan Omićević
·15 min read

# What You'll Learn#

This guide is a practical Next.js technical SEO audit checklist for Next.js App Router projects. Each step explains what to check, how to implement or fix it, and how it affects crawling, indexing, and performance.

If you want the strategic background first, read Why Next.js for SEO and then come back to this checklist for execution.

# Prerequisites and Tools#

ToolWhy you need itWhat to look for
Google Search ConsoleIndexing and canonical signalsCoverage, sitemaps, URL Inspection, rich results
Lighthouse or PageSpeed InsightsLab performanceLCP, CLS, INP, TTFB, render-blocking
Chrome DevTools PerformanceDebug runtime issuesLong tasks, layout shifts, hydration cost
CurlFast header and robots checksStatus codes, caching, X-Robots-Tag
Structured Data TestingValidate JSON-LDErrors, warnings, eligibility

# Step 1: Confirm Rendering Strategy per Route#

Your first SEO risk in Next.js is unintentionally shipping pages that look fine to users but are harder to crawl or index because critical content is gated behind client-only rendering or delayed data fetching.

Checklist#

CheckPass criteriaWhy it matters
Marketing pages render on the serverHTML contains primary content without JSBots and social crawlers get content instantly
Dynamic pages avoid client-only shellsNo empty div with content only after hydrationPrevents thin HTML and rendering delays
Status codes are correct200, 301, 404, 410 as intendedIndexing depends on accurate HTTP semantics

Quick verification#

  1. 1
    Open a page.
  2. 2
    View page source.
  3. 3
    Confirm main headings and body copy are present in HTML.

🎯 Key Takeaway: If your primary content is missing from the initial HTML, you are relying on Google to render JS. That increases crawl cost and delays indexing.

# Step 2: Audit Indexability Signals (Meta Robots and X-Robots-Tag)#

Indexing problems are often self-inflicted: accidental noindex, nofollow, or blocked paths.

What to check#

SurfaceWhere it can be setTypical failure
Meta robotsMetadata API or manual meta tagsnoindex applied globally in a layout
X-Robots-Tag headerReverse proxy, CDN, middleware, route handlersStaging header leaking into production
Robots.txtapp/robots.ts or static fileDisallowing important sections

Implement robots directives using Metadata API#

TypeScript
// app/(marketing)/layout.tsx
import type { Metadata } from 'next';
 
export const metadata: Metadata = {
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      'max-snippet': -1,
      'max-image-preview': 'large',
      'max-video-preview': -1,
    },
  },
};

Add noindex for non-public areas only#

TypeScript
// app/(app)/account/layout.tsx
import type { Metadata } from 'next';
 
export const metadata: Metadata = {
  robots: {
    index: false,
    follow: false,
  },
};

Header-level check with curl#

Bash
curl -I https://example.com/

Look for unintended x-robots-tag: noindex.

⚠️ Warning: A single global layout-level robots: { index: false } can deindex an entire site, even if your robots.txt and sitemap are correct.

# Step 3: Canonical URLs and Duplicate Content Controls#

Canonicals are how you tell search engines which URL is the “main” version when multiple URLs can show the same content.

In Next.js, duplication commonly happens due to:

  • Trailing slash variants
  • Query parameters
  • Pagination pages
  • Locale routes
  • Multiple category paths pointing to the same item

Canonical checklist#

CheckPass criteriaImpact
Every indexable page has a self canonicalCanonical matches preferred URLConsolidates ranking signals
Canonical points to a 200 indexable URLNo canonical to 404 or redirected URLPrevents canonical ignoring
Parameters do not create duplicate canonicalsCanonical strips tracking paramsAvoids splitting authority

Implement canonical with metadataBase and alternates.canonical#

TypeScript
// app/layout.tsx
import type { Metadata } from 'next';
 
export const metadata: Metadata = {
  metadataBase: new URL('https://example.com'),
};
TypeScript
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
 
export async function generateMetadata(
  props: { params: Promise<{ slug: string }> }
): Promise<Metadata> {
  const { slug } = await props.params;
 
  return {
    title: `Post: ${slug}`,
    alternates: {
      canonical: `/blog/${slug}`,
    },
  };
}

Normalize redirects for trailing slash#

Do this at the edge or in your platform rules. In Next.js, you can also use redirects.

JavaScript
// next.config.js
module.exports = {
  async redirects() {
    return [
      {
        source: '/:path*/',
        destination: '/:path*',
        permanent: true,
      },
    ];
  },
};

💡 Tip: Treat canonicals as a consistency test: if your canonical logic depends on request headers or query strings, you will eventually generate conflicting canonicals and confuse indexing.

# Step 4: Metadata Coverage Audit (Titles, Descriptions, Open Graph)#

Missing or duplicated metadata reduces CTR and can weaken relevance signals. With App Router, use generateMetadata so every dynamic page has deterministic metadata.

Minimum metadata checklist#

ElementRecommendationWhy it matters
TitleUnique, keyword-aligned, 50 to 60 charsCTR and topical relevance
Meta descriptionUnique, 140 to 160 charsCTR and snippet control
Open GraphTitle, description, image, URLSocial previews, sharing
Twitter cardsummary_large_imageConsistent previews

Example: dynamic metadata with Open Graph#

TypeScript
// app/products/[id]/page.tsx
import type { Metadata } from 'next';
 
export async function generateMetadata(
  props: { params: Promise<{ id: string }> }
): Promise<Metadata> {
  const { id } = await props.params;
 
  const product = await fetch(`https://api.example.com/products/${id}`, {
    cache: 'no-store',
  }).then((r) => r.json());
 
  return {
    title: `${product.name} | Example Store`,
    description: product.shortDescription,
    alternates: { canonical: `/products/${id}` },
    openGraph: {
      title: `${product.name} | Example Store`,
      description: product.shortDescription,
      url: `/products/${id}`,
      images: [
        { url: product.ogImageUrl, width: 1200, height: 630, alt: product.name },
      ],
      type: 'product',
    },
    twitter: {
      card: 'summary_large_image',
      title: `${product.name} | Example Store`,
      description: product.shortDescription,
      images: [product.ogImageUrl],
    },
  };
}

If you need a broader SEO foundation across teams, SEO for Developers is a good baseline for aligning code and content decisions.

# Step 5: Robots.txt and XML Sitemap (App Router-native)#

Robots.txt affects crawling. Sitemaps affect discovery and crawl efficiency. Both directly impact how fast new pages get indexed and how consistently large sites get revisited.

Implement robots.txt in App Router#

TypeScript
// app/robots.ts
import type { MetadataRoute } from 'next';
 
export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/account', '/checkout', '/api'],
      },
    ],
    sitemap: 'https://example.com/sitemap.xml',
  };
}

Implement sitemap.xml in App Router#

TypeScript
// app/sitemap.ts
import type { MetadataRoute } from 'next';
 
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 },
  }).then((r) => r.json());
 
  return [
    { url: 'https://example.com/', lastModified: new Date() },
    ...posts.map((p: { slug: string; updatedAt: string }) => ({
      url: `https://example.com/blog/${p.slug}`,
      lastModified: new Date(p.updatedAt),
    })),
  ];
}

Sitemap audit checklist#

CheckPass criteriaImpact
Sitemap returns 200No auth, no caching errorsGoogle can fetch reliably
URLs are absoluteFull https URLsPrevents parsing ambiguity
Only canonical URLs includedNo parameter variantsImproves crawl budget efficiency
lastModified is realisticMatches real updatesBetter recrawl timing

ℹ️ Note: Google does not guarantee using lastmod, but accurate timestamps correlate with smarter recrawling on large sites.

# Step 6: Pagination and Facets (Prevent Crawl Traps)#

Pagination and filters can explode into thousands of URLs. That can dilute crawl budget and create duplicate content clusters.

Pagination checklist#

CheckPass criteriaImpact
Paginated pages are indexable only if valuablePage 1 indexable, deeper pages case-by-caseAvoids thin index bloat
Canonical strategy is consistentSelf-canonical per page, or canonical to page 1Consolidates or preserves long-tail
Internal links are crawlableReal links, not JS-only handlersDiscovery and crawl paths

Implement paginated route with consistent canonicals#

TypeScript
// app/blog/page/[page]/page.tsx
import type { Metadata } from 'next';
 
export async function generateMetadata(
  props: { params: Promise<{ page: string }> }
): Promise<Metadata> {
  const { page } = await props.params;
  const pageNum = Number(page);
 
  return {
    title: `Blog - Page ${pageNum}`,
    alternates: {
      canonical: `/blog/page/${pageNum}`,
    },
  };
}

Prevent indexing of infinite filter combinations#

If you have facet URLs like /search?color=red&size=m, either:

  • Block crawling via robots.txt for the parameterized path pattern where possible, or
  • Keep crawlable but set noindex on low-value combinations.

A pragmatic rule: index only filters with search demand and stable inventory.

⚠️ Warning: Blocking via robots.txt prevents crawling, not indexing, when external links exist. If a URL is already discovered, noindex is the reliable deindexing directive, but it requires the page to be crawlable.

# Step 7: HTTP Status Codes, Redirects, and Error Pages#

Google treats redirects and errors as strong signals. A Next.js site with incorrect 200 responses for missing pages will accumulate soft 404s and waste crawl.

Checklist#

CheckPass criteriaImpact
404 pages return real 404Not a 200 with “Not found” textPrevents soft 404 classification
Redirect chains are avoidedOne hop max for common URLsFaster crawling, less loss of signals
410 used for permanently removed contentOptional but usefulFaster deindexing than 404 in practice

App Router 404 handling#

Use notFound() for missing records.

TypeScript
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
 
export default async function Page(
  props: { params: Promise<{ slug: string }> }
) {
  const { slug } = await props.params;
 
  const res = await fetch(`https://api.example.com/posts/${slug}`);
  if (res.status === 404) notFound();
 
  const post = await res.json();
  return <main><h1>{post.title}</h1></main>;
}

Keep redirects centralized and stable.

JavaScript
// next.config.js
module.exports = {
  async redirects() {
    return [
      { source: '/old-blog/:slug', destination: '/blog/:slug', permanent: true },
    ];
  },
};

# Step 8: Core Web Vitals Audit (LCP, INP, CLS) for App Router#

Performance is now inseparable from SEO. Google’s Core Web Vitals are evaluated on real-user data, typically at the 75th percentile.

Targets to hit:

  • LCP <= 2.5s
  • INP <= 200ms
  • CLS <= 0.1

For a deeper performance playbook, use Website Performance Optimization.

What to check in Next.js specifically#

MetricMost common Next.js causesFixes that move the needle
LCPUnoptimized hero image, slow TTFB, blocking fontsNext Image, caching, streaming SSR, font optimization
INPHeavy hydration, large client bundles, expensive event handlersMove logic server-side, reduce JS, code split, use Server Components
CLSLate-loading images, dynamic injected banners, font swapsExplicit sizes, stable layout, font-display strategy

LCP: fix hero image and priority#

TSX
// app/(marketing)/page.tsx
import Image from 'next/image';
 
export default function Home() {
  return (
    <main>
      <Image
        src="/images/hero.jpg"
        alt="Product screenshot"
        width={1600}
        height={900}
        priority
        sizes="(max-width: 768px) 100vw, 1200px"
      />
      <h1>Fast, SEO-friendly Next.js websites</h1>
    </main>
  );
}

INP: reduce client components and hydration surface#

Audit where you use "use client". In App Router, many UI pieces can be Server Components by default, which can cut JS shipped to the browser.

A simple rule: if it does not need browser-only APIs, do not make it a client component.

CLS: reserve space for dynamic elements#

If you inject banners or consent UI, reserve space to avoid shifts. Also ensure all images have explicit dimensions.

💡 Tip: Measure with real-user monitoring. Lab tools are good for debugging, but only field data tells you whether 75th percentile users meet LCP and INP thresholds.

# Step 9: Caching and Revalidation (Performance and Crawl Efficiency)#

Fast responses improve user experience and allow search engines to crawl more URLs per unit time. Poor caching often shows up as high TTFB and slow LCP.

Checklist#

CheckPass criteriaImpact
Static where possibleMarketing and evergreen pages pre-renderedFaster indexing and better CWV
Revalidation is intentionalrevalidate aligned with content freshnessAvoids stale SERP snippets
CDN caching not brokenCache headers are consistentReduces origin load and speeds bots

Example: revalidate content every hour#

TypeScript
// app/blog/[slug]/page.tsx
export const revalidate = 3600;

Example: fetch caching aligned with revalidation#

TypeScript
// app/blog/[slug]/page.tsx
const post = await fetch(`https://api.example.com/posts/${slug}`, {
  next: { revalidate: 3600 },
}).then((r) => r.json());

# Step 10: Structured Data (JSON-LD) Audit and Implementation#

Structured data improves eligibility for rich results and reduces ambiguity about entities like articles, products, breadcrumbs, and organizations. It does not guarantee rich results, but it increases your chances when content and policies match.

Schema checklist#

Schema typeWhere it helpsMinimum fields to validate
OrganizationBrand knowledge and trustname, url, logo
WebSite + SearchActionSitelinks search box eligibilityurl, potentialAction
BreadcrumbListBreadcrumb rich resultsitemListElement
ArticleArticle rich resultsheadline, image, datePublished, author
ProductProduct rich resultsname, offers, aggregateRating if present

Add JSON-LD safely in App Router#

In MDX and JSX, scripts are allowed, but keep it minimal and valid. The simplest approach is to render a script tag with application/ld+json.

TSX
// app/blog/[slug]/StructuredData.tsx
export function ArticleJsonLd(props: {
  url: string;
  title: string;
  imageUrl: string;
  datePublished: string;
  dateModified: string;
}) {
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    mainEntityOfPage: props.url,
    headline: props.title,
    image: [props.imageUrl],
    datePublished: props.datePublished,
    dateModified: props.dateModified,
    author: [{ '@type': 'Person', name: 'Adrijan Omićević' }],
    publisher: {
      '@type': 'Organization',
      name: 'Samioda',
      logo: {
        '@type': 'ImageObject',
        url: 'https://example.com/logo.png',
      },
    },
  };
 
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
    />
  );
}

Then include it on the page:

TSX
// app/blog/[slug]/page.tsx
import { ArticleJsonLd } from './StructuredData';
 
export default async function Page(
  props: { params: Promise<{ slug: string }> }
) {
  const { slug } = await props.params;
  const post = await fetch(`https://api.example.com/posts/${slug}`).then((r) => r.json());
 
  return (
    <main>
      <ArticleJsonLd
        url={`https://example.com/blog/${slug}`}
        title={post.title}
        imageUrl={post.ogImageUrl}
        datePublished={post.publishedAt}
        dateModified={post.updatedAt}
      />
      <h1>{post.title}</h1>
      <article>{post.content}</article>
    </main>
  );
}
TSX
// app/components/BreadcrumbJsonLd.tsx
export function BreadcrumbJsonLd(props: {
  items: Array<{ name: string; url: string }>;
}) {
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    itemListElement: props.items.map((item, index) => ({
      '@type': 'ListItem',
      position: index + 1,
      name: item.name,
      item: item.url,
    })),
  };
 
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
    />
  );
}

# Step 11: Internal Linking and Crawl Depth#

Even with perfect sitemaps, internal links drive most discovery and priority signals. Pages more than ~3 clicks deep often get crawled less frequently on medium-sized sites.

Checklist#

CheckPass criteriaImpact
Category and hub pages existClear topic hubs with links to key pagesBetter discovery and relevance
Links are actual anchor linksNot click handlers onlyCrawlable navigation
No orphan pagesEvery indexable URL linked internallyPrevents low discovery and weak signals

Use server-rendered navigation components and ensure links are standard Next.js Link with real href values.

# Step 12: Audit Output Consistency Between Environments#

A common production-only SEO bug is environment drift: staging differs from production in indexing, canonical hostnames, or robots directives.

Checklist#

CheckPass criteriaImpact
metadataBase uses the production domainNo staging URLs in canonicals or OG URLsPrevents wrong canonical consolidation
robots rules differ appropriatelyStaging is blocked, production is openAvoids staging indexing
Sitemaps list only production URLsNo mixed hostsPrevents wasted crawl and confusion

If your environment variables can influence metadata, lock them down and validate with automated checks in CI.

# Key Takeaways#

  • Treat indexability as a first-class feature: audit robots, meta robots, and X-Robots-Tag to prevent accidental deindexing.
  • Generate deterministic metadata and canonicals with the App Router Metadata API, especially for dynamic routes and pagination.
  • Ship App Router-native robots.ts and sitemap.ts and keep sitemaps strictly canonical to improve discovery and crawl efficiency.
  • Optimize Core Web Vitals with Next Image for LCP, fewer client components for INP, and stable layout sizing for CLS.
  • Add JSON-LD schema for the content types you publish and validate in Search Console to improve rich result eligibility.

# Conclusion#

A solid Next.js technical SEO audit checklist is mostly about consistency: consistent indexability signals, consistent canonicals, consistent metadata, and consistent performance. If you implement the steps above, you reduce crawl waste, speed up indexing, and protect rankings from regression during releases.

If you want Samioda to run this audit on your Next.js App Router project and ship the fixes, contact us through our site and we will provide a prioritized backlog tied to indexing impact and Core Web Vitals wins.

FAQ

Share
A
Adrijan OmićevićSamioda Team
All articles →

Need help with your project?

We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.