By Ding Xiang
I run a pool of 82 small independent websites by myself. 73 sit on their own domains; 9 live on pages.dev subdomains. Every one of them is a static site, built with Astro and deployed to Cloudflare Pages.
That probably sounds like the opposite of focus. Conventional advice says pick one product, pour everything into it, and ignore the rest. I tried to think that way, but my working style kept pulling me somewhere else: I like small, finished things. A tiny site with one job can be designed, built, launched, and understood end to end. A large site asks for a roadmap before it gives you feedback.
So instead of one big site, I built a pool. The current goal is to reach 100 sites. Along the way, stronger sites cross-link to weaker ones, so the pool behaves a bit like a small ecosystem rather than 82 unrelated side projects.
This is the engineering log of what that actually takes — and the quota incident that taught me the most expensive lesson so far.
Why lots of small sites?
Each site in the pool has a narrow job. That constraint is the whole point.
When a site does one thing, the page structure stays obvious. The content model stays small. The failure modes stay contained: if I break a template on one project, I have not taken down some monolith that everything depends on. If an idea turns out to be weak, retiring it is a small decision instead of a funeral.
There is also a learning effect. Repeating the loop — register the idea, set up the project, write the pages, deploy, submit for indexing, check what happened — turns vague skills into routines. Domain setup, metadata, sitemaps, canonical URLs, redirects, image handling: none of these are hard alone, but doing them carelessly across many sites teaches you exactly where your process leaks.
The honest trade-off is maintenance surface. 82 of anything is a lot. The pool only works because the sites share as much machinery as possible.
One template, many small configs
All the sites are static Astro builds on Cloudflare Pages. Under the hood, they lean on a shared-template approach: common layout pieces, shared styling conventions, shared SEO plumbing, shared deployment habits. What differs per site lives in configuration — the site's name, domain, navigation, content collections, feature toggles, and the small bits of identity that keep a site from feeling like a clone of its siblings.
That separation matters more than any single framework choice. When a fix belongs in the template, I make it once and let it flow to the sites that need it. When a change belongs to one site, it stays in that site's config and content instead of becoming a special case in shared code.
It also keeps me honest about scope. If a new idea needs a wildly different stack, it probably is not a pool site. The pool is for small, durable, mostly-static things. Experiments that need servers, queues, or user accounts belong somewhere else.
The D1 incident
Most of the pool is static, but some pieces use Cloudflare D1, and that is where I got burned.
D1's free tier counts rows read and rows written. That sounds abstract until you watch a day's quota disappear. I had a table with about 83k rows, and some pages ran full-table GROUP BY aggregations against it — the kind of query that feels harmless in local development, because the answer comes back and nothing complains.
In production, every one of those page loads scanned the table. Crawler traffic plus normal visits added up fast, and the aggregation queries burned through the whole day's read quota in roughly 3 hours. After that, the database simply refuses to cooperate until the quota resets.
No users were yelling at me — most of these are tiny sites — but the lesson landed anyway: on a metered free tier, an unindexed full-table scan is not a performance smell. It is a budget fire.
The precompute rule
The fix was not "optimize the query a little." The fix was to change the shape of the work.
If a number does not change between requests, it has no business being computed during a request. Totals, top lists, and grouped counts are now precomputed and stored, so pages read a finished answer instead of scanning raw rows. Static aggregates belong in the build or in a small precomputed table, not in a GROUP BY that runs for every visitor and every crawler.
The companion rule covers writes: batch writes are planned to use no more than 60–70% of the daily free quota. That buffer sounds wasteful until you need room for a mistake, a retry, or a second run on the same day. Free tiers reward people who leave slack in the system.
If you take one idea from this post, take that one: on metered infrastructure, design for the counter, not just for the request. The counter is always running, and crawlers are very good at finding your most expensive query.
Getting 82 sites discovered
Building the sites turned out to be the easy half. Getting them discovered is slower, less satisfying work.
For indexing, I submitted sitemaps to Google Search Console per site, and pushed a batch of 225 URLs across the pool through IndexNow. A concrete example from the pool is PicForge, a set of image tools that runs entirely in the browser — useful, finished, and exactly the kind of small site the pool exists for.
Submission, though, is not the same as discovery. A submitted URL is a request, not a result. That is why I have a discoverability retest scheduled for November 19, 2026. The retest will check which of the submitted pages search engines can actually find, whether the per-site sitemaps line up with what is discoverable, and whether the cross-links from stronger sites are helping weaker ones surface at all.
If the retest shows the submissions did little, that is useful too. A pool of small sites is, among other things, a cheap way to run many small experiments and keep the ones that produce evidence.
Maintenance is a calendar problem
People imagine the hard part of a site pool is building site after site. It is not. The hard part is remembering that all of them exist.
My maintenance rule is boring on purpose: template changes are treated as pool changes, not page tweaks. If I adjust shared metadata handling, I expect the change to make sense for a tool page, a content page, and a plain landing page. If it only makes sense for one site, it probably belongs in that site's config. New pages get the same unglamorous checklist as new sites: title, description, canonical URL, sitemap entry, and a quick look at the deployed result rather than the local build. Most pool failures are not dramatic crashes. They are small inconsistencies repeated patiently across many projects.
That is also why the pool has to stay mostly static. Every dynamic feature multiplies the number of things that can drift, expire, or consume quota while I am working on something else. A static page can wait for me. A neglected database cannot.
What I would tell another solo builder
Start smaller than your ambition. A finished tiny site teaches more than an unfinished platform.
Share the machinery, not the identity. Templates and deployment routines should be shared aggressively; each site's purpose and content should stay its own.
Treat free tiers as systems with counters. Read the quota model before you design the data layer, precompute anything static, and never let a crawler-discoverable page run an unbounded aggregation.
And keep a date on the calendar to check whether any of it worked. Building is loud; verification is quiet. The pool runs on both.
Top comments (0)