Обычные кастомные поля фиксируют страницу заранее. Заголовок, подзаголовок, картинка героя, три «преимущества», форма. Клиент просит поменять цитату и галерею местами, убрать один блок и добавить второй призыв к действию. В группе полей для этого нет кнопки: либо править шаблон PHP, либо плодить поля «на всякий случай», которые потом никто не заполняет.
Flexible Content в ACF PRO решает именно это. Разработчик заранее описывает набор layout’ов (текстовый блок, картинка, галерея, цитата, кнопка). Клиент в редакторе страницы сам добавляет, удаляет и перетаскивает эти блоки. Структура остаётся под контролем темы: нельзя вставить «случайный» виджет и сломать сетку, но порядок секций больше не зашит в шаблон.
Поле входит в ACF PRO вместе с Repeater, Gallery, Clone, Options Pages и ACF Blocks. Бесплатный ACF его не даёт. На момент подготовки материала актуальная ветка плагина: 6.8.9 (релиз 27 августа 2026), требования: WordPress 6.2+, PHP 7.4+.
Зачем Flexible Content, если уже есть кастомные поля
Повторитель (Repeater) тоже умеет добавлять строки, но каждая строка одинаковая: вопрос и ответ, слайд, пункт прайса. Страница лендинга так не устроена. Там чередуются разные секции, и завтра маркетинг захочет другой порядок.
Gutenberg умеет собирать страницу из блоков ядра. Для многих сайтов этого достаточно. Flexible Content нужен, когда вёрстка секций жёстко завязана на тему: своя сетка, свои классы, своя типографика, свои CTA. Редактор выбирает только содержимое из заранее описанных layout’ов, а HTML рисует PHP. Это ближе к конструктору с белым списком блоков, чем к произвольному редактору.
Типичный провал обычных кастомных полей выглядит так:
- Поля
block_1_title,block_2_title,block_3_title. Девятый блок уже некуда вписать без правки темы. - Условная логика «если заполнено поле X, покажи блок Y» разрастается в шаблоне и ломается на пустых значениях.
- Клиент просит «просто поменять два блока местами». Без Flexible Content это задача разработчика, а не правка в админке.
Flexible Content хранит страницу как список строк. У каждой строки свой тип (acf_fc_layout) и свой набор подполей. Порядок строк в метаданных и есть порядок секций на сайте.
Установка ACF PRO и лицензия на безлимит сайтов
ACF PRO ставится отдельно: это не «надстройка» поверх бесплатного плагина, а самостоятельный пакет с тем же кодом плюс поля PRO. Бесплатный ACF и PRO одновременно держать не нужно, данные групп полей при переходе не пропадают.
- Купите лицензию на странице ACF PRO и скачайте ZIP из личного кабинета.
- В админке: Плагины → Добавить новый → Загрузить плагин. Загрузите ZIP и активируйте.
- Введите лицензионный ключ на экране плагинов или в настройках ACF, чтобы получать обновления.
- Перед правками темы сделайте резервную копию файлов и базы. Шаблон конструктора лучше держать в дочерней теме, не в родительской.
Лицензии подписные, цены на официальной странице указаны в USD без налогов. На момент статьи:
- Personal: 1 сайт, 49 $/год.
- Freelancer: 10 сайтов, 149 $/год.
- Agency: безлимит сайтов, 249 $/год.
«Безлимит» здесь про число активаций, не про вечную лицензию. Подписку продлевают, чтобы и дальше получать обновления и функции PRO. Для студии и набора клиентских сайтов на WordPress разумно сразу брать Agency: один ключ, без игры в «какой сайт уже занял слот». Staging и development, по правилам активации ACF, в лимит не входят. Для WordPress multisite лицензия должна покрывать все подсайты, где PRO активирован: Freelancer до 10, Agency без ограничения.
Группа полей Flexible Content
Группа одна, поле внутри одно, layout’ов сколько нужно страницам. Имя поля латиницей, ярлыки layout’ов по-русски: клиент видит ярлык, шаблон опирается на имя.
- Custom Fields → Field Groups → Add New. Название группы: «Конструктор страницы».
- Add Field. Тип поля: Flexible Content. Label: «Блоки страницы». Name:
page_blocks. - Button Label: «Добавить блок». Так клиент не видит техническое «Add Row».
- Minimum Layouts / Maximum Layouts: для лендинга часто хватает минимума 1 и потолка от 20 до 30. Пустая страница без секций и простыня из сотни блоков одинаково вредны.
- Location Rules: Post Type is equal to Page. Если конструктор только у части страниц, привяжите группу к шаблону «Конструктор», а не ко всем страницам.
- В настройках группы можно скрыть редактор контента (Hide on screen → Content Editor), если весь контент идёт через блоки. Либо оставьте
the_content()как лид, а Flexible Content выведите ниже.
page_blocks, кнопка «Добавить блок», Expand All / Collapse All. Группа зарегистрирована из PHP, поэтому в «Группах полей» список может быть пустым.Дальше в самом поле добавляются layout’ы. У каждого: Label (то, что в списке «Добавить блок»), Name (то, что вернёт get_row_layout()), Display (для редактора удобнее Block, не Table), опционально лимит «этот layout не больше N раз на странице».
Пять layout’ов, которых хватает большинству лендингов
Не нужно сразу тащить «герой, таймлайн, команда, прайс, FAQ, карта». Пять секций закрывают типичный корпоративный лендинг. Остальное добавляется по факту, когда появляется реальный макет.
Текстовый блок (text_block)
Поля: title (Text), content (WYSIWYG). Заголовок секции отдельным полем, не внутри редактора: так проще стилизовать и не ловить случайный h1 от клиента.
Блок с картинкой (image_block)
Поля: image (Image, Return Format: Image Array), caption (Text, необязательно), position (Select). У Select задайте значения латиницей, ярлыки по-русски: left / Слева, right / Справа, full / На всю ширину. Если в Value оставить «на всю ширину», sanitize_html_class() в шаблоне отдаст кашу, не CSS-модификатор. Return Format у картинки лучше сразу Array: в шаблоне будут ID, URL, alt и подпись вложения.
Галерея (gallery_block)
Поле gallery типа Gallery (тоже PRO). Return Format: Image Array. Для простого ряда превью этого достаточно. Если нужны подписи к каждому кадру и ссылки, вместо Gallery берите Repeater с Image + Text, но для «просто фотоотчёт» Gallery короче и для клиента понятнее.
Цитата (quote_block)
Поля: quote (Textarea), author (Text), role (Text). Отзыв клиента, выноска из статьи, цитата основателя. Не WYSIWYG: лишняя панель форматирования здесь только мешает.
Кнопка CTA (cta_block)
Поля: heading (Text), text (Textarea), button (Link). Тип Link отдаёт массив url, title, target. Так кнопка ведёт и на внутреннюю страницу, и на внешний лендинг, и открывается в новой вкладке без отдельного чекбокса.
Вывод на фронтенде: have_rows, the_row, get_row_layout
Официальный цикл такой же, как у цикла постов в WordPress: сначала проверка, потом шаг по строкам. have_rows() смотрит, есть ли строки. the_row() переключает текущую. get_row_layout() возвращает Name layout’а. Значения подполей: get_sub_field() (вернуть) или the_sub_field() (сразу напечатать).
Код держите в шаблоне дочерней темы (page.php или отдельный template-builder.php), не в functions.php и не в ядре. Старый the_flexible_field() deprecated с версии 3.3.4: в документации прямо сказано использовать have_rows().
Каркас цикла:
<?php // Поле page_blocks из группы «Конструктор страницы». if ( have_rows( 'page_blocks' ) ) : echo '<div class="page-blocks">'; while ( have_rows( 'page_blocks' ) ) : the_row(); // С ACF 6.5 выключенный layout на фронт не рендерится. $layout = get_row_layout(); if ( 'text_block' === $layout ) : // Текстовый блок. elseif ( 'image_block' === $layout ) : // Картинка. elseif ( 'gallery_block' === $layout ) : // Галерея. elseif ( 'quote_block' === $layout ) : // Цитата. elseif ( 'cta_block' === $layout ) : // CTA. endif; endwhile; echo '</div>'; else : // Строк нет: либо клиент ещё не собрал страницу, либо группа не привязана. endif;
Полный пример для тех же пяти layout’ов. Картинку выводите через wp_get_attachment_image(), ссылку кнопки экранируйте, HTML из WYSIWYG пропускайте через wp_kses_post().
<?php
if ( have_rows( 'page_blocks' ) ) :
echo '<div class="page-blocks">';
while ( have_rows( 'page_blocks' ) ) :
the_row();
$layout = get_row_layout();
if ( 'text_block' === $layout ) :
$title = get_sub_field( 'title' );
$content = get_sub_field( 'content' );
?>
<section class="block block-text">
<?php if ( $title ) : ?>
<h2><?php echo esc_html( $title ); ?></h2>
<?php endif; ?>
<div class="block-text__body">
<?php echo wp_kses_post( $content ); ?>
</div>
</section>
<?php
elseif ( 'image_block' === $layout ) :
$image = get_sub_field( 'image' );
$caption = get_sub_field( 'caption' );
$position = get_sub_field( 'position' );
$mod = $position ? sanitize_html_class( $position ) : 'full';
if ( $image ) :
?>
<figure class="block block-image block-image--<?php echo esc_attr( $mod ); ?>">
<?php echo wp_get_attachment_image( (int) $image['ID'], 'large' ); ?>
<?php if ( $caption ) : ?>
<figcaption><?php echo esc_html( $caption ); ?></figcaption>
<?php endif; ?>
</figure>
<?php
endif;
elseif ( 'gallery_block' === $layout ) :
$gallery = get_sub_field( 'gallery' );
if ( $gallery ) :
echo '<ul class="block block-gallery">';
foreach ( $gallery as $img ) {
echo '<li>';
echo wp_get_attachment_image( (int) $img['ID'], 'medium' );
echo '</li>';
}
echo '</ul>';
endif;
elseif ( 'quote_block' === $layout ) :
$quote = get_sub_field( 'quote' );
$author = get_sub_field( 'author' );
$role = get_sub_field( 'role' );
?>
<blockquote class="block block-quote">
<p><?php echo esc_html( $quote ); ?></p>
<?php if ( $author ) : ?>
<footer>
<cite><?php echo esc_html( $author ); ?></cite>
<?php if ( $role ) : ?>
<span><?php echo esc_html( $role ); ?></span>
<?php endif; ?>
</footer>
<?php endif; ?>
</blockquote>
<?php
elseif ( 'cta_block' === $layout ) :
$heading = get_sub_field( 'heading' );
$text = get_sub_field( 'text' );
$button = get_sub_field( 'button' );
?>
<section class="block block-cta">
<?php if ( $heading ) : ?>
<h2><?php echo esc_html( $heading ); ?></h2>
<?php endif; ?>
<?php if ( $text ) : ?>
<p><?php echo esc_html( $text ); ?></p>
<?php endif; ?>
<?php
if ( $button ) :
$target = ! empty( $button['target'] ) ? $button['target'] : '_self';
$rel = '_blank' === $target ? 'noopener noreferrer' : '';
?>
<a class="btn" href="<?php echo esc_url( $button['url'] ); ?>" target="<?php echo esc_attr( $target ); ?>"<?php echo $rel ? ' rel="' . esc_attr( $rel ) . '"' : ''; ?>>
<?php echo esc_html( $button['title'] ); ?>
</a>
<?php endif; ?>
</section>
<?php
endif;
endwhile;
echo '</div>';
endif;
Когда layout’ов станет больше пяти, ветку if / elseif лучше разобрать на файлы: template-parts/blocks/text_block.php и так далее, а в цикле делать get_template_part( 'template-parts/blocks/' . $layout ). Имя файла совпадает с Name layout’а. Файлы подключаются внутри have_rows(), поэтому get_sub_field() в них работает как раньше. Неизвестный layout просто не выведется, страница не упадёт.
С ACF 6.5 у layout’а в редакторе есть состояние «выключен»: данные остаются, на фронт блок не рендерится. Отдельная проверка в PHP обычно не нужна. Если опираетесь на это поведение, проверяйте актуальную версию PRO и не считайте выключенный layout удалённым. Удаление строки чистит метаданные. Выключение их сохраняет.
Как этим пользуется клиент
В редакторе страница выглядит как стопка карточек. На боевом примере ярлык поля: «Блоки», имя в коде: page_blocks. Каждая карточка: название layout’а, поля этого блока, контролы справа, перетаскивание за заголовок. Порядок в стопке равен порядку секций на сайте. Готовый проект с таким конструктором: acasp.ru, сайт ООО «АСП» по аттестации сварки.
Добавление. Внизу стопки кнопка «Добавить блок». По клику список layout’ов. В учебном наборе это Текст, Картинка, Галерея, Цитата, CTA. На боевом сайте список шире: Герой, Полосы, Карточки, Отзыв, Форма заявки. Выбрали блок, появилась пустая карточка, заполнили, сохранили страницу. На сайте секция встаёт туда, где стоит карточка в стопке.
Перетаскивание. Захватили карточку за ручку (или за заголовок layout’а) и перенесли выше или ниже. Порядок в админке равен порядку на фронте. Менять PHP не нужно. Если карточек много, в ACF 6.5 есть массовое сворачивание и разворачивание layout’ов, плюс подсветка той карточки, в которой сейчас курсор.
Удаление. Крестик на карточке, подтверждение, сохранение. Блок исчезает и из редактора, и с сайта. С 6.5 можно не удалять, а выключить: секция останется в стопке серой, на фронт не выйдет. Удобно для сезонного оффера, который через месяц вернут.
Переименование экземпляра. В 6.5 карточку можно подписать своим именем, например «CTA на консультацию», не трогая Name layout’а. Ключ cta_block в PHP не меняется, клиент просто отличает две одинаковые кнопки на длинной странице.
Что показать клиенту на сдаче: одна страница для пробы со всеми пятью блоками. Пусть сам добавит цитату, перетащит её над галереей, выключит CTA и посмотрит превью. Если на этом шаге человек теряется, ярлыки layout’ов слишком технические или кнопка всё ещё называется Add Row.
Именование, вложенность, производительность
Имена layout’ов
Name: латиница, строчные, подчёркивания. text_block, не Текст и не block-1. Label: по-русски и коротко, так, как говорит клиент: «Текст», «Картинка», «Галерея», «Цитата», «Кнопка». Если в списке «Hero v2 (old)» и «content_module_type_A», конструктором это уже не пользуется никто, кроме автора темы.
Имя поля Flexible Content тоже не меняйте после запуска. От него зависят ключи в wp_postmeta: page_blocks, page_blocks_0_acf_fc_layout, page_blocks_0_title. Переименование поля без миграции оставляет на страницах «пустой» конструктор.
Вложенный Flexible Content
Внутрь layout’а можно положить ещё один Flexible Content или Repeater. Пример: layout «Колонки», внутри Repeater из двух или трёх колонок, в колонке снова набор мелких блоков. Технически это работает: внутренний цикл have_rows( 'columns' ) вызывается уже внутри внешнего the_row().
На практике больше одного уровня вложенности стоит закладывать редко. Админка тяжелеет, клиент теряет место в стопке, а postmeta растёт с каждым уровнем. Для «две колонки текста» достаточно Group или Repeater, не второго Flexible. Повторяющиеся наборы полей (заголовок + текст + кнопка) лучше вынести в Clone, чем копировать подполя в каждый layout.
В ACF 6.8.0 (30 марта 2026) починили очистку вложенных метаданных: при удалении родительского layout’а вложенный Flexible Content забирает свои строки из postmeta. На версиях ниже после «почистить страницу» в базе могли оставаться сироты. Перед большой чисткой конструктора на рабочем сайте всё равно нужна резервная копия базы.
Производительность
Каждое подполе каждой строки: отдельная запись в wp_postmeta. Двадцать секций по восемь полей это уже сотни строк на одну страницу. Вложенный Flexible умножает число строк. Само по себе это не катастрофа, пока страница одна. Ломается, когда конструктор висит на архиве, в цикле десяти постов, без объектного кеша, и каждый have_rows() тянет мета заново.
- Поставьте Maximum Layouts. Страница из 80 секций почти никогда не нужна, а админка от такого стонет.
- Не тащите Gallery на 50 фото в каждый layout «на будущее». Отдельный layout галереи, и только там.
- Включите Local JSON: каталог
acf-jsonв дочерней теме. Группы полей читаются с диска, синхронизируются через Git, меньше зависимость от записей в базе. - На фронте кешируйте страницу целиком (Nginx, плагин кеша). Конструктор меняется при сохранении страницы, не на каждый хит анонимного посетителя.
- Не вызывайте
get_field()по одному подполю вне цикла, если уже идёте черезhave_rows(). Два способа сразу легко рассинхронизировать.
Где обычно ломается вывод: Name layout’а в PHP не совпал с Name в группе (text против text_block), Image вернули URL, а шаблон ждёт массив с ID, внутренний have_rows() вызвали без внешнего the_row(), шаблон правили в родительской теме и его затёрло обновление. Проверка: WP_DEBUG на копии, страница со всеми пятью блоками, перестановка двух карточек, выключенный layout, Query Monitor по числу запросов к postmeta.
Коротко
ACF Flexible Content превращает страницу WordPress в стопку заранее описанных секций. Разработчик задаёт layout’ы и HTML. Клиент собирает порядок в админке: добавить, перетащить, выключить, удалить. Обычные кастомные поля так не умеют, Repeater умеет только одинаковые строки.
Для этого нужен ACF PRO. Если сайтов несколько, лицензия Agency (безлимит активаций, 249 $/год на момент статьи) закрывает вопрос со слотами. На фронте достаточно честного цикла have_rows() / the_row() / get_row_layout() в дочерней теме. Не плодите вложенные flexible «на вырост» и не оставляйте конструктор без потолка по числу блоков.
Набор из текста, картинки, галереи, цитаты и CTA закрывает большую часть лендингов. Новый layout добавляется тогда, когда он реально появился в макете, а не заранее списком из двадцати пунктов. Клиентские страницы собираются так: набор секций в теме, порядок на стороне заказчика.
Если нужен сайт без Elementor, но с гибким конструктором из готовых блоков, стоимость начинается от 60 000 рублей. Срочность, интеграции и нестандартная логика считаются отдельно. Написать можно через контакты, остальные ориентиры в прайсе.