# Gutenberg Block Development

> Best practices for creating custom WordPress Gutenberg blocks with React/TypeScript, covering scaffolding, block architecture, attributes, dynamic vs static rendering, and testing

- Skill: `daverobinson/gutenberg-block-development` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add daverobinson/gutenberg-block-development`
- Raw SKILL.md: https://api.skillmd.com/api/skills/daverobinson/gutenberg-block-development/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: DaveRobinson (https://skillmd.com/u/daverobinson)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/daverobinson/gutenberg-block-development

---


# WordPress Gutenberg Custom Block Development

This skill provides guidance for creating custom Gutenberg blocks for WordPress using modern JavaScript/TypeScript and React.

## Prerequisites & Environment Setup

**Required Knowledge:**
- WordPress plugin development fundamentals
- JavaScript ES6+ / TypeScript
- React basics (components, hooks, JSX)
- Understanding of WordPress block editor concepts

**Development Environment:**
- Node.js and npm installed
- WordPress development environment (Local, wp-env, or Docker)
- Code editor with TypeScript/React support

**Essential Packages:**
- `@wordpress/create-block` - Official scaffolding tool
- `@wordpress/scripts` - Build tooling (webpack, babel, etc.)
- `@wordpress/components` - UI component library
- `wp-env` (optional) - Local WordPress environment via Docker

## Quick Start: Scaffolding a Block

### Using @wordpress/create-block

**Basic scaffolding (static block):**
```bash
cd /path/to/wordpress/wp-content/plugins
npx @wordpress/create-block@latest my-custom-block --namespace=my-namespace
cd my-custom-block
npm start
```

**Dynamic block with standard template:**
```bash
npx @wordpress/create-block@latest my-dynamic-block --namespace=my-namespace --variant dynamic
```

**Interactive template (always dynamic, uses Interactivity API):**
```bash
# JavaScript variant (default)
npx @wordpress/create-block@latest my-interactive-block --template @wordpress/create-block-interactive-template

# TypeScript variant
npx @wordpress/create-block@latest my-interactive-block --template @wordpress/create-block-interactive-template --variant typescript
```

Note: Interactive template blocks are always dynamic (use `render.php`) and include the WordPress Interactivity API for reactive frontend experiences.

**Key flags:**
- `--namespace` - Your unique namespace (required to avoid conflicts)
- `--no-plugin` - Scaffold block files only (no plugin wrapper)
- `--wp-env` - Add wp-env configuration for local development
- `--category` - Set block category (text, media, design, widgets, theme, embed)

### Generated Structure

```
my-custom-block/
├── build/           # Compiled production code (don't edit)
├── node_modules/    # Dependencies (don't commit)
├── src/            # Your development files
│   ├── block.json  # Block metadata (THE BRAIN)
│   ├── edit.js     # Editor component
│   ├── save.js     # Frontend output (static blocks)
│   ├── style.scss  # Frontend styles
│   └── editor.scss # Editor-only styles
├── package.json
└── my-custom-block.php  # Plugin entry point
```

### Development Workflow

After scaffolding a block:

**1. Get the plugin into your WordPress environment:**
- Copy/move to your WordPress plugins folder, or
- Use `wp-env` for a containerized WordPress environment (see below)

**2. Start development or build for production:**

```bash
# Development (watches for changes, rebuilds automatically)
npm start

# Production (optimized, minified build)
npm run build
```

**Using wp-env:**
```bash
# 1. Start build process
npm start          # or npm run build for production

# 2. Launch WordPress environment
npx wp-env start

# Access at http://localhost:8888
# Admin: http://localhost:8888/wp-admin (admin/password)
```

Note: For development, run `npm start` in one terminal, then `wp-env start` in another (npm start runs continuously).

### Skill Scope: Single Block Per Plugin

**This skill focuses on the standard scaffolding pattern: one block per plugin.** The structure is:

```
my-custom-block/
├── src/
│   ├── block.json        # Block metadata at src root
│   ├── edit.js
│   ├── save.js
│   ├── style.scss
│   └── editor.scss
├── build/                # Compiled output
└── my-custom-block.php   # Plugin entry
```

**Multiple blocks per plugin** requires advanced build configuration and is outside this skill's scope. For theme-based blocks or multiple blocks, consult the [Block Development Examples](https://github.com/WordPress/block-development-examples) repository.

### Post-Scaffolding: Customize Default Styles

The scaffolded `style.scss` contains placeholder styles meant to be customized:

```scss
// src/style.scss - SCAFFOLDED PLACEHOLDER
.wp-block-my-namespace-my-block {
	background-color: #21759b;  // Placeholder - customize or remove
	color: #fff;                 // Placeholder - customize or remove
	padding: 2px;
}
```

**Why customize:**
- `style.scss` applies to BOTH editor AND frontend
- Placeholder styles may not match your design
- For reusable blocks, keep minimal to respect user themes

**Recommended approach:**
```scss
// src/style.scss - CUSTOMIZED
/**
 * Shared styles (editor and frontend).
 * Keep minimal for maximum theme compatibility.
 */

