Mass Landing Factory: 20 Templates, 200 Pages, One Renderer
In February 2025 a client asked for 200 landings for 40 towns and five services. He sent a Sheet with town, service, price, and phone. He wanted each page to look local and to rank. I quoted two weeks for the system, not for the pages. He asked where the pages come from. I showed him a CSV row becoming a live URL in 90 seconds. He approved the factory.
Screenshot: CSV import log 2025-02-18 14:32 shows 200 rows imported, 200 pages generated, sitemap.xml rebuilt with 200 urls, build time 94 seconds.
The three tables that carry the factory
I keep the factory in three tables, not in files. Files break when the count grows.
- landing_templates holds 15 to 20 templates. Each template names its blocks in order: hero, features, pricing, testimonials, faq, cta, and optional gallery or map. I store block order as JSON, not as code.
- content_blocks holds 5 to 10 variants per block. A hero variant for logistics shows a night dock. A hero variant for clinics shows a hallway. Each variant carries its copy slots: headline, sub, and CTA text.
- landing_pages holds one row per live page with town, service, template id, block variant ids, and SEO fields. An ab_test_variants table holds one extra row per test with traffic share 50/50 and a winner flag.
One renderer reads those tables and writes HTML. I call it LandingRenderer. It takes a landing_pages row, loads its template and block variants, fills slots from the row, and writes the page. The same renderer writes the sitemap and the RSS feed. No second system.
How a page builds from a row
The client uploads a CSV with columns town, service, price, phone, hero image. A server job validates the sheet, inserts rows into landing_pages, and picks a template by service and a block variant by town. Price and phone fill slots. Hero image fills the gallery block when present. The job then calls LandingRenderer for each new row and writes files under /l/[town]/[service]. The sitemap rebuilds after the last write. 200 pages go live without a deploy. The pipeline behind those writes took me from 8 minutes to 90 seconds per deploy, and I wrote the steps in How I Cut Deploy Time from 8 Minutes to 90 Seconds.
- Bulk import handles 200 rows in under two minutes on a 512 MB instance.
- Duplicate detection blocks the same town plus service plus template triple. Second upload updates, not duplicates.
- A/B variants share the same row with a suffix /a and /b. Traffic splits at the edge via a cookie and a 302, not via duplicate URLs.
- SEO stays clean: each page has one title, one description, one canonical, one hreflang entry when needed.
Why templates beat pages
A change to a template block updates all pages that use it. I changed the FAQ block copy once and 84 pages updated in one query. If I had changed pages, I would have edited 84 files. Templates give you one place to improve. Variants give you many faces.
I keep 15 to 20 templates because five services times four town types covers most combos without crowding the chooser. I keep 5 to 10 block variants because ten hero variants with ten pricing variants already yields a hundred combos. More variants raise choice without raising conversion. Each surviving variant I still read against a fixed list, and the list is in CRO Audit Template: 12 Checkpoints That Move Revenue.
What I measure
Each page logs visits, form submits, and the variant ids that rendered. After ten days I compare hero variants within one template and retire the bottom two. After thirty days I compare templates within one service and pause the bottom template. The factory learns while you sell. Learning needs analytics wired to money, and I lay that pipeline out in CRO as a Pipeline: From Analytics to Revenue.
How to copy the factory small
Build three tables first, then the renderer. Seed three templates and two variants per block. Import ten rows from a CSV. Ship ten pages. Watch the sitemap. Add variants when real pages ask for them, not before.