PVPURCELL · VENTURES
← All posts
Engineering · August 20, 2026 · 6 min read

Every post on my blog was telling Google not to index it

I found it this morning by accident, while checking something else. Nine posts, every one of them carrying a tag that says: this page is a duplicate, index the other one instead.

Nobody wrote that tag nine times. It was written once, in a file that had nothing to do with any individual post.

The one line

In app/blog/layout.tsx:

export const metadata: Metadata = { title: "Writing", alternates: { canonical: "/blog" }, // <- this };

That looks correct. It is the blog index, and its canonical URL is /blog. Fine.

The problem is what a layout is. In the Next.js App Router, a layout wraps its own page and every route nested underneath it. And metadata is inherited. So app/blog/layout.tsx wraps /blog, /blog/first-post, /blog/second-post, all of them, and hands every single one a canonical of /blog.

Which renders, on every post, as:

<link rel="canonical" href="https://purcellventures.co/blog"/>

What that actually tells a search engine

A canonical tag is not a hint about your preferred URL format. It is a declaration that this page and the target page are the same content, and that the target is the one worth keeping.

When a post says its canonical is the index, Google reads it as: this post is a duplicate of the listing page. So it consolidates the two, keeps the index, and drops the post. Not with an error. Not with a warning. It does exactly what it was told.

Everything on those pages went with it. The author markup. The structured data. The titles. All of it correct, all of it on pages that had asked to be ignored.

Why it survives so long

Three reasons, and they compound.

It looks right in the file. Every reviewer who opens that layout sees a canonical pointing at the page the file is named after. The bug is not in the line, it is in the inheritance.

Nothing fails. The build passes. The pages render. The links work. There is no error state, because the site is doing what it was configured to do.

The symptom is absence. The only evidence is pages that never show up in search, and a new site with pages that never show up in search looks exactly like a new site.

Check your own in two minutes

One line, no tools, no account:

curl -sL https://yoursite.com/blog/some-post | grep canonical

If the href is anything other than the URL you just requested, that page is pointing its value somewhere else. Check three or four pages from different sections. Section index pages are the usual culprits, so try a product page under a category and a post under a blog.

I found the same bug in two more places on my own site the same afternoon: three course pages canonicalising to /courses, and two pages canonicalising to /digital. Fourteen pages in total. It was not a blog problem. It was a layout-inheritance problem that happened to hit the blog first.

The fix, and the one that does not work

The obvious fix is to move the canonical from the layout onto the index page itself. That failed for me: app/blog/page.tsx is a client component, and client components cannot export metadata. Which is presumably how it ended up in the layout to begin with.

The correct fix is simpler. Delete it. A page with no canonical tag self-canonicalises: the engine treats the URL it fetched as the canonical one. That is the right answer for the index and the right answer for every post.

Then give each post its own, explicitly:

// app/blog/my-post/page.tsx export const metadata = { title: "My post", alternates: { canonical: "/blog/my-post" }, };

For posts that are client components and cannot export metadata, add a layout.tsx beside the page that does nothing but carry the canonical and render its children.

The general shape

This is the third bug of the same family I have hit this year. Something is configured once, inherited widely, and fails by producing nothing rather than by producing an error.

The pattern that catches them is not more careful reading. It is asserting that output arrives, rather than asserting that nothing threw. A test that fetches four live pages and checks each one canonicalises to itself would have caught this the day it shipped, and it is about eight lines.

A build that passes proves the code compiled. It proves nothing at all about whether the thing you wanted actually happened.