Шорткод — это короткая метка вида [my_block], которую вы вставляете в запись или страницу, а WordPress на лету заменяет её на сгенерированный HTML. Это самый дешёвый способ добавить сайту «функциональность блоком», не трогая редактор и не плодя страницы. Вы наверняка уже пользовались чужими: [gallerys], [contact-form-7-new], [woocommerce_products]. Разберём, как устроен свой шорткод изнутри: одна функция на add_shortcode(), атрибуты через shortcode_atts(), как не словить XSS на пользовательском вводе, почему шорткод нельзя вызывать из мета-поля «в лоб» и когда одной функции в functions.php пора вырастать в плагин.
Часть I. Анатомия одного шорткода
Всё, что нужно для простейшего шорткода, — функция и одна строка регистрации.
<?php
// functions.php темы (или в плагине — см. Часть V)
function cs_my_note( $atts, $content = null ) {
return '<p class="cs-note">' . esc_html( trim( (string) $content ) ) . '</p>';
}
add_shortcode( 'cs_note', 'cs_my_note' );В записи пишете: [cs_note]Важная мысль[/cs_note] — на выходе <p class="cs-note">Важная мысль</p>.
Что WordPress передаёт в функцию
$atts— массив атрибутов из тега ([cs_note color="red"]→['color' => 'red']). Безshortcode_atts()там будут только переданные ключи, поэтому дефолты задаём сами (Часть II).$content— текст между открывающим и закрывающим тегом. Для самозакрываемого[cs_note]этоnull.
Правило №1: return, а не echo
Функция шорткода обязана возвращать строку. Если написать echo, вывод уйдёт «в буфер раньше времени» — и вместо метки в тексте окажется пустое место, а HTML выпадет в начало страницы (потому что WordPress собирает контент через буфер). Классический баг «шорткод вывелся над шапкой» — это ровно echo вместо return.
Если очень нужно печатать (чужой рендер, ob_start()), делайте так:
function cs_my_note( $atts, $content = null ) {
ob_start();
// ... echo / template part ...
return ob_get_clean();
}Чек-лист части
- Понимаю, что шорткод = функция + `add_shortcode(тег, callback)`.
- Возвращаю строку через `return`, не `echo`.
- Знаю, что `$content` приходит между парных тегов, `$atts` — из атрибутов.
Часть II. Атрибуты через shortcode_atts()
Атрибуты делают шорткод переиспользуемым: [cs_note color="warn" icon="1"]. Рабочий инструмент — shortcode_atts() (не путать с wp_parse_args(), это другое): он сливает переданные пользователем значения с вашими дефолтами и возвращает полный набор.
<?php
function cs_button( $atts ) {
$a = shortcode_atts(
array(
'url' => '#', // дефолт, если атрибут не передали
'label' => 'Подробнее',
'style' => 'primary',
),
$atts,
'cs_button' // 3-й аргумент = идентификатор для фильтра
);
$class = 'cs-btn cs-btn--' . sanitize_html_class( $a['style'] );
$href = esc_url( $a['url'] );
$text = esc_html( $a['label'] );
return '<a class="' . $class . '" href="' . $href . '">' . $text . '</a>';
}
add_shortcode( 'cs_button', 'cs_button' );Ключевые детали:
- В
$a['style']всегда есть значение, даже если пользователь не написалstyle— падений «undefined index» нет. - Третий аргумент
shortcode_atts(..., 'cs_button')даёт хукshortcode_atts_cs_button, через который кто-то (или вы) может подменить дефолты точечно. - Все три значения пришли извне — значит каждое проходит свой фильтр:
esc_urlдля href,esc_htmlдля текста,sanitize_html_class/esc_attrдля class. Это мост к Части III.
Типы атрибутов, о которых забывают
Атрибут шорткода всегда приходит строкой. 'count' => '5' — это строка, а не число. Приводите явно: (int) $a['count'], (bool) $a['show'] (но bool из строки 'false' — тоже true! Сравнивайте со списком разрешённых: in_array($a['show'], ['1','yes','true'])). Тот же урок, что и валидация ввода в формах (см. XSS и CSRF).
Чек-лист части
- Задаю дефолты через `shortcode_atts()`, а не проверяю «есть ли ключ».
- Помню: атрибут приходит строкой → явно привожу к `(int)`/проверяю на allowlist.
- Экранирую каждое значение из атрибута (Часть III).
Часть III. Безопасность: шорткод — это тоже ввод
Атрибуты и контент шорткода приходят из поста, а пост мог писать кто-то, кому вы дали редактора. Значит шорткод — не «доверенный шаблон», а точка входа чужих данных, и XSS-конвенции к нему применяются те же.
Правило: какой HTML возвращаете — тем фильтруете
| Куда идёт значение | Чем чистить |
|---|---|
| текст внутри тега | esc_html() |
URL в href/src | esc_url() (он ещё отрезает javascript:) |
в атрибуты class/data-*/id | esc_attr() / sanitize_html_class() |
| «разрешённая разметка» от автора | wp_kses( $html, $allowed_args ) |
| целая rich-строка с тегов | wp_kses_post() |
Плохой шорткод, который вставляет атрибут в class как есть:
// ❌ дыра: [cs_note class="a\" onclick=\"alert(1)"]
return '<div class="' . $atts['class'] . '">…</div>';Хороший:
// ✅
return '<div class="' . esc_attr( $atts['class'] ) . '">…</div>';Не собирайте SQL и не передавайте аттрибут в базу
Если шорткод что-то вытаскивает (WP_Query, $wpdb), атрибут идёт как параметр/массив аргументов, а не «вклеивается в строку запроса». WP_Query(['category_name' => $a['cat']]) безопасен; "WHERE cat = {$a['cat']}" — нет. Та же инъекция, что в SQL-статье.
Контент ($content) тоже экранируется, но аккуратно
В $content человек мог написать обычный текст с разметкой. esc_html() съест его теги в текст; wp_kses_post() оставит только разрешённые WordPress-теги — чаще это то, что нужно. Если внутри $content должны работать вложенные шорткоды — см. Часть IV.
Чек-лист части
- Каждое значение из `$atts`/`$content` фильтрую по назначению (URL/атрибут/текст/kses).
- Атрибуты не попадают в строки SQL — только в аргументы `WP_Query`/подготовки.
- Возвращаемый HTML собран из экранированных кусков.
Часть IV. Вложенный контент и do_shortcode()
Тело между тегами — второй аргумент
[cs_box]Какой-то текст с **курсивом**[/cs_box]
$content = Какой-то текст с **курсивом** (строка, Markdown WordPress не конвертит — это ваш HTML). Хотите дать автору форматирование — верните wpautop( $content ) (добавит <p>), но всё равно прогоните через wp_kses_post().
Чтобы вложенные шорткоды сработали — зовите do_shortcode()
WordPress парсит шорткоды в контенте поста, но когда вы передаёте кусок текста своей функции-хелперу (не через обычный рендер), вложенные шорткоды в нём не раскроются сами:
function cs_accordion( $atts, $content = null ) {
// раскрываем шорткоды внутри тела (например [panel]…[/panel])
$inner = do_shortcode( (string) $content );
return '<div class="cs-accordion">' . $inner . '</div>';
}
add_shortcode( 'cs_accordion', 'cs_accordion' );Без do_shortcode() строка [panel]…[/panel] уйдёт на экран литералом.
«Почему шорткод не работает в виджете / заголовке / мета-поле?»
do_shortcode() — часть пайплайна the_content. В других местах (виджет-текст, the_title, ACF-поле, кастомная область темы) контент не прогоняется через парсер. Решение — вызвать самому:
<?php
echo do_shortcode( get_field('cta') ); // метаполе ACF
// или фильтр точечно:
add_filter( 'widget_text', 'do_shortcode' );Помните про XSS: вводить пользовательский шорткод в метаполе можно только если вы доверяете автору поля; echo без фильтра — возвращаемся к Части III.
Чек-лист части
- Для тела между парных тегов использую `$content`, а не пытаюсь распарсить сам.
- Вложенные шорткоды — через `do_shortcode( $content )`.
- В виджетах/заголовках/метаполях вызываю `do_shortcode()` руками (и экранирую источник).
Часть V. Когда functions.php превращается в плагин
Один-два шорткода живут в functions.php темы нормально. Плохо становится, когда:
- Тема обновляется и затирает
functions.php(если это не child-тема) — и весь ваш код пропадает прямо на проде. Плагин так не теряет. - Добавились настройки на странице админки, несколько тегов, AJAX-эндпоинты, активация/деактивация.
- Функциональность хочет жить независимо от темы (сменили Kadence на другую — она осталась).
Минимальный скелет плагина
<?php
/**
* Plugin Name: CodeStack Shortcodes
* Description: Набор шорткодов: [cs_note], [cs_button], [cs_box].
* Version: 1.0
* Author: CodeStack
*/
if ( ! defined( 'ABSPATH' ) ) exit; // прямой доступ к файлу — сразу выход
// регистрация шорткодов
add_shortcode( 'cs_note', 'cs_render_note' );
add_shortcode( 'cs_button', 'cs_render_button' );
function cs_render_note( $atts, $content = null ) {
return '<p class="cs-note">' . wp_kses_post( (string) $content ) . '</p>';
}Что здесь важно:
- Заголовок-комментарий (
Plugin Name:…) — по нему WordPress показывает плагин в списке «Плагины». if ( ! defined( 'ABSPATH' ) ) exit;— защита от прямого вызова файла через браузер.- Активация/деактивация (создать таблицу,Flush rewrite rules) —
register_activation_hook( __FILE__, 'cs_activate' ). ⚠️ По конвенции в хуках загрузки/__constructне вешают сетевые вызовы иsetcookie()— только лениво, в AJAX/cron (это же правило работает и в ваших инструментах Ветки B).
Структура «как у людей»
cs-shortcodes/
cs-shortcodes.php ← заголовок + add_shortcode()
inc/render.php ← функции cs_render_*
assets/shortcodes.css ← стили (enqueue, а не <style> в теле)
templates/note.php ← шаблон для сложной разметки
Стили/скрипты подключайте через wp_enqueue_* на wp_enqueue_scripts, а не выводите inline в return — иначе получите дублирование на каждой странице с шорткодом (см. про enqueue в Веб-компонентах и CSS-архитектуре).
Чек-лист части
- Держу логику, выходящую за пару шорткодов, в плагине, а не в `functions.php`.
- Есть `Plugin Name`-заголовок и `ABSPATH`-гейт.
- Активацию/таблицы/rewrite rules делаю через `register_*_activation_hook` лениво.
- Стили/JS — через enqueue, не inline в возвращаемом HTML.
Что дальше
Шорткод — это мост между «хочу блок с выводом» и «не трогаю редактор». Когда таких блоков становится несколько и они начинают тянуть зависимости (БД, HTTP), следующий шаг — оформить каждый как маленький инструмент со своей логикой и проверкой ввода; ровно так устроены плагины этой ветки (аудиторы конфигов, генераторы, калькуляторы). А чистоту шорткод-функций удобно держать тестами: чистая cs_render_* без побочных эффектов — идеальный кандидат на юнит-тест, потому что вход (атрибуты/контент) и выход (HTML) можно проверять без поднятого WordPress.
Материал написан простым языком; любой англоязычный термин (шорткод, тег, атрибут, хук, колбэк,
return/echo, buffer, экранирование,esc_*,wp_kses, allowlist,do_shortcode, enqueue, child-тема) объясняется при первом употреблении. Примеры — для WordPress 6.x, PHP 8; имена функций и префиксы (cs_*) условные, подставляйте свои; домены/пути —example.test. Регистрировать шорткод черезadd_shortcode()лучше наinit(или просто на загрузке плагина), а не в шаблоне страницы — иначе повторный вызов на той же странице удвоит хуки. Gutenberg-блоки (block editor) шорткодами не являются — это отдельная система (атрибуты+inspector+save()); здесь только классический[shortcode], потому что он работает и в Gutenberg, и в виджетах, и в ACF без extra-инфраструктуры.
Инструменты под задачу
Функция-хелпер или шорткод с кучей однотипной обвязки (shortcode_atts, экранирование, ob_start) — рутина. Опишите словами, что должен делать шорткод («сетка из 3 колонок с миниатюрами последних 5 записей, сортировка по дате»), укажите контекст (тема, есть ли WooCommerce) — и генератор вернёт готовый PHP для functions.php: с атрибутами по умолчанию, безопасным выводом и примером использования. Это ровно та обвязка, которую вы потом переносите в плагин по Части V.
Генератор снимает рутину синтаксиса, но ответственность за экранирование и «вводимый HTML» остаётся вашей (Часть III): прогоните [my_shortcode param="<svg onload=x>"] и проверьте, что вывод чистый, прежде чем класть шорткод в прод.
👤 Зарегистрируйтесь, чтобы добавить материал в закладки.
