# Content Modeling Anti-Patterns

> Source: https://agilitycms.com/docs/developers/content-modeling-anti-patterns

Most content model problems don't show up on day one. They show up six months later, when the site gets a redesign, a second locale, a mobile app or an AI agent, and the model can't take it. This page lists the mistakes we see most often in Agility content models. Each rule names the mistake, the symptom that tells you you've made it, and the fix.

Use it as a review checklist before you build, and again before you launch.

## The rules at a glance

| # | Rule | The named mistake |
| --- | --- | --- |
| 1 | Model meaning, not layout | Modeling for one page layout |
| 2 | Give each model one job | The catch-all model |
| 3 | Put data in fields, not in rich text | Rich text as a database |
| 4 | Link shared content, don't copy it | Copy-paste content |
| 5 | Keep nesting shallow | Russian-doll nesting |
| 6 | Name fields for the API, not just the editor | Field names that break API casing |
| 7 | Use locales, not one list per language | One list per locale |
| 8 | Always pass `take` | Trusting the default page size |
| 9 | Pick the linked content UI for the list's future size | The checkbox list that grew |
| 10 | Offer choices, not free text, for values code depends on | Typed-in values |
| 11 | Leave companion fields alone | Deleting the hidden fields you don't recognize |
| 12 | Read a model before you save it through the API | The partial model save |

## 1. Model meaning, not layout

**The mistake: modeling for one page layout.** Models and fields are named after where things sit on today's design: `LeftColumnText`, `BlueBoxHeading`, `HomepageBanner3`.

**Symptom.** A redesign means remodeling and migrating content. The same content can't be reused on another page, in an app or in an email, because its structure only makes sense in one layout.

**Fix.** Name models and fields for what the content *is*: `Event`, `Speaker`, `Summary`, `StartDate`. Keep layout decisions in Components and in the zones of your Page Models. Where editors do need to choose a look, give them a Drop-down List whose stored values your code switches on (for example `light` and `dark`), not field names tied to a position. See [Content-first Approach](/docs/developers/content-first).

## 2. Give each model one job

**The mistake: the catch-all model.** One `GenericContent` model, or one mega-Component, with dozens of optional fields that cover every case.

**Symptom.** Editors can't tell which fields matter for the thing they're making. Hide/when formulas multiply until nobody can predict what the form shows. Every API response carries fields nobody uses, and every front-end component has to handle every combination.

**Fix.** Split by purpose: one model per kind of content, one Component per kind of block. Use [hide/when formulas](/docs/developers/dynamic-content-control-implementing-hide-when-formulas-in-your-headless-cms-content-model) for small variations inside a model, not to merge several models into one. If the real need is grouping blocks on a page (tabs, accordions), use a small marker Component instead of a giant one: see [Grouping Components into Tabs, Accordions, and Sections](/docs/developers/grouping-components-into-tabs).

## 3. Put data in fields, not in rich text

**The mistake: rich text as a database.** Prices, dates, specifications, addresses or FAQ entries typed into an HTML field.

**Symptom.** You can't sort events by date, filter products by size, or show the FAQ in an app, because the API returns the HTML field as one string of HTML. Changing the formatting means editing every item.

**Fix.** Give structured values their own fields: Date/Time for dates, Number or Decimal for quantities, Drop-down List for fixed choices, URL for links, and a nested list for repeating entries such as FAQ items. Then the API returns each value separately, and you can filter and sort on them (see [GraphQL & Rest API Filtering](/docs/developers/graphql-operators)). Keep the HTML field for genuinely free-form prose. See [Field Types and What the APIs Return](/docs/developers/field-types-api-reference) for what each field type returns.

## 4. Link shared content, don't copy it

**The mistake: copy-paste content.** The same author bio, office address, disclaimer or call to action is typed into every item or Component that shows it.

**Symptom.** One change means finding and editing every copy, and some copies are always missed.

**Fix.** Put the shared content in its own content list and point at it with a Linked Content field (for one item, a Dropdown List render type). When the shared item changes, every place that links to it shows the change. See [Components with Content that is shared across your site](/docs/editors/components-with-content-that-is-shared-across-your-site) and [Nest, Link or Share?](/docs/developers/linked-content-field-types)

## 5. Keep nesting shallow

**The mistake: Russian-doll nesting.** Nested lists inside nested lists inside nested lists, because the design has sections, inside cards, inside tabs.

**Symptom.** Editors click through several levels to change one line. In the Content Fetch API, linked content is expanded only as deep as `ContentLinkDepth`, which defaults to `1` for items and `2` for pages (lists allow at most `5`), and each level makes responses larger. In the Management API, the `publish-cascade` routes publish an item's nested content **one level deep** (see [Batches and the Batch API](/docs/javascript/management-sdk/batches)), so automated publishing can miss deeper levels.

**Fix.** Aim for one level of nesting. Flatten where you can: a card's fields can usually live on the card item itself. When a Component needs a child list, fetch that list separately by its reference name rather than relying on deep expansion. If you need deeper structure, check how it publishes and how much it returns before you commit to it.

## 6. Name fields for the API, not just the editor

