Skip to content

Case study · Building

plants.place: an encyclopedia where the model has no right to facts

A houseplant encyclopedia in Ukrainian and English and a classifieds market for plants, built solo on Cloudflare. The interesting part isn't the code — it's the rules that keep AI-written pages honest.

plants.place
Role
Everything: product, SEO, content system, code via Claude Code
Launch
Aug 2026 · 211 commits in the first 9 days
Stack
One Cloudflare Worker, no framework · D1 · R2 · Turnstile
Today
133 species · 54 listings · Ukrainian + English
A species page on plants.place: the monstera card with photos, botany text and a characteristics table
The monstera card. The native range — southern Mexico and Central America — comes from botanical databases, not from the model. The first draft said "the Amazon rainforest", because that's what Ukrainian Wikipedia says.

The gap

It started at home. My partner keeps houseplants and couldn’t find proper botany in Ukrainian — not even by Latin name. I checked. Search for a plant’s Latin name in Ukrainian and you get shops and Wikipedia. Nobody had structured botany in Ukrainian: origin, size, toxicity for pets, care — in one place, from reliable sources.

Before writing a line of code, we mapped the demand. Three Ahrefs exports for Ukraine gave 375,863 keywords. After cleaning, 20.9% of the search volume turned out to be junk, and the rest folded into 345 topics — one per plant, not one per phrase.

Some of the junk was a sign of the times. “Gerbera” and “geranium 2” are also names of military drones: 2,906 junk keywords and about 79,000 searches a month came from war-related queries. The biggest junk category was food and raw materials — lavender, avocado and rosemary without any plant context.

The rest of the demand was clear: about 78% of searches are just the plant’s name. “Buy” and “price” are around 8%, care questions around 7%. People want to know the plant first.

What people search for about houseplants in Ukrainian

Just the plant name 78%, buy and price 8%, care 7%, everything else 7%.

Share of search volume after cleaning. Source: 375,863 keywords, three Ahrefs exports for Ukraine

The rule: the model has no right to facts

Latin name, author, family, size, native range, toxicity — none of it is generated. These facts come from GBIF, Wikipedia and POWO and go into the prompt as data. The model writes only the connecting text around them. No fact — the field stays empty.

It’s not pedantry. The very first generation placed the monstera in “the tropical forests of the Amazon”, because Ukrainian Wikipedia says so. The species comes from Central America. Without the rule, a source’s mistake would have gone into the encyclopedia as a fact, under our name. Now the verified-facts block has priority over everything, including Wikipedia.

Search demand→ Species list→ Facts from databases→ Model writes text only→ Code checks, sends complaints back→ Photos checked by vision→ Publish · translate
The content pipeline. Two steps keep it honest: facts come only from databases, and code checks every answer and sends complaints back.

Asking nicely doesn’t work. A list of complaints does.

You can write “at least two blocks after every heading” in the prompt as many times as you like. The model will still put one paragraph there. What works is checking the answer with code, collecting a concrete list of complaints, and sending it back to the model as the next message — together with its own previous answer. Three attempts at most.

The checks are boring on purpose: enough text after each heading, at least four sections, length limits for the lead and the meta tags, at least one list, no markdown asterisks, no phrases like “according to verified data” that sound like an excuse to a reader who never sees our data table.

On the first 50 cards the result was measurable: on average 29 blocks and 554 words per card, 73 tables, 205 lists, and not a single complaint on the final run.

MetricCheaper modelStronger model
Words, same card (monstera)277493
Tables0 in 20 cards73 in 50 cards
Attempts to pass checks1–3first try
Cost per species$0.007$0.065

The stronger model costs about $3 more for 50 cards. The cheaper one saves pennies and costs you the quality you later pay for in editing.

27% of stock photos showed the wrong plant

Photos come from stock libraries through their API, with the author and license under every picture. A search by Latin name often finds nothing; a search by common name finds the wrong thing. When we checked the live catalogue, 122 of 447 photos — 27% — didn’t show the right plant. One species page had a supermarket on it.

Stock photos in the live catalogue, checked one by one

447 photos: 325 showed the right plant, 122 did not.

Source: a check of the live plants.place catalogue

So the last line of defence is not the search query but a look: every photo goes through a vision model before it’s saved, and doubt means “no”.

A botanically perfect card can get zero impressions if it’s named with the wrong word. In Ukrainian, people search «герань» (geranium) 32,990 times a month and «пеларгонія» (pelargonium) 2,980 times. The card is now titled “Pelargonium (geranium)”: correct and findable. It is also the most-shown page on the site. And “money tree” turned out to mean four different plants in Ukrainian, while in English it means just one of them — so cards that people confuse now point to each other.

The plan for the classifieds part relied on “110,000 impressions on ‘buy’”. A closer look showed that private, person-to-person demand in search is tiny — about 3,200 impressions. The rest is people looking for a shop. Plants are traded hand to hand in Facebook and Telegram groups, which people visit directly.

So the market pages are closed from indexing, and listings appear on the species cards instead. There was also a tempting shortcut — filling the market with listings scraped from other boards. We didn’t take it: duplicate content, personal data, broken platform rules, and a visitor who writes to a “seller” that never answers.

Going English

Where to grow next was decided by a probe, not intuition: one API request per country, 32 countries. English-language markets together showed about 1.07 million probe searches — roughly four times more than the next language, French, with 272,000. The encyclopedia scales by language; the market scales by country.

For translation, a pilot on two cards named the cheapest model the winner. A blind test on ten cards, judged by another model, picked a different one, 8 to 2. Conclusions only from a proper sample.

The mechanics are plain. English now lives at the root and Ukrainian under /ua/. Every species page lists both versions plus an x-default, and each language has its own sitemap with the same links. A page without a translation returns a 404 in that language and stays out of hreflang and the sitemap, so Google never finds a link that doesn’t point back. 201 old addresses redirect to the new ones with one 301 each. One trap to remember: the folder is /ua/, but the language code is uk — “ua” is the country.

Is this programmatic SEO?

Yes: 133 species pages from one template and one pipeline. What keeps it from becoming the mass-produced kind is four limits. Facts come only from botanical databases. Code checks every answer and sends a list of complaints back. The prompts were calibrated on cards I read by hand before the pipeline ran on its own. And every photo is checked before it goes live — because 27% of them were wrong.

Results

Search Console · last 90 days, to Sep 29, 2026 That's the site's whole life in search so far: 5,526 impressions and 31 clicks. 99 pages were shown at least once, 88 of them species cards. September brought 3,864 impressions, more than twice August's 1,662. The most-shown page is "Pelargonium (geranium)", with 1,234 impressions. Small numbers, but they are growing.

plants.place in Google Search: impressions per month

August 1,662 impressions and 9 clicks; September 3,864 impressions and 22 clicks.

The domain was registered on July 31; September counts data to the 29th. Source: Google Search Console, final data

Two early signals: 78 pages were in Google’s index a week after launch, and the first search impressions came on the third day of the domain’s life.

What I’d do differently

  • Build the design system as a live page from day one. An audit later found 47 font sizes where 6 were planned, and page headings implemented in nine different ways.
  • Check the domain’s past before launch. Search Console’s crawl stats showed Google asking for WordPress addresses — a site we never had.
  • Run a logic audit earlier. The “shade-tolerant” filter returns 68 of 103 species, including plants that need bright light. It needs a real shade-tolerance field, not a word match, and that fix is still on the list.