Technical SEO for Next.js Sites: A Checklist Developers Can Run in CI

A Next.js SEO checklist is useful in CI only when each item can fail a build. This one turns six areas into assertions: what the server renders, App Router metadata, canonical URLs, the sitemap, robots and noindex rules, and status codes. Seven of the eight checks come with an excerpt from the test suite of Kanso's own Next.js repository, and the eighth is a labelled sketch. The checks catch regressions. They do not predict rankings.

What does a Next.js SEO checklist for CI cover?

It covers the technical layer: whether a crawler can fetch each page, read its title, description and canonical, find it in the sitemap and get the right status code. Next.js Learn calls this the technical pillar and defines it as optimizing a site for crawling and web performance, next to content creation and popularity.

A technical SEO check in CI is an automated assertion, run on every push, that a page's rendering, metadata, canonical URL, sitemap entry, robots rules or status code is what the site intends.

This layer suits automated checks because its failures are silent. A page that ships with a stray noindex or a missing canonical still loads normally for every person who visits.

  • Rendering: assert that the content and one h1 are in the server's response, not added later by script.
  • Metadata: assert a title, description, canonical and Open Graph URL for each route, taken from what the route exports.
  • Sitemap and robots: assert that every public route is listed and every private route is excluded or marked noindex.
  • Status codes: assert that unknown, draft and retired URLs answer 404 or redirect, and never 200.
  • Limits: these tests catch regressions. They do not measure rankings, indexing or traffic, and this page reports no such result.

Content quality and links are outside this checklist. For a broader audit, the forty-check SEO audit for small sites covers more ground, and a guide to which indexing stage is broken picks up where a passing checklist and a missing page meet.

Is Next.js SEO friendly without extra work?

Mostly yes, according to Google, but the setup has to be done before launch. In its June 2024 SEO office hours, Google's Search team said that for most platforms there are ways to build a site that works well for search and ways that do not, and that most platforms work fairly well out of the box.

Google attached two conditions. If you use a framework such as Next.js and do significant work with JavaScript, review Google's JavaScript SEO best practices, and for any new site set up the SEO side before launching, because adding it later is always much harder.

Strapi's Next.js SEO guide adds a mechanism: server-side rendering, static generation and incremental regeneration all send fully rendered HTML, so the choice among them is about freshness and server cost. That is one vendor's account, not Google guidance.

A CI checklist is the practical form of Google's advice to set SEO up before launch. It keeps the setup from decaying after launch, which is Kanso's reading rather than a documented claim.

What are the checks, and where do the snippets come from?

There are eight checks, and the table gives each assertion, the source of the rule and whether the snippet is a real excerpt from Kanso's repository. The table was built by reading the five supplied sources for what each says about a testable behavior, then matching each behavior to a test that already exists in Kanso's suite.

The eight checks. Source of the rule: Google Search Central, Next.js Learn or the named guide, read 5 November 2026. Example column: whether the snippet comes from Kanso's own test suite.
CheckWhat to assertSource of the ruleExample
RenderingThe content and one h1 are in the server response; content pages stay server componentsGoogle's Starter Guide: Google should see the page the way a user does. Server-component rule: Kanso's conventionKanso test excerpt, plus the crawl sketch
MetadataA title, description, canonical and Open Graph URL on every route, each uniqueThe Metadata API as Kanso uses it. Length targets: practitioner guidance (Strapi) and Kanso's own standardsKanso test excerpt
Structured dataThe JSON-LD says what the visible page says, including its categorynext-seo's documentation for JSON-LD. Consistency rule: Kanso's conventionKanso test excerpt
CanonicalsEach indexable page names itself; known duplicate URLs redirect insteadGoogle's Starter Guide: one URL per piece of content, redirect first, rel=canonical secondKanso test excerpt
SitemapEvery public route is listed, every private one is not, and a date is never inventedGoogle's Starter Guide: a sitemap is optional. The rest: Kanso's conventionKanso test excerpts
Robots and noindexThe one deliberate site-wide noindex is pinned, and each public section overrides it on purposeGoogle's Starter Guide lists several ways to keep a page out of search. The pinning: Kanso's conventionKanso test excerpts
Status codesUnknown and draft URLs answer 404, retired URLs redirect, nothing private answers 200Google's June 2024 office hours: redirects are critical when a site movesKanso test excerpts
Built-site crawlEvery sitemap URL answers 200 with a title, one h1 and a canonicalKanso's conventionA sketch, not from Kanso's repository

