За 16 лет работы с WordPress создал более 150 кастомных блоков — от простых карточек до сложных систем с real-time данными. Первые блоки писал на jQuery, переходил на Backbone, потом на React. В 2025 году разработка Gutenberg блоков достигла зрелости — инструментарий мощный, API стабильные, но документация все еще оставляет желать лучшего. Сегодня разберу продвинутые техники, которые редко описаны в официальных гайдах.
Динамические блоки: серверный vs клиентский рендеринг
Когда нужен server-side rendering
На проекте новостного портала создавал блок «Популярные статьи». Первая реализация через REST API в редакторе работала, но генерировала 3 дополнительных запроса на каждое открытие поста для редактирования. Миграция на серверный рендеринг сократила время загрузки редактора с 4.2 до 1.8 секунды.
SSR оправдан когда:
- Данные меняются часто (статистика, погода, курсы валют)
- Нужен доступ к данным, недоступным через REST API
- Требуются сложные WP_Query с учетом прав пользователя
- Критична безопасность (скрытие логики от клиента)
Правильная реализация динамического блока
block.json с render callback:
json<code>{ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "name": "mytheme/popular-posts", "title": "Popular Posts", "category": "widgets", "icon": "chart-bar", "attributes": { "numberOfPosts": { "type": "number", "default": 5 }, "category": { "type": "string", "default": "" }, "timeRange": { "type": "string", "default": "week", "enum": ["day", "week", "month", "year", "all"] }, "showExcerpt": { "type": "boolean", "default": true }, "showThumbnail": { "type": "boolean", "default": true } }, "textdomain": "mytheme", "editorScript": "file:./index.js", "editorStyle": "file:./index.css", "style": "file:./style-index.css", "render": "file:./render.php" } </code>render.php — серверная логика:
php<code><?php <em>/** </em><em> * @var array $attributes Block attributes </em><em> * @var string $content Block default content </em><em> * @var WP_Block $block Block instance </em><em> */</em> $number_of_posts = isset($attributes['numberOfPosts']) ? $attributes['numberOfPosts'] : 5; $category = isset($attributes['category']) ? $attributes['category'] : ''; $time_range = isset($attributes['timeRange']) ? $attributes['timeRange'] : 'week'; $show_excerpt = isset($attributes['showExcerpt']) ? $attributes['showExcerpt'] : true; $show_thumbnail = isset($attributes['showThumbnail']) ? $attributes['showThumbnail'] : true; <em>// Определение временного диапазона</em> $date_query = array(); switch ($time_range) { case 'day': $date_query = array('after' => '1 day ago'); break; case 'week': $date_query = array('after' => '1 week ago'); break; case 'month': $date_query = array('after' => '1 month ago'); break; case 'year': $date_query = array('after' => '1 year ago'); break; } $args = array( 'posts_per_page' => $number_of_posts, 'post_status' => 'publish', 'orderby' => 'meta_value_num', 'meta_key' => 'post_views_count', 'order' => 'DESC', 'ignore_sticky_posts' => true, ); if (!empty($category)) { $args['category_name'] = $category; } if (!empty($date_query)) { $args['date_query'] = array($date_query); } <em>// Кэширование запроса</em> $cache_key = 'popular_posts_' . md5(serialize($args)); $posts = wp_cache_get($cache_key); if (false === $posts) { $query = new WP_Query($args); $posts = $query->posts; wp_cache_set($cache_key, $posts, '', 3600); <em>// 1 час</em> } if (empty($posts)) { return '<div class="popular-posts-empty">No popular posts found.</div>'; } $wrapper_attributes = get_block_wrapper_attributes(array( 'class' => 'popular-posts-block' )); ?> <div <?php echo $wrapper_attributes; ?>> <ul class="popular-posts-list"> <?php foreach ($posts as $post): ?> <li class="popular-post-item"> <?php if ($show_thumbnail && has_post_thumbnail($post->ID)): ?> <div class="popular-post-thumbnail"> <a href="<?php echo get_permalink($post->ID); ?>"> <?php echo get_the_post_thumbnail($post->ID, 'thumbnail'); ?> </a> </div> <?php endif; ?> <div class="popular-post-content"> <h3 class="popular-post-title"> <a href="<?php echo get_permalink($post->ID); ?>"> <?php echo esc_html($post->post_title); ?> </a> </h3> <?php if ($show_excerpt): ?> <div class="popular-post-excerpt"> <?php echo wp_trim_words(get_the_excerpt($post->ID), 20); ?> </div> <?php endif; ?> <div class="popular-post-meta"> <span class="post-views"> <?php echo number_format_i18n(get_post_meta($post->ID, 'post_views_count', true)); ?> views </span> <span class="post-date"> <?php echo human_time_diff(get_the_time('U', $post->ID), current_time('timestamp')); ?> ago </span> </div> </div> </li> <?php endforeach; ?> </ul> </div> </code>Edit компонент с ServerSideRender:
jsx<code>import { __ } from '@wordpress/i18n'; import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; import { PanelBody, RangeControl, SelectControl, ToggleControl, Spinner } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import ServerSideRender from '@wordpress/server-side-render'; export default function Edit({ attributes, setAttributes }) { const blockProps = useBlockProps(); <em>// Получение списка категорий</em> const categories = useSelect((select) => { return select('core').getEntityRecords('taxonomy', 'category', { per_page: -1, orderby: 'name', order: 'asc' }); }, []); const categoryOptions = [ { label: __('All Categories', 'mytheme'), value: '' }, ...(categories || []).map((cat) => ({ label: cat.name, value: cat.slug })) ]; return ( <> <InspectorControls> <PanelBody title={__('Settings', 'mytheme')}> <RangeControl label={__('Number of Posts', 'mytheme')} value={attributes.numberOfPosts} onChange={(value) => setAttributes({ numberOfPosts: value })} min={1} max={20} /> <SelectControl label={__('Category', 'mytheme')} value={attributes.category} options={categoryOptions} onChange={(value) => setAttributes({ category: value })} /> <SelectControl label={__('Time Range', 'mytheme')} value={attributes.timeRange} options={[ { label: __('Last 24 Hours', 'mytheme'), value: 'day' }, { label: __('Last Week', 'mytheme'), value: 'week' }, { label: __('Last Month', 'mytheme'), value: 'month' }, { label: __('Last Year', 'mytheme'), value: 'year' }, { label: __('All Time', 'mytheme'), value: 'all' } ]} onChange={(value) => setAttributes({ timeRange: value })} /> <ToggleControl label={__('Show Excerpt', 'mytheme')} checked={attributes.showExcerpt} onChange={(value) => setAttributes({ showExcerpt: value })} /> <ToggleControl label={__('Show Thumbnail', 'mytheme')} checked={attributes.showThumbnail} onChange={(value) => setAttributes({ showThumbnail: value })} /> </PanelBody> </InspectorControls> <div {...blockProps}> <ServerSideRender block="mytheme/popular-posts" attributes={attributes} LoadingResponsePlaceholder={() => ( <div className="loading-placeholder"> <Spinner /> <p>{__('Loading popular posts...', 'mytheme')}</p> </div> )} ErrorResponsePlaceholder={({ response }) => ( <div className="error-placeholder"> <p>{__('Error loading posts. Please check your settings.', 'mytheme')}</p> {response?.message && <small>{response.message}</small>} </div> )} EmptyResponsePlaceholder={() => ( <div className="empty-placeholder"> <p>{__('No popular posts found.', 'mytheme')}</p> </div> )} /> </div> </> ); } </code>Оптимизация ServerSideRender
Стандартный компонент ServerSideRender имеет проблему — при каждом изменении атрибута показывает spinner, что раздражает. Решение через debounce:
jsx<code>import { useState, useEffect, useCallback } from '@wordpress/element'; import { debounce } from '@wordpress/compose'; import ServerSideRender from '@wordpress/server-side-render'; function OptimizedServerSideRender({ block, attributes, ...props }) { const [debouncedAttributes, setDebouncedAttributes] = useState(attributes); <em>// Debounce на 300ms</em> const updateAttributes = useCallback( debounce((newAttrs) => { setDebouncedAttributes(newAttrs); }, 300), [] ); useEffect(() => { updateAttributes(attributes); }, [attributes, updateAttributes]); return ( <ServerSideRender block={block} attributes={debouncedAttributes} {...props} /> ); } </code>useSelect и useDispatch: управление состоянием
Получение данных из WordPress stores
Самый мощный инструмент для работы с данными в Gutenberg — хуки useSelect и useDispatch:
jsx<code>import { useSelect, useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { store as editorStore } from '@wordpress/editor'; import { store as blockEditorStore } from '@wordpress/block-editor'; function AdvancedBlockEdit({ attributes, setAttributes, clientId }) { <em>// Получение данных из нескольких stores одновременно</em> const { post, postType, categories, media, blocks, isBlockSelected } = useSelect((select) => { const { getCurrentPost, getCurrentPostType } = select(editorStore); const { getEntityRecords, getMedia } = select(coreStore); const { getBlocks, isBlockSelected: checkSelected } = select(blockEditorStore); return { post: getCurrentPost(), postType: getCurrentPostType(), categories: getEntityRecords('taxonomy', 'category', { per_page: -1 }), media: attributes.mediaId ? getMedia(attributes.mediaId) : null, blocks: getBlocks(), isBlockSelected: checkSelected(clientId) }; }, [attributes.mediaId, clientId]); <em>// Диспатч действий</em> const { editPost } = useDispatch(editorStore); const { insertBlocks, removeBlock } = useDispatch(blockEditorStore); <em>// Пример: обновление мета-поля поста</em> const updatePostMeta = (key, value) => { editPost({ meta: { [key]: value } }); }; <em>// Пример: программная вставка блока</em> const insertRelatedContentBlock = () => { const newBlock = createBlock('mytheme/related-content', { postId: post.id }); <em>// Вставка после текущего блока</em> const rootBlocks = blocks; const currentIndex = rootBlocks.findIndex(block => block.clientId === clientId); insertBlocks(newBlock, currentIndex + 1); }; <em>// ... остальная логика блока</em> } </code>Продвинутый пример: синхронизация с custom post meta
jsx<code>import { useSelect, useDispatch } from '@wordpress/data'; import { useEffect } from '@wordpress/element'; import { store as editorStore } from '@wordpress/editor'; function MetaSyncedBlock({ attributes, setAttributes }) { const { meta, isSaving } = useSelect((select) => { const editor = select(editorStore); return { meta: editor.getEditedPostAttribute('meta'), isSaving: editor.isSavingPost() }; }, []); const { editPost } = useDispatch(editorStore); <em>// Синхронизация атрибутов блока с мета-полями</em> useEffect(() => { if (meta && meta.custom_title !== attributes.title) { setAttributes({ title: meta.custom_title || '' }); } }, [meta, attributes.title, setAttributes]); const handleTitleChange = (newTitle) => { <em>// Обновляем и атрибут блока, и мета-поле</em> setAttributes({ title: newTitle }); editPost({ meta: { custom_title: newTitle } }); }; return ( <div className="meta-synced-block"> <TextControl label="Title (synced with post meta)" value={attributes.title} onChange={handleTitleChange} disabled={isSaving} /> {isSaving && <Spinner />} </div> ); } </code>Block Context: передача данных между блоками
Создание родительского блока с контекстом
Block Context API позволяет передавать данные от родительского блока к дочерним без prop drilling:
Parent block (Container):
json<code><em>// block.json</em> { "name": "mytheme/product-container", "title": "Product Container", "category": "widgets", "attributes": { "productId": { "type": "number" }, "showPricing": { "type": "boolean", "default": true }, "currency": { "type": "string", "default": "USD" } }, "providesContext": { "mytheme/productId": "productId", "mytheme/showPricing": "showPricing", "mytheme/currency": "currency" }, "supports": { "html": false } } </code>jsx<code><em>// edit.js</em> import { InnerBlocks, useBlockProps, InspectorControls } from '@wordpress/block-editor'; import { PanelBody, SelectControl, ToggleControl } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; const ALLOWED_BLOCKS = [ 'mytheme/product-title', 'mytheme/product-image', 'mytheme/product-price', 'mytheme/product-description' ]; const TEMPLATE = [ ['mytheme/product-image'], ['mytheme/product-title'], ['mytheme/product-price'], ['mytheme/product-description'] ]; export default function Edit({ attributes, setAttributes }) { const blockProps = useBlockProps(); <em>// Получение списка продуктов</em> const products = useSelect((select) => { return select(coreStore).getEntityRecords('postType', 'product', { per_page: -1 }); }, []); const productOptions = products ? [ { label: 'Select a product', value: 0 }, ...products.map(product => ({ label: product.title.rendered, value: product.id })) ] : []; return ( <> <InspectorControls> <PanelBody title="Product Settings"> <SelectControl label="Product" value={attributes.productId} options={productOptions} onChange={(productId) => setAttributes({ productId: parseInt(productId) })} /> <ToggleControl label="Show Pricing" checked={attributes.showPricing} onChange={(showPricing) => setAttributes({ showPricing })} /> <SelectControl label="Currency" value={attributes.currency} options={[ { label: 'USD', value: 'USD' }, { label: 'EUR', value: 'EUR' }, { label: 'GBP', value: 'GBP' } ]} onChange={(currency) => setAttributes({ currency })} /> </PanelBody> </InspectorControls> <div {...blockProps}> {attributes.productId === 0 ? ( <div className="product-container-placeholder"> <p>Please select a product from the sidebar.</p> </div> ) : ( <InnerBlocks allowedBlocks={ALLOWED_BLOCKS} template={TEMPLATE} templateLock="all" /> )} </div> </> ); } </code>Дочерний блок, использующий контекст
Child block (Product Price):
json<code><em>// block.json</em> { "name": "mytheme/product-price", "title": "Product Price", "category": "widgets", "parent": ["mytheme/product-container"], "usesContext": [ "mytheme/productId", "mytheme/showPricing", "mytheme/currency" ], "attributes": { "showLabel": { "type": "boolean", "default": true } } } </code>jsx<code><em>// edit.js</em> import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; import { PanelBody, ToggleControl, Spinner } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; export default function Edit({ attributes, setAttributes, context }) { const blockProps = useBlockProps(); <em>// Получение данных продукта из контекста</em> const productId = context['mytheme/productId']; const showPricing = context['mytheme/showPricing']; const currency = context['mytheme/currency']; <em>// Загрузка продукта</em> const { product, isResolving } = useSelect((select) => { const { getEntityRecord, isResolving: checkResolving } = select(coreStore); return { product: productId ? getEntityRecord('postType', 'product', productId) : null, isResolving: checkResolving('getEntityRecord', ['postType', 'product', productId]) }; }, [productId]); if (!showPricing) { return ( <div {...blockProps}> <p className="pricing-hidden-notice"> Pricing is hidden by parent block settings </p> </div> ); } if (isResolving) { return ( <div {...blockProps}> <Spinner /> </div> ); } if (!product) { return ( <div {...blockProps}> <p className="no-product-notice">No product selected</p> </div> ); } const price = product.meta?.price || 0; const formattedPrice = new Intl.NumberFormat('en-US', { style: 'currency', currency: currency }).format(price); return ( <> <InspectorControls> <PanelBody title="Price Display"> <ToggleControl label="Show Label" checked={attributes.showLabel} onChange={(showLabel) => setAttributes({ showLabel })} /> </PanelBody> </InspectorControls> <div {...blockProps}> <div className="product-price"> {attributes.showLabel && ( <span className="price-label">Price: </span> )} <span className="price-amount">{formattedPrice}</span> </div> </div> </> ); } </code>InnerBlocks: вложенные структуры
Продвинутые паттерны с InnerBlocks
На проекте лендинга создавал блок «Feature Grid» с динамическим количеством колонок:
jsx<code>import { InnerBlocks, useBlockProps, InspectorControls } from '@wordpress/block-editor'; import { PanelBody, RangeControl, SelectControl } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { createBlock } from '@wordpress/blocks'; import { useEffect } from '@wordpress/element'; export default function Edit({ attributes, setAttributes, clientId }) { const blockProps = useBlockProps({ className: `columns-${attributes.columns}` }); <em>// Получение текущих innerBlocks</em> const { innerBlocks } = useSelect((select) => { return { innerBlocks: select(blockEditorStore).getBlocks(clientId) }; }, [clientId]); const { replaceInnerBlocks } = useDispatch(blockEditorStore); <em>// Автоматическое добавление/удаление колонок при изменении их количества</em> useEffect(() => { const currentCount = innerBlocks.length; const desiredCount = attributes.columns; if (currentCount === desiredCount) return; let newBlocks = [...innerBlocks]; if (currentCount < desiredCount) { <em>// Добавляем недостающие блоки</em> const blocksToAdd = desiredCount - currentCount; for (let i = 0; i < blocksToAdd; i++) { newBlocks.push(createBlock('mytheme/feature-item')); } } else { <em>// Удаляем лишние блоки</em> newBlocks = newBlocks.slice(0, desiredCount); } replaceInnerBlocks(clientId, newBlocks); }, [attributes.columns, innerBlocks.length, clientId]); const ALLOWED_BLOCKS = ['mytheme/feature-item']; return ( <> <InspectorControls> <PanelBody title="Grid Settings"> <RangeControl label="Number of Columns" value={attributes.columns} onChange={(columns) => setAttributes({ columns })} min={1} max={6} /> <SelectControl label="Gap Size" value={attributes.gap} options={[ { label: 'Small', value: 'small' }, { label: 'Medium', value: 'medium' }, { label: 'Large', value: 'large' } ]} onChange={(gap) => setAttributes({ gap })} /> </PanelBody> </InspectorControls> <div {...blockProps}> <InnerBlocks allowedBlocks={ALLOWED_BLOCKS} orientation="horizontal" renderAppender={false} /> </div> </> ); } </code>Block Variations: создание пресетов
Динамические вариации блока
Block Variations позволяют создавать пресконфигурированные версии блока:
js<code>import { registerBlockVariation } from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; <em>// Регистрация вариаций для блока кнопки</em> registerBlockVariation('core/button', { name: 'cta-primary', title: __('Primary CTA', 'mytheme'), description: __('Primary call-to-action button', 'mytheme'), icon: 'megaphone', attributes: { text: __('Get Started', 'mytheme'), className: 'is-style-primary-cta', backgroundColor: 'primary', textColor: 'white', fontSize: 'large', borderRadius: 50 }, scope: ['inserter', 'transform'] }); registerBlockVariation('core/button', { name: 'cta-secondary', title: __('Secondary CTA', 'mytheme'), attributes: { text: __('Learn More', 'mytheme'), className: 'is-style-secondary-cta', style: { border: { width: '2px' } } } }); <em>// Вариации с innerBlocks</em> registerBlockVariation('core/group', { name: 'hero-section', title: __('Hero Section', 'mytheme'), description: __('Pre-configured hero section layout', 'mytheme'), icon: 'cover-image', attributes: { align: 'full', className: 'hero-section', style: { spacing: { padding: { top: '80px', bottom: '80px' } } } }, innerBlocks: [ ['core/heading', { level: 1, content: __('Welcome to Our Site', 'mytheme'), textAlign: 'center' }], ['core/paragraph', { content: __('Your journey starts here', 'mytheme'), align: 'center', fontSize: 'large' }], ['core/buttons', { layout: { type: 'flex', justifyContent: 'center' } }, [ ['core/button', { text: __('Get Started', 'mytheme') }] ]] ], scope: ['inserter'] }); </code>Interactivity API: интерактивные блоки
Современный подход к интерактивности
В WordPress 6.5+ появился Interactivity API — декларативный подход к созданию интерактивных блоков без jQuery:
view.js — логика взаимодействия:
js<code>import { store, getContext } from '@wordpress/interactivity'; const { state } = store('mytheme/accordion', { state: { get isAnyOpen() { return state.items.some(item => item.isOpen); }, items: [] }, actions: { toggle: () => { const context = getContext(); context.isOpen = !context.isOpen; <em>// Закрытие других элементов (accordion behavior)</em> if (context.isOpen && context.closeOthers) { state.items.forEach((item, index) => { if (index !== context.index) { item.isOpen = false; } }); } }, openAll: () => { state.items.forEach(item => { item.isOpen = true; }); }, closeAll: () => { state.items.forEach(item => { item.isOpen = false; }); } }, callbacks: { logToggle: () => { const context = getContext(); console.log(`Item ${context.index} toggled:`, context.isOpen); } } }); </code>render.php — разметка с директивами:
php<code><?php $unique_id = wp_unique_id('accordion-'); $items = isset($attributes['items']) ? $attributes['items'] : array(); <em>// Инициализация состояния</em> wp_interactivity_state('mytheme/accordion', array( 'items' => array_map(function($item, $index) { return array( 'isOpen' => false, 'index' => $index ); }, $items, array_keys($items)) )); $wrapper_attributes = get_block_wrapper_attributes(array( 'class' => 'accordion-block', 'data-wp-interactive' => 'mytheme/accordion' )); ?> <div <?php echo $wrapper_attributes; ?>> <div class="accordion-controls"> <button data-wp-on--click="actions.openAll" class="accordion-control-btn" > <?php _e('Open All', 'mytheme'); ?> </button> <button data-wp-on--click="actions.closeAll" class="accordion-control-btn" > <?php _e('Close All', 'mytheme'); ?> </button> </div> <div class="accordion-items"> <?php foreach ($items as $index => $item): ?> <div class="accordion-item" data-wp-context='<?php echo wp_json_encode(array( 'isOpen' => false, 'index' => $index, 'closeOthers' => $attributes['closeOthers'] ?? true )); ?>' > <button class="accordion-header" data-wp-on--click="actions.toggle" data-wp-on-async--click="callbacks.logToggle" data-wp-class--is-open="context.isOpen" aria-expanded="false" data-wp-bind--aria-expanded="context.isOpen" > <span><?php echo esc_html($item['title']); ?></span> <svg class="accordion-icon" data-wp-class--rotated="context.isOpen" width="20" height="20" viewBox="0 0 20 20" > <path d="M5 7.5l5 5 5-5" stroke="currentColor" /> </svg> </button> <div class="accordion-content" data-wp-bind--hidden="!context.isOpen" data-wp-class--is-visible="context.isOpen" > <div class="accordion-content-inner"> <?php echo wp_kses_post($item['content']); ?> </div> </div> </div> <?php endforeach; ?> </div> </div> </code>Продвинутая типизация с TypeScript
Typed блок с полной type-safety
typescript<code><em>// types.ts</em> import { BlockEditProps } from '@wordpress/blocks'; export interface ProductAttributes { productId: number; showPrice: boolean; showDescription: boolean; layout: 'grid' | 'list'; columns: number; } export interface Product { id: number; title: { rendered: string; }; content: { rendered: string; }; meta: { price: number; sku: string; stock: number; }; featured_media: number; } export type ProductBlockEditProps = BlockEditProps<ProductAttributes>; <em>// edit.tsx</em> import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; import { PanelBody, RangeControl, ToggleControl } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import type { ProductBlockEditProps, Product } from './types'; export default function Edit({ attributes, setAttributes }: ProductBlockEditProps): JSX.Element { const blockProps = useBlockProps(); const product = useSelect<Product | null>( (select) => { if (!attributes.productId) return null; return select(coreStore).getEntityRecord<Product>( 'postType', 'product', attributes.productId ); }, [attributes.productId] ); const handleColumnsChange = (columns: number): void => { setAttributes({ columns }); }; return ( <> <InspectorControls> <PanelBody title="Settings"> <RangeControl label="Columns" value={attributes.columns} onChange={handleColumnsChange} min={1} max={4} /> <ToggleControl label="Show Price" checked={attributes.showPrice} onChange={(showPrice: boolean) => setAttributes({ showPrice })} /> </PanelBody> </InspectorControls> <div {...blockProps}> {product && ( <div className="product-display"> <h3>{product.title.rendered}</h3> {attributes.showPrice && ( <span className="price">${product.meta.price}</span> )} </div> )} </div> </> ); } </code>Продвинутая разработка Gutenberg блоков в 2025 — это симбиоз React, WordPress API и глубокого понимания архитектуры редактора. Динамические блоки с серверным рендерингом решают проблемы производительности, useSelect/useDispatch дают полный контроль над данными, Block Context элегантно передает информацию между блоками, а Interactivity API делает блоки по-настоящему интерактивными без костылей. Начинайте с простых статических блоков, постепенно добавляйте динамику, изучайте TypeScript для type-safety, используйте Interactivity API для фронтенда. Каждый проект — это шаг к пониманию всей мощи современного Gutenberg.