DEV Community

MORINAGA
MORINAGA

Posted on Edited on

I built 3 programmatic SEO sites for $25/month using Claude Haiku — here's the full architecture

Spent the past week running an experiment: can a programmatic SEO directory site survive Google's 2024 Helpful Content Update if every page is AI-generated?

Rather than pick one niche and bet the farm, I built three parallel sites with the same stack — different content categories, identical architecture, single content generation pipeline. They share a monorepo, deploy independently to Vercel, and refresh nightly via one GitHub Actions cron. (Hosting note added later: that was true as of May 2026 — on 2026-05-07 all three moved to Cloudflare Pages and the Vercel Pro plan was cancelled.)

This post is the architecture write-up. I'll publish actual revenue/traffic numbers in a follow-up after 6 months.

The three sites

  • 🤖 Top AI Tools — ~500 open-source models from HuggingFace, with Claude-generated summaries, use cases, and FAQ
  • 🎮 Find Games Like — "games like X" recommendations for indie titles on Steam, with AI-curated similarity reasoning and "avoid if" caveats
  • 🛠 Open Alternative To — open-source replacements for ~80 popular SaaS products, refreshed daily from GitHub stars and last-pushed timestamps

(Counts and URLs above are as of publication, 2026-05-01. The three sites later moved to their own domains — aiappdex.com, findindiegame.com, ossfind.com — and the catalogues have grown since.)

All three are static-generated, all three rebuild nightly, and all three pull the same TypeScript package for the Claude client and libSQL helpers. Structured data isn't shared: each app builds its own JSON-LD object inline in its pages and hands it to its layout. Neither is the editorial part — each app defines its own system prompt and its own parsing rules.

Why three sites instead of one

Three reasons:

  1. Cheap insurance against niche choice failure. I don't know which niche Google will tolerate in 2026. Three uncorrelated bets > one big one.
  2. Shared ETL infrastructure. The cost of running site #2 and #3 is mostly the marginal Claude API tokens (~$2/site/month). Hosting and code is amortized.
  3. A/B testing categories. AI-tools is the most saturated PSEO niche on Earth. SaaS-alternative goes head-to-head with alternativeto.net (DA80+, 20 years authority). Indie games is the underdog with the cleanest niche fit. After 6 months, the data tells me which thesis was right.

Stack at a glance

Layer Tool Why
Site framework Astro 5 (SSG) 100% static output, no runtime cost
Styling Tailwind v4 Newer engine, faster builds, smaller CSS
Content gen Claude Haiku 4.5 via Anthropic SDK Cheap, fast, sufficient for directory copy
Data store libSQL client, pointed at a local data/local.db file ETL state + idempotency tracking
Cron GitHub Actions matrix job Free, version-controlled, reliable
Hosting Vercel Pro Fast SSG deploys, image optimization
Monorepo pnpm + Turborepo Workspace-aware builds, cached output

One caveat on the data store: the code talks libSQL and there are TURSO_DATABASE_URL / TURSO_AUTH_TOKEN slots in .env.example, but I never provisioned a hosted Turso database. The cron doesn't pass those variables, so every refresh run falls back to a throwaway local SQLite file and the committed JSON export is what actually survives the job.

Total monthly cost

Item Cost
Vercel Pro $20
Anthropic API (Haiku 4.5, daily refresh) ~$5
Database (local SQLite file, no hosted Turso yet) $0
GitHub Actions (under 2k min/mo free quota) $0
Domains (Vercel subdomains until validation) $0
Total ~$25/month

