# Programmatic SEO with AI agents, the white-hat way

> How TechFabric runs programmatic SEO after Google's September 2026 spam update, with unique city pages, agents that source and draft, and people who approve.

Published: 2026-10-13
Author: Preetham Reddy
Tags: Applied AI, Thought Leadership, AI/ML
Canonical: https://www.techfabric.com/blog/programmatic-seo-google-spam-update-2026

---

Content marketing, and the organic leads it brings in, will make or break a service company like TechFabric. We have great engineers doing excellent work for our clients, and we're always putting the latest technology to work on their business problems, a pace that's only picked up with AI. But if we don't talk about that work, it's very hard for anyone to notice us.

For years we relied on paid marketing and word of mouth, and neglected SEO and every other organic channel. That changed a month ago. We decided to go all in on writing, and to use AI models to help, while keeping it authentic with manual reviews, fact checks, personal opinions, and review by decision models such as Clef, Jev and Microsoft-Decision-1.

We built a suite of AI agents that work for our writers. They surface material from the work our engineers are already doing, and they track what's happening in AI and Databricks. The agents collect the information and draft the articles. Our writers then review, add to, correct and approve each one before it goes out.

In this article I'll show what our programmatic SEO team built, and how it sped up our content without giving up authority, authenticity or personal attribution, using only white-hat techniques.

## Why the rules mattered more than the speed