.wp-block-my-namespace-my-block {
	// Add only essential structural styles
	// Let themes control colors, typography, spacing
}
```

**Note:** `editor.scss` is editor-only and can have more specific styling for the editing experience.

## Core Concepts

### 1. block.json - The Block's Brain

The `block.json` file is the single source of truth for block metadata:

```json
{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "my-namespace/my-block",
  "title": "My Custom Block",
  "category": "widgets",
  "icon": "smiley",
  "description": "A custom block example",
  "keywords": ["custom", "example"],
  "version": "1.0.0",
  "textdomain": "my-custom-block",
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css",
  "attributes": {
    "content": {
      "type": "string",
      "source": "html",
      "selector": "p"
    }
  },
  "supports": {
    "html": false,
    "anchor": true,
    "align": true
  }
}
```

**Critical fields:**
- `apiVersion` - Use 3 for latest features
- `name` - Must be unique: `namespace/block-name`
- `icon` - Visual identifier (see Icon Selection below)
- `attributes` - Define data structure and storage
- `supports` - Enable/disable core features

### Icon Selection

Icons appear in the block inserter and help users identify your block quickly.

**Built-in Dashicons (easiest):**
```json
"icon": "smiley"
```

Common choices:
- Text/content: `"text"`, `"editor-paragraph"`, `"editor-alignleft"`
- Media: `"format-image"`, `"format-video"`, `"format-gallery"`
- Layout: `"layout"`, `"columns"`, `"grid-view"`
- Interactive: `"button"`, `"forms"`, `"testimonial"`
- Data: `"chart-bar"`, `"analytics"`, `"database"`
- Social: `"share"`, `"email"`, `"twitter"`

Full list: https://developer.wordpress.org/resource/dashicons/

**Custom SVG icon:**
```json
"icon": {
  "src": "<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><path d='M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z' fill='currentColor'/></svg>"
}
```

**SVG with custom colors:**
```json
"icon": {
  "background": "#7e70af",
  "foreground": "#fff",
  "src": "<svg>...</svg>"
}
```

**Best practices:**
- Keep SVG viewBox at `0 0 24 24` for consistency
- Use `currentColor` for fill/stroke to respect theme colors
- Avoid complex SVGs (keep paths simple)
- Test icon visibility in both light and dark editor themes
- Choose icons that visually represent the block's purpose
- For brand blocks, use brand colors in background/foreground

### 2. Attributes - Data Management

Attributes control how blocks store and retrieve data:

```javascript
"attributes": {
  "title": {
    "type": "string",
    "source": "html",
    "selector": "h2",
    "default": "Default Title"
  },
  "isActive": {
    "type": "boolean",
    "default": false
  },
  "items": {
    "type": "array",
    "default": []
  },
  "settings": {
    "type": "object",
    "default": {}
  }
}
```

**Type options:** `string`, `boolean`, `number`, `integer`, `array`, `object`, `null`

**Source options:**
- `attribute` - Extract from HTML attribute
- `text` - Extract text content
- `html` - Extract inner HTML
- `query` - Extract multiple elements
- `meta` - Store in post meta

### 3. Static vs Dynamic Blocks

**Scaffolding Choice:**
- Static block: `npx @wordpress/create-block my-block`
- Dynamic block: `npx @wordpress/create-block my-block --variant dynamic`

Using `--variant dynamic` sets up the correct structure from the start.

**Static Blocks:**
- HTML saved to database at save time
- Content persists even if plugin deactivated
- Requires manual updates (re-save post)
- Best for: Content that rarely changes, distributed plugins

```javascript
// edit.js
export default function Edit({ attributes, setAttributes }) {
  return (
    <div {...useBlockProps()}>
      <RichText
        tagName="p"
        value={attributes.content}
        onChange={(content) => setAttributes({ content })}
      />
    </div>
  );
}

