@wpgaurav/wp-block-quick

125+ reusable AI agent skills for Claude Code, ChatGPT, Cursor, and other AI tools — WordPress, design, SEO, marketing, frontend

View in AI SkillSafe app
0 downloads
0 stars
0 demos
SKILL.md
namewp-block-quick
descriptionBuild custom Gutenberg blocks using the WordPress Block API and @wordpress/scripts.

Skill: WordPress Block Development

Build custom Gutenberg blocks using the WordPress Block API and @wordpress/scripts.

Before Starting

  1. Load /CLAUDE/context/wordpress-dev.md for standards
  2. Ensure Node.js 20+ and npm installed
  3. Understand React basics and WordPress data layer

Block Types

Static Block

Renders same content in editor and frontend from saved markup.

Dynamic Block

Renders via PHP callback, content generated server-side.

Interactive Block

Uses @wordpress/interactivity API for frontend interactivity without full React.

Project Setup

Using @wordpress/create-block

# Create new block plugin
npx @wordpress/create-block my-block --namespace gt

# Create dynamic block
npx @wordpress/create-block my-dynamic-block --namespace gt --variant dynamic

# Create interactive block
npx @wordpress/create-block my-interactive-block --namespace gt --template @wordpress/create-block-interactive-template

Manual Setup (for existing plugins)

# Initialize npm
npm init -y

# Install dependencies
npm install @wordpress/scripts --save-dev

# Add to package.json scripts
{
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start",
    "format": "wp-scripts format",
    "lint:css": "wp-scripts lint-style",
    "lint:js": "wp-scripts lint-js",
    "packages-update": "wp-scripts packages-update"
  }
}

File Structure

my-block/
├── my-block.php              # Plugin file / block registration
├── package.json
├── src/
│   ├── block.json            # Block metadata
│   ├── index.js              # Block registration
│   ├── edit.js               # Editor component
│   ├── save.js               # Save component
│   ├── style.scss            # Frontend + editor styles
│   ├── editor.scss           # Editor-only styles
│   └── view.js               # Frontend interactivity (optional)
└── build/                    # Compiled output (gitignored)

block.json

{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 3,
    "name": "gt/my-block",
    "version": "1.0.0",
    "title": "My Block",
    "category": "widgets",
    "icon": "smiley",
    "description": "A custom block that does something useful.",
    "keywords": ["custom", "example"],
    "textdomain": "my-block",
    "attributes": {
        "content": {
            "type": "string",
            "source": "html",
            "selector": "p"
        },
        "alignment": {
            "type": "string",
            "default": "left"
        },
        "backgroundColor": {
            "type": "string"
        },
        "showIcon": {
            "type": "boolean",
            "default": true
        },
        "iconSize": {
            "type": "number",
            "default": 24
        }
    },
    "supports": {
        "html": false,
        "align": ["wide", "full"],
        "anchor": true,
        "className": true,
        "color": {
            "background": true,
            "text": true,
            "link": true
        },
        "spacing": {
            "margin": true,
            "padding": true,
            "blockGap": true
        },
        "typography": {
            "fontSize": true,
            "lineHeight": true
        },
        "__experimentalBorder": {
            "color": true,
            "radius": true,
            "style": true,
            "width": true
        }
    },
    "example": {
        "attributes": {
            "content": "Example content for the block preview."
        }
    },
    "editorScript": "file:./index.js",
    "editorStyle": "file:./index.css",
    "style": "file:./style-index.css",
    "viewScript": "file:./view.js",
    "render": "file:./render.php"
}

Block Registration (PHP)

Static Block

<?php
/**
 * Plugin Name: My Block
 * Description: A custom Gutenberg block.
 * Version: 1.0.0
 * Author: Gaurav Tiwari
 * Text Domain: my-block
 *
 * @package MyBlock
 */

defined( 'ABSPATH' ) || exit;

/**
 * Register block.
 */
function gt_my_block_init() {
    register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'gt_my_block_init' );

Dynamic Block

<?php
/**
 * Register dynamic block.
 */
function gt_my_block_init() {
    register_block_type(
        __DIR__ . '/build',
        array(
            'render_callback' => 'gt_my_block_render',
        )
    );
}
add_action( 'init', 'gt_my_block_init' );

/**
 * Render callback.
 *
 * @param array    $attributes Block attributes.
 * @param string   $content    Block content.
 * @param WP_Block $block      Block instance.
 * @return string
 */
function gt_my_block_render( $attributes, $content, $block ) {
    // Get block wrapper attributes.
    $wrapper_attributes = get_block_wrapper_attributes(
        array(
            'class' => 'gt-my-block',
        )
    );

    // Build output.
    $output = sprintf(
        '<div %1$s><p>%2$s</p></div>',
        $wrapper_attributes,
        esc_html( $attributes['content'] ?? '' )
    );

    return $output;
}

