Nothing was broken. The link between my two homepages had never worked.

seoi18n

In my last article I wrote about translating documentation into nine languages, where every check came back green and a lot of it was broken. This one comes out of the same stretch of work and points the opposite way, and I think it’s the more uncomfortable of the two:

Nothing was broken. Something had still never worked.

Here’s the setup. My site is in English and Italian. Every page has a twin. There’s a little tag in the HTML whose entire job is to tell Google “this page and that page are the same page, in two languages” — it’s called hreflang, and it’s the reason a person searching in Italian gets sent to the Italian version instead of the English one.

Mine had never worked on the homepage. Not “worked badly”. Never worked, from the day I added it, which git says was 2026-07-12. Thirty-three days.

And in those thirty-three days, absolutely nothing looked wrong. Both homepages were live. Both rendered perfectly. Every link on the site clicked through to where it said it would. No error, no warning, no failing build, nothing red anywhere. I had four automated checks running on every build and all four were green — one of which I’d written specifically to protect indexing.

I only found it because I’d connected the site to Google Search Console a few days earlier, and it had finally collected enough data to say anything at all.

The short version

  • Search Console told me 228 of my 254 pages were indexed — 90%. It read like good news, and I nearly closed the tab.
  • Of the 26 that weren’t, six were filed under “page with redirect”. Those six are what led to everything else.
  • The cause was a missing slash at the end of my links. My host answers /about by bouncing you along to /about/. In a browser you never see it happen, so nobody notices — but I was publishing nine addresses that all took the detour.
  • Eight of those nine were ordinary links people click. The ninth was inside the tag that pairs my English and Italian homepages — and that tag only counts if the address it names answers directly. Mine named the detour, so Google was never actually told the two pages were a pair.
  • My 404 page was announcing two addresses that don’t exist — /404/ and /it/404/ — as though they were real translated pages. Two of the three “not found” errors in the report were my own site’s doing.
  • All nine addresses came out of one function, so a two-line fix corrected them all at once. Including the invisible one, which I would never have found by hand.
  • The check I wrote afterwards failed on its very first run, and caught a copy of the same mistake sitting in the one place I’d forgotten to look.

A redirect is not a bug, which is exactly the problem

Start with the boring mechanic, because everything else follows from it.

When you host a static site, a page like about usually lives in a folder as about/index.html. Ask for /about/ and you get it. Ask for /about — no slash — and most hosts, mine included, send back a redirect: “not here, try /about/.”

That’s not an error. It’s correct, standard behaviour, and it’s been that way forever. Type /about in your browser and you land on the About page, instantly, with no sign anything happened.

So when I wrote my navigation links without the trailing slash, nothing told me. Not the build. Not the browser. Not a single human clicking around the site. The links worked. They just took one extra hop first, every single time, and a hop nobody sees is a hop nobody mentions.

Nine URLs, across every page of the site: /about, /blog, /learn, /privacy, and the Italian twin of each, plus /it itself.

Google noticed. Google files it under “page with redirect” and, reasonably, doesn’t index the version that redirects — it indexes the one that answers. Which is fine! That’s the system working. Six wasted crawls is a rounding error and I’d have shrugged at it.

Then I looked at where the ninth one lived.

Eight of those URLs were links for people. Menu items, footer, buttons.

The ninth was /it — and it appeared in my homepage’s hreflang tag.

That tag isn’t a link anyone clicks. Nothing on the page points to it. It’s not navigation, it’s not content, it has no appearance at all. It’s a statement about a relationship: the English homepage and the Italian homepage are the same page in two languages.

And a statement like that has a requirement attached: the address you name has to be the real one — the canonical URL, the one that answers directly. Name a redirect instead and the statement doesn’t get honoured. You haven’t made a weak claim. You’ve made no claim.

So for thirty-three days my English homepage said “my Italian twin lives at /it, /it said “actually, /it/, and the pairing quietly didn’t happen.

Now sit with what that looked like from my side. Both pages existed. Both were in my sitemap. Both were being served, correctly, to anyone who asked. If you’d handed me the site and said “one thing here has never worked, find it”, I could have clicked every link, read every page in both languages, run every check I own, and found nothing — because there was nothing to find. The two pages were fine. It was the line between them that didn’t exist.

You can look at a thing. You can’t look at a relationship.

That’s the part I actually want to give you, and it took me a day to say it this simply.

