# Workflow: Create a Custom Block

Create a site-specific block with custom fields and rendering when system blocks don't meet your needs.

## When to Use Custom Blocks

**Use custom blocks when:**
- You need a completely unique block structure
- The functionality doesn't map to any system block
- You want site-specific branding or interactions

**Use block extensions instead when:**
- A system block does most of what you need
- You just need layout variants or extra options
- See `block-extensions.md` for that workflow

## Prerequisites

- `riverbank.config.ts` exists
- Block overrides file set up (e.g., `src/lib/block-overrides.tsx`)
- Understanding of the content structure you need

## Steps

### Step 1: Define Block in Config

Add your block to the `customBlocks` array in `riverbank.config.ts`:

```typescript
import { defineConfig } from '@riverbankcms/sdk/config';

export default defineConfig({
  siteId: '...',

  customBlocks: [
    {
      id: 'custom.our-flow',
      title: 'Our Flow',
      description: 'Interactive process steps with hover reveal',
      category: 'content',
      icon: 'Workflow',
      tags: ['process', 'steps', 'flow'],
      fields: [
        {
          id: 'title',
          type: 'text',
          label: 'Title',
          required: true,
        },
        {
          id: 'introText',
          type: 'text',
          label: 'Intro Text',
          multiline: true,
        },
        {
          id: 'steps',
          type: 'repeater',
          label: 'Steps',
          itemLabel: 'Step',
          minItems: 1,
          maxItems: 6,
          schema: {
            fields: [
              { id: 'title', type: 'text', label: 'Step Title', required: true },
              { id: 'description', type: 'richText', label: 'Description', format: 'html' },
              { id: 'icon', type: 'media', label: 'Icon', mediaKinds: ['image'] },
            ],
          },
        },
      ],
    },
  ],
});
```

### Step 2: Choose Field Types

Common field types for custom blocks:

| Type | Description | Key Properties |
|------|-------------|----------------|
| `text` | Single or multiline text | `multiline`, `maxLength` |
| `richText` | Rich text editor | `format: 'html'` or `'markdown'` |
| `select` | Dropdown selection | `options`, `multiple`, `defaultValue` |
| `boolean` | Toggle/checkbox | `defaultValue` |
| `media` | Image/video upload | `mediaKinds: ['image']` or `['video']` |
| `link` | Internal/external link | - |
| `number` | Numeric input | `min`, `max`, `step` |
| `datetime` | Date/time picker | - |
| `repeater` | List of nested fields | `schema`, `minItems`, `maxItems` |
| `group` | Grouped fields | `schema` |

### Step 3: Create React Override Component

Create a component to render your block:

```typescript
// src/components/OurFlowBlock.tsx
import type { BlockOverrideProps } from '@riverbankcms/sdk/rendering';
import type { TipTapNode } from '@riverbankcms/sdk/rendering';
import { RichText } from '@riverbankcms/sdk/rendering';

interface Step {
  title: string;
  description?: TipTapNode;
  icon?: { url: string; alt?: string };
}

interface OurFlowContent {
  title?: string;
  introText?: string;
  steps?: Step[];
}

export function OurFlowBlock({ content }: BlockOverrideProps<OurFlowContent>) {
  const { title, introText, steps = [] } = content;

  return (
    <section className="py-16 bg-white">
      <div className="container mx-auto px-4">
        {title && <h2 className="text-3xl font-bold mb-4">{title}</h2>}
        {introText && <p className="text-lg text-gray-600 mb-8">{introText}</p>}

        <div className="grid gap-6 md:grid-cols-3">
          {steps.map((step, index) => (
            <div key={index} className="p-6 bg-gray-50 rounded-lg">
              {step.icon?.url && (
                <img src={step.icon.url} alt={step.icon.alt || ''} className="w-12 h-12 mb-4" />
              )}
              <h3 className="text-xl font-semibold mb-2">{step.title}</h3>
              {step.description && <RichText content={step.description} />}
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}
```

### Step 4: Register in blockOverrides

Add your component to the block overrides:

```typescript
// src/lib/block-overrides.tsx
import type { BlockOverrides } from '@riverbankcms/sdk/rendering';
import { OurFlowBlock } from '@/components/OurFlowBlock';

export const blockOverrides: BlockOverrides = {
  'custom.our-flow': OurFlowBlock,
};
```

### Step 5: Push Config

Push the configuration to make the block available in the CMS:

```bash
riverbankcms push-config
```

### Step 6: Test with Preview

Preview your block with sample data:

```bash
riverbankcms preview custom.our-flow \
  --data '{"title": "Our Process", "steps": [{"title": "Step 1"}, {"title": "Step 2"}]}' \
  --open
```

### Step 7: Create Content

Add the block to a page:

```bash
riverbankcms block upsert services process-flow \
  --kind custom.our-flow \
  --data '{"title": "How We Work", "steps": [{"title": "Discovery"}, {"title": "Design"}, {"title": "Deliver"}]}'
```

## Field Types Reference

### text

```typescript
{
  id: 'title',
  type: 'text',
  label: 'Title',
  required: true,
  multiline: false,        // Single line (default) or textarea
  maxLength: 100,          // Optional max characters
  description: 'Helpful hint for editors',
  defaultValue: 'Default',
}
```

### richText

```typescript
{
  id: 'content',
  type: 'richText',
  label: 'Content',
  format: 'html',          // 'html' or 'markdown'
  required: true,
}
```

### select

