Jardine Studio
JOURNALENGINEERING

Technical SEO for Next.js: the 2026 reference

Technical SEO for Next.js in 2026: metadata API, structured data, Core Web Vitals, and the streaming metadata issue most articles miss.

April 22, 202624 min readBy Jardine Studio
SEONext.jsTechnical SEOapp routercore web vitalsstructured dataGEO

What technical SEO for Next.js actually covers in 2026

Technical SEO for Next.js in 2026 is not just metadata, sitemap files, and Core Web Vitals. It is the set of framework-layer decisions that determine whether crawlers, search engines, social scrapers, and AI retrieval systems can read the right HTML, trust the right metadata, and extract the right page structure.

This article is for developers, technical founders, and site owners who have shipped a Next.js site and want to know why it is not ranking, or who are about to launch and want to avoid that outcome. Before digging into the framework checklist, the Free Website Scan gives a page-level read on what crawlers, search engines, and AI answer systems can understand from a live URL.

All code snippets below were tested against Next.js 16.2.6, React 19.2.4, and TypeScript 5.9.3 with strict mode and tsc --noEmit. Last validated May 17, 2026. This is the framework-layer checklist used in Jardine Studio’s custom Next.js web development engagements.

The Next.js metadata API in 60 seconds

The Next.js App Router replaces most manual <Head> work with two server-side primitives: a static metadata object for routes with fixed values, and a generateMetadata async function for routes that need params or fetched data, per the Next.js generateMetadata reference (v16.2.4, April 2026). Both run on the server, both are type-safe, and both compose through nested layouts.

Static metadata, in a layout.tsx or page.tsx:

import type { Metadata } from 'next';

export const metadata: Metadata = {
  metadataBase: new URL('https://yoursite.com'),
  title: {
    template: '%s | Your Brand',
    default: 'Your Brand',
  },
  description: 'A clear sentence under 155 characters.',
  alternates: { canonical: '/' },
  openGraph: {
    type: 'website',
    siteName: 'Your Brand',
    images: ['/og.png'],
  },
};

Dynamic metadata, for routes that depend on params or fetched data:

import type { Metadata } from 'next';

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: { images: [post.coverImage] },
  };
}

Three rules catch most teams. First, metadataBase is required if you use relative URLs in openGraph.images or alternates.canonical. Without it, the build fails. See Next.js's metadataBase reference for the exact error. Second, generateMetadata is server-only; you cannot use it from a client component. Third, metadata merging is shallow: if a child segment defines openGraph, the entire parent openGraph object is replaced, not extended. To share nested fields, extract them to a separate file and import them on both sides.

Server components and the streaming-metadata trap

Next.js 15.2 introduced streaming metadata, which means generateMetadata no longer blocks the initial UI render. For most browsers and capable crawlers, including Googlebot, this is a perceived-performance win because Time to First Byte drops. For HTML-limited bots that do not execute JavaScript, including Twitterbot, Slackbot, and facebookexternalhit, metadata must remain in <head>, and Next.js detects those bots by User-Agent.

The production trap is the long tail. If you see <title> and <meta> tags rendering in <body> instead of <head> on a dynamic page, this may be expected for non-HTML-limited bots, but it can still cause real regressions when a crawler, social scraper, SEO tool, or AI retrieval bot you care about is not treated the way your workflow expects. Vercel's GitHub discussion of the issue collects the symptoms and the fix.

A 2025 case study documented a production Next.js 15 site that lost roughly two months of social-share visibility because metadata appeared in <body> for several scrapers the team relied on — the December 2025 case study on the streaming-metadata regression walks the full timeline. The fix is one line in next.config.ts:

import type { NextConfig } from 'next';

const config: NextConfig = {
  // Force every bot to receive blocking metadata in <head>.
  htmlLimitedBots: /.*/,
};

export default config;

This trades a small perceived-performance hit in bot scenarios for guaranteed <head> placement across every crawler, including the long tail of social, AI, and SEO scrapers. Apply it if your site depends on social-share previews, if you rely on niche AI crawlers, or if you have ever had to debug "why is my OG image not showing."

For search-critical sites, Jardine Studio’s default is to test the actual crawlers and preview tools that matter. If metadata placement is not consistent, we set htmlLimitedBots: /.*/ and re-evaluate only if measurement shows the tradeoff is material.