The excerpts are copied from the tests directory of the kansoai-frontend repository as of 5 November 2026, with indentation reduced and omitted lines marked. That suite runs with Node's built-in test runner, and Kanso's CI runs it on every push.

Most of these tests read a source file and match a pattern, or call a pure function. They prove what the source says, not what a server returns, and the sections below say where that gap matters.

How do you test that a page renders its content in the HTML?

Assert on the response the server sends, and keep content pages as server components so the content is in that response. Google's Starter Guide says Google should see a page the way an average user does, and points to the URL Inspection tool in Search Console to check it.

Kanso's version is a source-level proxy. Its use-case pages are server components, and this test forbids the use client directive and the state and effect hooks in the files that build them.

describe("server and client separation", () => {
  test("the hub, the page shell and the cards are server components (no 'use client', no state, no effects)", () => {
    for (const f of ["UseCasesHubPage.tsx", "UseCasePage.tsx", "UseCaseParts.tsx", "UseCaseIcon.tsx"]) {
      const src = read(`${K}${f}`);
      assert.doesNotMatch(src, /^\s*["']use client["']/m, f);
      assert.doesNotMatch(src, /\buse(State|Effect|Layout|Ref|Reducer)\b/, f);
    }
  });
});

The rule is Kanso's convention, not a Google requirement. It keeps client-only state out of the files that produce page content, and the crawl sketch later on checks the result in the response itself.

The rendering strategy decides how fresh that HTML is. Strapi's guide recommends static generation for marketing pages, regeneration on a timer for blog posts and product listings, and server rendering for personalized dashboards, so a test for a regenerated page should tolerate content that is minutes or hours old.

Broken markup is also worth a check in the head. Google said in the June 2024 office hours that a broken paragraph is unlikely to matter, but a broken head element might, so the response check asserts a title and a canonical rather than validating the whole document.

How do you test App Router metadata and structured data?

Test the metadata each route exports, field by field, and test the JSON-LD against the same source of truth the page prints. Kanso's root layout sets metadataBase to its site URL and a title template of the form page title plus a site suffix, so any length check has to count the suffix a route inherits.

This excerpt from Kanso's marketing-page tests asserts the title, description, canonical, robots and Open Graph URL for every page in a registry.

test("each page is indexable, with its own title, description, canonical and Open Graph URL", () => {
  for (const p of MARKETING_PAGES) {
    const m = marketingMetadata(p);
    assert.equal(m.title, p.seoTitle);
    assert.equal(m.description, p.description);
    assert.equal(m.alternates.canonical, p.route);
    assert.deepEqual(m.robots, { index: true, follow: true });
    assert.equal(m.openGraph.url, canonicalUrl(p.route));
    assert.equal(m.openGraph.title, `${p.seoTitle} | Kanso`);
  }
});

Length targets are practitioner guidance. Strapi's guide advises titles under 60 characters and descriptions of 150 to 160, and Kanso's own content standards say about 60 and about 155. A length assertion is a single comparison if you want one, but nothing in the supplied Google sources sets those numbers.

Do not assert that the title equals the h1. Asked exactly that in the June 2024 office hours, Google said no, do whatever makes sense from a user's perspective, so check that a title exists, is unique across routes and is a sensible length.

For structured data, the next-seo package says to use Next.js's built-in metadata function for standard meta tags and its own components for JSON-LD. Its README adds a testable condition: the components render an inline script, so under a strict Content Security Policy that script is blocked unless it carries a matching nonce.

The other failure is drift between the schema and the visible page. Kanso's blog tests compare each article's schema category with its listed category, so the two cannot disagree.

for (const a of ARTICLES) {
  const page = read(`app/blogs/${a.slug}/page.tsx`);
  const sections = [...page.matchAll(/articleSection:\s*"([^"]+)"/g)].map((m) => m[1]);
  assert.ok(sections.length > 0, `${a.slug}: no articleSection in the Article schema`);
  for (const s of sections) assert.equal(s, a.category, `${a.slug}: articleSection "${s}" differs from category "${a.category}"`);
  // ...
}

How do you test canonical URLs?

Assert that every indexable page declares a canonical for itself, and that the duplicates you know about redirect instead. The Starter Guide defines duplicate content as the same content under different URLs, says search engines choose one canonical URL per piece of content, and says Google will pick one itself if you do not.

Google's order of preference is stated plainly: make each piece of content reachable at one URL, redirect the non-preferred URLs to the best one, and use the canonical link element when you cannot redirect. The guide also says duplicate content is not a spam violation but can waste crawling on URLs you do not care about.

Kanso's agent-page test checks that each page sets a canonical equal to its own route, allows indexing, and builds its FAQ structured data from the FAQ it renders.

test(`${a.route} sets canonical, indexable robots and FAQ JSON-LD from the rendered FAQ`, () => {
  const page = read(pageFor(a.route));
  assert.ok(page.includes(`canonical: "${a.route}"`), "canonical");
  assert.match(page, /robots:\s*\{\s*index:\s*true/);
  assert.match(page, /"@type":\s*"FAQPage"/);
  assert.match(page, /\.map\(\(f\) => \(\{ "@type": "Question"/);
});

Read the limit of this test. It checks that the source contains a canonical such as /agents/coding, written as a path, while the absolute URL a crawler sees depends on the base URL set in the root layout. Only a request to the built site, like the crawl sketch below, asserts the final tag.

Strapi's guide lists query parameters, pagination and trailing slashes as common sources of duplicate paths. Adding one request per duplicate shape your site can produce, and asserting that it redirects or names the right canonical, is the direct way to cover them.

How do you test the sitemap in a Next.js app?

Assert that every public route is in it, every private route is out of it, and that no date in it is invented. Google's Starter Guide describes a sitemap as a file listing the URLs you care about and says it is not required, so the sitemap is a convenience to keep correct, not a switch for being indexed.

Kanso's app/sitemap.ts finds blog articles by reading the folders under app/blogs, lists agent pages by hand, and builds marketing and use-case pages from registries. Because the agent entries are hand-written, this test proves that every published agent appears in the sitemap, in robots.txt, in the llms.txt file and in the list the lastmod script reads.

for (const a of AGENT_REGISTRY.filter((x) => x.published)) {
  test(`${a.route} is in the sitemap, robots, llms.txt and the lastmod list`, () => {
    assert.ok(sitemap.includes("${base}" + a.route), "sitemap");
    assert.ok(robots.includes(`'${a.route}'`), "robots");
    assert.ok(llms.includes("`" + a.route + "`"), "llms.txt");
    assert.ok(lastmod.includes(`'${pageFor(a.route)}'`), "lastmod manifest list");
  });
}

Dates come from a checked-in manifest, not from git at build time. Kanso's content standards record the reason: Vercel builds from a shallow clone, where a git lookup can return the wrong commit, so a CI job with full history regenerates the manifest on pushes to main and a page with no entry is listed without a date.

The same document records a deployment-only failure. Reading the blogs folder at runtime needs outputFileTracingIncludes in next.config.ts, and without it the sitemap can fail for crawlers while looking fine in every manual check.

No build-time unit test can catch that bundling problem. Only a request to a preview or production deployment can, which is a reason to run the crawl check against a deployed URL as well as a local server.

How do you test robots.txt and noindex rules?

Pin the one deliberate site-wide noindex in a test, and assert that each public section overrides it on purpose, so a leak in either direction fails the build. Kanso's root layout sets robots to no-index, no-follow for the whole site as a pre-launch default, and the blog layout and each public page override it.

Kanso's layout comments explain the mechanism. Metadata is resolved per route segment, so a child segment's own robots field replaces the one it would inherit, and that is why the override works for one subtree and leaves every other route private.

test("it is in the sitemap (from the author records) and robots.txt allows /author/, while the site-wide noindex stays untouched", () => {
  const sitemap = read("app/sitemap.ts");
  assert.match(sitemap, /AUTHOR_SLUGS\.map\(\(slug\) => \(\{/);
  assert.match(sitemap, /\.\.\.authorUrls,/);
  assert.match(read("app/robots.ts"), /'\/author\/'\]/);
  assert.match(read("app/layout.tsx"), /robots: \{ index: false, follow: false \}/);
});

That test asserts three things at once: the author pages are in the sitemap, robots.txt allows their path, and the root layout still carries its noindex. The last assertion is the useful one, because a well-meaning edit to the root would otherwise change the whole site at once.

Match the test to the tool. The Starter Guide says Google supports various ways to opt a page or a whole site out of crawling and indexing, and a disallow rule in robots.txt and a noindex tag are different tools with different effects. A test should name which one a route is meant to use.

How do you test status codes and redirects?

Test the decision that picks the status, then confirm the real response from a running build. Kanso models each use-case page's lifecycle in one pure function, and its tests pin what a draft, a retired page and an unknown slug each do.

test("draft: 404 in production, renders in preview, never indexed, never in the sitemap", () => {
  assert.deepEqual(routeDecision(entry("draft"), prod), { kind: "not-found" });
  assert.deepEqual(routeDecision(entry("draft"), preview), { kind: "render" });
  assert.deepEqual(robotsFor(entry("draft")), { index: false, follow: false });
  assert.equal(isSitemapEligible(entry("draft")), false);
});

test("retired: redirects to its replacement (or the hub), never renders, never in the sitemap", () => {
  const all = clone();
  const retired = { ...all[0], status: "retired", replacedBy: "startups" };
  assert.deepEqual(routeDecision(retired, prod, all), { kind: "redirect", to: "/use-cases/startups" });
  // ...
  assert.equal(isSitemapEligible(retired), false);
});

A second excerpt shows the order of operations for a page that must not exist in production. The test asserts that the 404 guard sits before any rendering, and that the page is never indexable.

test("the route answers 404 BEFORE it renders anything, on every request, and is never indexable", () => {
  assert.match(page, /export const dynamic = "force-dynamic";/);
  const guard = page.indexOf("if (!authorDirectoryVisible()) notFound();");
  const render = page.indexOf("return (");
  assert.ok(guard > 0 && guard < render, "the guard comes first");
  assert.match(page, /robots: \{ index: false, follow: false \}/);
});

Redirects matter most when a site moves. In the June 2024 office hours Google answered an owner whose search traffic fell by more than 60% after a March 2024 migration to a new platform, and said redirects are critical for forwarding signals, while also noting that a core update had launched in the same period, so the cause was hard to isolate.

Google's suggested diagnosis was to compare the old and new site in Search Console and in the Internet Archive. A CI form of that advice is a fixed list of old URLs, each asserted to redirect and to land on a page that answers 200, which is Kanso's suggestion rather than Google's.

Be exact about what Kanso's own tests cover. A comment at the top of its marketing-page test file says status codes and links on the rendered site are verified by crawling a production build in a browser, so the pure-function tests above cover the decision and the crawl covers the response.

How do you run these checks in CI?

Run the fast source-level tests first, build the site, then start the production server and crawl the sitemap with a script. Kanso's workflow runs four steps in this order on every push, on Node 22, and none of them deploys anything.

- name: Lint
  run: npm run lint

- name: Typecheck
  run: npm run typecheck

- name: Unit tests
  run: npm test

- name: Production build
  run: npm run build

Kanso's workflow ends at the build. The crawl step below is not part of it and is a sketch written for this article, not an excerpt, and it was not run against a Kanso build.

// Sketch only. Start the production build first (next start), then run this file with Node.
import assert from "node:assert/strict";

const BASE = process.env.BASE_URL ?? "http://localhost:3000";
const xml = await (await fetch(`${BASE}/sitemap.xml`)).text();
const paths = [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => new URL(m[1]).pathname);

for (const p of paths) {
  const res = await fetch(BASE + p, { redirect: "manual" });
  assert.equal(res.status, 200, `${p} answered ${res.status}`);
  const html = await res.text();
  assert.match(html, /<title>[^<]+<\/title>/, `${p}: no title`);
  assert.equal((html.match(/<h1[\s>]/g) ?? []).length, 1, `${p}: expected exactly one h1`);
  assert.match(html, /<link[^>]+rel="canonical"[^>]*>/, `${p}: no canonical`);
}

The sketch reads every URL from the sitemap, so it checks the pages you told crawlers about. It fails on a non-200 answer, a missing title, anything other than one h1 and a missing canonical link, which covers the rendering, metadata, canonical and status checks in one pass.

Leave performance scores out of the pass or fail gate. Google said in the June 2024 office hours that PageSpeed Insights lab tests are simulations that vary with server configuration, and advised looking at field data and watching for unexpected changes over time instead of small movements. The weekly review in Kanso's weekly growth checklist is the better place for those numbers.

What is documented, and what is our analysis?

The first list is sourced and the second is Kanso's own interpretation. Keeping them apart lets you disagree with the second without doubting the first.

  • Documented: Google's June 2024 office hours say most platforms work fairly well for SEO, that JavaScript-heavy frameworks deserve a review against its JavaScript SEO guidance, and that SEO should be set up before launch.
  • Documented: Google's Starter Guide says to make each piece of content reachable at one URL, to redirect non-preferred URLs where possible and otherwise use a canonical link, and that a sitemap is optional.
  • Documented: the June 2024 office hours say a title does not need to match the h1, and that a broken head element might hurt where a broken paragraph probably will not.
  • Documented: the next-seo README says standard meta tags belong to the built-in metadata function and that its JSON-LD script needs a nonce under a strict Content Security Policy.
  • Documented: the Kanso excerpts are copied from its repository as of 5 November 2026, and its CI runs lint, typecheck, tests and build on every push.
  • Analysis: failures in this layer are silent, so they suit automated checks better than manual review.
  • Analysis: source-level tests prove what the code says, so a crawl of the built site is needed to prove what the server sends.
  • Analysis: a fixed list of old URLs that must redirect is the CI form of Google's migration advice, and performance scores should not gate a build.
  • Not covered: no measured effect of any check on rankings, indexing or traffic exists in this package, so none is claimed.

Where does Kanso's Coding Agent fit?

It fits a narrow part of this checklist, and not an App Router codebase. Kanso's Coding Agent prepares title, meta description and canonical fixes as GitHub pull requests that you review, and it never merges or deploys.

Its limits matter here. Kanso's page for the workflow says the agent looks for a page's static HTML file and stops if it cannot confirm which file produces the page, and the agent page says it does not edit framework, script, configuration or workflow files.

For a Next.js site that means the checks in this article are yours to run, and the agent is relevant for static HTML pages. The use case for shipping technical SEO fixes describes that flow, and pricing lists the plans.

Next.js SEO checklist: frequently asked questions

Is Next.js SEO friendly?

Mostly yes, according to Google: in its June 2024 SEO office hours the Search team said most platforms work fairly well for SEO out of the box, and that a framework doing significant JavaScript work is worth reviewing against Google's JavaScript SEO best practices. It also said that for a new site the SEO setup should be done before launch, because adding it later is always much harder.

How does Next.js handle SEO?

According to Strapi's guide, Next.js handles SEO through server-rendered HTML and a Metadata API: in the App Router you export a metadata object or a generateMetadata function from a layout or page, and Next.js merges them across nested layouts. Strapi describes this as centralizing titles, descriptions and canonical URLs, where the Pages Router scatters them across Head blocks.

What is the best way to set meta tags in the App Router?

Strapi's guide shows a metadata object for static values, a generateMetadata function when the values depend on data, and shared defaults such as a title template in the root layout. The next-seo package's own README says to use Next.js's built-in generateMetadata for standard meta tags and to use its components for JSON-LD structured data.

Do I need the next-seo plugin?

No, not for standard meta tags, because the package's README says to use generateMetadata for those. Its components are for structured data (JSON-LD), and they render an inline script, so a strict Content Security Policy blocks that script unless it carries a matching nonce.

Do SSR, SSG and ISR differ for SEO?

They differ in freshness and cost, not in crawlability, according to Strapi's guide, because all three send fully rendered HTML. Strapi suggests static generation for marketing pages, incremental regeneration for blog posts and product listings, and server rendering for personalized dashboards. That is one vendor's recommendation, not Google guidance.

Should the title tag match the H1?

No. In the June 2024 office hours Google's answer was to do whatever makes sense from a user's perspective, so a test should check that a title exists, is unique and is a sensible length rather than that it equals the h1.

What is SEO?

SEO, or search engine optimization, is the work of helping search engines understand your content and helping people find your site through search. Next.js Learn describes three pillars: technical (crawling and web performance), creation (content aimed at specific keywords) and popularity (backlinks), and this checklist covers only the technical one.

Can broken HTML hurt a Next.js page's SEO?

It depends on what is broken. In the June 2024 office hours Google said a broken paragraph or similar benign element is unlikely to hurt a site, while a broken head element might, which is why the crawl sketch in this article asserts that a title and a canonical are present in the response.

How long until a fix shows in search results?

Google's Starter Guide says some changes take effect in a few hours and others can take several months, and suggests waiting a few weeks before judging whether a change helped. A CI check therefore proves the fix shipped, not that search results moved.

Next.js or Astro for a marketing site?

This article's evidence does not compare them, so it gives no verdict. Google's own position in the June 2024 office hours is that most platforms can be set up well or badly for SEO, so the framework matters less than whether the checks in this article pass on the result.

Sources

Note: Rules are attributed to Google Search Central, Next.js Learn or the named guides and were read on 5 November 2026. The Kanso excerpts were copied from the kansoai-frontend repository on the same date. Nothing here is a measured result, and Kanso has no affiliation with Google, Vercel, Strapi or the next-seo project.

Search & AI discovery

Ready to grow?

Enter your website and let Kanso handle the rest.

Get started free. No credit card required.