# Content model

How content in a Source CMS [site](/start-here/glossary/#site) is structured, and how each structure surfaces in the [JSON:API](/start-here/glossary/#jsonapi). For querying instructions, see the [querying guide](/source-cms/content-api/guide/).

## Entity types and bundles

Every piece of content belongs to an [entity type](/start-here/glossary/#entity-type), and every entity type is divided into bundles (the bundle term varies by entity type). Machine names appear directly in endpoint paths and in the `type` member of every response (`{entity_type}--{bundle}`, for example `node--article`).

The model on one example bundle: scalar fields live under `attributes`; references to other entities live under `relationships`, one-to-one or one-to-many by the field's cardinality; media chains on to its file entity:

<ConceptDiagram name="content-model-er" height={480} />

| Entity type | Machine name | Bundle term | Endpoint pattern | Example |
|---|---|---|---|---|
| Content | `node` | [Content type](/start-here/glossary/#content-type-bundle) | `/api/node/{content_type}` | `/api/node/article` |
| Media | `media` | [Media type](/start-here/glossary/#media-type) | `/api/media/{media_type}` | `/api/media/image` |
| Taxonomy | `taxonomy_term` | [Vocabulary](/start-here/glossary/#vocabulary) | `/api/taxonomy/{vocabulary}` | `/api/taxonomy/tags` |
| Menu items | `menu_items` | Menu | `/api/menu_items/{menu}` | `/api/menu_items/main` |

One asymmetry: taxonomy term endpoints use the path segment `taxonomy`, not the entity type [machine name](/start-here/glossary/#machine-name); `/api/taxonomy_term/tags` is not a route. The `type` member in responses still reads `taxonomy_term--tags`. When in doubt, the API root index (`GET $DRUPAL_SITE_URL/api`) maps every resource type to its real path.

You can customize any bundle and create new ones, so the definitive list for your site is generated: check the API root (`GET $DRUPAL_SITE_URL/api`) or your site's [OpenAPI documentation](/start-here/glossary/#openapi-documentation) (see the [Content API reference](/source-cms/reference/content-api/#your-sites-generated-api-documentation)).

Used in: [query quickstart](/source-cms/content-api/quickstart/) (finding the machine name), [querying guide](/source-cms/content-api/guide/).

## CMS content vs. Pages

Source CMS has two fundamental kinds of content, and they behave differently in the API:

| | [CMS content](/start-here/glossary/#cms-content) | [Pages](/start-here/glossary/#pages-canvas) |
|---|---|---|
| What it is | Standard content entities (articles, events, custom content types) with a predefined field structure | Entries built visually in [Drupal Canvas](/start-here/glossary/#drupal-canvas) |
| Focus | Reusable, structured data | Visual presentation |
| Reusability | High (used in multiple places, accessed through the API) | Low (typically unique per page) |
| Management | Data-centric | Layout-centric |
| Content [workflows](/start-here/glossary/#workflow) | Apply | Do **not** apply |
| API access | Served over JSON:API as `node` entities with their fields as attributes/relationships; the primary way CMS content reaches other channels | Served as the `page` resource type at `GET /api/page` (no bundle segment; a fresh site's `/api/page` lists its Homepage and error pages) |

The distinguishing characteristic is the resource type: a Page appears in the API as `page`; CMS content appears as `node--{content_type}`. Because workflows govern CMS content only, workflow state never gates a Canvas page; the [writing content guide](/source-cms/content-api/writing-content/) covers the write-side consequences.

Used in: [writing content guide](/source-cms/content-api/writing-content/), [Canvas components section](/source-cms/canvas-components/).

## Where fields appear in a response

Each entry's [fields](/start-here/glossary/#field) appear under two keys, using their machine names verbatim. Note on naming: fields created through Source CMS's own tooling (the admin UI, the [MCP server](/start-here/glossary/#mcp-server)'s field tools) get unprefixed machine names (`image`, `subtitle`: verified), while the classic Drupal `field_` prefix (`field_image`) appears on types built with other tooling. Never assume either form, read it from a response:

- **`attributes`**: scalar and structured values stored on the entry itself.
- **`relationships`**: references to other entities (media, taxonomy terms, other content). A relationship contains type + id identifiers only; use `include` to embed the referenced entries (see the [query parameter reference](/source-cms/reference/query-parameters/#include-related-resources-in-one-request)).

## Field types → JSON:API response shapes

The field types available in Source CMS and the shape each takes in a response:

| Field type | Appears under | JSON shape | Notes |
|---|---|---|---|
| Plain text | `attributes` | `"string"` | Single line, 255-character limit. |
| Formatted text | `attributes` | `{ "value": "...", "format": "...", "processed": "<p>...</p>" }` | Render `processed` (sanitized HTML); `value` is raw editor input: don't render it directly. |
| Text (plain, long) | `attributes` | `"string"` | Multi-line, no formatting. |
| Number | `attributes` | `123` or `"12.50"` | Integers are JSON numbers; decimal values are returned as strings to preserve precision. |
| Date and time | `attributes` | `"2026-07-02T09:30:00+00:00"` | ISO 8601, stored and returned in UTC; convert to the viewer's timezone for display. Date-only fields return `"2026-07-02"`. |
| Boolean | `attributes` | `true` / `false` | |
| Link | `attributes` | `{ "uri": "https://...", "title": "...", "options": [] }` | `uri` may also be an internal scheme; `title` is the optional link text. |
| Email | `attributes` | `"person@example.com"` | Validated on input. |
| Selection list | `attributes` | `"option_key"` (or an array when multiple selection is enabled) | The stored key, not the display label. |
| Media | `relationships` | `{ "data": { "type": "media--image", "id": "<uuid>" } }` | References a media entity, which references its file via the `media_image` relationship (verified). To get a usable URL, `include` through to the file (e.g. `include=image.media_image`); the file's `uri.url` is a root-relative path: prefix it with `DRUPAL_SITE_URL`. For images, the relationship's `meta` carries `alt`, `title`, `width`, and `height` (verified); the file entity carries `filename`, `filemime`, and `filesize`. |
| Reference | `relationships` | `{ "data": { "type": "...", "id": "<uuid>" } }` | Points at content, media, taxonomy terms, or users, per the field's configuration. |

Fields configured to allow multiple values return arrays: a multi-value attribute is an array of the shapes above, and a multi-value relationship's `data` is an array of identifiers.

Used in: [querying guide](/source-cms/content-api/guide/), [direct-fetch guide](/source-cms/content-api/fetch-content/).

## Relationship shapes: one-to-one vs. one-to-many

Whether `data` is an object or an array is fixed by the field's cardinality:

```json
"relationships": {
  "image": {
    "data": { "type": "media--image", "id": "9a2f..." }
  },
  "tags": {
    "data": [
      { "type": "taxonomy_term--tags", "id": "1c7e..." },
      { "type": "taxonomy_term--tags", "id": "5b03..." }
    ]
  }
}
```

- **One-to-one** (single-value field): `data` is one identifier object, or `null` when empty.
- **One-to-many** (multi-value field): `data` is an array, `[]` when empty.

Identifiers alone are rarely enough: resolving them one request at a time is the N+1 pitfall. The [querying guide](/source-cms/content-api/guide/) shows the single-request `include` alternative.

Used in: [querying guide](/source-cms/content-api/guide/).

## Translated fields

Content translation produces per-language versions of an entry: the translatable fields differ per language while the entry keeps its identity, and every response carries the entry's language verbatim in its `langcode` attribute. A translation is requested with the `langCode` query parameter, provided by the [JSON:API Multilingual module](https://www.drupal.org/project/jsonapi_multilingual) that every Source CMS site ships. The parameter is strict, so a missing translation is a `404` naming the [langcodes](/start-here/glossary/#langcode) that do exist. Adding `includeFallback=1` opts the read into the site's language fallback chain. A langcode URL path prefix resolves but is not a reliable way to select the content language. For requesting a specific language and detecting fallback, see the [multilingual guide](/source-cms/content-api/multilingual/). Non-translatable fields carry the same value in every language; that is shared data, not a fallback signal.

Used in: [multilingual guide](/source-cms/content-api/multilingual/).

## Used in

- [Content API quickstart](/source-cms/content-api/quickstart/): machine name → endpoint mapping
- [Querying guide](/source-cms/content-api/guide/): field shapes, relationships, includes
- [Direct-fetch guide](/source-cms/content-api/fetch-content/): rendering field shapes in a frontend
- [Multilingual content guide](/source-cms/content-api/multilingual/): translated fields
- [Creating & updating content via the API](/source-cms/content-api/writing-content/): CMS content vs. Pages and workflow scope