A MacBook Air showing a Terminal window full of Homebrew uninstall output and warnings about openssl@3 configuration files that were not removed, the kind of debug session that surfaces a misbehaving metadata flag.

React Server Components themselves are not bad for SEO. They are the default you want for indexable content because they produce HTML on the server. The framing that matters is: if Google should rank it, it must be in the initial HTML response. Client Components, meaning anything in a file marked 'use client', hydrate after the server response, so any content that lives only inside a client component is at risk of being invisible to crawlers that do not execute JavaScript.

Keep navigation, headings, body copy, structured data, internal links, and conversion CTAs in server components. Keep forms, theme toggles, filters, and motion-heavy interactivity in client components. Treat 'use client' as a budget, not a default.

Rendering strategy: SSG, SSR, ISR, and PPR for SEO

Next.js gives you four rendering strategies, and your SEO outcome depends on choosing the right one per route. Static Site Generation (SSG) prerenders pages at build time and serves cached HTML. Server-Side Rendering (SSR) generates HTML per request. Incremental Static Regeneration (ISR) is SSG with periodic background revalidation. Partial Prerendering (PPR) ships a static shell immediately and streams in dynamic regions, per Next.js's next.config reference.

The SEO question is not which rendering mode sounds most advanced. It is which mode gives crawlers stable HTML, low latency, and the least amount of request-time uncertainty.

The decision table, with how each strategy behaves for search crawling and AI-engine retrieval:

Marketing pages, blog posts, docs

Rendering
SSG
Why
Cached HTML at the CDN edge, fastest LCP, infinite scale
AI citation suitability
Best. Deterministic HTML, consistent extraction

Product pages with frequent price updates

Rendering
ISR (revalidate: 60-3600)
Why
Static performance with controlled freshness
AI citation suitability
Excellent. Equivalent to SSG between revalidations

User dashboards, authenticated views

Rendering
SSR or PPR
Why
Per-user content; usually marked noindex so SEO is not a factor
AI citation suitability
Not applicable. Noindex pages are not retrieved or cited

Marketing pages with one personalized region (e.g. recommended posts)

Rendering
PPR
Why
Static shell prerendered, dynamic region streams in
AI citation suitability
Depends. Put indexable content in the static shell; treat the dynamic region as enhancement

Pages with thousands of variants generated from a database

Rendering
SSG + generateStaticParams
Why
Build-time generation; consider ISR fallback for new entries
AI citation suitability
Best. Pre-rendered HTML for every variant

The wrong default is SSR for everything. SSR per-request adds latency that hurts LCP, raises hosting cost, weakens CDN caching, and produces variable response timing that some crawlers and retrieval systems handle inconsistently. The right default for most marketing-and-content Next.js sites is SSG, with ISR for sections that change between deploys and PPR for the few routes that need a personalized region inside otherwise-static content.

Structured data in the App Router (without breaking things)

Structured data in the App Router uses JSON-LD rendered as a <script type="application/ld+json"> tag inside a server component, which means zero runtime cost on the client and the JSON ships in the initial HTML, per Next.js's JSON-LD guide. The canonical pattern:

export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const product = await getProduct(id);

  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    description: product.description,
    image: product.image,
    offers: {
      '@type': 'Offer',
      price: product.price,
      priceCurrency: 'USD',
      availability: 'https://schema.org/InStock',
    },
  };

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{
          __html: JSON.stringify(jsonLd).replace(/</g, '\\u003c'),
        }}
      />
      <h1>{product.name}</h1>
    </>
  );
}

Two non-obvious things matter here. First, sanitize the JSON by replacing < with its Unicode escape. Without this, a string in your data that contains a < character can break out of the script tag and execute as HTML. Vercel's own JSON-LD guide flags this as a security concern. Second, structured data should live as close to the data source as possible. Put the JSON-LD in the same server component that fetches the entity, so the markup and the visible content cannot drift apart.

The same pattern applies to Organization schema in the root layout, which gives search engines, crawlers, and retrieval systems a consistent structured handle on the brand:

// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  const organizationJsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Organization',
    name: 'Your Brand',
    url: 'https://yoursite.com',
    logo: 'https://yoursite.com/logo.png',
    sameAs: ['https://www.linkedin.com/company/yourbrand', 'https://x.com/yourbrand'],
  };

  return (
    <html lang="en">
      <body>
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{
            __html: JSON.stringify(organizationJsonLd).replace(/</g, '\\u003c'),
          }}
        />
        {children}
      </body>
    </html>
  );
}

FAQPage schema deserves a narrower treatment in 2026. Google restricts FAQ rich results to authoritative government and health sites, so ordinary business sites should not treat FAQPage as a reliable Google feature. The useful part is still the visible Q&A content: render real questions in HTML, keep the answers concise, and only emit FAQPage when there is a deliberate non-Google reason and the schema exactly matches visible content.

Worth shipping when the page actually supports it:

  • Organization schema on the root layout (your name, logo, URL, social profiles)
  • WebSite schema that identifies the site and publisher
  • WebPage schema on important static pages, tools, and service hubs
  • BreadcrumbList on every non-homepage route
  • Article or TechArticle on journal posts
  • Visible FAQ content in HTML, with FAQPage only when there is a deliberate reason beyond ordinary Google FAQ rich results
  • Product on commerce pages, Service on service pages, Event on event pages

What to skip in 2026: aggressive use of speculative schema types and markup that describes content the page does not visibly contain. Schema is a structural signal for AI and search; it is not a magic feature unlock. Stick to types that describe the page honestly and validate cleanly.

Core Web Vitals targets and how Next.js helps you hit them

Core Web Vitals in 2026 are three metrics: Largest Contentful Paint (LCP) at or under 2.5 seconds, Interaction to Next Paint (INP) at or under 200 milliseconds, and Cumulative Layout Shift (CLS) at or under 0.1, per web.dev's current Core Web Vitals thresholds. INP replaced First Input Delay in March 2024, so any Next.js SEO advice still centered on FID is stale.

Each metric has a Next.js feature that moves it most:

LCP (Largest Contentful Paint)

Target
≤ 2.5s
Next.js feature that helps
next/image with priority, next/font with display: swap, static or ISR rendering, edge caching

INP (Interaction to Next Paint)

Target
≤ 200ms
Next.js feature that helps
Server Components by default, reduce client JS, defer non-critical scripts with next/script strategy="lazyOnload"

CLS (Cumulative Layout Shift)

Target
≤ 0.1
Next.js feature that helps
next/image with fixed width/height or fill mode and an aspect-ratio container, next/font automatic fallback metrics

The practical rule for Next.js 15 is to default to server components and treat interactive islands as expensive until proven otherwise. Every island of client interactivity has an INP cost. Audit your 'use client' declarations the same way you would audit any production budget.

To measure in production, wire up useReportWebVitals and pipe the values to your analytics:

'use client';

import { useReportWebVitals } from 'next/web-vitals';

export function WebVitals() {
  useReportWebVitals((metric) => {
    // Send to your analytics endpoint.
    fetch('/api/vitals', {
      method: 'POST',
      body: JSON.stringify(metric),
      headers: { 'Content-Type': 'application/json' },
    });
  });
  return null;
}

Render <WebVitals /> once in the root layout. The hook gives you real-user data, not just lab data from PageSpeed Insights. If you host on Vercel, Speed Insights covers the same measurement layer without writing the hook yourself.

An open notebook page showing three hand-drawn bar charts in pencil and ink, with a fountain pen resting along the right edge on a wooden surface, the kind of paper-first measurement work that pairs with the dashboard data.

next/image, next/font, and the LCP win most teams miss

The highest-impact technical SEO change on many Next.js sites is correct image handling. next/image serves modern formats, generates a responsive srcset, and lazy-loads below-the-fold images. On image-heavy pages, getting the LCP image right can be the difference between a middling mobile score and a page that feels instantly available.

The non-negotiable pattern for any hero image (the typical LCP element):

import Image from 'next/image';

<Image
  src="/hero.jpg"
  alt="A specific, descriptive alt text"
  width={1600}
  height={900}
  priority
  sizes="(min-width: 1024px) 1024px, 100vw"
/>;

The priority prop tells Next.js to preload this image, which moves LCP earlier. The sizes attribute is the part most teams get wrong. The default sizes="100vw" makes the browser download the largest available variant on every viewport, which wastes bandwidth and hurts LCP. The right sizes value mirrors the image's actual rendered width at each breakpoint.

