← Back to Blog
development

Customizing Block Fields for Design Quirks

Your design has a hero heading with an accent word in a different color. Or a split layout where the left column is text and the right is an image. Default block fields won't handle these — you need custom blocks.

Splitting Heading Fields

Instead of a single heading: fields.text({ label: 'Heading' }), split it:

export const heroBlock = {
  label: 'Hero',
  schema: fields.object({
    headingLine1: fields.text({ label: 'Heading Line 1' }),
    headingAccent: fields.text({ label: 'Heading Accent Word', description: 'Rendered in brand color' }),
    headingLine2: fields.text({ label: 'Heading Line 2' }),
    subheading: fields.text({ label: 'Subheading', multiline: true }),
    ctaLabel: fields.text({ label: 'CTA Button Label' }),
    ctaUrl: fields.url({ label: 'CTA Button URL' }),
    alignment: fields.select({
      label: 'Alignment',
      options: [
        { value: 'left', label: 'Left' },
        { value: 'center', label: 'Center' },
      ],
      defaultValue: 'center',
    }),
  }),
};

Adding a New Block

  1. Create your block file in packages/keystatic-config/src/blocks/
  2. Register it in blocks/index.ts
  3. Create an Astro renderer in apps/www/src/components/sections/

The block renderer receives block.value as props, so your component can destructure the split fields:

---
const { headingLine1, headingAccent, headingLine2 } = Astro.props;
---
<h2>{headingLine1} <span class="text-brand">{headingAccent}</span> {headingLine2}</h2>

Reusing Blocks Across Pages

Blocks are registered in blocks/index.ts and exposed as pageBlocks. Any singleton or collection that uses pageBlocks gets all available blocks. The BlockRenderer component handles dispatch automatically.

Removing Blocks

Simply remove the block definition from blocks/index.ts. Existing content with that block type will silently not render — the BlockRenderer returns null for unknown discriminants.