Dyrected
Model ContentFields

Blocks

Flexible content built from a controlled set of typed block variants.

The blocks field lets editors build content from a controlled set of block types, each with its own fields. It is how you model flexible page layouts while keeping the frontend on known structures: each row records its blockType plus that block's fields. Use it for landing pages, marketing pages, and structured articles.

defineBlocksField({
  name: "sections",
  label: "Sections",
  blocks: [
    defineBlock({
      slug: "hero",
      labels: { singular: "Hero", plural: "Heroes" },
      fields: [defineTextField({ name: "heading", label: "Heading" })],
    }),
    defineBlock({
      slug: "richText",
      labels: { singular: "Rich text", plural: "Rich text" },
      fields: [defineRichTextField({ name: "body", label: "Body" })],
    }),
  ],
});

Each row stores the chosen block's slug as blockType, so your frontend can map each blockType to a component and render the list in order — the Blocks component does that mapping for you. Keep block slug values stable once content exists — renaming a slug orphans the rows already saved under the old name — and have your renderer fall back gracefully when it meets a blockType it does not recognize.

Reuse blocks across fields

This is the right home for block references because they change how a blocks field is authored, not what blocks are at a product level.

Use block references when the same block types should be available in more than one blocks field and you do not want to repeat the full block definitions everywhere.

The mental model is:

  • define reusable blocks once at the root config
  • point each blocks field at those shared blocks by slug
  • let Dyrected resolve the real block definitions at runtime for the Admin, API schema, and type generation

That keeps your config easier to read and gives you one place to update a shared block.

Inline blocks vs blockReferences

You now have two ways to configure a blocks field:

  • inline blocks when the field owns its block list and that list is local to the field
  • blockReferences when the block list should be shared across multiple fields

If only one field uses a block, inline blocks is still perfectly fine.

If two or more fields should expose the same blocks, blockReferences is usually the cleaner choice.

Define the shared block registry

Start by registering reusable blocks once in defineConfig({ blocks: [...] }).

const HeroBlock = defineBlock({
  slug: "hero",
  labels: { singular: "Hero", plural: "Heroes" },
  fields: [defineTextField({ name: "heading", label: "Heading" })],
});

const RichTextBlock = defineBlock({
  slug: "richText",
  labels: { singular: "Rich text", plural: "Rich text" },
  fields: [defineRichTextField({ name: "body", label: "Body" })],
});

export default defineConfig({
  blocks: [HeroBlock, RichTextBlock],
  collections: [
    defineCollection({
      slug: "pages",
      fields: [
        defineBlocksField({
          name: "layout",
          label: "Layout",
          blockReferences: ["hero"],
        }),
      ],
    }),
  ],
  globals: [],
});

Reference those blocks from fields

Once a block is in the shared registry, any blocks field can reference it by slug:

defineCollection({
  slug: "pages",
  fields: [
    defineBlocksField({
      name: "layout",
      label: "Layout",
      blockReferences: ["hero", "richText"],
    }),
  ],
});

You can reuse the same referenced blocks in another collection or global too:

defineGlobal({
  slug: "homepage",
  fields: [
    defineBlocksField({
      name: "sections",
      label: "Sections",
      blockReferences: ["hero", "richText"],
    }),
  ],
});

What Dyrected does with references

blockReferences is an authoring shortcut, not a different saved data shape.

Editors still create ordinary block rows, and the stored content still looks the same:

  • each row has a blockType
  • each row stores that block's field values
  • your frontend still renders by blockType the same way it already does

The difference is only how the schema is declared and shared.

Rules and limits

Keep these rules in mind:

  • block slugs in the root blocks registry must be unique
  • every slug in blockReferences must exist in that root registry
  • a blocks field should use either inline blocks or blockReferences, not both
  • blockReferences controls which block definitions a field can use; it does not change how existing content rows are stored

If a block is truly one-off and tied to one field, inline blocks is still simpler. Reach for references when reuse is real, not hypothetical.

Presentation variants

A single block can offer more than one layout through variants. Every variant shares the same fields; only the rendered presentation differs. The selected variant is stored under a reserved variant key and passed to your component, so editors can switch a testimonial between a card and a quote without losing what they wrote.

defineBlock({
  slug: "testimonial",
  labels: { singular: "Testimonial", plural: "Testimonials" },
  variants: [
    { slug: "card", label: "Card" },
    { slug: "quote", label: "Quote" },
  ],
  fields: [defineTextField({ name: "quote", label: "Quote" })],
});

