Skip to content

· 8 min read

Next.js SEO checklist for 2026: 12 fixes for the App Router

The checklist I run on every Next.js App Router site: metadata, canonicals, sitemap, robots, JSON-LD, headings, server-rendered content, speed and Search Console. With code you can copy.

Vishal Solanki · Full-stack developer, Noida

Most Next.js sites that don't rank aren't missing a secret trick. They're missing basics the framework already gives you: a unique title per page, one canonical URL, a sitemap with honest dates, and content that's actually in the HTML.

This is the checklist I run on every App Router site, including this one. Each item comes with code you can copy.

Short version: set metadataBase, give every page its own title and description, pick one canonical host, generate sitemap.ts and robots.ts, add JSON-LD, keep one real H1 with real text, make sure your content is server-rendered, and set up Search Console and Bing.

1. Set metadataBase and a title template

In the root layout, tell Next.js your production URL once. Every relative URL in your metadata (canonicals, Open Graph images) is resolved against it.

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

export const metadata: Metadata = {
  metadataBase: new URL("https://www.example.com"),
  title: { default: "Example — What you do, where you do it", template: "%s — Example" },
  description: "One or two sentences a searcher would actually click on.",
};

With the template, a page that sets title: "Pricing" shows up in search as "Pricing — Example", so your brand is on every result without repeating it by hand.

2. Give every page its own title and description

Duplicate titles are the most common problem I find in audits. For dynamic routes, use generateMetadata:

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
  const post = await getPost((await params).slug);
  return {
    title: post.title,
    description: post.description,
    alternates: { canonical: `/blog/${post.slug}` },
  };
}

Keep titles around 50–60 characters with the main keyword near the start. Descriptions don't change rankings directly, but they decide whether people click, so write them like a small ad: who it's for, what they get, one proof point.

3. Pick one canonical host and redirect the rest

example.com, www.example.com, your *.vercel.app address and http:// are four copies of the same site as far as Google is concerned. Pick one, use it as metadataBase, add a canonical to every page (as above) and permanently redirect the others.

On Vercel, set the primary domain in the dashboard. For an old deployment address, a host-based redirect in next.config.ts does the job. This one sends an old Vercel URL, and everything it ranked for, to the real domain:

// next.config.ts
async redirects() {
  return [
    {
      source: "/:path*",
      has: [{ type: "host", value: "old-project.vercel.app" }],
      destination: "https://www.example.com/:path*",
      permanent: true,
    },
  ];
}

4. Generate the sitemap, with honest dates

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

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getPosts();
  return [
    { url: "https://www.example.com", lastModified: "2026-10-24" },
    ...posts.map((p) => ({
      url: `https://www.example.com/blog/${p.slug}`,
      lastModified: p.updated ?? p.date,
    })),
  ];
}

Two details matter. Google ignores priority and changeFrequency, so don't spend time tuning them. It does use lastModified, but only while it stays accurate. If every URL shows the build date on every deploy, Google learns to ignore it. Change the date when the content changes, and only then.

5. Add robots.ts, and don't block what you want

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

export default function robots(): MetadataRoute.Robots {
  return {
    rules: { userAgent: "*", allow: "/" },
    sitemap: "https://www.example.com/sitemap.xml",
  };
}

If you want to show up in ChatGPT search and Perplexity answers, keep their crawlers (OAI-SearchBot, PerplexityBot) allowed. Also check your hosting firewall or CDN: some bot-protection settings block AI crawlers even when robots.txt allows them. I go deeper on this in how to get cited by ChatGPT, Perplexity and Google AI Mode.

6. Describe the page with JSON-LD

Structured data tells search engines what a page is: a person, a business, an article, a product. Render it as a script tag from a server component:

const schema = {
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  headline: post.title,
  datePublished: post.date,
  author: { "@type": "Person", name: "Your Name", url: "https://www.example.com" },
};

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

The .replace escapes < so no string inside your data can close the script tag early. The Next.js docs recommend it. Check the result with Google's Rich Results Test.

For a personal or freelance site, a Person and a ProfessionalService on the homepage plus a BlogPosting on each post covers most of what matters. Only mark up what's visible on the page.

7. One real H1, with real text

Search engines read the text inside your headings, not what they look like. Until recently, Google read my own homepage H1 as "I BUILD 01 IDEA THINGS 02 INTERACTION FOR THE WEB. 03 PRODUCT", because an animated word swap lived inside the heading. Visitors never saw that jumble. Crawlers saw nothing else.

