Сущности WordPress, свои поля и роль functions.php: нормальная модель данных без плагинов
Произвольные поля (post meta) для Custom Post Type не сохраняются или пустые после обновления. Решение через register_post_meta, add_meta_box и save_post с nonce — код, диагностика и типичные ошибки.

Запрос wordpress custom fields functions php или произвольные поля не сохраняются часто приводит к ситуации: вы завели Custom Post Type (CPT), добавили метабокс с полем в админке, при сохранении записи значение не попадает в базу — getpostmeta($postid, 'mykey', true) возвращает пустую строку. Ниже — одна конкретная проблема: почему meta для своего типа записи не пишется в wppostmeta и как это исправить кодом в теме без плагинов (registerpostmeta, nonce, приоритет хука). Если у вас ACF и поле не сохраняется, отдельный сценарий разобран в статье blank" rel="noopener noreferrer">ACF поле не сохраняется в WordPress.
Симптомы:
- В админке у записи CPT есть метабокс с полем, вы вводите значение и нажимаете «Обновить».
- После перезагрузки страницы редактирования поле пустое.
- В базе в таблице
wppostmetaдля этогоpostidнет строки с нужнымmeta_keyили значение не обновилось. - В шаблоне или в коде
getpostmeta($postid, 'repo_url', true)возвращает пусто.
Пример типичной ошибки в коде: сохранение без проверки nonce и без отсечения autosave/revision — в итоге meta перезаписывается пустым при автосохранении или ревизии:
// ❌ Плохо: срабатывает на автосохранении и ревизиях, перетирает meta
add_action('save_post', function ($post_id) {
update_post_meta($post_id, '_repo_url', $_POST['project_repo_url'] ?? '');
});
Почему возникает:
- Не проверяется nonce — запрос может быть подделан; кроме того, без nonce форма могла не отправить поле.
- Хук
savepostсрабатывает на автосохранении и ревизиях — WordPress передаёт в хук ID автосохранения или ревизии; запись в meta идёт «не в тот» пост или перезаписывается пустым$POST. - Ключ meta не зарегистрирован через
registerpostmeta— в современных версиях WordPress для корректной работы REST и админки meta для CPT лучше регистрировать. - Логика в
functions.phpбез разделения — десятки хуков и полей в одном файле приводят к конфликтам приоритетов и случайному перезаписыванию.
Итог: одна статья = одна задача — сделать так, чтобы произвольное поле для CPT стабильно сохранялось и читалось, с путём к файлам и проверкой результата.
Как быстро проверить: откройте запись CPT в админке, введите значение в своё поле и нажмите «Обновить». Перезагрузите страницу редактирования. Если поле пустое — проблема в сохранении (хук, nonce или autosave). Если в админке значение есть, а в шаблоне пусто — проверьте ключ в getpostmeta() и то, что вы читаете тот же postid (в цикле используйте getthe_ID()).
Используем один CPT (например, project) и одно метаполе (URL репозитория). Код разнесён по файлам темы, чтобы не превращать functions.php в свалку.
Стек: WordPress 6.x, PHP 7.4+, тема с доступом к functions.php и папке inc/.
Путь к файлам:
theme/
├── functions.php
├── inc/
│ ├── cpt.php
│ ├── meta.php
1) Регистрация CPT
Файл: inc/cpt.php
<?php
add_action('init', function () {
register_post_type('project', [
'label' => 'Проекты',
'public' => true,
'supports' => ['title', 'editor'],
'has_archive' => true,
'rewrite' => ['slug' => 'projects'],
]);
});
2) Регистрация meta и метабокс
Файл: inc/meta.php
Регистрируем meta для типа project через registerpostmeta, чтобы ключ был известен ядру (REST, админка). Ключ с подчёркиванием в начале (repourl) скрывает поле из блока «Произвольные поля» в редакторе.
<?php
add_action('init', function () {
register_post_meta('project', '_repo_url', [
'type' => 'string',
'single' => true,
'sanitize_callback' => 'esc_url_raw',
'auth_callback' => function ($allowed, $meta_key, $post_id) {
return current_user_can('edit_post', $post_id);
},
]);
});
add_action('add_meta_boxes', function () {
add_meta_box(
'project_repo',
'Репозиторий',
'render_project_repo_box',
'project'
);
});
function render_project_repo_box($post) {
wp_nonce_field('project_repo_save', 'project_repo_nonce');
$value = get_post_meta($post->ID, '_repo_url', true);
?>
<input
type="url"
name="project_repo_url"
value="<?php echo esc_attr($value); ?>"
style="width:100%;"
/>
<?php
}
3) Сохранение с проверкой nonce и без autosave/revision
Тот же файл: inc/meta.php
add_action('save_post_project', function ($post_id) {
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if (wp_is_post_revision($post_id)) {
return;
}
if (!isset($_POST['project_repo_nonce'])
|| !wp_verify_nonce($_POST['project_repo_nonce'], 'project_repo_save')) {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
if (!isset($_POST['project_repo_url'])) {
return;
}
update_post_meta(
$post_id,
'_repo_url',
esc_url_raw($_POST['project_repo_url'])
);
});
4) Подключение в теме
Файл: functions.php
<?php
require_once __DIR__ . '/inc/cpt.php';
require_once __DIR__ . '/inc/meta.php';
5) Вывод в шаблоне
<?php
$repo_url = get_post_meta(get_the_ID(), '_repo_url', true);
if ($repo_url) {
printf(
'<a href="%s" target="_blank" rel="noopener">Репозиторий</a>',
esc_url($repo_url)
);
}
Этого достаточно, чтобы поле стабильно сохранялось и отображалось. Дополнительные варианты (OOP, таксономия): blank" rel="noopener noreferrer">сниппет CPT и таксономия (OOP), blank" rel="noopener noreferrer">сниппет метабокс с безопасным сохранением (OOP).
Почему ключ с подчёркиванием: в редакторе WordPress блок «Произвольные поля» выводит все meta записи. Ключи, начинающиеся с , в этом блоке не показываются — так служебные поля не путаются с теми, что редактор может случайно изменить. Для полей, которые заполняет только ваш код или метабокс, используйте префикс . Третий параметр true в getpostmeta($id, 'repourl', true) означает «вернуть одно значение»; без него вернётся массив (нужно, если по одному ключу хранится несколько значений).
- Открыть запись типа «Проекты».
- Ввести URL в поле «Репозиторий», нажать «Обновить».
- Обновить страницу редактирования — значение должно остаться.
Подставьте свой post_id (например, 123):
wp post meta get 123 _repo_url
Ожидаемый вывод: строка с URL, например https://github.com/user/repo. Пустой вывод означает, что meta не записалось — тогда смотрите раздел «Типичные ошибки».
В шаблоне single для project или в цикле:
<?php
$value = get_post_meta(get_the_ID(), '_repo_url', true);
var_dump($value); // должно вывести сохранённый URL
После проверки var_dump уберите.
1. Хук на save_post без типа поста и без отсечения autosave/revision
Срабатывает на всех типах записей и на автосохранении — meta перетирается. Используйте savepostproject (или свой тип) и в начале обработчика проверяйте DOINGAUTOSAVE и wpispostrevision($post_id).
2. Не проверяется nonce
Без wpverifynonce и проверки currentusercan('editpost', $postid) возможна подмена запроса и запись meta от имени другого поста. Всегда проверяйте nonce и права.
3. Ключ meta без подчёркивания показывается в «Произвольных полях»
Редактор выводит все meta в блоке «Произвольные поля»; ключи с префиксом там не показываются. Для служебных полей используйте repourl, а не repourl.
4. Логика в одном большом functions.php
Сотни строк в functions.php усложняют отладку и порядок хуков. Выносите регистрацию CPT и meta в отдельные файлы и подключайте через require_once, как в блоке выше.
5. Читаете meta по другому ключу
Убедитесь, что в getpostmeta($postid, 'repourl', true) ключ совпадает с тем, что в updatepostmeta. Третий параметр true у getpost_meta возвращает одно значение (строку).
Если после правок meta по-прежнему не сохраняется: убедитесь, что хук именно savepostproject (с суффиксом типа записи), а не общий savepost. Проверьте, что в форме есть wpnoncefield() с тем же action, что и в wpverifynonce(). Временно отключите другие плагины и переключитесь на тему Twenty Twenty-Four — если поле начнёт сохраняться, конфликт в теме или плагине. Для отладки добавьте в начало обработчика savepostproject строку errorlog('savepostproject: ' . $post_id); и убедитесь, что хук срабатывает при нажатии «Обновить», а не только при автосохранении.
- Prod — решение предназначено для боевого сайта: регистрация meta, nonce и проверка прав обязательны.
- Docker / локальный стенд — те же файлы; удобно проверить через WP-CLI и отключить лишние плагины, чтобы исключить конфликт хуков.
- CI/CD — можно добавить smoke-тест: создание поста типа
project, запись meta через WP-CLI или код, чтение обратно и проверка значения.
Резюме: одна проблема — meta для CPT не сохраняется. Решение — регистрация meta через registerpostmeta, метабокс с nonce, хук savepost{posttype} с отсечением autosave и revision и проверкой прав. Структура темы: functions.php только подключает inc/cpt.php и inc/meta.php. Проверка — админка, WP-CLI, при необходимости временный vardump в шаблоне. Типичные ошибки — общий savepost, отсутствие nonce, ключ без , один огромный functions.php, несовпадение ключа при чтении. Для нескольких метаполей повторите блок registerpostmeta, метабокс и ветку в обработчике savepostproject для каждого ключа. Если CPT зарегистрирован в теме и вы планируете менять тему позже — данные в БД останутся, но тип записей в админке пропадёт, пока не подключите плагин или mu-plugin с той же регистрацией; для долгоживущих проектов регистрацию CPT и meta часто выносят в плагин. В REST API зарегистрированные через registerpostmeta поля автоматически доступны для чтения и записи при корректных authcallback и sanitizecallback.
Дополнительно по теме: blank" rel="noopener noreferrer">WordPress Core Web Vitals 2026: ускорение без магии, blank" rel="noopener noreferrer">Migrations Plugin: управление миграциями БД WordPress.