Blocks and Composition
For deeper layering than two inheritance hops, use reference / blocks fields. The LandingPage type extends ComposablePage (one hop) and has a blocks field that lists its layout components — heroBlock, ctaBlock, contentBlock, cardGridBlock. Any block can in turn reference other entries.
How blocks differ from references
Both store entry IDs. The difference is intent:
reference— a one-or-many pointer. Single field by default; multi whenisMultiple: true. Used for "this article belongs to this category".blocks— an ordered list of embedded entries. Always plural. Used for page composition where order matters (block A above block B).
blocks always renders inline in the consuming page; reference may not be rendered at all (it's a relationship, not a layout decision).
Recursion limits
GraphQL caps reference traversal at 3 hops (MAX_REFERENCE_DEPTH). REST's ?include= parameter accepts 0..3 with the same cap. Cycles return { _ref, _type } placeholders so the response is never infinite.
Allowed types
Both fields constrain target types via allowedTypeIds. For reference, empty = any type allowed. For blocks it differs: allowedTypeIds must be non-empty (empty → 422 blocks_require_types). Practical example for a LandingPage.blocks field:
{
"apiName": "blocks",
"fieldType": "blocks",
"allowedTypeIds": ["heroBlock", "ctaBlock", "contentBlock", "cardGridBlock"]
}
The block picker in the entry editor only shows entries whose content type matches the allowed list, and the new-block dropdown only shows the matching types.
Building a block
Block content types are normal content types — usually isRoutable: false, isPublishable: true. They live in _Data (or any folder you pick) under each site.
A minimal CTA block:
{
"apiName": "ctaBlock",
"name": "CTA Block",
"isRoutable": false,
"isPublishable": true,
"fields": [
{ "apiName": "headline", "name": "Headline", "fieldType": "text", "isRequired": true, "isLocalizable": true },
{ "apiName": "buttonText", "name": "Button text", "fieldType": "text", "isLocalizable": true },
{ "apiName": "buttonUrl", "name": "Button URL", "fieldType": "link" }
]
}
Rendering blocks
In a frontend, walk the blocks field and dispatch on each entry's content type:
import { KriosClient } from "@krios/sdk";
export async function LandingPage({ entryId }: { entryId: string }) {
const krios = makeClient();
const page = await krios.getEntry(entryId, { include: 1 });
const blocks = (page.fields.blocks as Array<{ id: string; contentType: string }>) ?? [];
return (
<main>
{blocks.map((b) => {
switch (b.contentType) {
case "heroBlock": return <Hero key={b.id} entryId={b.id} />;
case "ctaBlock": return <Cta key={b.id} entryId={b.id} />;
default: return null;
}
})}
</main>
);
}
include: 1 returns the block entries inlined; include: 0 returns just { _ref, _type } placeholders so you can fetch them lazily.
When to inherit, when to compose
Inheritance is great for shared field patterns (every page has a title, slug, SEO fields). It's not great for layout — three-deep inheritance trees are a smell.
For layout, compose blocks. A LandingPage doesn't extend ComposablePage ten levels deep — it composes blocks of various types. Renaming a block doesn't require a content-type migration.