Using render.php Template

<?php
/**
 * Block render template.
 *
 * @var array    $attributes Block attributes.
 * @var string   $content    Block content.
 * @var WP_Block $block      Block instance.
 *
 * @package MyBlock
 */

defined( 'ABSPATH' ) || exit;

$content_text = $attributes['content'] ?? '';
$show_icon    = $attributes['showIcon'] ?? true;
?>
<div <?php echo get_block_wrapper_attributes( array( 'class' => 'gt-my-block' ) ); ?>>
    <?php if ( $show_icon ) : ?>
        <span class="gt-my-block__icon" aria-hidden="true">★</span>
    <?php endif; ?>
    <p class="gt-my-block__content"><?php echo esc_html( $content_text ); ?></p>
</div>

Block JavaScript

index.js

/**
 * Block registration.
 */
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import save from './save';
import metadata from './block.json';

registerBlockType( metadata.name, {
    edit: Edit,
    save,
} );

edit.js (Simple)

/**
 * Editor component.
 */
import { __ } from '@wordpress/i18n';
import {
    useBlockProps,
    RichText,
    BlockControls,
    AlignmentToolbar,
    InspectorControls,
} from '@wordpress/block-editor';
import {
    PanelBody,
    ToggleControl,
    RangeControl,
} from '@wordpress/components';
import './editor.scss';

export default function Edit( { attributes, setAttributes } ) {
    const { content, alignment, showIcon, iconSize } = attributes;

    const blockProps = useBlockProps( {
        className: `has-text-align-${ alignment }`,
    } );

    return (
        <>
            <BlockControls>
                <AlignmentToolbar
                    value={ alignment }
                    onChange={ ( newAlignment ) =>
                        setAttributes( { alignment: newAlignment } )
                    }
                />
            </BlockControls>

            <InspectorControls>
                <PanelBody title={ __( 'Settings', 'my-block' ) }>
                    <ToggleControl
                        label={ __( 'Show Icon', 'my-block' ) }
                        checked={ showIcon }
                        onChange={ ( value ) =>
                            setAttributes( { showIcon: value } )
                        }
                    />
                    { showIcon && (
                        <RangeControl
                            label={ __( 'Icon Size', 'my-block' ) }
                            value={ iconSize }
                            onChange={ ( value ) =>
                                setAttributes( { iconSize: value } )
                            }
                            min={ 16 }
                            max={ 64 }
                        />
                    ) }
                </PanelBody>
            </InspectorControls>

            <div { ...blockProps }>
                { showIcon && (
                    <span
                        className="gt-my-block__icon"
                        style={ { fontSize: iconSize } }
                    >
                        ★
                    </span>
                ) }
                <RichText
                    tagName="p"
                    className="gt-my-block__content"
                    value={ content }
                    onChange={ ( value ) =>
                        setAttributes( { content: value } )
                    }
                    placeholder={ __( 'Enter text…', 'my-block' ) }
                />
            </div>
        </>
    );
}

save.js

/**
 * Save component.
 */
import { useBlockProps, RichText } from '@wordpress/block-editor';

export default function save( { attributes } ) {
    const { content, alignment, showIcon, iconSize } = attributes;

    const blockProps = useBlockProps.save( {
        className: `has-text-align-${ alignment }`,
    } );

    return (
        <div { ...blockProps }>
            { showIcon && (
                <span
                    className="gt-my-block__icon"
                    style={ { fontSize: iconSize } }
                    aria-hidden="true"
                >
                    ★
                </span>
            ) }
            <RichText.Content
                tagName="p"
                className="gt-my-block__content"
                value={ content }
            />
        </div>
    );
}

Advanced Patterns

Using InnerBlocks

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

const ALLOWED_BLOCKS = [ 'core/paragraph', 'core/heading', 'core/image' ];

const TEMPLATE = [
    [ 'core/heading', { placeholder: 'Enter heading...' } ],
    [ 'core/paragraph', { placeholder: 'Enter content...' } ],
];

export default function Edit() {
    const blockProps = useBlockProps();

    return (
        <div { ...blockProps }>
            <InnerBlocks
                allowedBlocks={ ALLOWED_BLOCKS }
                template={ TEMPLATE }
                templateLock={ false }
            />
        </div>
    );
}

export function save() {
    const blockProps = useBlockProps.save();

    return (
        <div { ...blockProps }>
            <InnerBlocks.Content />
        </div>
    );
}

Using useSelect for Data

import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';