For fonts, next/font self-hosts the font files at build time and inlines metric-matching fallback fonts so CLS stays near zero, per Next.js's next/font reference. Using Google Fonts directly through a <link> tag is one of the most common SEO regressions on Next.js sites, because Google Fonts blocks render and adds a DNS lookup. The fix:

import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={inter.variable}>
      <body>{children}</body>
    </html>
  );
}

This removes the external font stylesheet, avoids FOIT, and keeps fallback metrics stable while the web font loads. On a recent Jardine rebuild, switching from <link rel="stylesheet"> Google Fonts to next/font dropped CLS from 0.18 to 0.02 with no other changes.

Sitemap, robots, and the file conventions Next.js generates for you

The App Router includes file conventions that generate sitemap.xml and robots.txt from a single TypeScript or JavaScript file each, per Next.js's metadata file conventions. A dynamic sitemap that reads your content directory or database:

// app/sitemap.ts
import type { MetadataRoute } from 'next';
import { getAllPosts } from '@/lib/content';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts();
  const base = 'https://yoursite.com';

  return [
    { url: base, lastModified: new Date(), changeFrequency: 'weekly', priority: 1 },
    { url: `${base}/about`, lastModified: new Date(), priority: 0.8 },
    ...posts.map((post) => ({
      url: `${base}/journal/${post.slug}`,
      lastModified: post.updatedAt,
      changeFrequency: 'monthly' as const,
      priority: 0.7,
    })),
  ];
}

A robots.ts that allows everything important and blocks operational paths:

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

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

Both files render at /sitemap.xml and /robots.txt automatically. You do not need a build step, a third-party package, or a manual update process. The same convention applies to manifest.webmanifest (via app/manifest.ts) and to favicon files (placed directly in /app).

A sitemap is a promise to crawlers: every URL inside is a page you intend to be discovered and indexed. Mixing in noindex URLs breaks that promise. Two practical rules: do not include noindex URLs in the sitemap, and split the sitemap into an index if it grows beyond 50,000 URLs or 50 MB, per Next.js's sitemap reference.

What changed in Next.js 15 and 16 (and what to verify)

Next.js 15 and 16 introduced several changes that affect technical SEO. The most important ones to verify before a production deploy:

  1. Streaming metadata (15.2.0). generateMetadata no longer blocks the initial render for most browsers. Side effect: metadata may appear in <body> for non-HTML-limited bots. Verify with view-source on a dynamic page and apply the htmlLimitedBots fix above if you see metadata outside <head> for a crawler you care about.
  2. Async params and searchParams. As of v15, route params and search params are Promises. You must await them. Existing pages that destructure params synchronously will throw at runtime.
  3. Cache Components ('use cache' directive). Introduced in v15, stabilized in v16. Lets you mark expensive computations as cacheable. For generateMetadata, this means metadata that depends on external data but not request-time data can be cached cleanly.
  4. Partial Prerendering (PPR) stabilization. Now the recommended pattern for pages that mix static and dynamic regions.
  5. React 19 baseline. Concurrent rendering primitives, <form> actions, and the use hook. Verify all client components still work.
  6. Turbopack default in development. Faster local builds. Production builds still use webpack unless explicitly opted in to Turbopack.

Before a production deploy on a Next.js 15 or 16 upgrade, verify the things crawlers actually touch: open a dynamic route in production and view source to confirm <title> is in <head>; check Google Search Console URL Inspection on a representative page; run Lighthouse on the mobile preset; and re-test social previews on Twitter, LinkedIn, Slack, and Facebook.

What it looks like in practice: a Jardine case

The Black Salt Room build shipped on Next.js 15 with React Server Components by default, next/image and next/font throughout, automatic sitemap and robots generation, JSON-LD for LocalBusiness and Service on every relevant page, and htmlLimitedBots: /.*/ set in next.config.ts.

The site launched with Lighthouse 100 across Performance, Accessibility, Best Practices, and SEO on first run. The technical foundation is now in place to compound as content and links accrue. Without that foundation, future content work would have had a lower ceiling.

The Next.js technical SEO checklist

