DEV Community

Cover image for Your Next.js App Router pages aren't indexed? Check the canonical you didn't set
Eric Gitangu
Eric Gitangu

Posted on

Your Next.js App Router pages aren't indexed? Check the canonical you didn't set

I relaunched my portfolio on the Next.js App Router, submitted the sitemap, and waited. A week later Google Search Console still said 0 pages indexed — everything sat in "Crawled – currently not indexed."

My first instinct was the usual: time and domain authority. Both are real for a new site. But before you blame Google, it's worth ruling out the self-inflicted version — because in my case there was one, and it's a trap that's very easy to fall into with the App Router's metadata API.

The gotcha: an inherited canonical

In the App Router you set metadata in layout.tsx or page.tsx. It's tempting to put your canonical URL once, in the root layout:

// app/layout.tsx — DON'T do this
export const metadata: Metadata = {
  metadataBase: new URL("https://example.com"),
  alternates: { canonical: "/" },
};
Enter fullscreen mode Exit fullscreen mode

Metadata inherits. So every route under that layout — /about, /projects, every post — renders <link rel="canonical" href="https://example.com/">. You've just told Google that every page on your site is a duplicate of the homepage. Google obliges and indexes… the homepage, maybe.

The fix is a per-route canonical. A tiny helper keeps it honest:

// app/lib/seo.ts
import type { Metadata } from "next";

export function pageMetadata({ title, description, path }: {
  title: string; description: string; path: string;
}): Metadata {
  return {
    title,
    description,
    alternates: { canonical: path }, // self-referencing, per route
    openGraph: { title, description, url: path },
  };
}
Enter fullscreen mode Exit fullscreen mode
// app/about/layout.tsx
export const metadata = pageMetadata({
  title: "About",
  description: "…",
  path: "/about",
});
Enter fullscreen mode Exit fullscreen mode

Now each route points at itself. Verify it the way Google does — inspect a URL in Search Console and check that Google-selected canonical matches your user-declared canonical. If they disagree, nothing else you do will get that page indexed.

Three more App Router SEO fixes that actually moved the needle

1. Noindex your preview/alias domains. Vercel gives every deployment a *.vercel.app URL, and they're all live and crawlable. Left alone, they compete with your real domain as duplicates. Send a noindex header to every host except the canonical one — no middleware needed:

// next.config.mjs
async headers() {
  return [{
    source: "/:path*",
    missing: [{ type: "host", value: "example.com" }],
    headers: [{ key: "X-Robots-Tag", value: "noindex, nofollow" }],
  }];
}
Enter fullscreen mode Exit fullscreen mode

2. Self-host your Open Graph image. I'd been pointing og:image at a LinkedIn CDN URL — which expires and isn't fetchable by some scrapers (WhatsApp never rendered it). Generate it at the edge instead with next/og, so it's a real static asset at /opengraph-image:

// app/opengraph-image.tsx
import { ImageResponse } from "next/server";
export const size = { width: 1200, height: 630 };
export default function OG() {
  return new ImageResponse((/* your JSX */), size);
}
Enter fullscreen mode Exit fullscreen mode

3. Add real structured data. A Person + BreadcrumbList JSON-LD block in your layout gives Google an entity to attach your name to. It's cheap and it's one of the few signals you fully control on a new site.

And the part that is just time

After all that, some pages still read "Crawled – currently not indexed," and that's the honest truth about a new site: crawl ≠ index. Google crawled them fine (fetch successful, indexing allowed) and simply hasn't decided they're worth storing yet. The levers that change its mind are mundane — a sitemap it trusts, a handful of referring links from pages it already crawls often, internally-linked hub pages, and a few weeks. "Request indexing" nudges a specific URL; it doesn't buy authority.

So: fix the canonical first (that's the bug), then be patient about the rest (that's the physics).


The code above is live on my site — developer.ericgitangu.com — and if you want to see the per-page canonicals and JSON-LD in the wild, the projects section renders a SoftwareSourceCode block per entry. I'm Eric Gitangu, a software engineer working on distributed and fintech systems; happy to compare notes in the comments.

Top comments (1)

Collapse
 
citedy profile image
Dmitry Sergeev •

We need to write a short YouTube comment, casual, as a developer who watched the video. Must not be generic praise, must mention something specific about the video. Eg ask about how to set canonical in app router, maybe about why pages not indexed. Use casual voice, lowercase start, maybe "tbh". Keep short, one sentence, maybe fragment. No quotes, hashtags, markdown. No double hyphens. No URLs. Ensure no smart quotes, no em dash. Write comment only.