A month in, Google gave us a hard lesson in where the line sits. It finished its [September 2026 spam update](https://status.search.google.com/incidents/XhUDXP7A67iHCD2kmbVu) on 8 October and rewrote three pieces of guidance that matter to anyone publishing at scale. Two days later our Databricks Field Guide lost 75 of its 87 pages from the index. Our programmatic city pages on techfabric.com held, and kept showing up in AI Overviews.

The difference between those two outcomes shaped everything below.

## Where Google draws the line

Programmatic SEO is one template and one dataset producing many pages. Google has no problem with that. Its [spam policies](https://developers.google.com/search/docs/essentials/spam-policies) name two ways it goes wrong:

- **Doorway pages.** "Pages targeted at specific regions or cities that funnel users to one page", or substantially similar pages that are "closer to search results than a clearly defined, browseable hierarchy".
- **Scaled content abuse.** Using generative AI "to generate many pages without adding value for users", or stitching other pages together without adding anything.

The guidance that came with the update sharpened both. Google's page on [generative AI content](https://developers.google.com/search/docs/fundamentals/using-gen-ai-content) now tells publishers to "manually factcheck and review all AI-generated content" before it goes live. Its [helpful content guidance](https://developers.google.com/search/docs/fundamentals/creating-helpful-content) judges main content on effort, originality, talent or skill, and accuracy, and says credit to a source "doesn't replace the need for original effort". The same page warns against deceptive authorship.

Every rule in this post maps to one of those lines.

```mermaid
%% caption: What Google asks for, and the control in our pipeline that answers it.
flowchart LR
  A["Doorway policy: no near-identical city pages"] --> A1["A local section per city, and a similarity ceiling"]
  B["Scaled content: add value, don't restate"] --> B1["Originality floors and a decision panel"]
  C["AI content: a person fact-checks before publishing"] --> C1["An engineer and our head of content review every draft"]
  D["Main content: effort, originality, skill, accuracy"] --> D1["Fact-check, originality floors, and three models reading each draft"]
  E["Authorship: no deceptive bylines"] --> E1["The person named on a post has reviewed it"]
  class A1,B1,C1,D1,E1 accent
```

## The template: one page per service and city

Our network covers 81 pages across 25 metros, Phoenix included. Each page is a service in a city, at an address like `/databricks-consulting-austin` or `/ai-development-denver`. A generator script writes one Next.js route per page from a data file.

Half of every page is the service, and that half is the same everywhere because it's the same work. The other half is written for that city and that service, and it's what keeps the network on the right side of the doorway policy. Each page carries:

- **A map of the metro**, drawn from public US Census county boundaries as a plain image, with no third-party embed.
- **What's happening in that city for that service.** The AI development page in Austin covers AI Tinkerers' demo nights, the Hack AI meetup near the UT campus and UT's Center for Generative AI, each linked to its own page.
- **Why that matters to a buyer there**, written for that city and that service.

Across the network that's 302 listings from 297 sources, each opened on its organiser's own page and dated when we checked it.

A commercial search like "AI development company Denver" showed no map pack in the results we checked, so a firm without a Denver office can hold the position when its page answers the question honestly. That last word carries weight. The page says we're headquartered in Gilbert, Arizona, and claims no office or client in a city where we have none.

### The uniqueness check

A template makes it easy to ship 80 pages that differ by one word. So every city page is checked against the same service in every other city, and a page that fails blocks the build.

```mermaid
%% caption: The checks every city page passes before it ships.
flowchart TD
  A["City data: map, local groups, meetups, conferences, each with a source"] --> B["Page built from the template"]
  B --> C["Overlap with the same service in every other city, ceiling 0.50"]
  C -->|"over the ceiling"| F["Build fails until the page says more about its city"]
  C -->|"under it"| D["Same sentence on another city's page with only the name changed?"]
  D -->|"yes"| F
  D -->|"no"| G["Clef: is this true of this city in particular?"]
  G -->|"no"| F
  G -->|"yes"| E["Ships"]
  class C,G accent
```

The first check counts shared runs of words between two pages, against a ceiling of 0.50. The second catches a sentence that appears on another city's page with only the name swapped. The third asks [Clef, Cloudflare's decision model](https://blog.cloudflare.com/clef-decision-models/), whether a paragraph says something true of that city in particular, which catches the reworded copy a word count misses. Before this work our most similar Databricks pages overlapped at 0.69; now the highest is 0.48. Where a city has little going on for a service, we don't pad. The page leans on what is real there, a university program or a general data meetup, and says so.

### What it earned

In the four weeks after the first batch went up on 6 September, the city pages went from nothing to 8,832 impressions and 36 clicks, at an average position of 17.2. Custom software in Tampa reached position 9, and Databricks consulting in Phoenix and Austin sat between 7 and 8. On 11 October we checked 195 tracked searches and found our pages cited in Google's AI Overview for 9 of them, all city or hiring pages. For "databricks consulting san diego" the page ranked second and the Overview linked to it three times.

Two things we got wrong on the way are worth copying as checks. The first version wasn't crawled, because every city page linked to the same nine siblings and Google never found the rest. And the descriptions repeated one line about our team across dozens of pages that wasn't accurate. On a template, one wrong sentence becomes eighty, so we now read the shared half as carefully as the local one.

## The agents: who does what

Templates cover the pages that answer a standing search. Fresh posts cover what just changed. That's where agents help, and where they need the tightest rules.

```mermaid
%% caption: The jobs in the pipeline, and whether a model or a person does each one.
flowchart TD
  A["Sourcing agent: reads vendor feeds for announcements from the last two weeks"] --> B["Writer agent: drafts commentary, not a summary"]
  B --> C["Fact-check agent: every claim against its source"]
  C --> D["Decision panel: Clef, Jev, Microsoft-Decision-1"]
  D --> E["Engineer: reviews the preview, corrects by reply"]
  E --> F["Head of content: approves the exact version"]
  F --> G["Published and submitted to IndexNow"]
  G --> H["Index watch: daily impressions, weekly index sample"]
  class E,F accent
```

The sourcing agent reads announcements from AI vendors, Microsoft Azure, Dynamics 365 and Databricks, and only from the last two weeks. Anything older is a topic for an engineer who has done the work, through our ideas inbox. Each candidate story is scored by Clef on four questions: is it new, is it on our subjects, is it for our readers, and have we already covered it.

The writer, Claude Opus 5 through Cloudflare's AI Gateway, drafts commentary. What was announced gets a paragraph at most. The rest is the reader's side: who should care, what changes for them this quarter, what to try first, what it costs or risks, where it doesn't fit, and our recommendation.

We publish one post a day at most and three a week. Volume is the easiest signal of scaled content to send by accident.

## The checks every draft passes

```mermaid
%% caption: Every agent-sourced post, from a fresh announcement to an approved page.
flowchart TD
  A["Announcement from the last two weeks"] --> B["Commentary draft"]
  B --> C["Fact-check: every claim against its source"]
  C --> D["Originality floors: new material, and what it adds beyond the source"]
  D --> E["Decision panel: Clef, Jev, Microsoft-Decision-1"]
  E -->|"any model under any bar"| F["Revise against what each model flagged"]
  F --> C
  E -->|"every model clears every bar"| G["Preview deployed at its own address"]
  G --> H["Email: preview link, scores, approve link"]
  H --> I["Engineer and head of content review the preview"]
  I -->|"reply with corrections"| F
  I -->|"approve this exact version"| J["Published"]
  class E,I accent
```

### Fact-check

The writer reads its sources in full. A separate pass then reads the draft against them, one claim at a time, and corrects or removes anything it can't support. Numbers get the strictest treatment. A figure that appears in no source and in nothing an engineer told us is rewritten without the number, or dropped. Mistakes that have shipped before sit in a registry the build reads, so the same wrong claim can't ship twice.

### Originality floors

Two scores decide whether a draft is worth a reader's time before any model reads it. One measures how much of it is new material. The other measures what it adds beyond the pages it was written from, which is the one that catches a confident retelling of a vendor's announcement. An explainer needs 0.45 on the first and 0.70 on the second, on a scale where 1 is entirely original.

### The decision panel

Decision models don't write. You give one a state and a fixed set of typed questions, and it returns calibrated probabilities. We ask three of them the same questions, written from Google's own documents on spam, main content, AI content and authorship, and each model sees the policy text next to the draft.

| Model | From | Why it's on the panel |
| --- | --- | --- |
| Clef | [Cloudflare](https://blog.cloudflare.com/clef-decision-models/), open-sourced and hosted on Workers AI | It already scores every page we publish against Google's helpful-content questions |
| Jev | [TypeSafe](https://typesafe.ai/blog/introducing-system-one-models-and-jev) | The decision model Clef's API was built to match |
| Microsoft-Decision-1 | [Microsoft](https://www.testingcatalog.com/microsoft-launches-decision-1-model-in-foundry/), released 9 October 2026 | A 9-billion-parameter model post-trained for scoring |

```mermaid
%% caption: One draft through the panel, and what a low score does.
flowchart LR
  A["Draft plus policy excerpts"] --> B["Clef"]
  A --> C["Jev"]
  A --> D["Microsoft-Decision-1"]
  B --> E{"Every model, every question, above its bar?"}
  C --> E
  D --> E
  E -->|"yes"| F["Preview and email"]
  E -->|"no, under six rounds"| G["Revise the flagged passages"]
  G --> A
  E -->|"no after six rounds"| H["Dropped, reasons logged"]
  class E accent
```

Each model answers eight questions, from effort and originality to accuracy, scale and authorship. A draft has to clear every bar that blocks. When one model scores it under a bar, the writer gets the failed questions and the scores behind them, revises, and goes back through the fact-check and the panel. After six rounds without agreement the draft is dropped with its reasons logged.

We set the bars on 37 posts we already knew the answer for. Fifteen were written by our engineers from their own work. Twenty-two were the opposite, made up of agent notes Google had dropped, retellings we'd taken out of the index ourselves, and a draft we'd discarded by hand. With the bars set, all 15 good posts pass the panel and 21 of the 22 bad ones are stopped.

The calibration taught us more than the panel did. On effort, accuracy, overall quality and a satisfied reader, none of the three models could tell our engineers' posts from the summaries. Clef and Microsoft-Decision-1 even rated the summaries as more satisfying. One question separated them for every model, and it was whether the draft is mostly a summary of a source. Jev caught 91% of the bad set on that question alone. Microsoft-Decision-1 also blocks on whether a page looks mass-produced. So those questions block a draft, and the other answers go in the review email as information. A bar that would have failed posts our engineers wrote wasn't used to block anything.

### People last

The panel decides what's good enough for a person to read, and people decide what publishes. The review email goes to our head of content. It carries a link to the post rendered at its own preview address, exactly as it would look live, with the panel's scores and an approve link. The engineer whose name will be on the post reads it with them. If something's wrong we reply to the email in plain English, the writer revises, every check runs again, and a new preview arrives. The approve link publishes only the exact version we read.

## The lesson from the Field Guide

The Field Guide pages Google dropped were agent-written notes like "A SQL warehouse is the compute that runs your Databricks SQL." Each was accurate. None told a reader anything Databricks' own documentation didn't, and a run of them went live in the days around the update. We've taken those notes out of the index, and every new Field Guide note now starts as a short brief to our head of content and publishes only with their answer in it.

That's the whole pipeline in one example. The checks we had then measured whether a page was helpful in the abstract. The ones above measure what a page adds that its source doesn't, and they put a person at the end.

## A checklist for your own programmatic SEO

- **Give every templated page something only it can say.** Local events, local data, a local map, each with a source. Where a city has little, use what is real there and never pad.
- **Check the shared half as hard as the unique half.** One wrong sentence in a template is wrong on every page.
- **Measure the gap between a page and its source.** Helpful in the abstract isn't enough. Score what it adds.
- **Use more than one judge, from more than one company.** When they disagree, revise. Don't average.
- **Keep agents on fresh material and people on approval.** Agents are good at noticing what changed and drafting fast. A person who knows the subject decides what publishes.
- **Put a name on a page only when that person stands behind it.**
- **Publish at a pace you'd defend.** We cap agent posts at three a week.
- **Watch the index, not the publish count.** We compare each day's impressions with the 14-day median and sample how many pages are indexed every week, on both sites.