A single scannable list, in execution order. Each item maps to a section above for the full explanation.

  1. Set metadataBase in the root layout

    Point it to the production URL so relative paths in openGraph and alternates.canonical resolve correctly. Without it, the build fails.

  2. Define title.template in the root layout

    Every child route inherits a consistent title pattern without restating the brand name in every page.

  3. Define description, openGraph, and twitter defaults

    Set them once in the root layout. Override per-route where the page has unique content. Remember that nested openGraph is replaced, not merged.

  4. Use generateMetadata for routes with fetched data

    Wrap shared fetch logic in React's cache() so the same data is not re-requested between generateMetadata and the page component.

  5. Add htmlLimitedBots: /.*/ to next.config.ts

    Guarantees <head> placement of metadata for every crawler, not just the ones on Next.js’s built-in HTML-limited list. The streaming-metadata fix most articles miss.

  6. Default new components to Server Components

    Use 'use client' only where genuinely needed: forms, theme toggles, motion-heavy interactivity. Treat client-side islands as a production budget.

  7. Set Partial Prerendering as the default for marketing routes

    Static shell ships immediately; the small dynamic region streams in. Best fit for pages that mix evergreen content with one personalized region.

  8. Replace <link rel="stylesheet"> Google Fonts with next/font

    Self-hosts the fonts, removes the blocking external request, and inlines metric-matching fallbacks so CLS stays near zero.

  9. Replace <img> with next/image

    Add priority to hero LCP images. Set an explicit sizes attribute matching the actual rendered width at each breakpoint, not the default 100vw.

  10. Add app/sitemap.ts that reads from your content source

    Filter noindex URLs out of the sitemap so it does not contradict your robots rules. Search Console treats the contradiction as a low-quality signal.

  11. Add app/robots.ts

    Allow production paths and disallow /api/, /thanks/, /og/, and any other operational routes. The same file convention generates /robots.txt at build time.

  12. Add JSON-LD via a server-rendered <script> tag

    Place it close to the data source it describes. Sanitize the JSON with .replace(/</g, '\\u003c') to prevent script-tag breakout.

  13. Add Article, WebPage, and BreadcrumbList schemas

    Use them where they match the visible page. Use TechArticle for technical references; it accepts proficiencyLevel and dependencies fields.

  14. Wire useReportWebVitals to your analytics

    Captures real-user CWV data, not just lab data from PageSpeed Insights. Vercel Speed Insights covers this without writing the hook yourself.

  15. Submit sitemap.xml to Search Console and Bing Webmaster Tools

    Forces both engines to crawl on demand, surfaces indexing errors you would otherwise miss, and accelerates time-to-first-crawl on a fresh article.

How to verify your fix actually worked

Use five checks, each measuring a different failure mode:

  • Google Search Console URL Inspection. Run on a representative page after deploy. The "Crawled as" tab shows exactly what Googlebot sees, including the rendered HTML. If your metadata is missing or in the wrong place, this is where you catch it.
  • PageSpeed Insights (PSI). Real-user data from the Chrome User Experience Report plus a lab test. Use the mobile tab; mobile thresholds are stricter and represent the majority of traffic.
  • Lighthouse in Chrome DevTools. Use the mobile preset with throttling before deploy. Treat local Lighthouse as an engineering smoke test, then confirm production behavior with field data.
  • Vercel Speed Insights or analytics endpoint. Real-user CWV captured via useReportWebVitals. Catches regressions that lab tools miss.
  • Bing Webmaster Tools. Submit the same sitemap. Bing's crawl is slower than Google's but Bing's index feeds the AI engines that route through Microsoft (Copilot, ChatGPT web search in some configurations).

Schedule a Lighthouse CI run on every pull request. The configuration that matches Jardine's standard:

// .github/workflows/lighthouse.yml (excerpt of the assertions block)
{
  "assertions": {
    "categories:performance": ["error", { "minScore": 0.95 }],
    "categories:seo": ["error", { "minScore": 0.95 }],
    "categories:accessibility": ["error", { "minScore": 0.95 }]
  }
}

A failed Lighthouse run should block the merge until the regression is understood. Most obvious CWV regressions are caught here before they reach production.

The five mistakes I see on most Next.js site audits

