The first version of any CMS has one content type and it is called post.

It has a title, a body and a date, because that is what a blog is. Then somebody needs a page that is not a post, so there are two types. Then somebody needs an author with a photo and a bio. Then events, with a start time and a venue, and the venue turns out to be its own thing because three events happen at the same place.

Nobody sits down and designs this. It arrives one requirement at a time and the question is only whether the system has a place to put it.

WordPress answered with custom fields, which is a key value table attached to every post. It works, in the sense that you can store anything, and it is terrible, in the sense that nothing knows what is in there. A field is a string until proven otherwise. Two plugins can use the same key for different purposes. Querying on one means joining a table that holds every field of every post, which is why the plugin that fixes this for you, Advanced Custom Fields, is one of the most installed pieces of software on the web.

Drupal went the other way and made the model explicit from early on, at the cost of being harder to learn and producing a database schema that grows a table per field.

The distinction that matters is whether the model is data or code.

As data, the model lives in the database, edited through the admin. This is friendly. Someone who does not write code can add a field. It is also unversioned: your production site has a model your staging site does not, nobody can review a change to it, and moving between environments becomes an export and import ritual with its own bugs.

As code, the model lives in a file next to the application. It gets reviewed, branched and deployed with everything else. The cost is that adding a field is a deploy, which for a marketing team who wanted a checkbox is an unreasonable answer.

Most systems eventually offer both and are unhappy about it.

There is a second question underneath, which is what a field type is for. Declaring a field as a date rather than a string is doing several jobs at once: it tells the editor to show a date picker, tells the validator what to reject, tells the storage how to store it, and tells whoever reads the content later what to expect. Those four are usually tangled together and they do not have to be.

The failure mode to watch for is a model that is enforced in the form and nowhere else. The admin shows a required marker, the API accepts anything, and six months later a third of the records have a field the schema says is mandatory. I have seen this in every system I have worked on, including ones where I wrote the schema.

If the model is a promise, something has to keep it. If nothing keeps it, it is a comment.