The first variant is the default. Like block slugs, keep variant slugs stable once content is saved.

Showing a field only for some variants

Sometimes a field only makes sense in one layout — an avatar photo on a testimonial card, but not on a plain pull quote. Because the selected variant lives on the block row under the reserved variant key, any sibling field can read it in an admin.condition:

defineBlock({
  slug: "testimonial",
  labels: { singular: "Testimonial", plural: "Testimonials" },
  variants: [
    { slug: "card", label: "Card" },
    { slug: "quote", label: "Quote" },
  ],
  fields: [
    defineTextField({ name: "quote", label: "Quote" }),
    defineImageField({
      name: "avatar",
      label: "Avatar",
      relationTo: "media",
      admin: { condition: "variant == 'card'" },
    }),
  ],
});

Now the avatar picker appears only while the card variant is selected. The condition is a Jexl expression evaluated against the block row's own values, so variant always reflects the row's current selection — blocks added in the Admin start with the first variant already set. You can pass a function instead of a string if you prefer: condition: (data, siblingData) => siblingData.variant === 'card'.

Dyrected also validates declarative admin.condition expressions early. If a condition uses unsupported context or invalid syntax, Dyrected points to the exact config path so you can fix it before sync or runtime.

Hiding a field this way only affects the Admin form, not what is stored. If an editor fills in the avatar, switches to the quote variant, and switches back, the avatar is still there — consistent with the rule that changing variant never loses content.

Block rows written outside the Admin — seed scripts, direct API writes — may omit the variant key. That's fine: when the edit form loads, Dyrected fills in the first variant for any row that's missing one, so conditions like the example above see a real value either way.

Generated reference

The contract below is generated from the public @dyrected/core exports by @dyrected/knowledge, so it stays in sync with the package. See Fields for the shared field options every type accepts.

Block

Exported interface from @dyrected/core.

export interface Block {
  /** Stable identifier stored in each block row as `blockType`. */
  slug: string;
  /** Human-readable labels shown in the Admin block picker. */
  labels?: {
    /** Singular label, for example `Hero`. */
    singular: string;
    /** Plural label, for example `Heroes`. */
    plural: string;
  };
  /**
   * Lucide icon name shown on the block card and in the block library.
   * Falls back to a generic layout icon when omitted.
   */
  icon?: AdminIconName;
  /** Short one-line summary shown under the block name (block card subtitle). */
  description?: string;
  /**
   * Presentation variants for this block. All variants share the same `fields`;
   * only the rendered layout differs. The chosen variant is stored on each block
   * row under the reserved `variant` key and passed to the render component as a
   * `variant` prop. Switching variant preserves the author's content.
   */
  variants?: BlockVariant[];
  /** Fields that make up this block's payload. */
  fields: Field[];
}
OptionDescription
slug (required)Stable identifier stored in each block row as `blockType`.
labels (optional)Human-readable labels shown in the Admin block picker.
icon (optional)Lucide icon name shown on the block card and in the block library. Falls back to a generic layout icon when omitted.
description (optional)Short one-line summary shown under the block name (block card subtitle).
variants (optional)Presentation variants for this block. All variants share the same `fields`; only the rendered layout differs. The chosen variant is stored on each block row under the reserved `variant` key and passed to the render component as a `variant` prop. Switching variant preserves the author's content.
fields (required)Fields that make up this block's payload.

BlocksField

Flexible content built from a controlled set of typed blocks, stored as an ordered array where each row records its blockType.

export type BlocksField = TypedField<"blocks", unknown>;

BlockVariant

Exported interface from @dyrected/core.

export interface BlockVariant {
  /** Stable identifier stored on the block row as `variant`. */
  slug: string;
  /** Human-readable label shown in the variant switcher. Defaults to `slug`. */
  label?: string;
  /** Lucide icon name shown beside the variant label. */
  icon?: AdminIconName;
  /** Short one-line summary of what this variant looks like. */
  description?: string;
}
OptionDescription
slug (required)Stable identifier stored on the block row as `variant`.
label (optional)Human-readable label shown in the variant switcher. Defaults to `slug`.
icon (optional)Lucide icon name shown beside the variant label.
description (optional)Short one-line summary of what this variant looks like.

On this page

Dyrected| Cloud

Get your backend ready in minutes

Use a managed database, storage, APIs, and admin dashboard without setting up the infrastructure yourself.

Set Up My Backend