Inside the Towns
One codebase, one editorial model, seven Colorado towns. Each guide reads as if it were built for that town alone; the whole network is run by one person.
In short
- The problem. Growing Front Range towns have no single place to find what's on, where to eat or what living there is like, and one person had to run seven.
- What I built. Seven town guides and a hub from one system, each with its own look and local research, kept current in one weekly session.
- What it means for you. Several locations or brands can run from one site, and a weekly update is a quick edit, not a rebuild.
- Owner
- Lerner Works. I publish and edit every site in the network myself. None of them is a town government, a chamber of commerce or a campaign.
- Sector
- Community and visitor information, Colorado Front Range
- Scope
- Network strategy, content model, shared design system, the Astro build, per-town research and editorial, deployment, the weekly maintenance tooling
- Year
- 2026
- Status
- Seven town sites and the hub live at insidethetowns.com; figures below read from the repository on September 29, 2026, at its commit of September 27
At a glance
- eight sites — seven towns and the hub — built and deployed from one repository
- 1 codebase
- place and event records across the seven towns, each one a file in the repo
- 240 + 575
- people across the seven towns covered, by the 2020 census
- 72,350
- a week keeps all eight sites current: gather the events, validate, push
- 1 session
The situation
A growing Front Range town has a strange information gap. Ask where to eat on a Tuesday, what is on this weekend, which trail starts where, or what it is actually like to move there, and there is no single place to look. The official town site, where there is one, is built for permits and council agendas — that is its job, and it does not pretend otherwise. A chamber directory lists members. Facebook groups know the answer but will not tell you twice. The rest is a scatter of aggregator pages that were last true in 2019.
I had already built one answer to this for a single town. TownofNiwot.com was a nine-page independent guide to Niwot: a directory where every row carried a source and a date, a calendar with no scraper, and neutral coverage of a live incorporation election. It worked. Then the obvious question arrived, and it was not "what else should this site do." It was: Niwot is not the only town like this.
Lyons, Berthoud, Erie, Johnstown, Timnath, Elizabeth. Same gap, different town. Between the 2010 and 2020 censuses Timnath grew 938 percent, Berthoud 102, Johnstown 75, Erie 66 — while Lyons grew 9 and Niwot 7. Two very different problems sit under one format: towns arriving faster than their own information, and towns whose information nobody has bothered to gather.
The problem in one sentence: seven towns each need a site that feels like it was made for them, and one person has to be able to run all seven.
Constraints
One repository, no forks. The moment a town's difference lives in code rather than configuration, there are seven codebases wearing a trench coat and a fix has to be applied seven times. Town differences live in a config file and a content folder; nothing in a component may name a town, a color or a domain.
The sites must not look like a template. A reader in Lyons should not be able to tell that the same code serves Timnath. Shared structure, shared type, shared rhythm — distinct color and distinct photography, per town.
A new town under an hour. If adding the eighth town is a project, the network stops at seven. Scaffolding has to be a command.
A weekly content update must be a content edit. Not a deploy someone has to think about, and not a code change.
No invented local knowledge. Same rule the Niwot guide ran on: every event cites the organizer's own page, every image is logged with its license, and anything that cannot be checked is left out or marked rather than guessed.
What I built
Parent hub
Inside the Towns
Every town's weekend in one place, a seven-town comparison for people deciding where to move, and the front door for the next town added.
One amber holds it together. Seven sites with seven unrelated color schemes do not read as a network. A single warm amber, #F0A62E, appears in the same three places on every site — the rule across the top of every page, the footer eyebrows, the "our pick" marks — and is never used as text. That one shared mark is most of what makes eight different palettes read as one family.
A weekly session, not a weekly build. Once a week, one command lists what is coming up in each town, what has gone past, and what is missing or due for a re-check. The week's events go in, get validated, and one push rebuilds all eight sites.
Forms built dark, then switched on. The contact and submit-an-event forms on every site were built behind a provider id and shipped their fallback — an email address — until the id was set. They are live now, one endpoint per site rather than one shared, so a town that is ever handed to someone else takes its inbox with it. The build checks every host a form posts to against the sites' content-security policy, so a form cannot point somewhere the page would refuse.
How it's built
One chassis, selected at build time. The whole network is a single Astro project. A TOWN environment variable picks which site is being built — TOWN=lyons npm run build — and the config loader fails loudly if it is unset or names a town that does not exist, because a build with the wrong town is worse than no build. Eight Vercel projects point at the same repository, one per domain, each with its own TOWN. One push rebuilds all of them.
Town differences live in exactly two places. A config file in src/config/towns/ carries the domain, the tagline, the county, the census figures, the school district, the commute notes, the official links to link out to, and the three colors. A content folder in content/<town>/ carries the events, places, articles, pages and images. Twenty-eight components and two layouts are shared by all eight sites, and not one of them knows a town's name.
Two colors per town, doing different jobs. Each town config carries a bright accent that is only ever a fill, a rule or a mark; a darker accentDark that words are set in; and a paper tint the whole site sits on. Splitting the bright one from the readable one is what let the accents get genuinely saturated: only the dark one has to clear 4.5:1, so nothing constrains the bright one beyond staying visible.
A hue per category, so a list reads as a list of kinds. Each event category and place type carries two values: a bright dot for marks, and a dark ink for when the category is set as words. Place types borrow the event hues by what the place is for, rather than inventing nine more. Twenty upcoming events read as twenty kinds of thing rather than twenty gray rows.
The color system is proved, not asserted. npm run check-colors reads the real town configs and the real palette module — so it cannot drift from what renders — and checks every accent at 3:1 on its own paper, every dark accent at 4.5:1 both on paper and under white, the amber at 3:1 on every band, and every category color against all eight papers and white. CI runs it on every push. It is what made brightening the whole palette safe to attempt: the first pass put Timnath's ochre at 2.60:1, and the script caught it before anyone saw it.
npm run weekly prints, per town, the events whose last date falls inside the next seven days, the ones already past, the towns with fewer than five upcoming, the places with no photograph, and the listings not re-checked in ninety days. The week's events go into a CSV; npm run import-events validates every row against the schema and writes the files, with a --dry-run that is meant to be read. Then npm run validate, commit, push — and Vercel rebuilds all eight sites.
Finding things, without a server. Every site has a search page: Pagefind indexes the built output at the end of each build and the page loads that static index, so search works on a host that runs nothing. The directory lists every place by kind, with a name search and an "open now" pill that is decided in the reader's browser from the Denver clock, because a build is a snapshot and "open" has to stay true after it. The events page asks when before what — today, this weekend, the next seven days, free — and carries the chosen filters in the URL, so "free things this weekend" can be bookmarked, shared and linked from the email.
Three layers that keep "upcoming" true between deploys. A static build is correct on deploy day and quietly wrong a week later. So: a scheduled rebuild fires a deploy hook per project, daily and again on Thursday afternoon when people start looking at the weekend; every event carries the day it falls on and anything past is dropped in the reader's browser, so a stale build shows fewer events rather than wrong ones; and the hub's weekend page bakes three weeks and picks the weekend from the reader's clock. The layers exist because the first one has a specific failure mode worth designing around — GitHub disables a scheduled workflow by itself after sixty days without a push, silently, in exactly the quiet winter when nobody would notice.
Decisions and tradeoffs
Config and content, never a fork. The rule that nothing in a component may hard-code a town name, color or domain is the single decision the whole network rests on. It costs something real: every town-specific idea has to be generalized into a field on the config before it can ship anywhere, which is slower than special-casing one site. What it buys is that a fix written once is a fix everywhere, and that the eighth town inherits seven towns' worth of work for free.
Dark mode, built and then removed the same day. It worked. I took it out anyway: I do not want these sites turning dark on a reader's phone without being asked. Two things from it were worth keeping and stayed — the text color split out from the band fill as its own token, because those two pull in opposite directions the moment anything changes; and the derived tints mixing in plain sRGB rather than oklab, so the contrast checker can reproduce them exactly. The exercise paid for itself on the way through, catching a rule at 1.45:1, which is not a rule, it is nothing.
Twenty-one comparison pages were not generated. Seven towns make twenty-one pairs, and twenty-one pages built from one table with no argument in them is thin content — a search engine that decides that about one page tends to decide it about the set. A pair gets a page when somebody has actually written it. The list is a file, and Erie against Johnstown is the only one so far.
A column that is not drawn until it can be. Median home price is a field on every town config, and the comparison page renders the column the moment every live town has a price, a date and a named source — all three or none. A price with a gap invites the reader to read the gap as "cheap", and an undated one goes on looking authoritative for two years after it stops being true.
The newsletter shipped dark, then switched on. The signup block rendered nothing at all until a provider was configured on the hub — not a disabled form, not a "coming soon" input, nothing — because a form that takes an address and drops it is worse than no form. It is live now, through Buttondown: one email a week on Thursdays, with the reader's chosen towns carried as tags so the Erie issue goes to the people who asked for Erie. Each issue is drafted from the listings by a script and read by a person before it goes out, and the archive page fills as issues are sent.
Growth is census-to-census, and says so. The brief wanted five-year growth. The state demography office publishes annual estimates, which would be the right source, but serves them from interactive lookups rather than a file — so there was no way to get a consistent figure for seven towns. The decennial census is authoritative and identical in basis across all seven. It is a weaker number, honestly labeled, instead of a stronger one nobody could check.
How the network scales
Adding a town is not a build. One command sets up the new site's files. What remains is the part that cannot be automated and should not be: the research. Reading the town, finding the places, confirming the events against organizers' own pages, and sourcing photographs with a license attached.
That is the real shape of the cost curve. The first town cost a site. The seventh cost a week of local research and a color. The eighth will cost the same week — and nothing else, because the chassis, the editorial rules, the contrast checker, the weekly tooling and the deployment pattern are already built and already maintained.
The next one is a domain rather than a plan. carbonvalleyguide.com covers Frederick, Firestone and Dacono, and is wave eight in the build plan. It is also the first site in the network that is three towns rather than one, which is a content-model question the chassis has not been asked yet: the config gives a site one name and one set of official links, and three towns means three town governments to link out to.
It is not described as live anywhere on any of the sites, and will not be until it is.
How it's built
npm run new-town elizabeth "Elizabeth" writes the config file, appends the registry entry, creates the content folder with one sample event, place, article and moving-here page, generates a favicon, drops in a placeholder hero, and prints what to do next.
Result
The figures below are counted from the repository on September 29, 2026, at its commit of September 27. They are things that can be counted, not claims about reach; the study was first written on September 18 and the counts that moved since — five more components, two fewer events, the tests — moved because the network is being worked on.
The one reach figure the network does publish is on the hub's own home page: the seven live guides cover 72,350 people across four counties — Boulder, Larimer, Weld and Elbert — summed from the 2020 census. It is a census figure rather than an audience figure, and the hub labels it as one.
- Live sites
- 8 — seven towns and the hub, from 1 codebase
- Shared components
- 28 components, 2 layouts
- Tests
- 109, across 14 files
- Place records
- 240 across seven towns
- Event records
- 575 across seven towns
- Page routes
- 14 per town site, 8 on the hub, 7 shared
- Palettes checked
- 8, against WCAG AA, in CI
- Per-town code
- 1 config file, 1 content folder
- Feeds per town
- 2 RSS, 2 iCalendar: the calendar, and one per event
Not yet measured: traffic and newsletter signups. They'll be added with a date range and source after the first full month.
Where it stands
Seven town sites and the hub are live, one Vercel project per domain, all deploying from the same repository. The network runs on a weekly editorial session rather than a release cycle: gather the week's events, validate, push, and all eight rebuild.
TownofNiwot.com, where this started, now redirects to Inside Niwot. Every old section URL maps to its new page and everything else goes to the home page, so nothing that was ever linked is lost. The old case study stays published because the work in it is the foundation this network is built on — the sourcing rules, the dated records, the refusal to guess — and because a redirect is worth explaining rather than quietly performing.
Since the study was first written, the parts that were dark have been switched on: the newsletter signup, the contact and event forms on all eight sites, and a hello@ address at each of the eight domains that reaches a real inbox — set up on September 27 and tested the same day with a message from a separate account.
What is open is the honest part: the newsletter archive is empty until the first Thursday issue goes out; the median-home-price column stays undrawn until all seven towns have a sourced, dated figure; fifty of the 240 places carry a photograph and the rest are being asked for one, business by business; and Carbon Valley is a domain, not a site, until there is research behind it.
What this means for your project
You probably do not need seven websites. The decisions that made seven maintainable by one person are the same ones that make one site cheap to keep current.
- Content kept apart from design, so a weekly update is an edit rather than a rebuild, and one you can make yourself.
- Several locations or brands from one build. If you run three branches or two businesses, one codebase and one set of rules is the difference between a site that stays current and one that quietly stops.
- Color and contrast proved by a script, not judged by eye, so every page clears accessibility before it ships and keeps clearing it after every change.