Блоки Gutenberg в WordPress: пять ошибок, которые ломают контент
Поправили обёртку в save() — и редактор показывает «неожидаемый или некорректный контент» на всех страницах с блоком. Разбираем, откуда это берётся и какие решения стоят дешевле других.

Вы поменяли в save() обёртку с section на div, добавили класс, пересобрали ассеты — и редактор встречает вас сообщением «Этот блок содержит неожидаемый или некорректный контент» на каждой странице, где блок уже стоял. Очевидное решение (просто поправить разметку, как в обычном шаблоне темы) в Gutenberg не работает, потому что разметка статического блока — это не шаблон, а данные, уже разложенные по базе. Ниже — пять ошибок, из которых эта самая дорогая, и цена каждого варианта обхода. Всё проверялось на WordPress 6.x, apiVersion 3 и сборке через @wordpress/scripts.
Почему редактор считает ваш блок испорченным
Статический блок хранит готовый HTML прямо в post_content, между комментариями-разделителями вида <!-- wp:studio/callout-card {"message":"..."} -->. Когда вы открываете запись, редактор берёт атрибуты из этого комментария, прогоняет их через текущую функцию save() и сравнивает результат с разметкой, которая лежит в базе. Совпало — блок открылся молча. Не совпало — валидация падает, и вам предлагают «Преобразовать в HTML» или «Восстановить блок».
Сравнение не побайтовое: пробелы, порядок HTML-атрибутов и булевы атрибуты нормализуются. Но новый тег-обёртка, добавленный класс или изменившаяся вложенность — это расхождение дерева, и его хватает.
Любая правка
save()— это миграция данных, а не правка шаблона. Старая разметка уже разъехалась по записям, и новый код с ней не согласован.
Ошибка 1. Статический блок там, где содержимое должно быть свежим
Блок «Последние отзывы», сделанный статически, однажды запечёт три заголовка в HTML страницы — и будет показывать их год. Блок с ценой запечёт цену. Дальше начинается ручная работа: зайти в каждую запись, перевставить блок, сохранить.
У задачи три рабочих решения, и выбор между ними определяет, сколько будет стоить каждая следующая правка вёрстки.
| Критерий | Статический (save в JS) | Динамический (render.php) | ACF-блок |
|---|---|---|---|
| Где живёт разметка | в post_content каждой записи | в PHP-шаблоне | в PHP-шаблоне |
| Правка вёрстки | нужен deprecated, иначе битые блоки | задеплоить файл | задеплоить файл |
| Свежие данные | нет, значения замораживаются | да, считаются на каждый рендер | да |
| Сборка через npm | обязательна | нужна только для редактора | не нужна |
| Цена рендера страницы | нулевая, HTML готов | запросы к БД, нужен кеш | запросы к БД, нужен кеш |
| Кто поддерживает | фронтендер, знающий React | PHP-разработчик плюс немного JS | верстальщик на PHP |
Когда что выбирать
- Статический — если всё содержимое вводит редактор руками и оно не зависит от других сущностей сайта: цитата, callout, кнопка со ссылкой.
- Динамический — если блок что-то запрашивает: посты, товары, пользователя, настройки, текущую дату. Признак простой: в
editвы уже дёргаете данные через@wordpress/data. - ACF-блок — если в команде нет фронтендера под React, а блок нужен вчера. Платите зависимостью от плагина и тем, что поля блока жить будут по правилам ACF, а не ядра.
Динамический блок объявляется полем render в block.json (доступно с WordPress 6.1; в более старых версиях — через render_callback в register_block_type):
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "studio/latest-reviews",
"title": "Последние отзывы",
"category": "widgets",
"textdomain": "studio",
"attributes": {
"count": { "type": "number", "default": 3 }
},
"supports": {
"html": false,
"color": { "background": true, "text": true },
"spacing": { "padding": true, "margin": true },
"typography": { "fontSize": true }
},
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"viewScript": "file:./view.js",
"render": "file:./render.php"
}
Внутри render.php доступны переменные $attributes, $content и $block. Обёртку не собирайте руками — её атрибуты (классы выравнивания, цвета, отступы из supports) отдаёт get_block_wrapper_attributes():
<?php
// render.php, WordPress 6.1+
$count = isset( $attributes['count'] ) ? (int) $attributes['count'] : 3;
$reviews = get_posts( array(
'post_type' => 'review',
'posts_per_page' => $count,
) );
if ( empty( $reviews ) ) {
return;
}
?>
<ul <?php echo get_block_wrapper_attributes(); ?>>
<?php foreach ( $reviews as $review ) : ?>
<li><?php echo esc_html( get_the_title( $review ) ); ?></li>
<?php endforeach; ?>
</ul>
Обратите внимание на "html": false в метаданных. Это отключает пункт «Редактировать как HTML» — самый частый способ, которым контент-менеджер превращает валидный блок в невалидный, случайно сломав обёртку.
Ошибка 2. Правка save() без deprecated
Если статический блок уже в проде, старая разметка никуда не денется. Чтобы редактор узнал её и молча пересохранил в новом виде, прошлые версии save() объявляются в массиве deprecated. Это единственный дешёвый путь: альтернатива — массовая замена в post_content.
// WordPress 6.x, apiVersion 3
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps } from '@wordpress/block-editor';
import metadata from './block.json';
import Edit from './edit';
// Текущая разметка: div и класс callout-card
function save( { attributes } ) {
const blockProps = useBlockProps.save( { className: 'callout-card' } );
return (
<div { ...blockProps }>
<p>{ attributes.message }</p>
</div>
);
}
// Версия, которая уже разошлась по записям: section без класса
const v1 = {
attributes: metadata.attributes,
supports: metadata.supports,
save( { attributes } ) {
return (
<section { ...useBlockProps.save() }>
<p>{ attributes.message }</p>
</section>
);
},
};
registerBlockType( metadata.name, {
edit: Edit,
save,
deprecated: [ v1 ],
} );
Два правила, которые экономят вечер. Первое: в объект устаревшей версии копируйте те attributes и supports, что были на момент той разметки, а не нынешние из block.json — если состав атрибутов менялся, ссылка на metadata подойдёт только для версии с тем же набором. Второе: если атрибуты переименовались или поменяли тип, добавляйте в объект функцию миграции, которая вернёт новые атрибуты. Поведение несовпадающих версий удобно проверять на одной тестовой записи до деплоя.
Ошибка 3. Стили лежат не там, где их ищет редактор
Классика: на фронте блок выглядит как задумано, в редакторе — голый текст. Или наоборот. Разница между тремя полями метаданных не декоративная:
style— подключается и в редакторе, и на фронте. Здесь живёт вёрстка самого блока.editorStyle— только в редакторе. Здесь только то, что нужно для редактирования: подсветка пустого состояния, placeholder, отступы инструментов.viewStyle— только на фронте, для того, что в редакторе мешает (анимации, скрытые по умолчанию элементы).
Второй слой проблемы — iframe. Начиная с WordPress 6.3 канва редактора для блоков с apiVersion 3 рендерится в iframe, и стили, которые вы навесили на enqueue_block_editor_assets в functions.php, внутрь канвы могут не попасть. Надёжный путь — объявлять стили блока в block.json, а стили оформления темы отдавать через theme.json или add_editor_style().
Ошибка 4. Свои цвета и отступы вместо supports
Когда в CSS блока прописан #1a73e8, а в тулбаре блока нет выбора цвета, вы получаете блок, который не переживёт смену оформления и не подчинится глобальным стилям. Правильная цена вопроса: включить нужные возможности в supports — и половина панели настроек появится сама, без вашего React-кода. Выравнивание, цвет, отступы, типографика, тень, anchor — всё это ядро умеет отдавать само, а разметку подставит useBlockProps на клиенте и get_block_wrapper_attributes() на сервере.
Там, где всё же нужен свой CSS, опирайтесь на переменные, которые WordPress генерирует из theme.json — они выглядят как var(--wp--preset--color--primary) и var(--wp--preset--spacing--50). Слаги зависят от темы, так что точные имена смотрите в сгенерированных глобальных стилях конкретного сайта, а не копируйте из чужой статьи. Важная оговорка: включение supports для уже существующего блока меняет сохраняемую разметку, то есть снова требует deprecated.
Ошибка 5. Скрипт фронта подключён хуком темы
Если интерактивность блока живёт в скрипте, подключённом через wp_enqueue_scripts на всех страницах, вы платите лишним запросом на каждой странице без блока — и ловите ошибки инициализации там, где нужных узлов в DOM нет. Поле viewScript в block.json решает это за вас: WordPress подключит скрипт, когда блок действительно отрендерен на странице.
Дескрипторы таких скриптов генерирует ядро, и угадывать их имя для wp_set_script_translations() не стоит — получить их можно из объекта WP_Block_Type, который возвращает register_block_type(); точное имя свойства сверьте в справочнике класса. Сама регистрация блока — один вызов на init, с путём к папке сборки, где лежит block.json:
<?php
add_action( 'init', function () {
// Путь к папке сборки, а не к PHP-файлу
register_block_type( __DIR__ . '/build/callout-card' );
} );
Регистрация раньше init (например, прямо в теле плагина) — вторая по частоте причина блока, который «не появился в списке».
Что всплывает уже в проде
- Переименование поля
name. Для редактора это другой блок: все существующие вставки становятся неизвестными.deprecatedздесь не спасает, нужна замена строк вpost_content. Проще не переименовывать. - Кеш ассетов.
@wordpress/scriptsпишет версию в файл*.asset.php, и ядро использует её как параметр версии. Если вы подключаете ассеты в обходblock.json, браузеры и CDN будут месяцами отдавать старый JS. - Невалидные блоки после обновления темы. Тема, меняющая разметку через фильтры вывода блока, расходится с тем, что ждёт валидатор. Фильтруйте результат рендера, а не сохранённую разметку.
- Динамический блок без кеша. Три запроса к БД в
render.phpна популярной странице — это три запроса на каждый просмотр, если перед сайтом нет страничного кеша.
Что делать дальше
Начните с ревизии: пройдите по своим блокам и проверьте, сколько из них статические при динамических данных — это ваш будущий долг. Добавьте $schema в каждый block.json, чтобы редактор кода подсказывал опечатки в полях. Перед любой правкой save() найдите все записи с блоком (поиск по строке wp:ваш-namespace/имя в post_content, удобно через WP-CLI) и прогоните открытие пары из них на staging. Для динамических блоков поставьте Query Monitor и посмотрите, сколько запросов добавляет блок к рендеру шаблона — это тот показатель, который начнёт болеть первым.
Инвалидация кеша для блоков и Interactivity API — отдельные темы, здесь мы говорили только про регистрацию, валидацию и стили.
Статический блок — блок, чья HTML-разметка формируется функцией save() в JavaScript и сохраняется в базе вместе с текстом записи. Динамический блок — блок, который в базе хранит только атрибуты, а HTML собирает на сервере при каждом показе страницы. deprecated — массив прошлых версий блока, по которым редактор распознаёт старую разметку и переводит её в актуальный вид.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: Block API Reference — WordPress Block Editor Handbook, Metadata in block.json — WordPress Developer Resources
Частые вопросы
Можно ли убрать «неожидаемый или некорректный контент» без ручного обхода страниц?
А если у меня WordPress старее 6.3 — работает ли apiVersion 3 и поле render?
Стили видны на фронте, но в редакторе блок без оформления. Что проверить первым?
Нужно ли деприкейтить динамический блок, если я поменял вёрстку?
Нативный блок или ACF-блок — по какому критерию решать?
Какие подводные камни чаще всего всплывают после деплоя?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.