Skip to main content

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 when isMultiple: 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.