Reuse content instead of copying it#
A relation links one entry to another entry. Use it when the linked information has its own editor, lifecycle, and reuse across multiple records.
Article ── author relation ──> Author
Article ── relatedPosts ────> Article, Article, ArticleWhen an editor updates an Author bio, every Article that uses that Author can show the updated value. That is safer than copying the same name and biography into many entries.
Choose Relation or Multi Relation#
| Field type | Use it for | Example |
|---|---|---|
| Relation | Exactly one related entry | Article → Author |
| Multi Relation | An ordered list of related entries | Article → Related Articles |
Each relation field is configured to target exactly one content type or one component type. In normal content modeling, target a content type such as author, category, or product.
Build an Article → Author relation#
1. Create the target type first#
Create an author content type with fields such as:
| Field | Code | Type |
|---|---|---|
| Name | name | Text |
| Biography | bio | Rich Text |
| Photo | photo | Image |
Create and publish an Author entry such as “Jordan Lee.”
2. Add the relation to Article#
Open the article content type and add a field:
| Setting | Value |
|---|---|
| Name | Author |
| Code | author |
| Field type | Relation |
| Related content type | Author |
| Required | Choose based on whether every Article needs an author |
Save the model. In the Article editor, the Author field now uses a relation picker that shows Author entries for the selected website.
3. Select instead of copying#
For the Article “Hello, Contoprix,” choose Jordan Lee in the Author picker. The stored data is an entry reference; delivery expands it into usable related data.
What the frontend receives#
In the CMS, a relation is stored as an entry ID. In delivery, Contoprix expands the direct relation to a small related-entry object:
{
title: "Hello, Contoprix",
author: {
id: "author-entry-id",
contentTypeCode: "author",
slug: "jordan-lee",
data: {
name: "Jordan Lee",
bio: "Product writer and content strategist."
}
}
}Map it defensively because a relation can be optional or unavailable:
type AuthorRelation = {
id: string;
contentTypeCode: string;
slug?: string | null;
data: { name?: string };
};
type ArticleData = {
author?: AuthorRelation | null;
};
export function ArticleByline({ article }: { article: ArticleData }) {
const name = article.author?.data.name;
return name ? <p>By {name}</p> : null;
}The author field is under entry.data.author, not entry.author.
Relation delivery is intentionally shallow#
Contoprix expands direct relation fields in a delivered entry. It does not recursively expand every relation inside the related entry forever. This avoids unpredictable response sizes and cyclic data.
If an Author itself relates to another record, plan a separate request or use an appropriate API shape for that screen. Do not model a page that requires unbounded relation traversal.
Publish order matters#
Before publishing a source entry with a relation, Contoprix checks that the target entry has a publishable version. The simplest workflow is:
- Create and publish the target, such as an Author.
- Select it from the relation picker on the source, such as an Article.
- Preview the source.
- Publish the source.
If a previously published target is later unavailable, delivery may produce null for a single relation or an empty/missing item in a multi relation. Your frontend should always have an empty state.
Multi Relation example: related Articles#
Add this field to article:
| Setting | Value |
|---|---|
| Name | Related articles |
| Code | relatedArticles |
| Field type | Multi Relation |
| Related content type | Article |
Editors choose up to the number of entries your design can present. Delivery retains the relation order, so render it in the same order:
type RelatedArticle = {
id: string;
slug?: string | null;
data: { title?: string };
};
export function RelatedArticles({ items = [] }: { items?: RelatedArticle[] }) {
if (items.length === 0) return null;
return (
<aside>
<h2>Related articles</h2>
<ul>
{items.map((item) => (
<li key={item.id}>
<a href={`/blog/${item.slug ?? ""}`}>{item.data.title ?? "Read article"}</a>
</li>
))}
</ul>
</aside>
);
}Avoid common modeling mistakes#
| Avoid | Prefer |
|---|---|
| Copying an author name into every Article | Relation to Author |
| One enormous “related content” field with mixed record types | Separate focused relations or an intentional component/dynamic-zone model |
| Making a required relation when the UI can work without it | Optional relation plus a clear empty state |
| Deep chains such as Article → Author → Team → Office on every card | A small view model or a screen-specific request strategy |
| Deleting a target without checking references | Review inbound references in the CMS first |
Read Content Types for field-design basics and Content Entries for the publishing workflow around linked content.