85 phút
Gutenberg Blocks và REST API
Block Editor Basics
Setup plugin cho blocks
npx @wordpress/create-block my-blocks
cd my-blocks
npm start
Block registration
// src/block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-plugin/hero",
"version": "1.0.0",
"title": "Hero Section",
"category": "design",
"icon": "cover-image",
"description": "A hero section with title, subtitle, and button",
"keywords": ["hero", "banner", "header"],
"supports": {
"html": false,
"align": ["wide", "full"],
"color": {
"background": true,
"text": true
},
"spacing": {
"padding": true
}
},
"attributes": {
"title": {
"type": "string",
"default": "Welcome"
},
"subtitle": {
"type": "string",
"default": ""
},
"buttonText": {
"type": "string",
"default": "Learn More"
},
"buttonUrl": {
"type": "string",
"default": ""
},
"backgroundImage": {
"type": "object",
"default": null
}
},
"textdomain": "my-plugin",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css"
}
Edit component
// src/edit.js
import { __ } from '@wordpress/i18n';
import {
useBlockProps,
RichText,
MediaUpload,
MediaUploadCheck,
InspectorControls,
} from '@wordpress/block-editor';
import {
PanelBody,
TextControl,
Button,
} from '@wordpress/components';
export default function Edit({ attributes, setAttributes }) {
const {
title,
subtitle,
buttonText,
buttonUrl,
backgroundImage,
} = attributes;
const blockProps = useBlockProps();
return (
<>
<InspectorControls>
<PanelBody title={__('Button Settings', 'my-plugin')}>
<TextControl
label={__('Button URL', 'my-plugin')}
value={buttonUrl}
onChange={(value) => setAttributes({ buttonUrl: value })}
/>
</PanelBody>
<PanelBody title={__('Background', 'my-plugin')}>
<MediaUploadCheck>
<MediaUpload
onSelect={(media) => setAttributes({
backgroundImage: { id: media.id, url: media.url }
})}
allowedTypes={['image']}
value={backgroundImage?.id}
render={({ open }) => (
<Button onClick={open} variant="secondary">
{backgroundImage
? __('Replace Image', 'my-plugin')
: __('Choose Image', 'my-plugin')}
</Button>
)}
/>
</MediaUploadCheck>
{backgroundImage && (
<Button
onClick={() => setAttributes({ backgroundImage: null })}
variant="link"
isDestructive
>
{__('Remove Image', 'my-plugin')}
</Button>
)}
</PanelBody>
</InspectorControls>
<div
{...blockProps}
style={{
backgroundImage: backgroundImage
? `url(${backgroundImage.url})`
: 'none',
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
>
<div className="hero-overlay">
<RichText
tagName="h1"
value={title}
onChange={(value) => setAttributes({ title: value })}
placeholder={__('Enter title...', 'my-plugin')}
className="hero-title"
/>
<RichText
tagName="p"
value={subtitle}
onChange={(value) => setAttributes({ subtitle: value })}
placeholder={__('Enter subtitle...', 'my-plugin')}
className="hero-subtitle"
/>
<div className="hero-button">
<RichText
tagName="span"
value={buttonText}
onChange={(value) => setAttributes({ buttonText: value })}
className="button-text"
/>
</div>
</div>
</div>
</>
);
}
Save component
// src/save.js
import { useBlockProps, RichText } from '@wordpress/block-editor';
export default function Save({ attributes }) {
const {
title,
subtitle,
buttonText,
buttonUrl,
backgroundImage,
} = attributes;
const blockProps = useBlockProps.save({
style: {
backgroundImage: backgroundImage
? `url(${backgroundImage.url})`
: 'none',
backgroundSize: 'cover',
backgroundPosition: 'center',
},
});
return (
<div {...blockProps}>
<div className="hero-overlay">
<RichText.Content tagName="h1" value={title} className="hero-title" />
<RichText.Content tagName="p" value={subtitle} className="hero-subtitle" />
{buttonText && (
<a href={buttonUrl || '#'} className="hero-button">
<RichText.Content tagName="span" value={buttonText} />
</a>
)}
</div>
</div>
);
}
Block registration trong PHP
<?php
// my-plugin.php
function my_plugin_register_blocks() {
register_block_type(__DIR__ . '/build/hero');
register_block_type(__DIR__ . '/build/pricing-table');
}
add_action('init', 'my_plugin_register_blocks');
function my_plugin_enqueue_editor_assets() {
wp_enqueue_script(
'my-plugin-blocks',
plugins_url('build/index.js', __FILE__),
['wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n'],
filemtime(plugin_dir_path(__FILE__) . 'build/index.js')
);
}
add_action('enqueue_block_editor_assets', 'my_plugin_enqueue_editor_assets');
Dynamic Blocks (PHP render)
// blocks/latest-books/block.json
{
"name": "my-plugin/latest-books",
"title": "Latest Books",
"category": "widgets",
"icon": "book",
"attributes": {
"count": { "type": "number", "default": 5 },
"showExcerpt": { "type": "boolean", "default": true }
},
"render": "file:./render.php"
}
// blocks/latest-books/render.php
<?php
$count = $attributes['count'] ?? 5;
$show_excerpt = $attributes['showExcerpt'] ?? true;
$books = Book_Manager::get_all_books(['limit' => $count]);
$wrapper_attributes = get_block_wrapper_attributes([
'class' => 'latest-books-block',
]);
?>
<div <?php echo $wrapper_attributes; ?>>
<h2><?php esc_html_e('Latest Books', 'my-plugin'); ?></h2>
<ul>
<?php foreach ($books as $book): ?>
<li>
<strong><?php echo esc_html($book->title); ?></strong>
<span>by <?php echo esc_html($book->author); ?></span>
<?php if ($show_excerpt && $book->description): ?>
<p><?php echo esc_html(wp_trim_words($book->description, 20)); ?></p>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>
Custom Block Variations
// src/variations.js
import { registerBlockVariation } from '@wordpress/blocks';
registerBlockVariation('my-plugin/hero', {
name: 'hero-dark',
title: 'Dark Hero',
description: 'Hero with dark background',
attributes: {
backgroundColor: '#000000',
textColor: '#ffffff',
},
isDefault: false,
});
Block Patterns
// register-patterns.php
add_action('init', function () {
register_block_pattern_category('my-plugin', [
'label' => __('My Plugin Patterns', 'my-plugin'),
]);
register_block_pattern('my-plugin/hero-cta', [
'title' => __('Hero with CTA', 'my-plugin'),
'description' => __('A hero section with call to action button', 'my-plugin'),
'categories' => ['my-plugin', 'call-to-action'],
'content' => '
<!-- wp:my-plugin/hero -->
<div class="wp-block-my-plugin-hero">
<div class="hero-overlay">
<h1 class="hero-title">Build Amazing Things</h1>
<p class="hero-subtitle">Get started with our platform today</p>
<a href="#" class="hero-button"><span>Get Started</span></a>
</div>
</div>
<!-- /wp:my-plugin/hero -->
',
]);
});
REST API Integration
// Trong block editor
import apiFetch from '@wordpress/api-fetch';
export default function Edit({ attributes, setAttributes }) {
const [books, setBooks] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
apiFetch({ path: '/wp/v2/posts?per_page=5' })
.then(posts => {
setBooks(posts);
setLoading(false);
})
.catch(err => {
console.error(err);
setLoading(false);
});
}, []);
// ...
}
Block Transforms
// Từ paragraph sang hero
transforms: {
from: [
{
type: 'block',
blocks: ['core/paragraph'],
transform: ({ content }) => {
return createBlock('my-plugin/hero', {
title: content.replace(/<[^>]+>/g, ''),
});
},
},
],
to: [
{
type: 'block',
blocks: ['core/paragraph'],
transform: ({ title, subtitle }) => {
return createBlock('core/paragraph', {
content: `<h1>${title}</h1><p>${subtitle}</p>`,
});
},
},
],
}
Block Toolbar và Contextual Controls
import { BlockControls, AlignmentToolbar } from '@wordpress/block-editor';
import { ToolbarGroup, ToolbarButton } from '@wordpress/components';
import { formatBold } from '@wordpress/icons';
export default function Edit({ attributes, setAttributes }) {
return (
<>
<BlockControls>
<AlignmentToolbar
value={attributes.textAlign}
onChange={(value) => setAttributes({ textAlign: value })}
/>
<ToolbarGroup>
<ToolbarButton
icon={formatBold}
label="Toggle bold"
onClick={() => setAttributes({ bold: !attributes.bold })}
/>
</ToolbarGroup>
</BlockControls>
{/* block content */}
</>
);
}
Inner Blocks
import { useInnerBlocksProps, useBlockProps } from '@wordpress/block-editor';
const TEMPLATE = [
['core/heading', { level: 2, placeholder: 'Section title' }],
['core/paragraph', { placeholder: 'Section content' }],
];
export default function Edit() {
const blockProps = useBlockProps();
const innerBlocksProps = useInnerBlocksProps(blockProps, {
template: TEMPLATE,
allowedBlocks: ['core/heading', 'core/paragraph', 'core/image'],
});
return <div {...innerBlocksProps} />;
}
WP REST API
Đăng ký custom endpoint
add_action('rest_api_init', function () {
register_rest_route('my-plugin/v1', '/books', [
[
'methods' => 'GET',
'callback' => 'my_plugin_rest_get_books',
'permission_callback' => '__return_true',
'args' => [
'per_page' => [
'default' => 10,
'sanitize_callback' => 'absint',
],
'author' => [
'sanitize_callback' => 'sanitize_text_field',
],
],
],
[
'methods' => 'POST',
'callback' => 'my_plugin_rest_create_book',
'permission_callback' => function () {
return current_user_can('edit_posts');
},
],
]);
register_rest_route('my-plugin/v1', '/books/(?P<id>\d+)', [
'methods' => 'DELETE',
'callback' => 'my_plugin_rest_delete_book',
'permission_callback' => function () {
return current_user_can('delete_posts');
},
]);
});
function my_plugin_rest_get_books(WP_REST_Request $request): WP_REST_Response {
$books = Book_Manager::get_all_books([
'limit' => $request->get_param('per_page'),
'author' => $request->get_param('author') ?? '',
]);
$response = new WP_REST_Response($books, 200);
$response->header('X-Total-Count', count($books));
return $response;
}
function my_plugin_rest_create_book(WP_REST_Request $request): WP_REST_Response|WP_Error {
$title = $request->get_param('title');
$author = $request->get_param('author');
if (empty($title) || empty($author)) {
return new WP_Error(
'missing_data',
__('Title and author are required.', 'my-plugin'),
['status' => 400]
);
}
$id = Book_Manager::create_book([
'title' => sanitize_text_field($title),
'author' => sanitize_text_field($author),
'isbn' => sanitize_text_field($request->get_param('isbn') ?? ''),
'year' => (int) ($request->get_param('year') ?? 0),
]);
if (!$id) {
return new WP_Error(
'create_failed',
__('Failed to create book.', 'my-plugin'),
['status' => 500]
);
}
return new WP_REST_Response(Book_Manager::get_book($id), 201);
}
function my_plugin_rest_delete_book(WP_REST_Request $request): WP_REST_Response|WP_Error {
$id = (int) $request['id'];
if (!Book_Manager::get_book($id)) {
return new WP_Error(
'book_not_found',
__('Book not found.', 'my-plugin'),
['status' => 404]
);
}
Book_Manager::delete_book($id);
return new WP_REST_Response(['deleted' => true], 200);
}
Custom post type REST support
register_post_type('book', [
'public' => true,
'show_in_rest' => true, // Enable REST API
'rest_base' => 'books',
'rest_controller_class' => 'WP_REST_Posts_Controller',
'supports' => ['title', 'editor', 'thumbnail', 'custom-fields'],
'taxonomies' => ['genre', 'author'],
]);
// Custom REST field
add_action('rest_api_init', function () {
register_rest_field('book', 'rating', [
'get_callback' => function ($post_array) {
return (float) get_post_meta($post_array['id'], 'rating', true);
},
'update_callback' => function ($value, $post) {
update_post_meta($post->ID, 'rating', (float) $value);
},
'schema' => [
'type' => 'number',
'minimum' => 0,
'maximum' => 5,
],
]);
});
Bài tập thực hành
Hãy tạo custom Gutenberg block và REST API!