In Jardine Studio's audit work, the same mistakes account for most Next.js SEO regressions. Each one has a named fix and a section above for the full context. If your site is underperforming, start here.

  1. Indexable content lives inside 'use client' components. A team marks a hero section as a client component because it has an entrance animation, and the H1 and body copy live inside it. Crawlers that do not execute JavaScript see an empty page. Fix: extract the static parts (H1, body copy, schema, internal links) to the parent server component. Keep only the animation logic inside 'use client'.

  2. metadataBase is missing or set to the wrong URL. Without metadataBase in the root layout, any relative URL in openGraph.images or alternates.canonical either errors at build time or silently produces broken absolute URLs in production. Fix: set metadataBase: new URL('https://yoursite.com') in app/layout.tsx once. Every relative metadata URL in the app then resolves against it correctly.

  3. <img> tags instead of next/image, or next/image without sizes. Plain <img> skips AVIF/WebP conversion and lazy-loading; next/image without sizes downloads the largest variant on every viewport. Both crater mobile LCP. Fix: every <img> becomes next/image. Every hero gets priority. Every image gets a sizes attribute matching its actual rendered width at each breakpoint.

  4. The sitemap includes noindex URLs. A /thanks page or a /preview route ends up in sitemap.xml even though the page itself is marked noindex. This sends contradictory crawl signals and makes the sitemap less trustworthy. Fix: explicitly filter noindex routes out of app/sitemap.ts. Cross-check Search Console > Pages > Excluded for "Indexed, though blocked by robots.txt" warnings.

  5. SSR is the default for everything. A team rebuilds on the App Router, marks every page dynamic = 'force-dynamic' because "we might need real-time data later," and now every request hits the origin. LCP suffers, hosting cost rises, CDN caching is wasted, and crawlers get inconsistent response times. Fix: default to SSG. Add ISR (export const revalidate = 60 or longer) only where content changes between deploys. Use SSR only for authenticated pages that are noindex anyway.

The pattern across all five mistakes is the same: a Next.js feature was used in a way that overrode the SEO-friendly default. The framework gives you the right defaults; the audits we run mostly find places where teams have opted out of them without noticing.

When this is not enough

Technical SEO is necessary but not sufficient. A site that is technically clean but has no inbound links, thin content, and no topical authority will still struggle to rank. The technical layer raises the ceiling; content, links, and brand do the work of reaching it. Google's May 15, 2026 guidance makes the same point: "From Google Search's perspective, optimizing for generative AI search is optimizing for the search experience, and thus still SEO" (Google for Developers, 2026). The framework matters less than whether the page deserves to rank for what it claims.

If you have shipped a Next.js site that meets every item in the checklist above and it still is not ranking, the technical layer is probably not the constraint. The constraint is usually one of four things: not enough referring domains, content that does not match search intent, missing topical depth across the site, or a brand that has not earned enough trust in the topic cluster.

Those problems are solved with SEO engagements, content strategy, and outreach, not framework configuration. When the framework layer itself is the work, the studio runs technical SEO as a standalone audit, focused engagement, or the technical layer of a new build. For full rebuilds where rankings need to survive the migration, the studio runs dedicated SEO migration projects. If you want passage-level discipline applied to the writing itself, passage optimization covers the writing side of the same problem.

FAQ

