Skip to content
Neaptidestudio
blog

Next.js App Router SEO Checklist: SSR Visibility, Alternates, hreflang and AI Crawlers

Neaptide · October 30, 2026 · 8 min read

Verify what Next.js App Router actually serves in raw HTML: visible SSR headings without opacity:0, complete alternates metadata, crawlable locale links and root redirects.

On this page
Next.js App Router SEO checklist — 1 SSR HTML (<h1> visible) · 2 Alternates (6 hreflang + RSS) · 3 Crawl links (<a hreflang>) · 4 Schema (JSON-LD + sitemap).

A Next.js App Router site can pass visual QA in a browser while still hiding primary headings behind initial animation styles, dropping layout-level alternate links in child routes, or keeping language switchers inside unmounted client state. Audit the raw HTTP response and server-rendered HTML — not just the hydrated DOM in DevTools — before concluding that a page is ready for Googlebot and AI search crawlers.

Server Components render HTML on the server by default, but client wrappers, per-page generateMetadata() overrides and edge middleware can still alter what crawlers receive on the first request. The checklist below documents five concrete issues we audited and fixed on Neaptide’s own multilingual Next.js 15 codebase, along with repeatable checks your engineering team can run before every release.

What did our live Next.js 15 App Router audit find and fix?

In October 2026, we inspected the server-rendered HTML, HTTP headers, metadata output and sitemap of Neaptide’s six-language Next.js 15 website. The table below records the five technical defects identified in the code and verified after deployment.

Five Next.js App Router SEO checks from Neaptide’s production audit
Layer inspectedDefect in initial SSR outputImplemented fix & verified result
Reveal.tsx & Hero.tsx animation wrappersPrimary <h1>, lead copy and cards rendered with inline style="opacity:0;transform:translateY(12px)" until hydration (~1.2s delay)Removed opacity:0 from SSR state; headings and body copy render visible in raw HTML immediately
lib/meta.ts generateMetadata() alternatesPage-level alternates: { canonical, languages } replaced layout alternates and dropped <link rel="alternate" type="application/rss+xml">Added types: { "application/rss+xml": "https://www.neaptide.ai/feed.xml" } inside shared pageMetadata()
components/LangSwitcher.tsx locale menuLocale options lived inside an unmounted dropdown using <button> elements with no crawlable hrefRendered all 6 locales in SSR DOM as <a href={...} hrefLang={l} lang={l}> links
middleware.ts root (/) redirectBots hitting / without geo or Accept-Language headers had no explicit Vary header on redirectAdded deterministic fallback to x-default (/en) plus Vary: Accept-Language, Cookie, x-vercel-ip-country, cf-ipcountry
lib/schema.ts & app/sitemap.tsThree service pages hardcoded areaServed: United States; sitemap omitted inline case and article imagesAligned Service schema with global areaServed and added localized cover + evidence images to sitemap.xml

These observations describe technical defects and verified code changes on our own site. Fixing them removes rendering and discovery bottlenecks; organic rankings and AI citations still depend on query competition, external authority and ongoing measurement.

Why does style="opacity:0" in entrance animations hurt LCP and non-JS crawlers?

Many Next.js marketing sites wrap hero titles, lead paragraphs and service cards in client reveal components or Framer Motion initial states with opacity: 0. Because Next.js pre-renders Client Components on the server using their initial state, the server HTML arrives with style="opacity:0" on the main <h1> and above-the-fold text.

That creates two avoidable problems. First, the browser cannot count the hero heading or lead block as painted for Largest Contentful Paint (LCP) until JavaScript downloads, hydrates and runs the intersection observer or animation timer — often adding 800–1,200 ms on mobile connections. Second, crawlers and AI retrieval bots that inspect initial HTML without executing client scripts see crucial headings marked invisible in inline styles.

Keep server-rendered text visible from the first byte. Use a subtle transform (such as transform: translateY(12px)) without opacity: 0 in the SSR markup, or apply entrance opacity only after a client-side class confirms JavaScript is active and respects prefers-reduced-motion.

How can generateMetadata alternates silently drop layout tags?

In the Next.js App Router, metadata fields are shallowly merged down the route tree. When a root or locale layout defines alternates (for example, an RSS feed link), and a child page’s generateMetadata() returns its own alternates object with canonical and languages, the child object replaces the parent’s alternates object entirely.

Centralise canonical, hreflang and feed alternates in a single helper called by every indexable page so no route accidentally drops x-default, a language variant or the RSS feed.

