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, generatesitemap.tsandrobots.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/imagewithwidthandheight(orfillinside a sized parent) so space is reserved. Addpreloadto the one image that is your largest paint. In Next.js 16,preloadreplaced the oldpriorityprop. - 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 setdynamicParams = falseon statically generated routes so unknown URLs 404 automatically. - Add an
app/not-found.tsxthat 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.xmland 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
| # | Check | Where |
|---|---|---|
| 1 | metadataBase and a title template | app/layout.tsx |
| 2 | Unique title and description on every page | generateMetadata |
| 3 | One canonical host, the rest redirected | alternates.canonical, redirects() |
| 4 | Sitemap with accurate lastModified | app/sitemap.ts |
| 5 | Search and AI crawlers allowed | app/robots.ts, CDN or firewall rules |
| 6 | JSON-LD, validated | Server component script tag |
| 7 | One real H1 per page | Your components |
| 8 | Key text in the server HTML | The curl test |
| 9 | Open Graph image per page | opengraph-image.tsx |
| 10 | Good Core Web Vitals | PageSpeed Insights, Search Console |
| 11 | Real 404s, no redirect chains | not-found.tsx, notFound() |
| 12 | Search Console and Bing set up | Their 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.