**The mistake: field names that break API casing.** Field names that start with an acronym or that you plan to rename later.

**Symptom.** Both the Content Fetch API and GraphQL return fields keyed by the field name with the **first letter lowercased**. A field named `Title` comes back as `title`, and a field named `URL` comes back as `uRL`. Code that expects `url` gets nothing. Renaming a field later breaks every query and type that uses the old name.

**Fix.** Use clear PascalCase words, and avoid leading acronyms (`LinkUrl`, not `URL`). Decide names before content is entered, and treat them as part of your API. Generate or write your TypeScript types with the lowercase-first names. Also note that a Text field is limited to 128 characters, and the [Fields](/docs/developers/fields) article ties that limit to any field named `title`. See [Field Types and What the APIs Return](/docs/developers/field-types-api-reference#how-field-names-appear-in-responses).

## 7. Use locales, not one list per language

**The mistake: one list per locale.** `BlogPostsEN`, `BlogPostsFR` and `BlogPostsDE` as separate lists, or separate models per language.

**Symptom.** Translations aren't connected, so a language switcher can't find the matching item. The models drift apart as someone adds a field to one and forgets the others.

**Fix.** Use one list and add locales. When you copy an item into another locale, the copies share the same `contentID`, and each locale holds its own field values. Mark fields that must never differ (codes, sort orders) as **Constant across all languages**. See [Choosing a Localization Strategy](/docs/developers/choosing-a-localization-strategy).

## 8. Always pass `take`

**The mistake: trusting the default page size.** Code that requests a list without saying how many items it wants.

**Symptom.** Lists silently stop growing. The Content Fetch API returns **10** items by default, and GraphQL container lists return **50**. Everything works in development with a handful of items, then breaks in production.

**Fix.** Pass `take` on every list request (the maximum is 250), page with `skip` when a list can be longer, and pass `take` on nested GraphQL lists too.

## 9. Pick the linked content UI for the list's future size

**The mistake: the checkbox list that grew.** A Checkbox List render type pointing at a list that starts with ten items and ends up with hundreds.

**Symptom.** The editor form becomes a wall of checkboxes.

**Fix.** Use a Checkbox List only for short lists that will stay short. Use a Search List Box for anything that can grow, such as tags, products or people. A Checkbox List can be converted to a Search List Box later. See [Nest, Link or Share?](/docs/developers/linked-content-field-types)

## 10. Offer choices, not free text, for values code depends on

**The mistake: typed-in values.** Editors type a theme name, an icon name or a colour into a Text field, and the front end matches on the string.

**Symptom.** `Dark`, `dark` and `dark ` all exist in production, and two of them render wrong.

**Fix.** Use a Drop-down List: the API returns the selected choice's **value**, so your code switches on values you control while editors see readable labels. For icons and colours, the [Picker Fields](/docs/apps/picker-fields) app adds picker fields that store a plain text value. For values that need a pattern rather than a fixed list, add [regular expression validation](/docs/developers/advanced-field-validation-with-regular-expressions).

## 11. Leave companion fields alone

**The mistake: deleting the hidden fields you don't recognize.** Someone tidies a model and removes `Category_ValueField` and `Category_TextField` because nobody remembers adding them.

**Symptom.** Linked content selections stop saving, and filters on the value field return nothing.

**Fix.** Dropdown, checkbox and search list box linked fields store the selected IDs and display text in hidden companion fields. The [Fields](/docs/developers/fields) article is explicit: don't delete them or disable them. Name them consistently (`{Field}_ValueField`, `{Field}_TextField`) so their purpose is obvious.

## 12. Read a model before you save it through the API

**The mistake: the partial model save.** A script or AI agent saves a Content Model or Component Model with only the fields it meant to change.

**Symptom.** Fields disappear, and so does their content. When you save a model through the Management API or the MCP server, the field list you send replaces the stored one, and properties you leave undefined are treated as empty. The MCP server refuses a save that would drop fields unless you explicitly allow it, and also refuses to convert a model between a Content Model and a Component, because conversion orphans nested content.

**Fix.** Read the model first (`get_content_model_details` or `get_component_model_details`), change only what you mean to change, and send everything back. Read it again after saving. See [MCP and Agility Gotchas to Know Before You Build](/docs/developers/vibe-coding-gotchas).

## Before you build: a quick review

- Could this model survive a redesign without being renamed?
- Does every model have one clear job you can state in a sentence?
- Is every value you'll filter, sort or reuse in its own field?
- Is anything typed in more than one place?
- Is anything nested more than one level deep, and do you know how it publishes?
- Do field names read well in lowercase-first JSON?
- Is there exactly one list per kind of content, whatever the number of locales?
- Does every list request pass `take`?

## Related

- [Nest, Link or Share? Choosing Linked Content Field Types](/docs/developers/linked-content-field-types)
- [Designing a Taxonomy That Scales](/docs/developers/designing-a-taxonomy)
- [Map Your Design System to Component Models](/docs/developers/design-system-to-component-models)
- [Make Your Content Model Agent-Friendly](/docs/developers/agent-friendly-content-models)
- [Content Modeling Strategy](/docs/training-guide/architect-content-strategy)
