All build notes

The admin panel I didn't write

Every client project has a moment where you realise how much of the work is going to be admin screens. Not the site — the site is the fun part. The admin. A form for the lineup. A form for the dates. A form for the venue blurb. Each one a little bespoke panel that does almost exactly what the last one did, with different field names.

For the Clash of the Titans festival site, that pile of forms was going to be most of the two-week build. So I didn't write them. I wrote the thing that writes them.

The actual problem wasn't editing — it was next year

The festival ran on a Google Sites page. The organizers could technically edit it, which meant the real problem wasn't access, it was that editing was miserable enough that nobody did it. Dates drifted out of sync. The lineup lagged behind reality.

And a festival site has a specific shape of problem: it isn't a site that gets edited continuously. It's a site that gets edited furiously for six weeks, then abandoned for ten months, then edited furiously again — by a partly different set of people, who have forgotten everything they learned last year.

That reframes what a good CMS is here. It isn't the most powerful editor. It's the one that a volunteer can open in October, having last touched it in March, and not be confused by.

Deriving the editor from the schema

The site's content already had a precise definition — the Prisma schema backing Postgres. Every field a person could edit was already declared there, with its type, whether it was optional, and how it related to everything else.

Writing admin forms by hand means restating all of that a second time, in JSX, and then keeping the two copies in sync forever. The second copy is pure liability: every schema change is now a change in two places, and the failure mode is a silent one where the form quietly stops matching the data.

So the admin reads the model and builds its own interface. A string becomes a text input. A longer text field becomes a textarea. A date becomes a date picker. A relation becomes a picker over the related rows. Add a field to the schema, run the migration, and the editor for it already exists.

// Illustrative shape, not the literal source: the point is that the
// field list is *derived*, never hand-maintained alongside the schema.
const editors = {
  String:   TextInput,
  Text:     TextArea,
  DateTime: DatePicker,
  Boolean:  Toggle,
  Relation: RelationPicker,
};

function AdminForm({ model }) {
  return model.fields.map((field) => {
    const Editor = editors[field.kind];
    return <Editor key={field.name} field={field} />;
  });
}

What this buys, and what it costs

The win is that admin work stops scaling with content types. The tenth content type costs roughly what the second one did, which is what made a genuinely self-editable CMS fit inside a two-week build at all.

It is not free, though, and it's worth being straight about the trade:

  • A generated form is generic by default. Anywhere the editing experience genuinely needs to be bespoke, you're overriding the generator — and an override is more code than just writing the form would have been.
  • The abstraction has to be legible. A generator nobody can debug at 3am is worse than fifty boring forms, because the boring forms fail one at a time.
  • It pushes correctness into the schema, which is good — but it does mean a sloppy schema produces a sloppy admin, immediately and visibly.

The part that mattered more than the generator

The detail the organizers actually noticed had nothing to do with any of this. The entire brand is driven by a single CSS colour token. Each year's edition gets a new look by changing one value — no design pass, no developer, no rebuild of anything.

That's a one-line piece of engineering that does more for the client's independence than the whole generated-admin system, because it turns an annual "can you re-skin the site" request into something they do themselves in a minute.

It's a useful reminder that the impressive part of a build and the valuable part are frequently not the same part.

The finished site went from 37 to 85 on mobile speed and 69 to 98 on desktop against the Google Sites page it replaced, with layout shift eliminated and perfect 100s on accessibility and SEO. But the measure I actually care about is that the organizers have been running it themselves since launch.

This post draws on a real build. Read the Clash of the Titans case study →

Related. Next.js website revamps · the Google Sites alternative

Next up. Your photo gallery is just a Google Drive folder

Let’s get your new site live by September 10.

// free 20-min scoping call · no obligation · reply within 1 business day