®

Let's talk
Engineering

The content model is the website

Before anyone chooses a typeface, someone decides what a page is made of. That decision outlives the typeface, the framework, and usually the people who made it.

A bag of blocks

A page builder stores a page as an ordered list of components: row, hero, card grid, call to action. The column is called something like blocks, and its type is an array of anything. The model is the sequence, and the sequence is the layout.

An editor can drop a quote halfway down a page on a Friday afternoon without asking us. No ticket, no deploy, no meeting. For a 12-page site with 1 author, that is often the right trade.

What it makes expensive is asking the content a question. Which pages still mention the old pricing? That text sits in rich text, in the 7th block of 40, on whichever pages used it. You can query a JSON column, and most block editors ship a search, so an answer comes back. It is a string match against a shape nothing enforces, and a miss looks like an absence.

A better query does not touch the structural cost, because the block list is an open union and nothing constrains which shapes appear in it. Scan for the testimonial type and you get every page using it today, but not a database that refuses the rename, and not an answer that survives the next variant. The hard part was never finding the pages. It is that nothing tells you when you have found them all.

Typed entities, and where the reference is wrong

The other model starts by naming the things: a Project, a Client, a Person, a Service. Each has typed fields, and references that point at other entities instead of copying them. A page becomes a query plus a template, and the same Project renders in the index, on its own page and in the sitemap.

Edit the row and every surface is correct. That is the pitch for a reference, and it is wrong in exactly the cases a portfolio site cares about. A case study describes work done for a company as it was named then, and following the reference rewrites the past silently.

So some values get copied on purpose. The client name at the time of the work, a contributor's role on that project, a byline for somebody who has left. Knowing which fields those are is the real skill: ask whether a field describes the thing now or describes it then, because a reference answers only the first.

Publish state is the other hard part, because it does not follow the arrow. A published Project pointing at an unpublished Client has to resolve into something. Hide the row, drop the link, or return a 404: whichever you choose, that choice belongs in the model, not in whichever template reaches it first.

The bill arrives a year in

A model is cheap to change while empty and expensive the moment it is populated, and the price rises with every row, every editor and every indexed URL. A schema with 4000 rows behind it, written by 6 people over a year, is a migration with a comms plan attached.

What you did not capture cannot be recovered. If author was typed as a string, turning it into a reference to a Person means resolving every value by hand: the misspellings, the shared surnames, the rows where somebody typed the word team. A Project with 1 Service field cannot describe work that was 2 services, so every historic row becomes a guess. Routes go the same way: a category the model never had means a redirect map somebody keeps correct for as long as the old URLs exist.

The quiet failure is the worst of them. Editors rarely report a bad model. They work around it in free text: a prefix on the title to mean archived, a stray character to mean featured. Once a convention lives in prose, the data is worse than missing, because it looks like it means something.

A workaround is evidence about the model, not about the editor, which makes it a cost of the model argued for here. The answer is a deliberate escape hatch: a body field with a small set of embeds, typed and listed like every other field, so the exception lands somewhere queryable instead of in a title. The test is repetition. Twice is a field you failed to model. Once in a year stays an embed.

Model against the content, not against a screen

Modelling first does not mean modelling in an empty room. A schema drawn with no content in front of it comes out tidy and unusable, every field a guess about the work. Names taken off a layout fail from the other side: heroSubtitle and bottomBlurb record a position, and a position is the first thing a redesign changes.

Lay 20 real projects on a table before you name any fields. The awkward rows are the point: work that was 2 services at once, an internal project with no client, a lead contributor who has since left. A layout shows a project at its best, on a good day.

A model fits on a sheet of paper, so you can be wrong all morning and bin it. Ask 3 questions of a field before it exists, and most bad ones never get made.

  • Is this a fact about the thing, or a decision about a page? Facts belong in the model, decisions belong in the template.
  • Who edits it, and how often? A field 6 people touch weekly needs a type that refuses bad input, because free text drifts into 6 conventions.
  • What does the page do when it is empty? Every optional field is a layout you have agreed to design.

The field names are the vocabulary

A model is also a shared language. If the schema says Launch and the team says project, they drift apart, and things get filed in the wrong place. People file content against the words they already use, not the words in the admin. Naming entities after what the team calls them is data quality, not decoration.

There is a test for a model, and it is boring. Read the field names aloud to somebody who does not work on the site, with no screens in the room. If they can tell you what the thing is, the names came out of the domain, which changes slower than any layout. If they need the page first, the names came off positions, and positions go first in a redesign.

Catechiste writing