export default function Edit( { attributes } ) {
    const { postId } = attributes;

    const post = useSelect(
        ( select ) => {
            if ( ! postId ) return null;
            return select( coreStore ).getEntityRecord(
                'postType',
                'post',
                postId
            );
        },
        [ postId ]
    );

    const isLoading = useSelect(
        ( select ) => {
            if ( ! postId ) return false;
            return select( coreStore ).isResolving( 'getEntityRecord', [
                'postType',
                'post',
                postId,
            ] );
        },
        [ postId ]
    );

    if ( isLoading ) {
        return <p>Loading...</p>;
    }

    return (
        <div { ...useBlockProps() }>
            { post ? post.title.rendered : 'No post selected' }
        </div>
    );
}

Post/Term Selector

import { useState } from '@wordpress/element';
import { ComboboxControl } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';

function PostSelector( { value, onChange } ) {
    const [ search, setSearch ] = useState( '' );

    const posts = useSelect(
        ( select ) => {
            return select( coreStore ).getEntityRecords( 'postType', 'post', {
                per_page: 10,
                search,
                _fields: 'id,title',
            } );
        },
        [ search ]
    );

    const options = ( posts || [] ).map( ( post ) => ( {
        value: post.id,
        label: post.title.rendered,
    } ) );

    return (
        <ComboboxControl
            label="Select Post"
            value={ value }
            onChange={ onChange }
            options={ options }
            onFilterValueChange={ setSearch }
        />
    );
}

Interactivity API

view.js (Frontend Interactivity)

/**
 * Frontend interactivity.
 */
import { store, getContext } from '@wordpress/interactivity';

store( 'gt/my-block', {
    state: {
        get isOpen() {
            const context = getContext();
            return context.isOpen;
        },
    },
    actions: {
        toggle() {
            const context = getContext();
            context.isOpen = ! context.isOpen;
        },
        open() {
            const context = getContext();
            context.isOpen = true;
        },
        close() {
            const context = getContext();
            context.isOpen = false;
        },
    },
    callbacks: {
        onToggle() {
            const context = getContext();
            console.log( 'Toggled:', context.isOpen );
        },
    },
} );

render.php with Interactivity

<?php
/**
 * Interactive block render.
 */

$unique_id = wp_unique_id( 'gt-accordion-' );
?>
<div
    <?php echo get_block_wrapper_attributes(); ?>
    data-wp-interactive="gt/my-block"
    <?php echo wp_interactivity_data_wp_context( array( 'isOpen' => false ) ); ?>
>
    <button
        data-wp-on--click="actions.toggle"
        data-wp-bind--aria-expanded="state.isOpen"
        aria-controls="<?php echo esc_attr( $unique_id ); ?>"
    >
        <?php esc_html_e( 'Toggle Content', 'my-block' ); ?>
    </button>

    <div
        id="<?php echo esc_attr( $unique_id ); ?>"
        data-wp-bind--hidden="!state.isOpen"
        data-wp-watch="callbacks.onToggle"
    >
        <?php echo wp_kses_post( $content ); ?>
    </div>
</div>

Styles

style.scss (Frontend + Editor)

