Project

SEO Deck

A Chrome extension that collapses on-page auditing, Search Console performance and AI-search readiness into one popup scoped to the tab you are already on. Built for the content team I manage, replacing two earlier extensions and, before those, a folder of loose scripts.

INTERNAL TOOL

Built with

Vanilla JSChrome Manifest V3Search Console APINo build step
Read how it works
The SEO Deck panel open in Chrome over a fictional Dubai tourism page, showing an overall score of 78, a checklist of on-page checks, and the page’s heading structure.

The motivation

Why I built it

A writer auditing a live page needs four things at once: whether it covers the keywords it was briefed on, whether its links behave, how it is actually performing in search, and whether an AI crawler can read it at all. That was four tabs, two logins and a property picker that had to be re-filtered for every single URL. None of those tasks is hard. The context switch costs more than the task, and a page nobody checks is a page that quietly decays.

The trade-off

The decision that mattered

The headline feature was killed by its own measurement. "Our lazy loading hides content from AI crawlers" had been approved as the flagship of the AI-readiness tab. I measured it before building it: the server HTML carried 129% of the rendered page’s word count, so the crawlers were seeing more than the browser showed, not less. The premise was false. The feature died and a structured-data gap detector replaced it, which targets a defect that is actually present.

The SEO Deck panel open over a fictional Dubai tourism page, beside a comparison of that page’s server HTML and its rendered DOM showing the server version holds 129% of the rendered page’s word count, so nothing is hidden from a crawler.

The problem

Four tabs, two logins, and a filter you re-apply every time

The friction was never the work. It was everything around the work.

Checking a page against its brief, checking its links, checking its search performance and checking whether an answer engine can read it are four separate jobs in four separate places. Search Console in particular makes you pick a property and apply a page filter for every URL, which is a small tax paid dozens of times a day.

The second problem is newer. Teams have years of habit around ranking in a search engine and almost none around being readable by an answer engine. Structured-data gaps, crawler-visible content and robots rules for AI bots are all mechanically checkable, and almost nobody checks them, because the tools that would do it were built for a different era of search.

This is just the initial version (technically, version 2.5, version 1 was all those scattered scripts!)

From the announcement I wrote for the team

Where it came from

Scripts, then two extensions, then one

It started as a folder of loose scripts people pasted into the browser console. Those became an audit extension, shared round the team with install instructions in a Slack thread. Separately, a second extension pulled Search Console data for the current page, because the property picker was the single most repeated piece of friction anyone had.

Two extensions solving halves of one problem is worse than either, so they were merged. Deliberately assembled as a new build rather than grafted onto whichever was larger, because grafting would have inherited the weaker half’s structure permanently.

The old extensions were left exactly where they were, unmoved and unrenamed. Chrome loads an unpacked extension from an exact path, so relocating them would have broken the tools the team used daily, in the middle of building their replacement, for no benefit.

What already exists

Every check exists somewhere. None of them is where the page is.

Nothing here is a gap in capability. It is a gap in place: each tool answers its own question somewhere other than the tab the writer is already looking at.

Search Console. The authoritative source for how a page is actually performing, and the reason for most of the friction. Every URL means picking a property and re-applying a page filter, a small tax paid dozens of times a day.

Crawler suites. Thorough, and built to sweep a whole site on a schedule. That is the opposite shape to a writer with one page open who wants an answer in four seconds.

General SEO extensions. Right place, right speed, and no view of performance or of whether an answer engine can read the page. They were designed for an era of search that did not have to worry about the second thing.

The loose scripts. Where this actually started: a folder of snippets pasted into the console. Fast for whoever wrote them, unshippable to a team, and impossible to keep consistent across people.

SEO Deck. One popup, scoped to the tab already open, with the Search Console property matched automatically. The AI-readiness tab is the part nothing else was doing at all.

Feature sets as checked, September 2026

How it works

Three tabs, and the fourth thing nobody was checking.

Everything is scoped to the page in the tab, so nothing has to be pasted, picked or re-filtered.

Step 01 / 03

Audit the page in front of you

Topic gaps against a saved keyword cluster, focus-keyword placement, and a full snapshot: title, meta, heading tree with skipped levels flagged, canonical, robots directives, social tags, hreflang, structured-data types, word count, missing alt text. Plus anchor text, external links and image checks.

What happens

  • Topic gaps against a saved keyword cluster
  • Heading tree, with skipped levels flagged
  • Canonical, robots, social tags, hreflang, structured-data types
  • Anchor text, external links and missing alt text

The decision that shaped it

I measured the flagship feature and it disappeared

The most useful thing this project produced was a number that killed its own headline.

The belief was widely held and sounded right: our pages lazy-load their content, so an AI crawler that does not execute JavaScript sees very little of them. It was approved as the centrepiece of the AI-readiness work.

