SkillsAggSubmit Skill

gutenberg-block-development

Clean

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

0 stars🍴 0 forks0 installs

Install Command

npx skills add DaveRobinson/claude-skill--gutenberg-block
Author
DaveRobinson
Repository
DaveRobinson/claude-skill--gutenberg-block
Discovered via
github topic
Weekly installs
0
Quality score
10/100
Last commit
11/12/2025

SKILL.md

---
name: gutenberg-block-development
description: Best practices for creating custom WordPress Gutenberg blocks with React/TypeScript, covering scaffolding, block architecture, attributes, dynamic vs static rendering, and testing
license: MIT
---

# 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

Similar Skills

Use before you execute and edit any codes related to React Framework and developing ecosystem.

npx skills add ArkPLN/better-react-skills
better-authClean

Complete Better Auth documentation in markdown format. Use when implementing authentication in TypeScript projects - covers OAuth providers (Google, GitHub, etc.), email/password, passkeys, 2FA, session management, database adapters (Prisma, Drizzle), and framework integrations (Next.js, SvelteKit, etc.).

npx skills add leonaaardob/lb-better-auth-skill
nextjsClean

Complete Next.js 16 documentation in markdown format. Use when working with Next.js projects, building React applications, configuring routing, data fetching, rendering strategies, deployment, or migrating from other frameworks. Covers App Router, Pages Router, API routes, server components, server actions, caching, and all Next.js features.

npx skills add leonaaardob/lb-nextjs16-skill

Code generation guard for Node.js/TypeScript/Next.js - prevents OWASP Top 10 vulnerabilities while writing code

npx skills add subhashdasyam/security-antipatterns-javascript
gutenberg-block-development | SkillsAgg