Loading...
Loading...
WordPress custom Gutenberg block development with server-side PHP rendering. Includes block registration patterns, media upload integration, multiple item blocks, proper escaping/sanitization, and editor UI best practices. Maintains separation of concerns where editors control content while developers control design.
npx skill4agent add oiler/claude-skills wordpress-blocksinc/blocks/
├── block-name.php # PHP registration and render
├── js/
│ └── block-name.js # Editor JavaScript
└── css/ # Optional
└── block-name.css # Block-specific styles// custom gutenberg blocks
require get_template_directory() . '/inc/blocks/hp-lede.php';/inc/blocks/block-name.php<?php
/**
* Block Name Block
*/
function register_block_name_block() {
register_block_type('theme/block-name', array(
'render_callback' => 'render_block_name_block',
'attributes' => array(
'blockTitle' => array(
'type' => 'string',
'default' => 'Default Title'
),
'blockDescription' => array(
'type' => 'string',
'default' => 'Default description text.'
),
'blockLink' => array(
'type' => 'string',
'default' => '/default-link/'
),
),
));
}
add_action('init', 'register_block_name_block');
function render_block_name_block($attributes) {
// Sanitize and escape all attributes
$block_title = isset($attributes['blockTitle']) ? esc_html($attributes['blockTitle']) : '';
$block_description = isset($attributes['blockDescription']) ? esc_html($attributes['blockDescription']) : '';
$block_link = isset($attributes['blockLink']) ? esc_url($attributes['blockLink']) : '';
ob_start();
?>
<section class="block-name">
<h2><?php echo $block_title; ?></h2>
<p><?php echo $block_description; ?></p>
<a href="<?php echo $block_link; ?>" class="button">Learn More</a>
</section>
<?php
return ob_get_clean();
}
function enqueue_block_name_block_editor_assets() {
wp_enqueue_script(
'block-name-block',
get_template_directory_uri() . '/inc/blocks/js/block-name.js',
array('wp-blocks', 'wp-element', 'wp-editor', 'wp-components'),
filemtime(get_template_directory() . '/inc/blocks/js/block-name.js'),
false
);
}
add_action('enqueue_block_editor_assets', 'enqueue_block_name_block_editor_assets');/inc/blocks/js/block-name.js(function(wp) {
const { registerBlockType } = wp.blocks;
const { TextControl, TextareaControl } = wp.components;
const { createElement: el } = wp.element;
registerBlockType('theme/block-name', {
title: 'Block Name',
icon: 'admin-post',
category: 'common',
attributes: {
blockTitle: {
type: 'string',
default: 'Default Title'
},
blockDescription: {
type: 'string',
default: 'Default description text.'
},
blockLink: {
type: 'string',
default: '/default-link/'
}
},
edit: function(props) {
const { attributes, setAttributes } = props;
return el('div', {
className: 'block-name-editor',
style: { padding: '20px', border: '1px solid #ddd' }
},
el('h3', {}, 'Block Name'),
el(TextControl, {
label: 'Block Title',
value: attributes.blockTitle,
onChange: function(value) {
setAttributes({ blockTitle: value });
}
}),
el(TextareaControl, {
label: 'Description',
value: attributes.blockDescription,
onChange: function(value) {
setAttributes({ blockDescription: value });
},
rows: 4
}),
el(TextControl, {
label: 'Link',
value: attributes.blockLink,
onChange: function(value) {
setAttributes({ blockLink: value });
}
})
);
},
save: function() {
return null; // Using PHP render callback
}
});
})(window.wp);'textField' => array(
'type' => 'string',
'default' => 'Default text'
),el(TextControl, {
label: 'Text Field',
value: attributes.textField,
onChange: function(value) {
setAttributes({ textField: value });
}
})'textareaField' => array(
'type' => 'string',
'default' => 'Default longer text'
),el(TextareaControl, {
label: 'Textarea Field',
value: attributes.textareaField,
onChange: function(value) {
setAttributes({ textareaField: value });
},
rows: 6
})'numberField' => array(
'type' => 'number',
'default' => 0
),el(TextControl, {
label: 'Number Field',
type: 'number',
value: attributes.numberField,
onChange: function(value) {
setAttributes({ numberField: parseInt(value) });
}
})'imageId' => array(
'type' => 'number',
'default' => 0
),
'imageUrl' => array(
'type' => 'string',
'default' => ''
),const { MediaUpload, MediaUploadCheck } = wp.blockEditor;
const { Button } = wp.components;
// In edit function:
el(MediaUploadCheck, {},
el(MediaUpload, {
onSelect: function(media) {
setAttributes({
imageId: media.id,
imageUrl: media.url
});
},
allowedTypes: ['image'],
value: attributes.imageId,
render: function(obj) {
return el('div', { className: 'media-upload-wrapper' },
attributes.imageUrl ?
el('div', {},
el('img', {
src: attributes.imageUrl,
style: { maxWidth: '200px', display: 'block', marginBottom: '10px' }
}),
el(Button, {
onClick: obj.open,
className: 'button'
}, 'Change Image'),
el(Button, {
onClick: function() {
setAttributes({
imageId: 0,
imageUrl: ''
});
},
className: 'button',
style: { marginLeft: '10px' }
}, 'Remove')
) :
el(Button, {
onClick: obj.open,
className: 'button button-primary'
}, 'Upload Image')
);
}
})
)// Get image URL from ID
$image_url = '';
if (isset($attributes['imageId']) && $attributes['imageId']) {
$image_url = wp_get_attachment_image_url(absint($attributes['imageId']), 'full');
} elseif (isset($attributes['imageUrl'])) {
$image_url = esc_url($attributes['imageUrl']);
}
// Render in template
<?php if ($image_url) : ?>
<img src="<?php echo esc_url($image_url); ?>" alt="" class="block-image">
<?php endif; ?>'attributes' => array(
'blockTitle' => array(
'type' => 'string',
'default' => 'Additional Resources'
),
// Item 1
'item1ImageId' => array('type' => 'number', 'default' => 0),
'item1ImageUrl' => array('type' => 'string', 'default' => ''),
'item1Header' => array('type' => 'string', 'default' => 'Item 1 Title'),
'item1Subhead' => array('type' => 'string', 'default' => 'Item 1 description'),
'item1Link' => array('type' => 'string', 'default' => '/item-1/'),
// Item 2
'item2ImageId' => array('type' => 'number', 'default' => 0),
'item2ImageUrl' => array('type' => 'string', 'default' => ''),
'item2Header' => array('type' => 'string', 'default' => 'Item 2 Title'),
'item2Subhead' => array('type' => 'string', 'default' => 'Item 2 description'),
'item2Link' => array('type' => 'string', 'default' => '/item-2/'),
// Item 3
'item3ImageId' => array('type' => 'number', 'default' => 0),
'item3ImageUrl' => array('type' => 'string', 'default' => ''),
'item3Header' => array('type' => 'string', 'default' => 'Item 3 Title'),
'item3Subhead' => array('type' => 'string', 'default' => 'Item 3 description'),
'item3Link' => array('type' => 'string', 'default' => '/item-3/'),
),function renderMediaUpload(itemNum) {
const imageIdAttr = 'item' + itemNum + 'ImageId';
const imageUrlAttr = 'item' + itemNum + 'ImageUrl';
return el(MediaUploadCheck, {},
el(MediaUpload, {
onSelect: function(media) {
const attrs = {};
attrs[imageIdAttr] = media.id;
attrs[imageUrlAttr] = media.url;
setAttributes(attrs);
},
allowedTypes: ['image'],
value: attributes[imageIdAttr],
render: function(obj) {
return el('div', { className: 'media-upload-wrapper' },
attributes[imageUrlAttr] ?
el('div', {},
el('img', {
src: attributes[imageUrlAttr],
style: { maxWidth: '200px', display: 'block', marginBottom: '10px' }
}),
el(Button, {
onClick: obj.open,
className: 'button'
}, 'Change Image'),
el(Button, {
onClick: function() {
const attrs = {};
attrs[imageIdAttr] = 0;
attrs[imageUrlAttr] = '';
setAttributes(attrs);
},
className: 'button',
style: { marginLeft: '10px' }
}, 'Remove')
) :
el(Button, {
onClick: obj.open,
className: 'button button-primary'
}, 'Upload Image')
);
}
})
);
}
// Use in edit function:
el('h4', {}, 'Item 1'),
renderMediaUpload(1),
el(TextControl, {
label: 'Header',
value: attributes.item1Header,
onChange: function(value) {
setAttributes({ item1Header: value });
}
}),
// ... more fields// Text
$title = isset($attributes['title']) ? esc_html($attributes['title']) : '';
// Attributes
$class = isset($attributes['className']) ? esc_attr($attributes['className']) : '';
// URLs
$link = isset($attributes['link']) ? esc_url($attributes['link']) : '';
// Multi-paragraph text (preserves formatting)
$description = isset($attributes['description']) ? wp_kses_post(wpautop($attributes['description'])) : '';// Integers (for image IDs, etc.)
$image_id = isset($attributes['imageId']) ? absint($attributes['imageId']) : 0;
// Numbers
$count = isset($attributes['count']) ? intval($attributes['count']) : 0;// CORRECT:
get_template_directory_uri() . '/inc/blocks/js/block-name.js'
get_template_directory_uri() . '/assets/img/site/hero.jpg'
// Use filemtime for cache busting
filemtime(get_template_directory() . '/inc/blocks/js/block-name.js')wp_enqueue_script(
'block-name',
get_template_directory_uri() . '/inc/blocks/js/block-name.js',
array(
'wp-blocks', // Core block functionality
'wp-element', // React elements
'wp-editor', // Editor components
'wp-components', // UI components
'wp-block-editor' // For MediaUpload
),
filemtime(get_template_directory() . '/inc/blocks/js/block-name.js'),
false // Load in header for editor
);icon: 'admin-post' // Document
icon: 'megaphone' // Announcement/Lede
icon: 'admin-links' // Resources/Links
icon: 'info' // Information
icon: 'warning' // Urgent/Warning
icon: 'games' // Sports/Games
icon: 'awards' // Achievement
icon: 'media-document' // Articleel('div', {
className: 'block-editor',
style: { padding: '20px', border: '1px solid #ddd' }
},
el('h3', {}, 'Block Title'),
el('hr'),
el('h4', {}, 'Section 1'),
// fields...
el('hr'),
el('h4', {}, 'Section 2'),
// more fields...
)el('div', { style: { marginTop: '15px', padding: '10px', backgroundColor: '#f0f0f0' } },
el('strong', {}, 'Preview:'),
el('p', { style: { marginTop: '10px' } }, attributes.description)
)/inc/blocks/hp-lede.php<?php
/**
* HP Lede Block
*/
function register_hp_lede_block() {
register_block_type('theme/hp-lede', array(
'render_callback' => 'render_hp_lede_block',
'attributes' => array(
'ledeHeader' => array(
'type' => 'string',
'default' => ''
),
'ledeSubhed' => array(
'type' => 'string',
'default' => ''
),
'box1Title' => array(
'type' => 'string',
'default' => ''
),
'box1Cta' => array(
'type' => 'string',
'default' => ''
),
'box1Link' => array(
'type' => 'string',
'default' => ''
),
'box2Title' => array(
'type' => 'string',
'default' => ''
),
'box2Cta' => array(
'type' => 'string',
'default' => ''
),
'box2Link' => array(
'type' => 'string',
'default' => ''
),
),
));
}
add_action('init', 'register_hp_lede_block');
function render_hp_lede_block($attributes) {
$lede_header = isset($attributes['ledeHeader']) ? esc_html($attributes['ledeHeader']) : '';
$lede_subhed = isset($attributes['ledeSubhed']) ? esc_html($attributes['ledeSubhed']) : '';
$box1_title = isset($attributes['box1Title']) ? esc_html($attributes['box1Title']) : '';
$box1_cta = isset($attributes['box1Cta']) ? esc_html($attributes['box1Cta']) : '';
$box1_link = isset($attributes['box1Link']) ? esc_url($attributes['box1Link']) : '';
$box2_title = isset($attributes['box2Title']) ? esc_html($attributes['box2Title']) : '';
$box2_cta = isset($attributes['box2Cta']) ? esc_html($attributes['box2Cta']) : '';
$box2_link = isset($attributes['box2Link']) ? esc_url($attributes['box2Link']) : '';
$hero_image = get_template_directory_uri() . '/assets/img/site/hero-image-2.jpg';
ob_start();
?>
<section class="fullwidth-container page-home home-hero bg-yellow">
<div class="lede-image">
<img class="image" src="<?php echo esc_url($hero_image); ?>" alt="">
</div>
<div class="home-hero-text-container">
<div class="home-hero-text">
<h3 class="page-title"><?php echo $lede_header; ?></h3>
<p class="page-subtitle"><?php echo $lede_subhed; ?></p>
</div>
<div class="home-hero-box-container">
<a href="<?php echo $box1_link; ?>" class="home-lede-box box-1">
<h2><?php echo $box1_title; ?></h2>
<button class="button primary rounded red"><?php echo $box1_cta; ?></button>
</a>
<a href="<?php echo $box2_link; ?>" class="home-lede-box box-2">
<h2><?php echo $box2_title; ?></h2>
<button class="button primary rounded white"><?php echo $box2_cta; ?></button>
</a>
</div>
</div>
</section>
<?php
return ob_get_clean();
}
function enqueue_hp_lede_block_editor_assets() {
wp_enqueue_script(
'hp-lede-block',
get_template_directory_uri() . '/inc/blocks/js/hp-lede.js',
array('wp-blocks', 'wp-element', 'wp-editor', 'wp-components'),
filemtime(get_template_directory() . '/inc/blocks/js/hp-lede.js'),
false
);
}
add_action('enqueue_block_editor_assets', 'enqueue_hp_lede_block_editor_assets');/inc/blocks/js/hp-lede.js(function(wp) {
const { registerBlockType } = wp.blocks;
const { TextControl } = wp.components;
const { createElement: el } = wp.element;
registerBlockType('theme/hp-lede', {
title: 'HP Lede',
icon: 'megaphone',
category: 'common',
attributes: {
ledeHeader: {
type: 'string',
default: ''
},
ledeSubhed: {
type: 'string',
default: ''
},
box1Title: {
type: 'string',
default: ''
},
box1Cta: {
type: 'string',
default: ''
},
box1Link: {
type: 'string',
default: ''
},
box2Title: {
type: 'string',
default: ''
},
box2Cta: {
type: 'string',
default: ''
},
box2Link: {
type: 'string',
default: ''
}
},
edit: function(props) {
const { attributes, setAttributes } = props;
return el('div', { className: 'hp-lede-editor' },
el('h3', {}, 'HP Lede Block'),
el('h4', {}, 'Header Section'),
el(TextControl, {
label: 'Lede Header',
value: attributes.ledeHeader,
onChange: function(value) {
setAttributes({ ledeHeader: value });
}
}),
el(TextControl, {
label: 'Lede Subhed',
value: attributes.ledeSubhed,
onChange: function(value) {
setAttributes({ ledeSubhed: value });
}
}),
el('h4', {}, 'Box 1'),
el(TextControl, {
label: 'Box 1 Title',
value: attributes.box1Title,
onChange: function(value) {
setAttributes({ box1Title: value });
}
}),
el(TextControl, {
label: 'Box 1 CTA Text',
value: attributes.box1Cta,
onChange: function(value) {
setAttributes({ box1Cta: value });
}
}),
el(TextControl, {
label: 'Box 1 Link',
value: attributes.box1Link,
onChange: function(value) {
setAttributes({ box1Link: value });
}
}),
el('h4', {}, 'Box 2'),
el(TextControl, {
label: 'Box 2 Title',
value: attributes.box2Title,
onChange: function(value) {
setAttributes({ box2Title: value });
}
}),
el(TextControl, {
label: 'Box 2 CTA Text',
value: attributes.box2Cta,
onChange: function(value) {
setAttributes({ box2Cta: value });
}
}),
el(TextControl, {
label: 'Box 2 Link',
value: attributes.box2Link,
onChange: function(value) {
setAttributes({ box2Link: value });
}
})
);
},
save: function() {
return null;
}
});
})(window.wp);/inc/blocks//inc/blocks/js/register_block_type()esc_html()esc_url()esc_attr()savenullconst { registerBlockType } = wp.blocks;
const { TextControl, TextareaControl, Button } = wp.components;
const { MediaUpload, MediaUploadCheck } = wp.blockEditor;
const { createElement: el } = wp.element;'string' => array('type' => 'string', 'default' => '')
'number' => array('type' => 'number', 'default' => 0)
'boolean' => array('type' => 'boolean', 'default' => false)
'array' => array('type' => 'array', 'default' => [])
'object' => array('type' => 'object', 'default' => {})