What is technical SEO for Next.js?
Technical SEO for Next.js is the set of framework-layer decisions (rendering strategy, metadata API, structured data, image and font handling, Core Web Vitals, sitemap and robots conventions) that determine whether Google and AI engines can crawl, index, and cite your pages. It is distinct from content SEO. The framework defines what is structurally possible; the writing and the links determine what is actually achievable.
Is Next.js good for SEO?
Yes, when used correctly. Next.js produces server-rendered HTML by default in the App Router, which is exactly what crawlers and AI engines want. The framework also includes built-in support for metadata, structured data, image optimization, sitemaps, and Core Web Vitals measurement. The risk is not the framework; it is misuse, such as putting indexable content inside 'use client' components, missing metadataBase, or hitting the streaming-metadata trap.
Does the Next.js App Router hurt SEO?
The App Router itself does not hurt SEO. It moves SEO concerns from manual Head management into a server-first metadata API that is more reliable when used as documented. The one gotcha is that streaming metadata (Next.js 15.2+) can place title and meta tags in body for non-HTML-limited bots. The fix is a one-line htmlLimitedBots config in next.config.ts, documented in Vercel's own generateMetadata reference.
Why is my Next.js metadata showing in body instead of head?
This is the streaming-metadata behavior introduced in Next.js 15.2.0. For HTML-limited bots (Twitterbot, Slackbot, Bingbot, facebookexternalhit), metadata blocks page rendering and appears in head. For full browsers and capable crawlers (including Googlebot, which executes JavaScript), metadata streams to body after the initial UI sends. To force head placement for every crawler, set htmlLimitedBots /.*/ in next.config.ts. Source: Next.js v16.2.4 docs.
Should I use SSR or SSG in Next.js for SEO?
SSG is the better default for SEO in 2026 because it produces cached HTML served from the CDN edge, which gives you the best LCP and lowest cost. Use SSR only for routes that genuinely need per-request data (typically authenticated pages, which are usually noindex anyway). Use ISR for routes that change periodically (product pages, blog indexes). Use Partial Prerendering for marketing routes that mix static content with one personalized region.
Are React Server Components good for SEO?
Yes. As of Next.js 15, all components in the App Router are Server Components by default. Server Components render to HTML on the server, which is exactly what search engines and AI crawlers need to read. The misconception is that 'Server Components' sounds like a runtime feature; it is actually closer to 'Server-Rendered Components' and produces server-generated HTML. The SEO risk is not Server Components; it is content placed inside 'use client' components that may not be in the initial HTML response.
Do I need next-seo in 2026?
No. The next-seo npm package was useful for the old Pages Router, where metadata required manual Head management. In the App Router (Next.js 13+), the built-in Metadata API and generateMetadata function replace next-seo entirely with a type-safe, server-first equivalent. New Next.js projects in 2026 should use the native APIs.
Does Next.js automatically generate sitemap and robots files?
Yes, if you use the App Router file conventions. Place a sitemap.ts in your /app folder that exports a default function returning a sitemap array, and Next.js renders it at /sitemap.xml. Place a robots.ts in the same folder and Next.js renders it at /robots.txt. Both update on every deploy. The same convention applies to manifest.ts for the web app manifest and to favicon files placed directly in /app.

References (12)
  1. Next.js docs (v16.2.4, updated April 15, 2026). generateMetadata API reference. Vercel. https://nextjs.org/docs/app/api-reference/functions/generate-metadata
  2. Next.js docs (v16.2.4, updated April 15, 2026). Metadata and OG images. Vercel. https://nextjs.org/docs/app/getting-started/metadata-and-og-images
  3. Next.js docs (updated May 11, 2026). Guides: JSON-LD. Vercel. https://nextjs.org/docs/app/guides/json-ld
  4. Next.js docs. File conventions: metadata (sitemap, robots, opengraph-image, favicon). Vercel. https://nextjs.org/docs/app/api-reference/file-conventions/metadata
  5. Next.js docs. useReportWebVitals hook reference. Vercel. https://nextjs.org/docs/app/api-reference/functions/use-report-web-vitals
  6. Vercel GitHub Discussion #84518. Next.js 15 App Router: SEO meta tags showing in body instead of head. https://github.com/vercel/next.js/discussions/84518
  7. JavaScript in Plain English. (December 10, 2025). Next.js 15 App Router Killed Our SEO for 2 Months (And How We Fixed It). https://javascript.plainenglish.io/
  8. web.dev. (current 2026 thresholds). Web Vitals: LCP, INP, CLS. Google Chrome team. https://web.dev/articles/vitals
  9. Google for Developers. (updated May 15, 2026). Optimizing your website for generative AI features on Google Search. https://developers.google.com/search/docs/fundamentals/ai-optimization-guide
  10. Search Engine Land (Leigh McKenzie). (February 11, 2026). Generative engine optimization (GEO): How to win AI mentions. https://searchengineland.com/what-is-generative-engine-optimization-geo-444418
  11. Nwachukwu, E.. (2025). Optimizing Core Web Vitals with Next.js 15. Medium
  12. Adeel Imran. (December 9, 2025). Next.js SEO: Complete Implementation Guide for 2026. https://www.adeelhere.com/

Want to talk about your site?

Tell Jardine Studio what is moving and what is stuck. We reply within one business day.