// save.js
export default function Save({ attributes }) {
  return (
    <div {...useBlockProps.save()}>
      <RichText.Content tagName="p" value={attributes.content} />
    </div>
  );
}
```

**Dynamic Blocks:**
- Rendered via PHP at runtime
- Always current (updates automatically)
- More database queries
- Best for: Theme-specific blocks, data that changes, client projects

**Scaffolded with --variant dynamic:**
```php
// render.php (automatically created)
<?php
/**
 * Available variables:
 * @var array    $attributes The block attributes
 * @var string   $content    The block default content
 * @var WP_Block $block      The block instance
 */
?>
<div <?php echo get_block_wrapper_attributes(); ?>>
  <?php echo esc_html( $attributes['content'] ?? 'Default content' ); ?>
</div>
```

```javascript
// index.js - No save function needed
registerBlockType( metadata.name, {
  edit: Edit,  // Only edit function
} );
```

**block.json configuration:**
```json
{
  "render": "file:./render.php",
  "viewScript": "file:./view.js"  // Optional frontend JS
}
```

**Converting static to dynamic:**
If you scaffolded a static block and need to convert it:
1. Create `render.php` in the block directory
2. Add `"render": "file:./render.php"` to `block.json`
3. Remove the save function from `index.js`
4. Rebuild with `npm run build`

**Decision Guide:**
- Theme-specific design/layout → Dynamic (theme-coupled)
- Functionality/features → Static (plugin)
- Real-time data (posts, users, API) → Dynamic
- User-generated content → Static
- Open source distribution → Static

### 4. Block Controls

**Toolbar Controls (quick access):**
```javascript
import { BlockControls } from '@wordpress/block-editor';
import { ToolbarGroup, ToolbarButton } from '@wordpress/components';

<BlockControls>
  <ToolbarGroup>
    <ToolbarButton
      icon="admin-links"
      label="Add Link"
      onClick={() => {/* handle */}}
    />
  </ToolbarGroup>
</BlockControls>
```

**Inspector Panel (detailed settings):**
```javascript
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ToggleControl, TextControl } from '@wordpress/components';

<InspectorControls>
  <PanelBody title="Settings" initialOpen={true}>
    <ToggleControl
      label="Enable Feature"
      checked={attributes.isActive}
      onChange={(isActive) => setAttributes({ isActive })}
    />
    <TextControl
      label="Custom Text"
      value={attributes.customText}
      onChange={(customText) => setAttributes({ customText })}
    />
  </PanelBody>
</InspectorControls>
```

## TypeScript Integration

### Setting Up TypeScript (Manual)

**1. Install dependencies:**
```bash
npm install --save-dev typescript @types/react @types/wordpress__block-editor @types/wordpress__blocks @types/wordpress__components
```

**2. Create tsconfig.json:**
```json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "lib": ["dom", "esnext"],
    "jsx": "react",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}
```

**3. Update webpack.config.js:**
```javascript
const defaultConfig = require('@wordpress/scripts/config/webpack.config');

module.exports = {
  ...defaultConfig,
  resolve: {
    ...defaultConfig.resolve,
    extensions: ['.tsx', '.ts', '.js', '.jsx']
  },
  module: {
    ...defaultConfig.module,
    rules: [
      ...defaultConfig.module.rules,
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/
      }
    ]
  }
};
```

**4. Type your Edit component:**
```typescript
// edit.tsx
import { useBlockProps } from '@wordpress/block-editor';

interface EditProps {
  attributes: {
    content: string;
    isActive: boolean;
  };
  setAttributes: (attrs: Partial<EditProps['attributes']>) => void;
  className?: string;
}

export default function Edit({ attributes, setAttributes, className }: EditProps) {
  const blockProps = useBlockProps();
  
  return (
    <div {...blockProps}>
      {/* Your block UI */}
    </div>
  );
}
```

## Common Patterns

### Pattern: Custom Post Query Block

```javascript
// Dynamic block that queries posts
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';

