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
- Create your block file in
packages/keystatic-config/src/blocks/ - Register it in
blocks/index.ts - 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.