Case Study ยท SEO Platform

The SEO tool that is not allowed to touch the store

Bonita Smoke Shop sells cigars across a 3,622-page storefront, and nobody could have told you how many of those pages had a title worth reading. So I built the thing that could โ€” it audits every page, has Claude draft the missing copy, checks that copy against a tobacco-category policy before a human ever sees it, then stops and waits for someone to click Approve.

Role
Scoped it, directed the build, shipped it. AI-assisted development.
Timeline
v1 running August 2026 ยท dry-run
Stack
Next.js 15 ยท TypeScript ยท Postgres ยท Anthropic API ยท Vercel
Team size
One

The problem

You cannot fix what nobody has counted

In July I rewrote titles and meta descriptions for the store by hand, one page at a time. It worked โ€” impressions went from 93.3K to 145K per 28 days and average position moved from about 25 to 18.6 inside ten days. It also took a fortnight and covered a rounding error's worth of the catalog. That is the whole argument for building something: the manual version was effective and completely unscalable.

Three things made this harder than a spreadsheet and a free weekend. There was no inventory of the damage โ€” nobody knew how many pages were missing a description, or how many titles Google was quietly truncating. Cigars are a regulated category, so generated copy that drifted into health claims, prices, superlatives or anything with youth appeal would be worse than no copy at all. And the store was weeks from moving off BigCommerce onto WooCommerce, which meant anything written against one platform's API was throwaway work by Christmas.

AI proposes. Code validates. A person approves. Nothing else is allowed to write.

That sentence was the spec. Everything below is the consequence of taking it literally.

First production run ยท 26 August 2026

By the numbers

3,622pages discovered and audited, zero fetch errors
10minutes for a full crawl-and-audit pass
865products found with no meta description at all
0changes written to the live store, by design

How it works

Five steps, and a wall before the last one

Discover

The crawler walks the store's XML sitemap and registers every URL it finds. On the first run that was 3,622 pages โ€” the first time anyone had a count.

Audit

Every page is fetched and checked against about twenty signals: title and meta length, missing descriptions, H1 structure, JSON-LD Product and LocalBusiness completeness, canonicals, Open Graph and Twitter cards, image alt text. Findings stay browsable and filterable down to the exact URLs affected.

Sync the catalog

Products and categories come in through the BigCommerce API with their current SEO fields attached, so proposals are compared against what is actually live rather than what someone remembers. The token is read-only on purpose.

Propose, then validate

Claude drafts a title and description per product, grounded in the facts on that product's own spec table โ€” size, wrapper, strength, packaging โ€” and nothing else. Each draft then goes through a deterministic validator: no health or safety claims, no prices or discounts, no comparatives or superlatives, no youth-appeal language, hard length limits, and no title that duplicates one anywhere else on the site.

Wait for a human

Whatever survives lands in a review queue as a before-and-after card carrying the validator's verdict and a note on where each fact came from. Approve, reject, or edit and revalidate. Copy can also arrive by CSV โ€” from a spreadsheet, a copywriter, another Claude session โ€” and it is held to exactly the same validator.

Scope

What I actually had to build

Audit engine

  • Sitemap crawler with per-URL registration
  • ~20 on-page SEO checks via cheerio
  • JSON-LD Product and LocalBusiness completeness
  • Findings filterable to the exact URLs
  • robots.txt parser with an AI-crawler report

Proposals and policy

  • Claude drafts grounded in the product's own spec table
  • Deterministic tobacco-category validator
  • Site-wide duplicate-title detection
  • Before/after review queue with character counts
  • CSV import and export, same validation either way

Safety architecture

  • Dry-run by default โ€” the app writes nothing
  • Apply gated behind an env flag that is off
  • Read-only API token as the second lock
  • Append-only history table for rollback
  • Password-protected deployment

Built to outlive the platform

  • Adapter layer: BigCommerce now, WooCommerce next
  • Switching platforms is an env-var change
  • Both platforms' history kept side by side
  • Chunked job runner with cursors in Postgres
  • 13 unit tests, fixtures taken from real live pages

What the first run found

The audit was the product

I expected the AI copy to be the interesting part. It was not. The interesting part was ten minutes of crawling turning four years of vague unease into a ranked list.

  • The age-gate widget renders as the first <h1> on all 3,622 pages. Every page on the site was telling Google its heading is a date-of-birth prompt โ€” a template bug nobody had seen, because nobody had ever looked at 3,622 pages at once.
  • 865 products with no meta description at all, and 1,991 more under 70 characters.
  • 1,514 titles under 30 characters; 305 over 60 and truncated in results.
  • LocalBusiness schema missing sameAs, geo and image on 3,621 pages; 2,757 product pages with no review or rating schema; 712 with no brand.

Then the robots.txt audit, which I had built almost as an afterthought. It reports agent by agent which AI crawlers can read the store and how fast โ€” and it found five answer-and-search agents, ChatGPT's and Gemini's among them, sitting under a Crawl-delay: 10 that Claude's and Bing's crawlers were not. On a catalog this size a ten-second delay means those agents sample a handful of pages and leave. Nobody had decided that. It had just been quietly true for years.

Of the first 46 drafts, the validator rejected 34 before a human saw one.

I like that number more than any number about how good the AI was. Thirty-four catches on length and policy is evidence the guardrail works on any author โ€” a model, a copywriter, or me at eleven at night. The validator was never there to grade Claude. It was there to make it impossible for anything unchecked to reach a page about a regulated product.

The build

What it looks like

The Bonita SEO dashboard showing job controls, site state and a ranked table of the most common findings across 3,622 pages
One screen answers what was previously unknowable: the state of all 3,622 pages, and which problems affect how many of them.
The Pages screen filtered to the finding that the first H1 on every page is the age gate
Every finding filters down to the exact URLs affected โ€” here, the age-gate H1 on all 3,622 of them.
The robots.txt audit listing AI crawlers by owner and role with their crawl delay and access status
Agent by agent: who can read the store, and how fast. Five search and answer agents throttled; Claude's and Bing's not.
The catalog screen filtered to products with a missing SEO title or meta description
710 products missing a title or a description, one filter away.
The proposals review queue showing before and after cards with approve, reject and edit controls plus CSV import and export
The review queue. Bulk actions and CSV in both directions โ€” and nothing ships without someone clicking Approve.
A single proposal card comparing an empty current title and description against the proposed versions with character counts
One card, close up: current versus proposed with character counts, and a note recording which facts came off the product's own page โ€” and which are worth a human double-checking.

What I would take from it

The constraint was the design

The version of this app that writes straight to the store would have been quicker to build, and I would not trust it near a regulated catalog. Making the safe path the only path โ€” dry-run by default, an apply flag that is off, a read-only token behind it, and every future write snapshotting the old value first โ€” cost about a day, and it is the reason the thing can run against a live business at all.

The other lesson is about scale changing the question. Fixing one page is a copywriting task. Seeing 3,622 at once turns it into an engineering one, and the most expensive bug on the site โ€” an age gate occupying every H1 โ€” was invisible at page-by-page resolution and obvious at catalog resolution.

Next: apply-with-rollback once the queue has been worked through, the WooCommerce cutover the adapter layer was written for, and Search Console data pulled in so findings can be ranked by what they are actually costing.

Next.js 15 TypeScript Postgres Anthropic API Vercel BigCommerce API Technical SEO AI-assisted development

The app is password-protected and the repository is private, so the screenshots above are the evidence. Happy to walk anyone through it live.

Same store, different problem

This catalog is also the one I moved off BigCommerce โ€” that story is next door.

Read the migration case study โ† All projects