The fix was to draw the decorative words with CSS (::before and ::after with content: attr(data-word)), so the heading's text is simply "I build things for the web." The animation looks exactly the same.

  • One <h1> per page.
  • No decorative fragments, counters or icons with text inside it.
  • Don't hide keyword text from visitors. Google treats hidden text as spam.

8. Make sure the content is in the HTML

Server components render to HTML by default, which is exactly what you want. Problems start when important text only appears after client-side JavaScript runs: inside a useEffect, behind a tab that only mounts on click, or drawn on a canvas. Google can render JavaScript, but later and less reliably, and most AI crawlers don't run it at all.

The quick test: fetch the page the way a crawler does and search for a sentence you care about.

curl -s -A "Mozilla/5.0 (compatible; Googlebot/2.1)" https://www.example.com/ | grep -c "a sentence from your page"

If it prints 0, that text isn't in the HTML a crawler receives.

9. Open Graph images per page

Add an opengraph-image.tsx file to a route segment and Next.js generates the image and the meta tags for you:

// 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 Image({ params }: { params: Promise<{ slug: string }> }) {
  const post = await getPost((await params).slug);
  return new ImageResponse(
    <div style={{ display: "flex", fontSize: 64, padding: 80 }}>{post.title}</div>,
    size,
  );
}

It won't change your rankings, but links shared on LinkedIn, WhatsApp and X look like something worth opening instead of a grey box.

10. Speed: images, fonts and third-party scripts

Core Web Vitals feed into ranking, and they decide whether visitors stay long enough to read anything.

  • Images: use next/image with width and height (or fill inside a sized parent) so space is reserved. Add preload to the one image that is your largest paint. In Next.js 16, preload replaced the old priority prop.
  • Fonts: use next/font. It self-hosts the font and generates a size-matched fallback, which removes most font-swap layout shift.
  • Third-party scripts: load analytics and tag managers late. On this site, Google Tag Manager loads on the first interaction, or after a few seconds if there isn't one, so it never competes with the first paint.

The full playbook is in Core Web Vitals in 2026: fixing LCP, INP and CLS in Next.js.

11. Real 404s and clean redirects

  • A missing page must return a 404 status, not a friendly page with a 200. Call notFound() for unknown slugs, and set dynamicParams = false on statically generated routes so unknown URLs 404 automatically.
  • Add an app/not-found.tsx that matches your design and links to your most useful pages.
  • Use permanent redirects (308) when a URL has moved for good and temporary ones (307) when it hasn't.
  • Avoid redirect chains. Each extra hop costs crawl time and a little speed for visitors.

12. Tell Google and Bing the site exists

  • Verify your domain in Google Search Console with a DNS record (that covers every subdomain and protocol), submit sitemap.xml and request indexing for your key pages.
  • Import the property into Bing Webmaster Tools. Bing also powers Microsoft Copilot and feeds ChatGPT search, so it's worth the two minutes.
  • Check the Pages report weekly. "Crawled – currently not indexed" usually means a page is thin, duplicated or not linked from anywhere.

The checklist

#CheckWhere
1metadataBase and a title templateapp/layout.tsx
2Unique title and description on every pagegenerateMetadata
3One canonical host, the rest redirectedalternates.canonical, redirects()
4Sitemap with accurate lastModifiedapp/sitemap.ts
5Search and AI crawlers allowedapp/robots.ts, CDN or firewall rules
6JSON-LD, validatedServer component script tag
7One real H1 per pageYour components
8Key text in the server HTMLThe curl test
9Open Graph image per pageopengraph-image.tsx
10Good Core Web VitalsPageSpeed Insights, Search Console
11Real 404s, no redirect chainsnot-found.tsx, notFound()
12Search Console and Bing set upTheir dashboards

FAQ

Is Next.js good for SEO?

Yes. Server rendering, the Metadata API, file-based sitemaps and robots, and built-in image and font optimisation cover most technical SEO out of the box. What it can't do is choose your keywords, write your content or earn links. That part is still on you.

Do I still need a package like next-seo?

Not with the App Router. The built-in Metadata API covers what next-seo used to do for the Pages Router.

How long until a new Next.js site ranks?

Your own name usually ranks within a few weeks of being indexed. Competitive service keywords take months of useful content and links. Search Console shows you where you stand long before the traffic arrives.


Want this checklist run on your site, with the fixes done for you? That's what I do.

  • Next.js
  • SEO
  • App Router