Шорткоды WordPress своими руками: от одной функции до маленького плагина

Шорткод — это короткая метка вида [my_block], которую вы вставляете в запись или страницу, а WordPress на лету заменяет её на сгенерированный HTML. Это самый дешёвый способ добавить сайту «функциональность блоком», не трогая редактор и не плодя страницы. Вы наверняка уже пользовались чужими: [gallerys], [contact-form-7-new], [woocommerce_products]. Разберём, как устроен свой шорткод изнутри: одна функция на add_shortcode(), атрибуты через shortcode_atts(), как не словить XSS на пользовательском вводе, почему шорткод нельзя вызывать из мета-поля «в лоб» и когда одной функции в functions.php пора вырастать в плагин.

Часть I. Анатомия одного шорткода

Всё, что нужно для простейшего шорткода, — функция и одна строка регистрации.

PHP
<?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()), делайте так:

PHP
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
<?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/srcesc_url() (он ещё отрезает javascript:)
в атрибуты class/data-*/idesc_attr() / sanitize_html_class()
«разрешённая разметка» от автораwp_kses( $html, $allowed_args )
целая rich-строка с теговwp_kses_post()

Плохой шорткод, который вставляет атрибут в class как есть:

PHP
// ❌ дыра: [cs_note class="a\" onclick=\"alert(1)"]
return '<div class="' . $atts['class'] . '">…</div>';

Хороший:

PHP
// ✅
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 парсит шорткоды в контенте поста, но когда вы передаёте кусок текста своей функции-хелперу (не через обычный рендер), вложенные шорткоды в нём не раскроются сами:

PHP
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
<?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 темы нормально. Плохо становится, когда:

  1. Тема обновляется и затирает functions.php (если это не child-тема) — и весь ваш код пропадает прямо на проде. Плагин так не теряет.
  2. Добавились настройки на странице админки, несколько тегов, AJAX-эндпоинты, активация/деактивация.
  3. Функциональность хочет жить независимо от темы (сменили Kadence на другую — она осталась).

Минимальный скелет плагина

PHP
<?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.

🧩 Генератор шорткодов
Гость
Токены: 0 / 50 000
Гостевой лимит сбрасывается раз в месяц. Зарегистрируйтесь — лимит выше.
Последние записи с миниатюрами Кнопка со стилями Форма обратной связи FAQ-аккордеон Информация о пользователе
0 / 10 000
Результат
Опишите задачу и нажмите «Сгенерировать шорткод». Описание, PHP-код, пример использования и таблица атрибутов появятся здесь.
×

Лимит токенов исчерпан

Ваш бесплатный лимит закончился.

Генератор снимает рутину синтаксиса, но ответственность за экранирование и «вводимый HTML» остаётся вашей (Часть III): прогоните [my_shortcode param="<svg onload=x>"] и проверьте, что вывод чистый, прежде чем класть шорткод в прод.

👤 Зарегистрируйтесь, чтобы добавить материал в закладки.