export default function Edit({ attributes }) {
  const { postsPerPage = 5 } = attributes;
  
  const posts = useSelect((select) => {
    return select(coreStore).getEntityRecords('postType', 'post', {
      per_page: postsPerPage,
      _embed: true
    });
  }, [postsPerPage]);
  
  if (!posts) return <Spinner />;
  
  return (
    <div {...useBlockProps()}>
      {posts.map(post => (
        <article key={post.id}>
          <h3>{post.title.rendered}</h3>
        </article>
      ))}
    </div>
  );
}
```

### Pattern: Block with InnerBlocks (Static)

```javascript
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';

// edit.js
export default function Edit() {
  const TEMPLATE = [
    ['core/heading', { level: 2, placeholder: 'Section Title' }],
    ['core/paragraph', { placeholder: 'Section content...' }]
  ];

  return (
    <div {...useBlockProps()}>
      <InnerBlocks template={TEMPLATE} />
    </div>
  );
}

// save.js
export default function Save() {
  return (
    <div {...useBlockProps.save()}>
      <InnerBlocks.Content />
    </div>
  );
}
```

### Pattern: Dynamic Block with InnerBlocks

**⚠️ Important Official Guidance:** Per the [Block Editor Handbook](https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/creating-dynamic-blocks/):

> "For many dynamic blocks, the `save` callback function should be returned as `null`... **If you are using InnerBlocks in a dynamic block you will need to save the InnerBlocks in the save callback function using `<InnerBlocks.Content/>`**"

This pattern is essential when you need to process or transform inner blocks on the server.

```javascript
// edit.js
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';

export default function Edit() {
  const TEMPLATE = [
    ['core/group', {
      className: 'slot-one',
      metadata: { name: 'slot-one-content' }
    }, [
      ['core/paragraph', { placeholder: 'First slot content...' }]
    ]],
    ['core/group', {
      className: 'slot-two',
      metadata: { name: 'slot-two-content' }
    }, [
      ['core/paragraph', { placeholder: 'Second slot content...' }]
    ]]
  ];

  return (
    <div {...useBlockProps()}>
      <InnerBlocks template={TEMPLATE} />
    </div>
  );
}

// save.js
// Must save InnerBlocks even though block is dynamic!
export default function save() {
  return (
    <div {...useBlockProps.save()}>
      <InnerBlocks.Content />
    </div>
  );
}
```

**PHP Render Callback:**

The `$block` parameter provides access to inner blocks as `WP_Block` objects. Use the `render()` method to output them ([Code Reference](https://developer.wordpress.org/reference/classes/wp_block/)).

```php
function my_block_render_callback( $attributes, $content, $block ) {
  // Inner blocks are WP_Block objects
  $inner_blocks = $block->inner_blocks;

  $slot_one = '';
  $slot_two = '';

  // Render using ->render() method for WP_Block objects
  if ( isset( $inner_blocks[0] ) ) {
    $slot_one = $inner_blocks[0]->render();
  }

  if ( isset( $inner_blocks[1] ) ) {
    $slot_two = $inner_blocks[1]->render();
  }

  // Transform inner content for custom output
  return sprintf(
    '<custom-element><div slot="one">%s</div><div slot="two">%s</div></custom-element>',
    $slot_one,
    $slot_two
  );
}