Measuring it took an afternoon. The raw server HTML held 129% of the word count of the fully rendered page, because the rendered version collapses content into accordions that the raw markup simply contains. The crawlers were seeing more, not less. The premise was not merely overstated, it was backwards.

So the feature was cut and replaced with structured-data gap detection, which targets a defect that measurably is present. The raw-versus-rendered comparison survives as a cheap health check rather than a headline, which is the right size for a thing that confirms a good state.

The measurement stands at a sample of one page, and it is recorded that way. It is enough to disprove a claim made with confidence and not enough to call the good state systemic.

Found by measuring

Bugs I found by measuring rather than looking

  • A double-click was needed on several tools and had been documented to the team as a quirk. The cause was not the tools: a browser popup is destroyed the instant its tab navigates, so nothing survived to act after the reload it had just triggered. Moving the work to the background worker fixed it. Proved by disabling only that listener and watching the bug return exactly.
  • The muted text colour used for every description and hint failed accessibility contrast at 3.5:1 against a 4.5:1 requirement. Measured with computed styles and the real luminance formula, not judged by eye. Recalculated to 5.1:1.
  • A list of six badges placed in a table cell squeezed its own label to zero width. A screenshot made it look fine; measuring the element’s bounding box did not.
  • Segment counts on the link audit disagreed with the rows each filter revealed, because the label and the filter were computed separately. Now derived from the same function, and asserted by clicking each segment and comparing.
  • Two links to saved assistants were fabricated. I had typed plausible URLs instead of reading them from the source. They are now extracted by pattern from the original files and never retyped.

A usability change worth the measurement

Removing help made it more accessible, not less

Every tool button carried a permanent description line. The feedback was that they ate the popup, which was true: measured, removing them recovered 102 pixels of 446 visible on one tab alone, the difference between everything fitting and needing to scroll.

The obvious fix, hiding them and showing a tooltip on hover, would have removed the descriptions from screen readers too. Instead they were moved into a described-by target and visually clipped rather than hidden, so assistive technology still receives the full text regardless of hover state. Strictly better, not a trade.

The tooltip itself introduced a bug during the build. The first-use hint dismissed on hover, and dismissing it collapsed the banner and shifted every button up a row, so the cursor landed on a different button while the user was still reading the first one. Caught in a real browser, because a synthetic click never reproduces a cursor that stays where it is. Moving the dismiss to click and focus fixed it: a click has already resolved against its pre-shift target, and keyboard focus is not coordinate-based.

Tools across six tabs

Merged from two predecessor extensions, plus three that had been built and never wired to a button.

Server HTML vs rendered page

The measurement that killed the flagship feature. A sample of one page, recorded as such.

Contrast, after an accessibility fix

Below the 4.5:1 floor before. Measured with computed styles, not judged by eye.

Where it stands

Shipped internally, with a known route to being a product

It is in daily use by the team it was built for. Three bugs are open and written down rather than quietly carried: two SERP tools depend on a search engine’s result markup and will break when it changes, and the image-weight tool can only time images the browser actually fetched, so on a warm cache it reports fewer than are present. It states the count it measured, which makes the figure partial but honest.

One change stands between this and something anyone could install: each user authorising their own Search Console rather than sharing a connection. That is specified. It also makes onboarding harder, and I have not measured whether correctness costs more adoption than it earns.

In hindsight

What I would do differently

  • Measure the premise before scoping the feature. The lazy-loading belief survived a long time because it sounded right, and an afternoon of measurement would have saved it being approved as a headline.
  • Decide the distribution model before the build rather than after. Sharing one connection made the first version trivial to install and is now the only thing blocking the tool from going anywhere.
  • Treat a screenshot as a hypothesis. Three of the bugs above looked fine in a picture and were obvious the moment anything was actually measured.

The wider point

Managing a content team and building its tools are one job.

The friction I removed is friction I felt, the checks it runs are the checks I was asking people to run by hand, and the feature I killed was one I had believed myself.

BUILT

One extension, assembled rather than grafted

Two earlier extensions solved halves of one problem. Merging them into a new build meant not inheriting the weaker half’s structure permanently, and the originals were left untouched so nobody lost a working tool mid-build.

IN USE

Daily, by the team it was built for

Three bugs are open and written down rather than quietly carried. Two depend on a search engine’s result markup and will break when it changes; the third reports a partial count and says so.

UNDECIDED

One change stands between this and a product

Each user authorising their own Search Console instead of sharing a connection. It is specified, it makes onboarding harder, and I have not measured whether correctness costs more adoption than it earns.

Project documents

Explore the discipline

Let’s build something that scales.

If you are trying to make content compound rather than accumulate, that is the problem I like most. Email is quickest, and my calendar is open.