Skip to content
Baack

Content modelling without the layout debt: The Baack content matrix modelling best practices

Browse the docs
Developer

If you have spent any time building digital products with modern Headless CMS platforms, you have likely run into the nested schema trap.

You start with a simple page layout, build a component hierarchy in your CMS schema, and before you know it, your API responses are filled with deeply nested arrays of "slices," "blocks," and "wrappers." Content becomes tightly coupled to visual presentation, rendering your content model rigid, fragile, and difficult to query across multiple channels.

Baack takes a fundamentally different approach to content modelling. Instead of forcing your content into arbitrary nested trees that mirror front-end page structures, Baack decouples content from presentation using a Sparse Content Matrix.

Concepts

Content matrix of items within an entity

  • Entity: represents a page like set of content e.g. a blog post or in the app context an Android main view fragment or iOS app UI View Controller. The content entity groups together all of the content needed for the presentation of a screen or view. You can use multiple entities in the same rendering context with one entity per content fragment. In frameworks like Astro you may decide to use an entity per island for example to hold the content for the navigation or menu for a separate component.
    • Entity names are addressed with a folder / directory name and file name e.g. /home
    • Entities have a locale associated with them e.g. en-GB. You may have multiple entities with the name /home with different translation locales to allow delivering different localised experiences dependent on the language preference of the user.
    • Entities have an variant to allow personalisation and experimentation. The default variant is "" (none) which is returned when no alternative variant is selected or available.
  • Item: Entities contain a collection of items of different types which are strongly typed. The most primitive content type is text and each content entity typically has a large number of text items. Items are typically accessed in an entity context via the collections of items returned when reading the entity.
    • Items are named as the combination of their name field and the sortOrder field to allow them to be accessed in a matrix.
    • Different types of content items are returned in the strongly typed collection fields of an entity.

Here is how the Baack content matrix approach works and why flattening your content model unlocks better performance, flexibility, and developer experience.

Page/entity-level content, not arbitrarily deep component trees can make it seem like validation of content shapes in a collection could be a risk, that's why the console warns content owners about differences in shapes of content across languages, variants or collections. These warnings will be enhanced in future versions of the platform with custom validation rules for content owners.

If truly nested content is required then entities do support arbitrary JSON fields as a fallback.

A note on entity relationships and validation

The matrix model is optimised for what lives inside an entity, flat, strongly typed, sequenced content. It is deliberately not trying to be a relational database while avoiding the need to define schema and manage migrations when the schema changes. JSON support does allow arbitrary data structures as a fallback.

For content reuse use references (e.g. an author bio link used by multiple posts, a product referenced from multiple pages), the recommended pattern is to model the shared content as its own entity and reference it from the consuming entity, rather than duplicating or nesting it inline. This keeps the matrix flat while still giving you a single source of truth. Experiences are then built by composing entities together at read time, not flattening everything into one giant record or nesting content arbitrarily leading to duplication.

Similarly, the matrix intentionally pushes field-level business rules like required fields, length limits etc out of the schema layer and into separate rules, your front-end or edge logic. The admin console does offer warnings when an entity in a group is missing item fields missing in its peers. We will also offer this capability via the API via warnings (coming soon). This is a trade off worth naming plainly: you gain a content model that never needs a migration when your validation rules change, at the cost of validation living at a different layer rather than mixed in the content. For teams with lightweight editorial governance this is a net win. For teams that need strict, centrally enforced content contracts, pairing the matrix with a thin validation layer with rules driven by webhooks supports that guarantee without reintroducing nested schema debt.

1. The core concept: The sparse naming matrix

At its heart, every Baack Entity represents a page-like collection of items. Rather than constructing a complex object tree, content within an entity is modelled using a sparse two-dimensional matrix.

Each content item within an entity is defined primarily by two key properties:

  • Name (string): An arbitrary string serving as the key for the item.
  • Sort Order (integer): An integer value defining the sequence or row position.

A content text item may look something like:

{
  "urn": "...",
  "url": "/n/v1/textitem/..."
  "name": "hero_title",
  "sortOrder": 0,
  "value": "Build Better Digital Experiences"
}

Best practice: Use web-friendly identifiers

While name can technically be any arbitrary string, the recommended best practice is to use identifiers compatible with HTML IDs or CSS class names (e.g., kebab-case or snake_case like hero-title, pricing-col-pro, author-bio).

By adhering to browser-native naming conventions, consuming content on the front end becomes effortless. Your content keys map directly to DOM identifiers, design system token names, or component prop targets without complex transformation pipelines.