(Cost note added later: these are the May 2026 numbers. By 2026-05-08 the total was about $2.25/month — Vercel Pro cancelled in favour of Cloudflare Pages' free tier, the Anthropic key removed from the cron, and three at-cost domains as the only remaining line item.)

Repo layout

seo-farm/
├── apps/
│   ├── ai-tools/          # topaitools.vercel.app
│   ├── indie-games/       # findgameslike.vercel.app
│   ├── oss-alternatives/  # openalternativeto.vercel.app
│   └── dashboard/         # internal status page
├── packages/
│   ├── shared/            # Anthropic client, DB schema, monetization helpers
│   └── publish/           # the script that posted this article
└── .github/workflows/
    └── refresh-content.yml  # nightly cron
Enter fullscreen mode Exit fullscreen mode

The three sites are intentionally separate Vercel projects (different roots), but share @seo-farm/shared for the Claude client, libSQL helpers, and the AdSense + Amazon affiliate helpers (JSON-LD stays per-app). Each site has its own ETL — HuggingFace API for ai-tools, Steam Web API for indie-games, GitHub repo discovery for oss-alternatives. (There's a RAWG client sitting in the shared package too, but the indie ETL doesn't call it yet.)

Content generation pipeline

The cron runs daily at 02:00 UTC and processes one app at a time (matrix max-parallel: 1 to stay below Anthropic burst limits):

strategy:
  matrix:
    app: [ai-tools, indie-games, oss-alternatives]
  max-parallel: 1
env:
  ETL_LIMIT: "500"
  GENERATE_LIMIT: "300"
Enter fullscreen mode Exit fullscreen mode

Per app, the pipeline is:

  1. ETL stage — fetch source data (HuggingFace models / Steam games / GitHub repos), upsert into the libSQL database
  2. Detect missing content — select rows that have no content row yet, or whose model_used is still fallback-template / seeded-from-json
  3. Generate with Haiku 4.5 — one call per entry, one combined prompt that returns the whole entry as a single JSON object
  4. Cache & dedupe — upsert back into the database with a new generated_at timestamp and the model that produced it
  5. Trigger build — only if content changed, push commit and let Vercel rebuild

The hard part is step 3 prompt design. Generic "summarize this tool" prompts produce slop. What worked:

  • One system prompt per site, not one generic prompt for all three — each asks for that site's whole entry (summary / use cases / pros / cons for ai-tools) as a single JSON object
  • Strict format constraints ("3-5 concrete use cases", "no hype", "Output ONLY a JSON object … no prose outside the JSON")
  • Narrow, structured input — the prompt gets only the registry metadata I already store (model ID, name, pipeline tag, tags). I don't fetch the model card text, which keeps entries thin but also keeps Claude away from numbers I can't verify
  • An "avoid if" caveat for game recs — most game directories only gush. Find Games Like prompt explicitly asks Claude to be honest about limitations

Honesty check on that last one: the prompt asks for it, but the data I've shipped so far hasn't delivered it. All 120 games in the current export carry the identical template pair —

You prefer AAA production values
You dislike experimental mechanics

— because those generation runs fell back to the template instead of reaching Claude. If the Claude pass lands, per-game caveats are one of the few places AI summaries could beat human-written game directories, which all default to marketing-speak. Right now it's a promise the data hasn't kept.

Ranking strategy (or: things Google may kill anyway)

I'm not delusional about this. Google's March 2024 update specifically targeted "scaled content abuse" and de-indexed thousands of programmatic SEO sites. The bet here:

  1. Source-grounded content — every detail page links to canonical authoritative source (HuggingFace model card, Steam store page, GitHub repo). Reader can verify in one click.
  2. Real utility — directory + comparison tables that genuinely save time vs reading 30 docs pages
  3. Honest framing — "AI-generated, here's the source" disclosed in footer, no hiding
  4. Per-page structured dataSoftwareApplication + FAQPage on ai-tools detail pages, VideoGame on indie-games, ItemList + BreadcrumbList on oss-alternatives
  5. Low quantity, daily freshness — 880 total pages at launch, refreshed daily so star counts and modification dates stay current. Not 100k pages of stale garbage.

I genuinely don't know if any of this is enough. The whole point of the experiment is to find out. If two of three sites get deindexed by month 3, that itself is a useful data point.

What's wired up

  • AdSense site-wide (currently in review — sites are <2 weeks old)
  • Amazon Associates components are in the code — category-relevant amazon.com search-by-keyword links, no fake product recs — but they only render once PUBLIC_AMAZON_TAG is set, and I haven't registered for the program yet
  • GA4 per site with separate properties
  • Newsletter — the Beehiiv publication exists ("Indie Discovery Weekly", indiediscovery.beehiiv.com, created 2026-04-29) and every site ships a Beehiiv iframe component, but that component only renders once PUBLIC_NEWSLETTER_ACTION is set — still an unchecked box on my own setup dashboard
  • Sitemaps generated at build time; robots.txt and llms.txt are static files in each app's public/

Milestones I'm watching

Month Question Threshold
1 Did Google index the pages? Search Console impressions >1k/day = yes
3 Is organic traffic growing? >50% organic share of GA4 sessions = healthy
6 Is monetization viable? AdSense approved, RPM measurable, decision: delete or double down
12 Is it sellable? 3 consecutive months of profit > 0 = list on Empire Flippers / similar

Open questions for readers

I'd genuinely value feedback on:

  1. PSEO survivors of March 2024 — what categories are still ranking?
  2. Schema markup — am I missing anything obvious for directory sites?
  3. AI content disclosure — how transparent is too transparent? Does the footer "AI-generated" disclosure help or hurt?
  4. Niche durability — which of the three do you think survives 12 months? Place your bets.

Repo isn't public yet — I might open source it after the 6-month checkpoint depending on how the experiment goes. Happy to share specific snippets in the comments if anyone's curious about a particular piece (Astro content collection layout, the Claude prompts, the structured-data helper, the Vercel Pro deploy config).

Next update in 30 days with actual numbers, regardless of how ugly they look.

Top comments (0)