export function pageMetadata(locale: Locale, opts: PageMetaInput): Metadata {
  const canonicalUrl = `${SITE.url}${localeHref(locale, opts.path)}`;
  const languages: Record<string, string> = {};
  for (const l of LOCALES) {
    languages[l] = `${SITE.url}${localeHref(l, opts.path)}`;
  }
  languages["x-default"] = `${SITE.url}${localeHref(X_DEFAULT_LOCALE, opts.path)}`;

  return {
    alternates: {
      canonical: canonicalUrl,
      languages,
      types: {
        "application/rss+xml": `${SITE.url}/feed.xml`,
      },
    },
  };
}

Why must language switchers and root redirects be explicit for crawlers?

A language switcher that only mounts its menu items after a click event — or switches locales via router.push() inside a <button> — gives search engines zero internal HTML links between language versions. Even when <link rel="alternate" hreflang="..."> exists in <head>, rendering crawlable <a href="/fr/..." hrefLang="fr" lang="fr"> links in the header DOM strengthens internal discovery across every localized route.

Similarly, check what happens when a crawler requests the root path / without cookies, geo IP headers or an Accept-Language header. Your middleware should deterministically redirect to your x-default locale (such as /en) and send Vary: Accept-Language, Cookie, x-vercel-ip-country, cf-ipcountry so CDNs and caches do not serve a geo-targeted redirect to a global crawler.

How to verify a Next.js App Router release in six steps

Run these six verification checks against your staging or production URLs before signing off an SEO release:

  1. Raw SSR visibility: fetch the page with curl -s and confirm the single <h1> and primary body copy appear in the HTML without style="opacity:0" or hidden attributes.
  2. Canonical & hreflang parity: verify that every localized URL outputs a self-referential <link rel="canonical">, all active language alternates, an x-default link and any RSS/Atom alternate link.
  3. Crawlable navigation & locale switcher: confirm header, footer and language switcher links are rendered as standard <a href="..."> elements in the initial HTML.
  4. Root and prefixless route behaviour: test / and any legacy paths with curl -I and an empty Accept-Language header to confirm a single permanent redirect (308 or 301) to the intended destination.
  5. Structured data alignment: check that WebPage, Service, Article, BreadcrumbList and FAQPage JSON-LD match the visible text, language, dates and geographic scope of the page.
  6. Sitemap & llms.txt coverage: confirm every indexable route and its key images appear in /sitemap.xml, and keep /llms.txt synchronised for AI retrieval engines.
curl -s https://www.neaptide.ai/en | grep -E '<h1|rel="canonical"|hrefLang|application/rss\+xml'
curl -I -H 'Accept-Language:' https://www.neaptide.ai/

If your team wants an engineering-level review of a Next.js marketing site, B2B platform or multilingual catalogue, Neaptide delivers a technical SEO and GEO audit with reproducible HTTP checks and developer-ready tickets.

faq

The short version

Does using Next.js Server Components guarantee good technical SEO?

No. Server Components render HTML on the server, but client animation wrappers, incomplete generateMetadata() objects, unmounted dropdown menus and middleware redirects can still hide content or break canonical and hreflang signals.

Why is style="opacity:0" in SSR HTML a problem if JavaScript reveals the text?

Initial opacity:0 delays Largest Contentful Paint (LCP) until client hydration finishes and hides headings from crawlers and AI retrieval bots that parse raw HTML without executing client JavaScript.

Why did my layout RSS link disappear on inner pages in Next.js App Router?

Next.js merges metadata shallowly. When a child page defines alternates: { canonical, languages }, it overwrites the parent layout’s alternates object unless alternates.types is included in the shared page metadata helper.

Do I still need <a hreflang> links if hreflang is already in <head>?

Yes, using real <a href="..." hrefLang="..."> elements in your language switcher lets crawlers traverse between language versions directly through the DOM instead of relying only on header tags or sitemaps.

What status code and headers should the root / redirect return on a multilingual Next.js site?

When `/` is not a standalone page, redirect visitors to their matched locale and send bots without language headers to your x-default locale (such as `/en`) with a Vary header covering Accept-Language, Cookie and geo headers.

What is included in Neaptide’s Next.js SEO audit?

We inspect raw SSR HTML, Core Web Vitals bottlenecks, canonical/hreflang matrices, structured data, sitemaps and AI crawler access, delivering a prioritised issue register with file-level tickets for your developers.