register_block_type( __DIR__ . '/build', [
  'render_callback' => 'my_block_render_callback'
] );
```

**Key Methods (WordPress 6.0+):**
- [`WP_Block::render()`](https://developer.wordpress.org/reference/classes/wp_block/render/) - For `WP_Block` objects from `$block->inner_blocks`
- [`render_block($array)`](https://developer.wordpress.org/reference/functions/render_block/) - For parsed block arrays from `parse_blocks()`

**Common Use Cases:**
- Wrapping inner blocks with custom HTML (web components, special layouts)
- Adding server-side data or context to nested content
- Conditionally showing/hiding sections based on attributes
- Processing inner blocks based on user permissions or state

### Pattern: Block Variations

```javascript
// block.json
"variations": [
  {
    "name": "blue-variant",
    "title": "Blue Style",
    "icon": "admin-appearance",
    "attributes": {
      "backgroundColor": "blue"
    }
  },
  {
    "name": "red-variant",
    "title": "Red Style",
    "icon": "admin-appearance",
    "attributes": {
      "backgroundColor": "red"
    }
  }
]
```

### Pattern: Frontend JavaScript with Blocks

Blocks can load frontend JavaScript using `viewScript` in `block.json`:

```json
{
  "viewScript": "file:./view.js"
}
```

Or reference multiple script handles (your own, core, or third-party):
```json
{
  "viewScript": ["file:./view.js", "wp-api-fetch", "my-external-library"]
}
```

External scripts are registered using standard WordPress [`wp_register_script()`](https://developer.wordpress.org/reference/functions/wp_register_script/) in your plugin's main PHP file.

**Important:** Define all scripts in block.json's `viewScript` array. Don't use `view_script_handles` in `register_block_type()` as it overrides the `viewScript` from block.json.

**Note:** Scripts only enqueue on pages where the block is used.

## Testing & Quality Assurance

### Manual Testing Checklist

When developing or modifying a block, verify:

**Registration & Discovery:**
- [ ] Block appears in inserter with correct icon/title
- [ ] Block appears in correct category
- [ ] Block can be searched by keywords

**Functionality:**
- [ ] Block can be added to editor without errors
- [ ] All toolbar controls work as expected
- [ ] Inspector panel controls update attributes correctly
- [ ] Attributes save and restore correctly
- [ ] Block renders correctly on frontend
- [ ] Dynamic blocks fetch and display current data

**Compatibility:**
- [ ] Works with block themes and classic themes
- [ ] Responsive across mobile/tablet/desktop
- [ ] No JavaScript console errors or warnings
- [ ] Works in posts, pages, and custom post types
- [ ] Works in widget areas (if applicable)
- [ ] Compatible with Full Site Editing (if applicable)

**Accessibility:**
- [ ] Keyboard navigation works throughout
- [ ] Screen reader announces elements correctly
- [ ] ARIA labels present where needed
- [ ] Focus indicators visible
- [ ] Color contrast meets WCAG standards

**Performance:**
- [ ] No unnecessary re-renders
- [ ] Scripts/styles only load when block present
- [ ] Images optimized and lazy-loaded
- [ ] No blocking JavaScript

### Validation Testing

**Block Deprecation:**
When changing save output, always test migration:
```javascript
deprecated: [
  {
    attributes: { /* old structure */ },
    save: OldSaveFunction,
    migrate: (attributes) => {
      // Transform old to new
      return newAttributes;
    }
  }
]
```

**Browser Testing:**
- Chrome/Edge (Chromium)
- Firefox
- Safari (especially for CSS grid/flexbox)

**WordPress Version Testing:**
- Current stable release
- One version back (if supporting older sites)
- Beta/RC (if planning ahead)

## Common Pitfalls & Solutions

### Pitfall 1: Block Validation Errors

**Problem:** "This block contains unexpected or invalid content"

**Cause:** Save function output changed between versions

**Solution:**
- Use block deprecations when changing save output
- Never change existing attribute sources
- Test migration before deploying

### Pitfall 2: Infinite Re-renders

**Problem:** Block constantly re-renders, editor freezes

**Cause:** Creating new objects/arrays in render

**Solution:**
```javascript
// ❌ Bad - creates new array every render
const items = [];

// ✅ Good - memoize or use state
const [items, setItems] = useState([]);
```

### Pitfall 3: Missing useBlockProps

**Problem:** Block wrapper styling doesn't work

**Cause:** Forgot `useBlockProps()` in edit or save

**Solution:**
```javascript
// Always wrap your block
<div {...useBlockProps()}>
  {/* content */}
</div>
```

### Pitfall 4: RichText Content Loss

**Problem:** Content disappears on save

**Cause:** Missing `RichText.Content` in save function

**Solution:**
```javascript
// edit.js
<RichText
  tagName="p"
  value={attributes.content}
  onChange={(content) => setAttributes({ content })}
/>

// save.js
<RichText.Content tagName="p" value={attributes.content} />
```

### Pitfall 5: Placeholder Styles Not Customized

**Problem:** Block has unexpected styling that clashes with themes

**Cause:** Scaffolded `style.scss` contains placeholder styles

**Location:** `src/style.scss` (applies to both editor and frontend)

**Solution:**
Customize or remove the placeholder styles after scaffolding:

```scss
// Scaffolded placeholder
.wp-block-my-namespace-my-block {
	background-color: #21759b;  // Remove or customize
	color: #fff;                 // Remove or customize
	padding: 2px;
}