Why flat beats nested every time

  • The anti-pattern: Most headless CMSs encode the visual layout directly into the schema (Page -> Section -> Grid -> Column -> Component). When design systems update or new platforms (like mobile apps, digital signage, or AI agents) consume the API, these layout-encoded schema become dead weight.
  • The Baack content matrix approach:
    • Zero schema pollution: The API returns flat, predictable data structures regardless of layout complexity.
    • True Omni-channel readiness: Content remains pure data. Front-end frameworks decide how to lay out items based on name and sortOrder, leaving the content schema clean. This is critical for supporting content across different types of devices where the nesting of the schema may not be consistent.
    • Effortless schema maintenance: Adding a new field or content block doesn't require rewriting complex nested relational schema or re-nesting migrations.
    • Easier for AI agents: Matrix tabular data is much easier for AI agents to consume. By breaking the content down into smaller chunks tasks like translation or refinement become much easier as the agent doesn't have to understand the semantics of nesting of the content encoded into the schema.

2. Practical example 1: Modelling a standard blog post

To see this in action, let's look at how a typical blog post entity is structured using the Baack matrix.

Instead of nesting title metadata inside a header wrapper and body content inside a rich_text_block container, the blog post entity simply houses strongly typed items side-by-side:

Name Type Sort Order Content / Purpose
meta-title text 0 SEO page title
meta-description text 0 SEO Meta description
banner-image image 0 Featured header image asset
article-body markdown 0 Full long-form post content

Because items are strongly typed, your front-end web app or mobile SDK knows precisely how to parse and render each piece of data without needing to traverse a deep component tree. This also helps simplify the components rendering the content.

3. Practical example 2: Unlocking 2D layouts (pricing tables)

Because the matrix naturally supports two dimensions (name * sortOrder), rendering tabular or multi-column data becomes trivial without needing special "table" field plugins or nested arrays.

Consider a Pricing Table:

  • Use the name field to represent the Column (e.g., SKU, plan, key-feature, price).
  • Use the sortOrder field to represent the Row Number (e.g., 0 for the free plan, 1 for the pro plan).

Built-in strongly typed field powers

Baack entities support specialised, strongly typed primitives out of the box. For instance, pricing models benefit directly from the native money item:

  • Supports all currency codes: Standard ISO 4217 support (USD, EUR, GBP, JPY, etc.).
  • Built-in formatting outputs: Automatically returns sanitised values and localised string representations for display.

The entity and it's items may look like:

{
  "urn": "...",
  "name": "/pricing",
  "texts": [
    { "name": "SKU", "sortOrder": 0, "value": "FT1" },
    { "name": "plan", "sortOrder": 0, "value": "Free tier" },
    { "name": "SKU", "sortOrder": 1, "value": "PP1" },
    { "name": "plan", "sortOrder": 1, "value": "Professional plan" },
  ],
  "booleans": [
    {"name":"key-feature","sortOrder":0,"value":false},
    {"name":"key-feature","sortOrder":1,"value":true}
  ],
  "money":[
    { "name": "price", "sortOrder": 0,"currencyCode" : "GBP","unitValue" : 0,"formattedValue" : "0.00","currencySymbol" : "£"},
    { "name": "price", "sortOrder": 1,"currencyCode" : "GBP","unitValue" : 2500,"formattedValue" : "25.00","currencySymbol" : "£"},
  ]
}

Rendering this on the front end simply requires grouping by name or sorting by sortOrder, no complex data flattening logic required. The API also provides helpful currency formatting and symbol values that make building schema.org tagging a breeze.

4. Entity organisation: file-system routing style

Content modelling isn't just about what lives inside an entity; it's also about how entities relate to each other across your site hierarchy.

Baack entities utilise file-name/path-like naming conventions (e.g., /blog/2026/content-modelling-best-practices or /products/pricing).

This directory-based hierarchy provides two major benefits:

  1. Native directory structure: Your content repository naturally maps to URL routes or file-system routing patterns popular in frameworks like Next.js, Remix, or SvelteKit. Using the client you can resolve the path for the entity right from the URL.
  2. Contextual scoping: Querying all articles in a specific folder (e.g., /blog/*) is built directly into the core routing paradigm. This keeps content organised and easy to find.

Summary checklist for content modelling best practices on Baack

  1. Keep It Flat: Resist the urge to nest. Use name and sortOrder to group and sequence content items.
  2. Use Browser-Compatible Names: Name items using HTML ID/class friendly string keys (kebab-case or snake_case).
  3. Leverage the 2D Matrix for Arrays & Grids: Map matrix coordinates directly to UI rows and columns for components like pricing tables or feature grids.
  4. Utilise Strongly Typed Items: Take advantage of native primitives like money, image, and markdown for cleaner front-end consumption and built-in formatting.
  5. Organise via Path Conventions: Treat entity names like system file paths for straightforward routing and folder-based content organisation.