.wp-block-gt-my-block {
    padding: 1.5rem;
    border: 1px solid #ddd;
    border-radius: 4px;

    &__icon {
        display: inline-block;
        margin-right: 0.5rem;
        color: var(--wp--preset--color--accent, #0073aa);
    }

    &__content {
        margin: 0;
    }

    // Alignment variations
    &.has-text-align-center {
        text-align: center;
    }

    &.has-text-align-right {
        text-align: right;
    }

    // Support for color settings
    &.has-background {
        padding: 2rem;
    }
}

editor.scss (Editor Only)

.wp-block-gt-my-block {
    // Editor-specific styles
    outline: 2px dashed transparent;
    transition: outline-color 0.2s;

    &:focus-within {
        outline-color: var(--wp-admin-theme-color, #007cba);
    }

    // Placeholder styling
    .components-placeholder {
        margin: 0;
    }
}

Block Variations

import { registerBlockVariation } from '@wordpress/blocks';

registerBlockVariation( 'core/group', {
    name: 'gt-card',
    title: 'Card',
    description: 'A card container with shadow and padding.',
    attributes: {
        className: 'is-style-gt-card',
        style: {
            spacing: {
                padding: {
                    top: 'var:preset|spacing|40',
                    right: 'var:preset|spacing|40',
                    bottom: 'var:preset|spacing|40',
                    left: 'var:preset|spacing|40',
                },
            },
            border: {
                radius: '8px',
            },
        },
        backgroundColor: 'base',
    },
    isActive: ( blockAttributes ) =>
        blockAttributes.className?.includes( 'is-style-gt-card' ),
    scope: [ 'inserter', 'transform' ],
    icon: 'id-alt',
} );

Block Transforms

import { createBlock } from '@wordpress/blocks';

const transforms = {
    from: [
        {
            type: 'block',
            blocks: [ 'core/paragraph' ],
            transform: ( { content } ) => {
                return createBlock( 'gt/my-block', {
                    content,
                } );
            },
        },
        {
            type: 'shortcode',
            tag: 'my_shortcode',
            transform: ( { named: { content } } ) => {
                return createBlock( 'gt/my-block', {
                    content: content || '',
                } );
            },
        },
    ],
    to: [
        {
            type: 'block',
            blocks: [ 'core/paragraph' ],
            transform: ( { content } ) => {
                return createBlock( 'core/paragraph', {
                    content,
                } );
            },
        },
    ],
};

// Add to block registration
registerBlockType( metadata.name, {
    edit: Edit,
    save,
    transforms,
} );

Deprecations

const deprecated = [
    {
        attributes: {
            content: {
                type: 'string',
                source: 'html',
                selector: '.my-block-content', // Old selector
            },
        },
        save( { attributes } ) {
            return (
                <div className="my-old-block">
                    <p className="my-block-content">{ attributes.content }</p>
                </div>
            );
        },
        migrate( attributes ) {
            return {
                ...attributes,
                // Transform old attributes to new format
            };
        },
    },
];

registerBlockType( metadata.name, {
    edit: Edit,
    save,
    deprecated,
} );

Testing Blocks

Jest Tests

/**
 * @jest-environment jsdom
 */
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Edit from '../edit';

// Mock WordPress packages
jest.mock( '@wordpress/block-editor', () => ( {
    useBlockProps: () => ( { className: 'test-block' } ),
    RichText: ( { value, onChange, placeholder } ) => (
        <input
            value={ value }
            onChange={ ( e ) => onChange( e.target.value ) }
            placeholder={ placeholder }
        />
    ),
    InspectorControls: ( { children } ) => <div>{ children }</div>,
    BlockControls: ( { children } ) => <div>{ children }</div>,
} ) );

describe( 'Edit component', () => {
    const defaultAttributes = {
        content: '',
        alignment: 'left',
        showIcon: true,
        iconSize: 24,
    };

    it( 'renders correctly', () => {
        const setAttributes = jest.fn();

        render(
            <Edit
                attributes={ defaultAttributes }
                setAttributes={ setAttributes }
            />
        );

        expect( screen.getByPlaceholderText( 'Enter text…' ) ).toBeInTheDocument();
    } );

    it( 'updates content on change', async () => {
        const setAttributes = jest.fn();
        const user = userEvent.setup();

        render(
            <Edit
                attributes={ defaultAttributes }
                setAttributes={ setAttributes }
            />
        );

        await user.type( screen.getByPlaceholderText( 'Enter text…' ), 'Hello' );

        expect( setAttributes ).toHaveBeenCalled();
    } );
} );

Build Commands

# Development (watch mode)
npm start

# Production build
npm run build

# Lint JavaScript
npm run lint:js

# Lint CSS
npm run lint:css

# Format code
npm run format

# Update WordPress packages
npm run packages-update

# Create production zip
npm run plugin-zip

Quality Checklist

  • block.json has all required fields
  • Block registered in PHP init hook
  • All strings translatable with text domain
  • Styles work in both editor and frontend
  • Block supports validated (color, spacing, typography)
  • Deprecations handle old saved content
  • No console errors in editor
  • Responsive on all screen sizes
  • Accessible (ARIA labels, keyboard navigation)
  • Passes @wordpress/scripts lint
  • Unit tests written and passing

Embed badges

Add these to your README to show the skill's verification status.

SkillSafe verified badge
Verified badge
[![SkillSafe verified badge](https://api.skillsafe.ai/v1/badge/@wpgaurav/wp-block-quick/verified)](https://skillsafe.ai/skill/@wpgaurav/wp-block-quick/)
Installs badge
Installs badge
[![Installs badge](https://api.skillsafe.ai/v1/badge/@wpgaurav/wp-block-quick/installs)](https://skillsafe.ai/skill/@wpgaurav/wp-block-quick/)
Scan badge
Scan badge
[![Scan badge](https://api.skillsafe.ai/v1/badge/@wpgaurav/wp-block-quick/scan)](https://skillsafe.ai/skill/@wpgaurav/wp-block-quick/)
Eval pass rate badge
Eval pass rate
[![Eval pass rate badge](https://api.skillsafe.ai/v1/badge/@wpgaurav/wp-block-quick/eval)](https://skillsafe.ai/skill/@wpgaurav/wp-block-quick/)