A page has an appearance. If it’s broken, something looks broken — a gap, a wrong colour, a sentence that stops. You can catch that by looking, and looking is cheap, and that’s why most of our instincts about “checking our work” are really instincts about looking at things.

A relationship between two pages has no appearance. There is nothing to render. It doesn’t live on either page in a form a human reads; it lives in a claim one page makes about the other, addressed to a machine that will visit later and decide whether to believe it. If the claim is malformed, both pages still look perfect, because both pages are perfect. The defect isn’t in either of them.

Which means the entire category is invisible to looking. Not “hard to see”. Structurally invisible — there’s no view in which it appears.

Once I had that sentence, I went and made a list of everything on my own site that’s in that category, and it’s longer than I expected:

  • hreflang — a relationship between two pages. This one.
  • canonical — a claim about which address is the real one when several could serve the same page.
  • Structured data — a description of the page written for machines, which no visitor ever sees. Mine is a block of JSON in the <head>. It could describe a completely different website and the page would look identical.
  • Redirects — things that work, just not necessarily for the purpose you needed them to.
  • robots directives — a single wrong word here removes you from search, over weeks, with no visible symptom at any point. That one scares me enough that it has its own check.

Everything on that list has the same shape. None of it is content. All of it is a message about the page, addressed to a search engine.

Which is why opening your own site tells you nothing about any of it. What you see in a browser is the page. These things aren’t on the page — they’re notes pinned to it for somebody else to read, and that somebody isn’t you. You can stare at a perfectly rendered page all day and learn nothing about whether the note attached to it was correct, or contradictory, or addressed to an apartment that doesn’t exist.

So the only thing that can tell you is the recipient. For me that’s Google Search Console — free, and it reports on a lag of about a week. I’d connected it earlier the same month and hadn’t read it yet, on purpose: a brand-new property has no data in it, and there was genuinely nothing to see. This was the first time it had enough history to say anything, and the first thing it said was that something had been wrong the whole time.

The fix was one function, and that is the whole reason it was small

Here’s the part that felt good, and it’s the one piece of ordinary engineering advice in this article.

All nine URLs came out of the same place. I have exactly one function that builds a path for a given language — the menu uses it, the footer uses it, the buttons use it, the breadcrumbs use it, and the hreflang tags use it. It looked like this:

// English has no prefix; other languages get theirs.
export function localizePath(path, lang) {
  const clean = path === '/' ? '' : path;
  return lang === defaultLang ? path : `/${lang}${clean}`;
}

Read it and you can see the whole bug. It faithfully passes through whatever you hand it, and I’d been handing it /about. And for the homepage it does something slightly worse: path === '/' becomes an empty string, so / in Italian comes out as /it, with no slash at all. That’s the homepage case. That’s the one that cost thirty-three days.

The fix is two lines:

export function localizePath(path, lang) {
  const withSlash = path.endsWith('/') ? path : `${path}/`;
  return lang === defaultLang ? withSlash : `/${lang}${withSlash}`;
}

One function, and the menu, the footer, the buttons, the breadcrumbs and the hreflang tags on 240 pages were all correct at the same time.

I want to be clear about why I’m telling you this, because “put it in one place” is advice you’ve heard until it’s wallpaper. The point isn’t that centralising is elegant. The point is what the alternative would have looked like on this specific bug. If those paths had been typed by hand in nine components, I’d have fixed the six that Search Console named, felt finished, and left the hreflang — the only one that actually mattered — sitting there. Because it’s the one I couldn’t see.

So here is the reason that’s worth more than tidiness. The bug I could see and the bug I couldn’t were the same bug, in the same two lines. Fixing the one Google complained about — a menu link — repaired the hreflang at the same moment, without me knowing that was the important part. I got the fix I couldn’t have found for free, as a side effect of the fix I could.

That only works because one function built both. Nine hand-written copies and I’d have fixed the six I was told about and stopped.

The check I wrote, and what it caught in the first ten seconds

I added a check that runs on every build. It reads the finished site and, for every URL that points at my own domain — in links, in canonical, in hreflang, and inside the structured data — it asserts two things: that the address ends in a slash, and that something is actually there.

I ran it expecting a clean pass, because I’d already fixed everything by hand and checked it.

It failed immediately.

