Food and Climate Tech Network
Taking over a hand-written static site and rebuilding it in a week
Developer · Jul 2026 · Food and Climate Tech Network

What I walked into
The site already existed. Another developer had built it over about ten months as hand-written HTML and CSS: thirteen pages, a 3,600-line stylesheet, and six forms that had nowhere to submit to. Somebody filled in a volunteer form and nothing happened to it.
My job was not to design a new site. It was to keep what worked, rebuild it on something maintainable, and give the organisation a way to actually receive what people were sending them.
The whole rebuild landed in one week: 168 files changed, around 11,700 lines added and 6,400 deleted, including the old stylesheet and every legacy HTML page.
Six forms, one table
Contact, volunteer, partner, training signup, advocacy kit, newsletter. Six different shapes, and the obvious approach is six tables.
I used one. A single submissions table with a form-type enum and a JSON payload column, each shape validated by its own schema before it is written. An index on type and date drives the filtered admin view.
For a brochure site whose forms will change whenever the organisation runs a new programme, that is the right trade. Adding a seventh form means writing one schema and one page, not a migration. If the data ever needs real relational structure it can be normalised later, but it does not need it now and building for that would have been building for an imaginary requirement.
Keeping the spam out without adding a dependency
Every submission goes through the same short path: honeypot, rate limit, validate, write.
The honeypot is a field bots fill in and humans never see. It is positioned off-screen rather than hidden with display: none, because some bots specifically check for that. When it is filled, the response is a cheerful "thank you" rather than a rejection, so the bot has no signal that it was caught.
The rate limiter is an in-memory sliding window, five requests per ten minutes per address. It resets on deploy and would not survive multiple replicas. For a single container serving an NGO brochure site, reaching for Redis would have been infrastructure for its own sake, and the limitation is written down rather than glossed over.
Admin access, no user table
There is one admin password and no user accounts, because there is one organisation reading one inbox.
The session is a stateless HMAC-signed token with a twelve-hour expiry, verified with a constant-time comparison. No session table, no auth library. The password is a bcrypt hash in an environment variable, which produced my favourite small bug on this project: Next's environment loader performs variable interpolation even inside quotes, and bcrypt hashes are full of dollar signs, so the hash silently arrived mangled. Every dollar sign has to be escaped in the env file. That is now the first thing the README says.
Not losing the old URLs
The previous site's pages were all .html files that had been live long enough to be indexed and linked. Throwing them away would have thrown away whatever search ranking the organisation had.
Every old path redirects permanently to its clean equivalent: thirteen pages plus the index. Unglamorous, ten minutes of work, and the difference between a migration nobody notices and a migration that costs the client their traffic.
Sharing a server with another project
This runs on the same VPS as another site I maintain, behind one shared Caddy instance. Neither app publishes a public port; Caddy reaches each by container name and handles TLS.
That produced a real trap. Docker Compose registers a service's key as a DNS alias on every network it joins, so naming this one app, the obvious name, would have collided with the other project's own service on the shared network and broken routing for both. It is called foodandtech-app for that reason, and the reason is in a comment, because a future me would otherwise have renamed it back.
Honest status
I rebuilt and now maintain this site. I did not originate it, and the earlier developer's work is still visible in the content and structure.
The reports page lists documents marked "coming soon" and the donations page shows placeholder payment details, both pending content from the organisation. There is no payment integration on the donations page, it is informational.