Build pages from blocks

7 min read

Most pages are a stack of sections: a hero, some features, a quote, a call to action. A Blocks field keeps those sections inside the page entry itself, each one typed, validated and delivered in order. Editors get collapsible cards to add, reorder and remove; your front end gets a predictable list it can map to components.

Blocks or references?

Define the block types

In the model editor, add a field of type Blocks to your page type and describe its block types in the box. One block type per line, its fields indented beneath as name: type. Add ! to make a field required on publish, and put select options in brackets.

block types
hero: Hero
  heading: symbol!
  body: rich_text
  image: media
features: Feature list
  title: symbol!
  items: text
quote: Quote
  text: text!
  author: symbol
  tone: select(light, dark)

Block fields can be symbol, text, rich_text, number, boolean, date, select, media, location or json. Reference fields inside blocks are not supported yet - if a section needs to point at another entry, use a References field on the page for now.

Set Min and Max on the field to limit how many blocks a page can have, for example exactly one hero.

Or define them over the API

The same model as a field definition, for scripts and agents. config.blocks maps each block type to its label and fields:

add a blocks field
curl -X POST https://mojimoto.com/api/v1/your-project/manage/content-types/page/fields \
  -H "Authorization: Bearer mjmt_your_write_token" \
  -H "Content-Type: application/json" \
  -d '{
    "field": "sections",
    "definition": {
      "type": "blocks",
      "config": {
        "label": "Sections",
        "blocks": {
          "hero": {
            "label": "Hero",
            "fields": {
              "heading": { "type": "symbol", "config": { "label": "Heading", "required": true } },
              "image": { "type": "media", "config": { "label": "Image" } }
            }
          }
        }
      }
    }
  }'

Edit them

In the entry editor a Blocks field shows one card per block. Add a block from the menu of types you defined, drag cards to reorder, collapse the ones you are not working on, and remove any you no longer need. Media and rich text work inside blocks exactly as they do anywhere else.

When something is missing, publish tells you exactly where: "Sections, block 2 (Hero): Heading is required." Over the API the same error comes back keyed by position, as sections.1.data.heading.

Write them

A blocks value is an ordered list of { type, data }. Each block is delivered with an id your front end can use as a key. Blocks added in the editor keep theirs for good; when writing over the API, send your own id to keep it stable across edits.

document data
{
  "title": "Home",
  "sections": [
    { "type": "hero", "data": { "heading": "Welcome", "image": { "asset_id": 12, "fit": "cover" } } },
    { "type": "quote", "data": { "text": "It just works.", "author": "Sam" } }
  ]
}

Render them

The delivery API returns each block as { id, type, data }, with media expanded to full objects and rich text in its delivered shape. In React, SliceZone maps each block type to a component:

page.tsx
import { SliceZone, type BlockComponents } from '@mojimoto/react';

const components: BlockComponents = {
  hero: ({ slice }) => <h1>{slice.data.heading as string}</h1>,
  quote: ({ slice }) => <blockquote>{slice.data.text as string}</blockquote>,
};

export function Page({ page }) {
  return <SliceZone slices={page.data.sections} components={components} />;
}

Generated TypeScript types include a type for each block, so slice.data is fully typed once you run npx @mojimoto/cli types.

Next

Build it for real

Spin up a project and try this yourself - your first project is free.

© 2026 Your Ikigai Ltd · Registered in England & Wales, no. 15274889 文 · モジモト