Шорткоды в WordPress: как делать правильно в 6.x
Шорткод с WP_Query и wp_enqueue_style внутри колбэка работает до первой страницы без кеша. Разбираем каркас, подключение ассетов, кеширование и четыре подхода с их ценой.

Самый частый шорткод в проектах на WordPress 6.x выглядит так: add_shortcode, внутри WP_Query на двадцать товаров, echo разметки и wp_enqueue_style тут же, в колбэке. Работает — пока страницу не откроют без кеша. Вокруг блока вылезают пустые <p></p>, CSS уезжает в подвал и блок на долю секунды показывается неоформленным, а на странице с тремя такими вставками база получает три лишних набора запросов. Ниже — каркас шорткода, который не придётся переписывать при следующем редизайне, и четыре подхода к той же задаче с понятной ценой каждого.
Каркас: атрибуты, экранирование, return
Атрибуты приходят строками и в нижнем регистре
Парсер шорткодов приводит имена атрибутов к нижнему регистру, поэтому maxItems="3" в редакторе превратится в maxitems и ваш $atts['maxItems'] окажется пустым. Второй капкан: значения всегда строки. highlight="false" — непустая строка, и в обычном if она даёт истину.
<?php
// WordPress 6.x. Файл плагина или functions.php дочерней темы.
add_action( 'init', function () {
add_shortcode( 'blog_price_card', 'blog_render_price_card' );
} );
/**
* [blog_price_card plan="pro" currency="RUB" highlight="true"]текст[/blog_price_card]
*/
function blog_render_price_card( $atts, $content = null, $tag = '' ) {
$atts = shortcode_atts(
array(
'plan' => 'base',
'currency' => 'RUB',
'highlight' => 'false',
),
$atts,
$tag
);
$plan = sanitize_key( $atts['plan'] );
$currency = sanitize_text_field( $atts['currency'] );
// строку "false" в булево значение приводим явно
$highlight = filter_var( $atts['highlight'], FILTER_VALIDATE_BOOLEAN );
$classes = 'price-card price-card--' . $plan;
if ( $highlight ) {
$classes .= ' is-highlighted';
}
$html = '<div class="' . esc_attr( $classes ) . '">';
$html .= '<span>' . esc_html( $currency ) . '</span>';
// вложенные шорткоды внутри $content сами не раскрываются
$html .= '<div>' . do_shortcode( wp_kses_post( (string) $content ) ) . '</div>';
$html .= '</div>';
return $html;
}
Три вещи здесь сделаны не для красоты:
- третий аргумент
shortcode_atts— имя тега. Без него не работает фильтрshortcode_atts_{$tag}, через который чужой плагин или дочерняя тема меняют значения по умолчанию, не трогая ваш код; do_shortcode( $content )— иначе вложенная галерея или кнопка внутри вашего блока выведется как текст в квадратных скобках;- экранирование на выходе:
esc_attrдля атрибутов,esc_htmlдля текста,wp_kses_postдля разметки, пришедшей из редактора. Атрибуты шорткода — это пользовательский ввод: их пишет любой, у кого есть доступ к редактированию записи.
return, а не echo
Колбэк вызывается внутри фильтра the_content, поэтому echo печатает разметку раньше, чем фильтр вернёт результат — визуально блок прыгает в начало контента или вообще выше него. Если рендер живёт в отдельном файле-шаблоне с обычной разметкой, перехватите вывод буфером:
<?php
function blog_render_reviews( $atts ) {
ob_start();
include plugin_dir_path( __FILE__ ) . 'templates/reviews.php';
return ob_get_clean();
}
Стили и скрипты: не в колбэке
К моменту вызова шорткода wp_head уже отработал, так что wp_enqueue_style изнутри колбэка в лучшем случае отправит CSS в подвал страницы. Правильная точка — wp_enqueue_scripts с предварительной проверкой контента через has_shortcode:
<?php
add_action( 'wp_enqueue_scripts', 'blog_maybe_enqueue_price_card' );
function blog_maybe_enqueue_price_card() {
if ( ! is_singular() ) {
return;
}
$post = get_post();
if ( ! $post || ! has_shortcode( $post->post_content, 'blog_price_card' ) ) {
return;
}
wp_enqueue_style(
'blog-price-card',
plugins_url( 'assets/price-card.css', __FILE__ ),
array(),
'1.2.0'
);
wp_enqueue_script(
'blog-price-card',
plugins_url( 'assets/price-card.js', __FILE__ ),
array(),
'1.2.0',
// массив $args c in_footer и strategy — начиная с WordPress 6.3
array( 'in_footer' => true, 'strategy' => 'defer' )
);
}
Цена подхода: has_shortcode смотрит только в post_content переданной записи. Шорткод в виджете, в метаполе, в шаблоне или в контенте, который собирает другой фильтр, так не найдётся. Для таких мест остаётся либо грузить ассет на всех страницах раздела, либо регистрировать стиль заранее и подключать его из колбэка, согласившись на вывод в подвале.
Шорткод — это точка входа для редактора, а не место для бизнес-логики. Если ваш рендер нельзя вызвать из шаблона темы, из блока и из письма, вы написали не функцию, а ловушку для того, кто придёт после вас.
Тяжёлый запрос внутри шорткода
Колбэк выполняется при каждом рендере контента: страница, фид, ответ REST API, предпросмотр в редакторе, выдержки в поиске. Плагин постраничного кеша прячет проблему от анонимных посетителей и ничего не делает для авторизованных. Поэтому запрос к базе внутри шорткода стоит заворачивать в кеш:
<?php
function blog_render_top_products( $atts ) {
$atts = shortcode_atts( array( 'limit' => 5 ), $atts, 'blog_top_products' );
$limit = max( 1, min( 20, absint( $atts['limit'] ) ) );
// атрибуты входят в ключ: иначе limit=5 и limit=20 перетирают друг друга
$cache_key = 'blog_top_products_' . $limit;
$html = get_transient( $cache_key );
if ( false === $html ) {
$query = new WP_Query( array(
'post_type' => 'product',
'posts_per_page' => $limit,
'no_found_rows' => true, // пагинация не нужна, экономим подсчёт总
) );
$html = '<ul class="top-products">';
while ( $query->have_posts() ) {
$query->the_post();
$html .= '<li><a href="' . esc_url( get_permalink() ) . '">'
. esc_html( get_the_title() ) . '</a></li>';
}
$html .= '</ul>';
wp_reset_postdata(); // обязательно: иначе ломается основной цикл страницы
set_transient( $cache_key, $html, 15 * MINUTE_IN_SECONDS );
}
return $html;
}
Платите вы устаревшими данными: новый товар появится в блоке не сразу. Сброс кеша по save_post — отдельная тема, здесь важно только то, что ключ обязан включать все атрибуты, влияющие на результат.
Четыре подхода и их цена
| Подход | Что получаете | Чем платите |
|---|---|---|
| Шорткод «как есть» | 5 минут на реализацию, работает в любом редакторе и в классических темах | Запросы на каждый рендер, в редакторе видна только строка в скобках, ассеты подключаются вслепую |
| Шорткод + транзиент | Стабильное время ответа при тяжёлом запросе, предсказуемая нагрузка на базу | Данные отстают на время жизни кеша, нужна логика сброса и аккуратные ключи |
Динамический блок (register_block_type с block.json и render_callback) | Превью и настройки в редакторе, валидация атрибутов по схеме, корректная работа в блочных темах | Сборка, JS-часть, знание Block API; старые записи с шорткодами всё равно нужно поддерживать |
| Функция-рендер + тонкий шорткод над ней | Один рендер для контента, шаблона, блока и письма; тестируется отдельно от WordPress | Лишний слой и договорённость в команде о том, что логика живёт не в колбэке |
Когда что выбирать
- Вставку делает редактор в произвольном месте текста, данные меняются раз в день — шорткод плюс транзиент.
- Редактору нужно видеть результат и крутить настройки прямо в редакторе, тема блочная — динамический блок, а шорткод оставить как совместимость для старых записей.
- Тот же фрагмент нужен в контенте, в шаблоне и в письме — выносите рендер в функцию, а шорткод делайте обёрткой в три строки.
- В базе уже сотни записей с этим шорткодом — не удаляйте тег.
remove_shortcodeна проде означает, что в тексте статей появятся квадратные скобки.
Что ломается чаще всего
- Пустые абзацы вокруг блока.
wpautopоборачивает переводы строк в<p>, а блочная разметка внутри абзаца закрывает его досрочно. Не оставляйте переносов до и после шорткода в редакторе и не начинайте возвращаемую строку с\n. - Тег не зарегистрирован. Вместо блока видно
[blog_price_card]. Обычно это выключенный плагин, регистрация в хуке, который на этой странице не срабатывает, или дефис в имени тега: документация прямо советует дефисов избегать и ограничиться строчными буквами, цифрами и подчёркиванием. - Коллизия имён.
[button]и[gallery]наверняка уже заняты темой или другим плагином, а побеждает тот, кто зарегистрировался последним. Префикс в имени тега обязателен. - Квадратные скобки в значении атрибута. Парсер обрывает шорткод на первой закрывающей скобке. Если такое значение нужно, передавайте его в содержимом между открывающим и закрывающим тегом, а не в атрибуте. Для вывода самого шорткода как текста есть двойные скобки:
[[blog_price_card]]. - Забытый
wp_reset_postdataпосле собственногоWP_Query— заголовок страницы и комментарии начинают относиться к последнему товару из списка.
Что делать дальше
- Прогоните страницу с шорткодом через Query Monitor с отключённым кешем и сравните число запросов и время генерации с обычной страницей. Это единственный честный способ понять, нужен ли транзиент.
- Проверьте вывод в трёх контекстах: предпросмотр записи, RSS-фид и ответ REST API для этой записи. Шорткоды, завязанные на глобальные переменные страницы, ломаются именно там.
- Если блок уезжает в шаблоны блочной темы, посмотрите Block API в официальной документации: регистрация через
block.jsonдаёт схему атрибутов, которой у шорткода нет в принципе. - Зафиксируйте версию ассетов в
wp_enqueue_styleиwp_enqueue_scriptстрокой, а неnull: иначе после правки CSS посетители увидят старый файл из браузерного кеша.
Транзиент — встроенный в WordPress кеш с временем жизни. По умолчанию значения лежат в таблице опций, а при подключённом объектном кеше (Redis, Memcached) уходят в него и могут быть вытеснены раньше срока. Поэтому код обязан корректно работать и в случае, когда get_transient вернул false через секунду после записи.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Shortcodes — WordPress Plugin Handbook, shortcode_atts() — WordPress Code Reference
Частые вопросы
Вместо блока на странице виден текст [my_shortcode]. С чего начать поиск?
У меня WordPress 5.9, а не 6.x. Код из примеров подойдёт?
Почему вокруг вывода шорткода появляются пустые абзацы и как это убрать?
Обязательно ли переходить с шорткодов на блоки?
Какие подводные камни ждут в проде, если шорткод делает запрос к базе?
Можно ли принимать HTML в атрибутах шорткода?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.