My 404 page — which I had just been through, specifically to strip out the wrong URLs — was still announcing /404/ inside its structured data. I’d fixed the tags in the <head> and never thought about the block of JSON further down, which describes the page by naming its own address. Same wrong URL, second hiding place, and I’d walked past it.

There’s a detail in why I walked past it that I’ll pass on, because it’s a trap with my name on it. When I checked the structured data by hand, I searched it for a pattern. The pattern was written the way JSON looks when it’s formatted for humans, with a space after the colon. My site minifies it, so there’s no space. My search matched nothing, printed nothing, and I read the silence as a clean bill of health.

If that failure sounds familiar it’s because it’s the subject of my previous article — I’d done the identical thing with a different search on a different project, and published a whole piece about it days before doing it again here. So I’m not going to re-teach it: it’s over there, and it’s the better version.

What I will say is that writing it down did not stop me. I had described this exact mistake in detail, in public, in the article right before this one, and I still made it. Understanding a trap turns out to be almost no protection against falling into it. Which is the argument for the check — not because I don’t know better, but because knowing better demonstrably isn’t enough.

And it’s why the check does the job differently than I did. I searched the text for a pattern; the check reads the structured data as data. It parses it into a real object and walks through every address inside. That difference is the whole point: a text search that finds nothing looks exactly like a text search that found nothing wrong, and you cannot tell the two apart from the outside. A parser can’t do that to you. Either it reads the structure and checks every address in it, or it fails loudly because the structure wasn’t what it expected. There is no silent, empty, reassuring middle.

And then I did the one thing I’ve actually learned to do: I broke the site on purpose to see if the check would notice. I put a slash-less link in a page — caught. I pointed an hreflang at a page that doesn’t exist — caught. I removed one slash from a URL that appears only inside the structured data, nowhere else — caught. Then I put everything back.

Ten minutes. Without it I’d have a green tick I had no reason to believe.

So what would I tell you to do

Three things, and the first one is the only one I’d insist on.

Go open Search Console. Right now, before you finish reading. Not to fix anything — just to look at the coverage report and see what it says about pages you thought were fine. It’s free, you’ve probably already got it connected, and it is the only thing you own that can see the category of problem I’ve described. And if you’ve only just set it up, put a reminder in your calendar for a couple of weeks out, because an empty property tells you nothing and it’s easy to glance at it once, see nothing, and never go back. Ninety percent indexed is not the same as nothing to see here — I’d have told you it was, right up until I read the other 10%.

When you find something, follow it past the first explanation. Six redirecting URLs is a boring finding. I nearly wrote it off, and the interesting thing was underneath it — not in the six pages Google named, but in the ninth URL from the same cause that Google had no way to report, because from Google’s side there was nothing to report. It had simply never been told the two pages were related.

Make your own list of the things with no symptom. This is the real generalisation and it goes well past hreflang. To save you starting from scratch, here is a concrete list — every one of these can be wrong on a site that looks completely fine:

  • The status code of your 404 page. A “page not found” screen can be served with a 200 OK, which tells search engines every mistyped address on your site is a real page. It looks identical either way. Mine did this for months and I found it by accident.
  • Your sitemap, quietly listing pages you deleted, or missing ones you added.
  • The card people see when they share your link — the image, the title, the description. You are the one person who never sees it.
  • Redirects after a move. They work for the three URLs you tested by hand.
  • Whether your contact form’s email actually arrives. The form says thank you either way.
  • robots rules, canonical, structured data, hreflang — the ones this article is about.

Go through it and check the ones that apply to you. Not by looking at your site: by finding the thing on the other end that can actually answer. It took me about an hour for all of them, and I’d been treating every single one as though it were the kind of thing you can just see.

The comfortable version of this story is that I found a bug and fixed it. The true version is that a small, invisible thing was wrong for thirty-three days while every instrument I owned said fine — and the only reason I know is that a new instrument finally had enough data to disagree, and I got curious about a number that looked good.

The things this article is about

The check is a small script that reads the built site; there’s nothing clever in it, and its whole value is that it fails. If you want the companion piece — the same failure mode from the other side, where every check passed and the pages were full of holes — that’s Every check passed. A lot of it was broken. →

And if you’d rather read about being wrong in public in a more literal way, I once shipped nine identifiers that do not exist: Nine wrong names shipped →

If you take one thing from this: a green check tells you a thing is fine. It cannot tell you two things are connected. Go and ask something outside your project.

Have an awesome journey,

Francesco