One Entity, Many Contexts: How I Structure Blocks and Content Types in Storyblok

René Kulik on 24.07.2026

Introduction

Every Storyblok project I have worked on eventually runs into the same question: I have a piece of content that shows up in more than one place, looking different each time. How do I model it so I do not end up copying the same data all over the space?

Take a blog. An article appears on its own page with the full story: the body, images, code blocks, everything. The same article also shows up in a “featured posts” carousel on the homepage, this time as a small card with just a title, an excerpt, and a cover image. It appears again in a “related posts” strip at the bottom of other articles, and once more in a category listing. Same article, four different presentations.

The tempting first move is to build an articleCard block, drop it into the carousel, and type the title, excerpt, and cover image straight into it. It works immediately, so it feels right. Then you feature the same article in a second place, and a third. Now the same article lives in several places: its page and every card. When you rename the article, you are hunting through the whole space to update every copy, and you will miss one. This is a plain DRY violation, and it is the villain of this post.

The deeper lesson underneath the blog example is this: an article is data, and how it is displayed is presentation. Once you separate the two, the same entity can wear different clothes in every context without ever being duplicated.

The naive approach and why it hurts

It is worth being precise about how you end up duplicating data, because the mistake is subtle. You rarely set out to copy an article. You reach for a block.

Blocks in Storyblok are inline content. When you place a nestable block, you are creating a fresh, self-contained copy of its fields right there in the parent story. An articleCard block with title, excerpt, and coverImage fields feels reusable, but every placement is a new copy with no link back to a shared source. Two carousels featuring “the same” article actually hold two independent snapshots that drift apart the moment one is edited.

That is the tell: a block is a copy, not a reference. The instant you need the same content in two places and want it to stay in sync, a block is the wrong tool.

The fix: model the entity as a Content Type

An article is not a piece of layout. It is an entity with an identity, and in Storyblok entities are Content Types, not blocks.

So I create an article Content Type. It is routable and lives under its own path, for example /blog/one-entity-many-contexts, because every article has a page anyway. It holds two kinds of fields:

The article is the page. There is no separate “article page” story pointing at the content, and no place where the body is duplicated. Everything an article owns lives in one story, in one place.

The rule I follow to decide what goes where:

The entity owns everything that is one-to-one with it. The context owns everything that varies by context.

The title, excerpt, cover image, and body are all one-to-one with the article, so they live on the article. The card layout varies by context (a featured carousel, a related-posts strip, a category listing all look different), so it is derived by each context from the article’s data, never stored on the article.

This pattern comes in two flavors, depending on whether the entity has a page of its own:

Either way the principle is the same: model the entity once, reference it everywhere. An article references its author. A carousel references its articles. Nothing is copied.

The schema

Here is the whole structure. Four components carry the example: the article Content Type, the author it references, the articleCarousel block, and the page Content Type that hosts the carousel.

Component Kind Field Field Type Purpose
article Content Type title Text Key figure
article Content Type excerpt Textarea Key figure
article Content Type coverImage Asset Key figure
article Content Type publishedAt Date/Time Key figure
article Content Type author Multi-Options (References), max 1 Points at an author story
article Content Type content Blocks Article body
author Content Type name, avatar, bio Text, Asset, Textarea Global, non-routable
articleCarousel Nestable block articles Multi-Options (References) Points at article stories
page Content Type body Blocks Hosts the carousel

The articleCarousel holds no article data at all. Its articles field is a Multi-Options reference that stores the UUIDs of the article stories it points at. There is a single source of truth for every article, and every context borrows from it.

Rendering the same entity in different contexts

With one entity in place, each context simply picks the fields it needs:

The author entity earns its keep the same way. It is rendered as a full byline with avatar and bio on the article page, and as a plain name in a compact “written by” line inside a card. One author story, two presentations, no duplication.

This is the payoff. When you add a fifth context tomorrow, you do not touch the article at all. You write a new presentation that reads the fields it cares about. The context decides which fields to render, and the entity never changes.

Resolving references without over-fetching

A reference field stores UUIDs, not the referenced content. To turn those UUIDs into real data you have to resolve them, and how you resolve them matters a lot for payload size.

The obvious way is resolve_relations. You pass the component and field you want resolved, in the form componentName.fieldName. On the article page, resolving the author is exactly right, because you render it and it is small:

import StoryblokClient from "storyblok-js-client";

const storyblok = new StoryblokClient({
  accessToken: process.env.STORYBLOK_TOKEN,
});

const { data } = await storyblok.get(
  "cdn/stories/blog/one-entity-many-contexts",
  {
    version: "published",
    resolve_relations: ["article.author"],
  },
);

Because author is a Multi-Options field capped at one, it resolves to an array with a single author object, so you read it as author[0] rather than as a bare object.

The same technique on a carousel is where it goes wrong. Resolving articleCarousel.articles inlines the entire referenced story, including the heavy content blocks. A carousel of ten articles pulls ten full article bodies just to render ten small cards, even though the cards need only three fields each. You are downloading the whole article ten times to show a thumbnail and an excerpt.

Storyblok also caps the number of relations it resolves per request, so leaning on resolve_relations for large lists is a trap on both payload and completeness.

The fix is to not resolve at all in list contexts. Instead, fetch exactly the articles you need, ordered, and strip the field you do not want:

const uuids = carousel.articles; // array of article UUIDs from the reference field

const { data } = await storyblok.get("cdn/stories", {
  version: "published",
  by_uuids_ordered: uuids.join(","),
  excluding_fields: "content",
});

by_uuids_ordered returns the articles in the order the editor arranged them in the carousel, which a plain UUID lookup would not guarantee. excluding_fields: 'content' tells the CDN to leave the heavy Blocks field out of the response entirely, server-side. What comes back is just the key figures: exactly what a card needs and nothing more.

So the division of labor is:

Co-locating the body and the key figures on a single entity costs you nothing at read time, as long as you resolve deliberately.

Conclusion

The blog is just a vehicle. The pattern is the point: when a piece of content shows up in more than one place, model it once as an entity, reference it everywhere, and let each context choose which of its fields to render. Reach for a Content Type when you need identity and reuse, a routable one when the entity has a page and a global folder one when it does not, and a block only for genuinely inline, one-off layout.

Separate the data from its presentation and duplication simply has nowhere to live. One entity, many contexts.