Project
Zero-config site platform
I run a handful of small sites and got tired of every new one meaning another round of config edits: a pipeline entry here, a build target there, a deploy step somewhere else. Worse was the drift: every site sat on a slightly older everything, some on old CRA, a few on Gatsby, some on Next.js. After a while the chore alone had turned most of my projects into abandonware.
So: one size fits all, I guess. There was a legitimate technical angle that would make migrating almost all of the old projects into a single stack seamless. The premise is simple enough. A monorepo, a shared UI kit, and Next.js printing the static sites out the end. They are all on Cloudflare Pages already, so the deploy script may as well be unified too.
It is a pnpm workspace under Turborepo. pnpm keeps one lockfile and symlinks the shared kit into every site, so a change there is visible everywhere without a publish step. Turborepo owns the task graph: build declares a dependency on its own upstream builds, which is what makes the kit compile before any site importing it, and its cache means an untouched site costs seconds instead of minutes.
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "out/**"]
},
"test:visual": {
"dependsOn": ["^build"],
"cache": false
}turbo.json – "^build" is the whole ordering rule: build my dependencies first.
Nothing about a new site is registered anywhere. The deploy script walks the sites/ tree and treats any directory holding a package.json as a site, and the same walk covers workers/, on the same terms.
const sites = readdirSync(sitesDir).filter((dir) => {
const path = join(sitesDir, dir);
return statSync(path).isDirectory() && existsSync(join(path, 'package.json'));
});scripts/deploy-sites.mjs – the whole of site discovery.
That package.json is the entire contract. Alongside the usual fields, a small cloudflare block says where the built output belongs: projectName is the handle of the Cloudflare Pages project it deploys into, directory is what to upload, and disabled keeps a site in the repo without shipping it, which is how this one sat here while it was being written.
{
"name": "demo-kavehaz-budapest",
"cloudflare": {
"projectName": "kavehaz-budapest",
"directory": "out",
"disabled": false
}
}Everything a new site declares about its own deployment.
And since the script diffs against the last deploy before doing anything, most runs ship nothing at all.

Deployment was the easy half, and so was standing the sites up. Following through on my own premise — actually unifying the markup — is the part that turned out to be hard. Every site already derives its MUI theme from the shared kit, so the design tokens are genuinely one source. The components are not, and for a good while that was the point: these sites are proofs of concept and migrations, each one a sample of what actually recurs. Two examples are not enough to tell a shared shape from a coincidence.
So every site kept its own header and footer, and everything between them (listings, cards, section blocks), written inline, page by page, across fifty-odd files. Somewhere along the way the proof of concept sample turned into a backlog.
This is where Storybook comes into full play. A component built there can be seen and found rather than remembered, tested in isolation (unit, behaviour and accessibility), and previewed under every site chrome it has to survive, instead of being checked by opening seven sites and looking at them.
The one thing lacking here was any trust in myself to pull it off. Turns out I do not have to: just over-engineer giant visual regression tooling that keeps the drift discoverable and manageable as I go.