agentsclimarketplace

Claude skill gutenberg block

Skill DaveRobinson/claude-skill--gutenberg-block

Best practices for creating custom WordPress Gutenberg blocks with React/TypeScript, covering scaffolding, block architecture, attributes, dynamic vs static rendering, and testingFrom its SKILL.md

Install
npx -y skills add DaveRobinson/claude-skill--gutenberg-block

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

26.0 KB, ~6.3k tokens by cl100k_base, as published. Nobody here has run it

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):

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:

npx @wordpress/create-block@latest my-dynamic-block --namespace=my-namespace --variant dynamic

Interactive template (always dynamic, uses Interactivity API):

# 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:

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

# Production (optimized, minified build)
npm run build

Using wp-env:

# 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 repository.

Post-Scaffolding: Customize Default Styles

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

// 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:

// 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:

{
  "$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):

"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:

"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:

"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:

"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
// 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:

// 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>
// index.js - No save function needed
registerBlockType( metadata.name, {
  edit: Edit,  // Only edit function
} );

block.json configuration:

{
  "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):

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):

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:

npm install --save-dev typescript @types/react @types/wordpress__block-editor @types/wordpress__blocks @types/wordpress__components

2. Create tsconfig.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:

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:

// 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

// 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)

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:

"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.

// 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).

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+):

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

// 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:

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

Or reference multiple script handles (your own, core, or third-party):

{
  "viewScript": ["file:./view.js", "wp-api-fetch", "my-external-library"]
}

External scripts are registered using standard WordPress 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:

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:

// ❌ 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:

// 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:

// 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:

// 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: "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:

// ❌ 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:

// ❌ 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:

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:

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:

Learning Platforms:

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

What ships with it: 1 file

923 B alongside SKILL.md

Keep looking

Skills are one crate of 326,537. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.