← All work
Own project

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
The Inside the Towns hub: the wordmark over the headline "Independent guides to the small towns worth knowing", and a strip reading 7 guides live, 4 counties, 72,350 people covered

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

insidethetowns.com

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.

  • The Inside Niwot home page: the wordmark over a photograph of open grassland with the Front Range behind

    Boulder County

    Inside Niwot

    insideniwot.com

    4,306 people. Unincorporated — no town hall, and a vote on that in 2026. +7% since 2010.

  • The Inside Lyons home page: the wordmark over a photograph of the sandstone walls at the canyon mouth

    Boulder County

    Inside Lyons

    insidelyons.com

    2,209 people. The canyon mouth: sandstone, the river, the music, a town that rebuilt after 2013. +9%.

  • The Inside Berthoud home page: the wordmark over a photograph of the town

    Larimer County

    Inside Berthoud

    insideberthoud.com

    10,332 people. The Garden Spot: a real Main Street and the lakes, short of Fort Collins prices. +102%.

  • The Inside Erie home page: the wordmark over a photograph of the town

    Weld County

    Inside Erie

    insideerie.com

    30,038 people. A coal town that became a commuter town, with a downtown that caught up. +66%.

  • The Inside Johnstown home page: the wordmark over a photograph of the town

    Weld County

    Inside Johnstown

    insidejohnstown.com

    17,303 people. Sugar-beet town at the I-25 interchange, filling the gap to Greeley. +75%.

  • The Inside Timnath home page: the wordmark over a photograph of the town

    Larimer County

    Inside Timnath

    insidetimnath.com

    6,487 people. A farm village of 600 that became a town of 6,000, mostly since the Costco. +938%.

  • The Inside Elizabeth home page: the wordmark over a photograph of the prairie on the Palmer Divide

    Elbert County

    Inside Elizabeth

    insideelizabeth.com

    1,675 people. Prairie and ponderosa on the Palmer Divide, sold five acres at a time. +23%.

Every site in the network, with the accent, dark accent and paper each one actually ships. The three swatches on each card are the values in that town's config file. Population is the 2020 census, and the percentage is census-to-census growth from 2010 — the same basis for all seven, so the column is comparable rather than merely impressive.

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.
See what a build includes
Next case studylernerworks.comRead it →