@wpgaurav/wp-block-quick
125+ reusable AI agent skills for Claude Code, ChatGPT, Cursor, and other AI tools — WordPress, design, SEO, marketing, frontend
No skills found in this collection.
SKILL.md
| name | wp-block-quick |
| description | Build 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
- Load
/CLAUDE/context/wordpress-dev.mdfor standards - Ensure Node.js 20+ and npm installed
- 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
Loading...
Select a file to preview
Analyzing security...
Checking scan reports and verification data.
Bill of Materials
Everything this skill can do — files, network, commands, and more.
No demos yet. To add one, ask your AI agent: "Submit a demo for @wpgaurav/wp-block-quick" — or upload via the API.
To add a demo, ask your AI agent: "Submit a demo for @wpgaurav/wp-block-quick"
No eval data yet. Eval results are published via the SkillSafe API.