Skip to content

Solution

How to make a site that ranks, verified against the output

A site can be perfectly built, thoroughly reviewed, and rank as a single page for a year, because the thing that is wrong is only visible in the output and everybody was reading the source.

The short answer

Serve real HTML containing the actual content, emit exactly one canonical derived from the page's own path, and verify both by reading the built output rather than the source. Then hold Core Web Vitals with reserved space for images, no layout shift, and as little JavaScript as the page can survive on.

The mistake that motivates all of this

A site had three separate commits, by three engineers, each adding the correct canonical tag to page templates. Each verified their own change. The site continued to rank as a single URL.

The cause was one line in a shared root layout that also emitted a canonical, pointing at the homepage. The framework merged both heads, so every page served two canonicals, and the homepage one won on document order. Every page was telling Google it was the homepage.

Nobody saw it because nobody read the merged output. They read their own template, and their own template was correct.

The lesson is the method, not the bug: verify against the output, never against the source. That is the principle everything below is built on.

Step 1. Serve the content as HTML

Load the page with JavaScript disabled and read it. Whatever remains is what you can be confident a crawler indexes reliably.

If the headline, the body, the price, the specification or the navigation is missing, that content is dependent on execution. Crawlers do run JavaScript, on a delay and within a budget, and for a page whose purpose is to be found, making them work for it is a disadvantage you chose.

Two specific traps:

Counters and statistics that animate from zero. A component initialised at zero and animated to the real value serves 0 in the HTML. The rendered DOM reads “0+ customers”. Render the real value in the markup and let the script animate toward a number that is already there.

Content locked in a dialog or a tab. Text that only exists after an interaction is text that only exists after an interaction. If it is worth ranking for, it needs a URL.

Step 2. Exactly one canonical, derived not declared

Make the canonical impossible to get wrong rather than easy to get right.

  • One place emits head tags. One layout. Nothing else, ever.
  • The canonical is computed from the page’s own path, never accepted as a parameter. Remove the ability to pass a wrong value and the wrong value cannot be passed.
  • Normalise deliberately. Pick trailing slash or no trailing slash and apply it everywhere. Strip query parameters that do not change content.
canonical = origin + normalise(currentPath)

There is no argument to this function. That is the design.

Step 3. Fail the build on the output

The check that would have caught the original bug in a minute.

After building, walk every emitted HTML file and assert:

  • Exactly one canonical, and it matches the file’s own location.
  • Exactly one title, one meta description, one h1.
  • Title within about sixty characters, description within about a hundred and fifty five.
  • Every JSON-LD block parses.
  • No aggregateRating or review markup unless the reviews are real, because inventing them is a policy violation with real consequences.
  • Every image has alt text and explicit dimensions.
  • Every internal link resolves to a page that was actually built.

That last one is worth its weight on its own. A site that links to its own missing pages is worse than a smaller site, and clicking everything is not a strategy.

This site runs exactly that script, and it caught a stray HTML file left over from a previous version on its first run.

Step 4. Structured data that is true

Emit the types that match the page, and only claims you can defend.

every page      Person or Organization, WebSite, BreadcrumbList on nested pages
articles        Article or TechArticle, with real dates
products        Product with a real price and availability
pages with FAQs FAQPage, matching visible text on the page

Two rules. The markup must match what a human sees on the page, because markup describing content that is not there is a manual action waiting to happen. And never invent ratings. It is the single most common structured data violation and the temptation is obvious.

Step 5. Hold Core Web Vitals with structure, not tuning

The three metrics, and what actually moves each one.

Largest contentful paint. Usually the hero image or the headline. Serve the content in HTML, so there is no round trip before anything renders. Give the LCP image explicit dimensions and modern formats, and preload it only if it genuinely is the LCP element. Self host fonts with font-display: swap and preload the one face used above the fold.

Cumulative layout shift. Almost always images without dimensions, ads or embeds injected after paint, or a font swap that changes metrics. Every image gets width and height. Every slot that will be filled later reserves its space first. This is the cheapest of the three to get to zero and the most commonly ignored.

Interaction to next paint. Driven by how much JavaScript is executing. The reliable fix is less of it. On a content site the correct amount for most pages is none, and shipping none is a framework decision more than an optimisation effort.

Then set a budget and enforce it in CI, because performance without a gate regresses one component at a time.

LCP  under 1.5s
INP  under 100ms
CLS  0
JS   under 20KB on content pages

Step 6. Sitemap and robots that tell the truth

Generate the sitemap from real content with real lastmod values taken from the content dates. A sitemap claiming every page changed today, every day, is a trust signal you are spending for nothing.

Write robots.txt as a static file with directives at column zero. A templated robots file with indented lines is not valid, and it is the kind of thing that is never noticed because nobody reads it after the first day.

Never list a page in the sitemap that does not exist yet. Inviting a crawler to an empty “coming soon” page teaches it that your sitemap is unreliable.

The order to do it in

  1. Load the site with JavaScript off. Fix whatever is missing.
  2. Reduce head emission to one place and derive the canonical from the path.
  3. Add the build gate that reads the output. Watch it fail. Fix what it finds.
  4. Add structured data, only what is true.
  5. Set explicit dimensions on every image and drive CLS to zero.
  6. Cut JavaScript until INP is comfortable.
  7. Generate sitemap and robots honestly, then submit and request indexing.

What good looks like

Turning JavaScript off changes nothing about what you can read. The build fails if anybody adds a second canonical. Every internal link resolves. CLS is zero rather than small. And the answer to “why does this page rank” is a set of properties you can list, rather than a hope.

Questions people actually ask

Is client side rendering fatal for SEO?
Not fatal, and not free either. Crawlers do execute JavaScript, on a delay and with a budget. For content whose entire purpose is being found, making a crawler work to see it is a self inflicted disadvantage with no upside.
What is the single most damaging technical SEO mistake?
A canonical emitted from a shared layout, because it applies to every page at once and it silently tells a search engine that your whole site is one URL. It is invisible in the source of any individual page.
How do you catch these before they ship?
A post build script that reads the emitted HTML and fails the build. Reviewing the source cannot catch a problem that only exists after two templates are merged.

Published July 2026, updated July 2026.

Hiring someone to own work like this?

I am open to senior backend and full stack roles, remote and permanent.

Get in touch / Read the CV