```typescript
{
  id: 'variant',
  type: 'select',
  label: 'Variant',
  options: [
    { value: 'grid', label: 'Grid Layout' },
    { value: 'list', label: 'List Layout' },
  ],
  defaultValue: 'grid',
  multiple: false,         // Single or multi-select
}
```

### media

```typescript
{
  id: 'image',
  type: 'media',
  label: 'Featured Image',
  mediaKinds: ['image'],   // ['image'], ['video'], or ['image', 'video']
  required: false,
}
```

### repeater

```typescript
{
  id: 'items',
  type: 'repeater',
  label: 'Items',
  itemLabel: 'Item',
  minItems: 1,
  maxItems: 10,
  schema: {
    fields: [
      { id: 'title', type: 'text', label: 'Title', required: true },
      { id: 'description', type: 'text', label: 'Description', multiline: true },
    ],
  },
}
```

### Conditional Fields

Show/hide fields based on other values:

```typescript
{
  id: 'buttonLink',
  type: 'link',
  label: 'Button Link',
  ui: {
    visibleWhen: {
      field: 'buttonAction',
      equals: 'link',
    },
  },
}
```

## Complete Examples

### Example 1: Color Tiles (Grid Layout)

A grid of colored tiles with configurable columns:

```typescript
{
  id: 'custom.color-tiles',
  title: 'Color Tiles',
  description: 'Grid of solid color tiles with decorative icons',
  category: 'content',
  icon: 'LayoutGrid',
  tags: ['tiles', 'grid', 'cards'],
  fields: [
    { id: 'heading', type: 'text', label: 'Heading' },
    { id: 'subheading', type: 'text', label: 'Subheading', multiline: true },
    {
      id: 'columns',
      type: 'select',
      label: 'Columns',
      options: [
        { value: '2', label: '2 columns' },
        { value: '3', label: '3 columns' },
      ],
      defaultValue: '2',
    },
    {
      id: 'tiles',
      type: 'repeater',
      label: 'Tiles',
      itemLabel: 'Tile',
      minItems: 2,
      maxItems: 6,
      schema: {
        fields: [
          { id: 'title', type: 'text', label: 'Title', required: true },
          { id: 'description', type: 'text', label: 'Description', multiline: true },
        ],
      },
    },
  ],
}
```

### Example 2: Pricing Tiers (Complex Marketing Block)

Pricing cards with nested features and conditional button actions:

```typescript
{
  id: 'custom.pricing-tiers',
  title: 'Pricing Tiers',
  description: 'Pricing comparison cards with features and CTAs',
  category: 'marketing',
  icon: 'CreditCard',
  tags: ['pricing', 'tiers', 'comparison'],
  fields: [
    { id: 'heading', type: 'text', label: 'Heading' },
    { id: 'subheading', type: 'text', label: 'Subheading', multiline: true },
    {
      id: 'showButtons',
      type: 'boolean',
      label: 'Show CTA buttons',
      defaultValue: true,
    },
    {
      id: 'tiers',
      type: 'repeater',
      label: 'Pricing Tiers',
      itemLabel: 'Tier',
      minItems: 2,
      maxItems: 3,
      schema: {
        fields: [
          { id: 'title', type: 'text', label: 'Title', required: true },
          { id: 'price', type: 'text', label: 'Price', required: true },
          { id: 'description', type: 'text', label: 'Description', multiline: true },
          {
            id: 'color',
            type: 'select',
            label: 'Accent Color',
            options: [
              { value: 'green', label: 'Green' },
              { value: 'tan', label: 'Tan' },
              { value: 'teal', label: 'Teal' },
            ],
            defaultValue: 'green',
          },
          {
            id: 'features',
            type: 'repeater',
            label: 'Features',
            itemLabel: 'Feature',
            minItems: 1,
            maxItems: 10,
            schema: {
              fields: [
                { id: 'text', type: 'text', label: 'Feature text', required: true },
                { id: 'included', type: 'boolean', label: 'Included', defaultValue: true },
              ],
            },
          },
          { id: 'buttonLabel', type: 'text', label: 'Button Label', defaultValue: 'Get a quote' },
          {
            id: 'buttonAction',
            type: 'select',
            label: 'Button Action',
            options: [
              { value: 'quote', label: 'Open Quote Form' },
              { value: 'link', label: 'Link to page' },
            ],
            defaultValue: 'quote',
          },
          {
            id: 'buttonLink',
            type: 'link',
            label: 'Button Link',
            ui: {
              visibleWhen: {
                field: 'buttonAction',
                equals: 'link',
              },
            },
          },
        ],
      },
    },
  ],
}
```

## Block Configuration Options

| Property | Description |
|----------|-------------|
| `id` | Unique identifier, must start with `custom.` |
| `title` | Display name in CMS |
| `description` | Helpful description for editors |
| `category` | `'content'`, `'marketing'`, `'media'`, etc. |
| `icon` | Lucide icon name (e.g., `'Workflow'`, `'CreditCard'`) |
| `tags` | Searchable tags for block picker |
| `fields` | Array of field definitions |

## Troubleshooting

### Block not appearing in CMS

- Verify the ID starts with `custom.`
- Run `riverbankcms push-config` to sync
- Check for syntax errors in config

### Component not rendering

- Verify override is registered with exact block ID
- Check the key in `blockOverrides` matches (e.g., `'custom.our-flow'`)
- Ensure component is exported correctly

### Repeater items not working

- Verify `schema.fields` is properly defined
- Check `minItems` and `maxItems` constraints
- Ensure repeater field has unique `id`

### Type errors in component

- Create an interface matching your field structure
- Use `BlockOverrideProps<YourContentType>`
- Handle optional fields with defaults or conditionals
