Why the model is the most expensive thing to change
Content modeling is the practice of defining the structure of your content before you write any of it: which content types exist, what fields each type holds, how those types relate to each other, and which pieces are reusable across the site.
Every headless CMS asks you to do this on day one, usually before you know enough about the project to do it well. This page is the guide we wish we could hand every team on that first day.
Start modeling free · Read the API docs
Copy is cheap to change. Design is moderately expensive. The content model sits underneath both, and everything downstream depends on its shape:
- Your queries. Every API call and template references field keys by name.
- Your components. A React or Vue component that expects
metadata.author.titlebreaks the moment authors stop being a relationship. - Your editors' habits. People learn where things live. Moving fields costs retraining.
- Your existing content. Restructuring 4,000 published objects is a migration script, a review pass, and a deploy.
The goal is not a perfect model. The goal is a model that absorbs change without a migration. That means being deliberate about a small number of decisions up front.
The four building blocks
In Cosmic, the whole model is made of four concepts. Most headless CMSs use the same ideas under different names, so this maps cleanly if you are coming from somewhere else.
1. Object Types are your content types: Posts, Authors, Products, Landing Pages. A type defines the schema that every entry of that type follows.
2. Objects are the individual entries: one blog post, one author, one product. Each object has a title, a slug, a status, and its metadata.
3. Metafields are the fields inside a type. Cosmic ships a wide set of field types, covered in the table below.
4. Relationships connect objects to each other. A post points to an author. A product points to many categories. These are real references, not copied text, which is what makes a model maintainable.
Field types you can model with
Picking the right field type is most of the job. A select and a text field look similar in the dashboard and behave very differently after two years of use.
| Field type | Use it for | Stores |
|---|---|---|
text | Short single-line values: SEO titles, SKUs, button labels | String |
textarea | Plain multi-line copy: teasers, meta descriptions | String |
markdown / rich-text | Long-form body content | Markdown string |
number | Prices, ordering, durations, counts | Number |
select | One choice from a fixed list: status, tier, size | String |
multi-select | Several choices from a fixed list | Array of strings |
switch | True/false flags: featured, archived | Boolean |
date | Publish dates, event dates, expiries | Date string |
file / files | Images, video, PDFs, galleries | Media reference |
object | A single relationship: post to author | Object reference |
objects | A many relationship: post to tags | Array of references |
repeater | Repeating groups of fields: FAQ rows, feature cards | Array of field groups |
json | Structured data with a shape your app owns | JSON |
color, emoji | Theming and small visual tokens | String |
Three rules of thumb that save real pain:
- Use
selectinstead oftextfor anything with a fixed set of values. Free-text status fields turn intoPublished,published, andPublishinside a month, and then your filters quietly miss rows. - Use
objectorobjectsinstead of repeating a name. If you type an author's name into every post, renaming that author is a find-and-replace across your entire archive. - Use
fileorfilesfor media instead of a text field holding a URL. A real media reference returns both the original asset and a transformable URL, so every crop, width, and format is a query parameter rather than another upload to track. The image CDN page covers how those URLs behave, and why modeling images as references is what makes responsive delivery a template concern rather than a content one.
A worked example: a blog that scales
The most common first model is a blog, and the most common mistake is putting everything on one type. Here is the shape that holds up.
Authors (authors)
| Field key | Type |
|---|---|
image | file |
bio | textarea |
role | text |
twitter | text |
Categories (categories)
| Field key | Type |
|---|---|
description | textarea |
Posts (posts)
| Field key | Type |
|---|---|
image | file |
content | markdown |
teaser | textarea |
published_date | date |
author | object, related to authors |
category | object, related to categories |
tags | objects, related to tags |
featured | switch |
seo_title | text |
seo_description | textarea |
Three separate types, joined by references. Now the author bio lives in exactly one place, category pages are a filter rather than a hand-maintained list, and the SEO fields are explicit instead of derived from the title.
Notice that image on both Authors and Posts is a file, not a text field. That one choice is what lets a template request an author avatar at 64px and the same post hero at four responsive widths, from a single stored original.
Fetching it with the Cosmic JavaScript SDK looks like this:
npm install @cosmicjs/sdk
import { createBucketClient } from '@cosmicjs/sdk'; const cosmic = createBucketClient({ bucketSlug: process.env.COSMIC_BUCKET_SLUG!, readKey: process.env.COSMIC_READ_KEY!, }); // One request returns posts with their author and category resolved const { objects: posts } = await cosmic.objects .find({ type: 'posts' }) .props(['title', 'slug', 'metadata', 'created_at']) .depth(1) .limit(10);
depth(1) is the payoff for modeling relationships properly. One request returns each post with its related author and category objects already populated, so your template can read post.metadata.author.metadata.image without a second round trip.
Because image is a media reference, that same response also carries the transformable URL for each asset, which is how a responsive srcset gets built without a second API call or a build-time image step:
const base = posts[0].metadata.image.imgix_url; const hero = `${base}?w=1200&auto=format,compress`;
Filtering by a relationship is a query rather than a code change:
const { objects } = await cosmic.objects .find({ type: 'posts', 'metadata.category': categoryId, }) .props(['title', 'slug', 'metadata.teaser']) .depth(1);
Cosmic exposes content over a REST API and this TypeScript SDK. There is no GraphQL layer to learn, and props gives you field-level control over payload size, which is usually the reason people reach for GraphQL in the first place.
Modeling relationships
One to one. A post has one author. Use a single object field. Keep the direction on the side that changes most often, which is almost always the child.
One to many. A category has many posts. Do not add a posts field to Categories. Model the reference once on Posts, then query posts filtered by category. Storing the relationship on both sides means two places to keep in sync, and they will drift.
Many to many. A post has many tags, a tag has many posts. Use an objects field on the side you query from most, usually Posts.
Singletons. Pages that exist exactly once, like a homepage or a pricing page, deserve their own type with named copy slots rather than a wall of markdown. Named fields mean an editor can change the hero headline without touching anything else, and your component contract stays stable.
Reusable blocks and repeaters
Two patterns cover most "the marketing team wants to rearrange this page" requests.
Repeaters model a list of identical structures. An FAQ section is a repeater of question and answer pairs. A feature grid is a repeater of icon, title, and description. Editors add and reorder rows without a developer.
Reusable content blocks model a snippet that appears in many places and must stay identical everywhere: a support address, a compliance disclaimer, a promotional callout. Define it once, reference it from any rich-text field, and update it in one place.
The test for which to use: if the same words need to appear in twelve articles, it is a block. If the same shape needs to repeat within one page, it is a repeater.
Modeling media and alt text
Media deserves the same deliberate treatment as text, and it usually gets none.
The first decision is the one above: file or files, never a text field holding a URL. A stored URL is a dead end. A media reference is queryable, and it comes back with a transformable URL so a single upload serves every size and format you will ever need.
The second decision is where alt text lives. Put it on the media record itself rather than adding an image_alt text field to every type that happens to use an image. Alt text describes the asset, so it belongs to the asset. Store it once and every object referencing that image inherits the description, instead of the same photo carrying three different descriptions on three different pages.
The third is folders. Media libraries get messy faster than content does, because uploads happen in a hurry. Agree on a folder convention early, in the same conversation where you agree on field keys.
How the delivery side of this works, including resizing, format negotiation, and what actually counts against a plan allowance, is covered on the image CDN page.
Seven anti-patterns to avoid
- The god type. One
pagestype with forty optional fields, most of them blank on any given entry. Split it by shape, not by subject. - HTML in a text field. Pasting markup into a plain text field puts presentation inside your content and makes the same content unusable on another surface.
- Free-text values that should be a
select. Covered above, and worth repeating because it is the single most common one. - Duplicating instead of referencing. Author names, company names, and prices typed into multiple objects. Every duplicate is a future inconsistency.
- Modeling your page layout. Fields named
left_column_textandrow_3_headingtie your content to one design. The next redesign forces a migration. - No SEO fields. Deriving every meta description from a teaser works until someone needs a different one. Add explicit
seo_titleandseo_descriptionfields from the start. - Skipping helper text. A required field with no explanation gets filled with whatever the editor guessed. One sentence of helper text prevents a category of bad data.
A content modeling checklist
Run through this before you build your types, and again before you publish at volume.
- Every distinct shape of content has its own type.
- Anything repeated across entries is a relationship, not typed text.
- Every fixed set of values is a
selectormulti-select. - Every type has explicit SEO title and description fields.
- Media is modeled as
fileorfiles, never a text field holding a URL. - Alt text lives on the media record, not duplicated per type.
- Field keys are lowercase, snake_case, and stable. Renaming a key breaks queries.
- Required fields are genuinely required, so drafts are not blocked by fields nobody has yet.
- Every non-obvious field has helper text.
- Long-form body content is in a markdown or rich-text field, never a plain text field.
- No field name references a layout position.
- Someone who is not a developer has tried to create one entry of each type without help.
That last item is the one people skip, and it finds more problems than the other eleven combined.
Modeling for AI agents as well as editors
A new consideration in 2026: your content model is now also an interface for AI. When an agent connects to your CMS through the Cosmic MCP server, the thing it reads to understand your content is your schema.
That makes model quality directly practical. Descriptive type names, clear field keys, and helper text are the context an agent uses to decide where content belongs. A type named content_2 with fields f1 and f2 is guesswork for a human and guesswork for a model. A type named blog-posts with markdown_content, teaser, and author is self-documenting to both.
The same discipline that makes a model easy for a new hire makes it usable by an agent.
What good modeling actually buys you
The outcome of a clean model is that content changes stop being engineering tickets. That is the value FINN describes:
"Cosmic is: us never having to ask a developer to change anything on the backend of our website."
Maximilian Wuhr, Co-Founder at FINN
That is a modeling result before it is a product result. The fields were named well and scoped tightly enough that non-developers could own them.
Start with a model you will not regret
You can build and test a complete content model on Cosmic in an afternoon, on the free plan, without a credit card. Create your types, add a handful of objects, and query them with the SDK before you commit to a shape.
Create a free Bucket · Talk to our CEO about your model
Go deeper: Content modeling best practices · API-first CMS · Image CDN · Cosmic MCP server