// Customized for production
.wp-block-my-namespace-my-block {
	// Only essential structural styles
	// Let themes control appearance
}
```

**Remember:**
- `style.scss` → Both frontend + editor (keep minimal)
- `editor.scss` → Editor only (can be more specific)

### Pitfall 6: InnerBlocks Content Lost in Dynamic Blocks

**Problem:** InnerBlocks content disappears after saving in a dynamic block

**Cause:** Returning `null` in save.js when using InnerBlocks

**Official Guidance:** Per the [Block Editor Handbook](https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/creating-dynamic-blocks/): _"If you are using InnerBlocks in a dynamic block you will need to save the InnerBlocks in the save callback function using `<InnerBlocks.Content/>`"_

**Solution:**
```javascript
// ❌ WRONG - InnerBlocks content won't persist
export default function save() {
  return null;
}

// ✅ CORRECT - Save InnerBlocks even for dynamic blocks
export default function save() {
  return (
    <div {...useBlockProps.save()}>
      <InnerBlocks.Content />
    </div>
  );
}
```

### Pitfall 7: Fatal Error with Inner Block Rendering

**Problem:** `Fatal error: Cannot use object of type WP_Block as array`

**Cause:** Using wrong rendering method for `WP_Block` objects

**Context:** `$block->inner_blocks` returns an array of `WP_Block` objects, not arrays.

**Solution:**
```php
// ❌ WRONG - Causes fatal error
$inner_blocks = $block->inner_blocks;
$output = render_block( $inner_blocks[0] ); // Fatal!

// ✅ CORRECT - Use ->render() method
$inner_blocks = $block->inner_blocks;
$output = $inner_blocks[0]->render();
```

**Reference:**
- [`WP_Block::render()`](https://developer.wordpress.org/reference/classes/wp_block/render/) - For `WP_Block` objects
- [`render_block()`](https://developer.wordpress.org/reference/functions/render_block/) - For parsed block arrays

## Performance Best Practices

1. **Minimize attribute updates** - Batch `setAttributes` calls
2. **Lazy load dependencies** - Import heavy libraries only when needed
3. **Optimize asset loading** - Load scripts/styles only on pages using block
4. **Use block context** - Share data between nested blocks efficiently
5. **Debounce user input** - For search/filter controls

## Internationalization (i18n)

Always wrap text strings:

```javascript
import { __ } from '@wordpress/i18n';

const title = __('My Block Title', 'my-text-domain');
const label = _x('Settings', 'block settings label', 'my-text-domain');
const plural = _n('1 item', '%d items', count, 'my-text-domain');
```

## Resources

**Official Documentation:**
- Block Editor Handbook: https://developer.wordpress.org/block-editor/
- Block Development Examples: https://github.com/WordPress/block-development-examples
- Block API Reference: https://developer.wordpress.org/block-editor/reference-guides/
- **Gutenberg Storybook**: https://wordpress.github.io/gutenberg/ (interactive component reference)

**Learning Platforms:**
- Learn WordPress: https://learn.wordpress.org/ (courses on block development)
- WordPress Developer Blog: Latest features and tutorials

**Community:**
- Make WordPress Slack: #core-editor channel
- GitHub Discussions: WordPress/gutenberg repository

## Decision Trees

### Should I Build a Custom Block?

```
Need custom functionality?
├─ No → Use core blocks or block patterns
└─ Yes → Is it reusable across sites?
    ├─ Yes → Build as plugin (static blocks)
    └─ No → Is it theme-specific design?
        ├─ Yes → Build in theme (dynamic blocks)
        └─ No → Consider if block is best solution
```

### Static vs Dynamic?

```
Will content change frequently without editor intervention?
├─ Yes → Dynamic block
└─ No → Is this for wide distribution?
    ├─ Yes → Static block
    └─ No → Are you comfortable with theme coupling?
        ├─ Yes → Dynamic block (simpler development)
        └─ No → Static block (portable)
```

## Version Notes

- WordPress 6.0+: `apiVersion: 3` introduced
- WordPress 5.8+: block.json is standard approach
- WordPress 5.5+: InnerBlocks improvements
- Always check compatibility requirements in plugin header

---

**This skill should be used when:**
- Creating new custom Gutenberg blocks
- Scaffolding block plugins
- Deciding between static/dynamic rendering
- Setting up TypeScript for block development
- Troubleshooting block validation errors
- Planning block architecture

