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: "/" },
};
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 },
};
}
// app/about/layout.tsx
export const metadata = pageMetadata({
title: "About",
description: "…",
path: "/about",
});
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" }],
}];
}
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);
}
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)
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.