Wordpress
- Разработка темы
- sitemap.xml
- Ядро wordpress
- Как работают is_category() и is_tag()
- Установка $wp_query->set_404()
- Можно ли убрать link rel="shortlink" из блока <head> страницы?
- Архитектура поддержки переводов интерфейсов на другие языки для wordpress в целом и в связке с React
- Что такое TRANSIENT?
- Ядро wordpress, hooks
- Плагины
- Admin-панель wordpress
Разработка темы
Создание 404 страницы
Создайте файл 404.php в папке темы.
Минимальный пример 404.php
<?php get_header(); ?>
<main class="container">
<h1>404</h1>
<p>Страница не найдена.</p>
<a href="<?php echo home_url(); ?>">Вернуться на главную</a>
</main>
<?php get_footer(); ?>
4️⃣ Нужно ли что-то настраивать в админке?
Нет. WordPress сам:
-
определяет, что страница не существует
-
отправляет HTTP-статус 404
-
подключает файл
404.php, если он есть
Если файла нет — используется index.php. и отсутствие страниц в буфере результатов поиска нужно определять там.
sitemap.xml
Общие сведения
Начиная с WordPress 5.5, встроенный sitemap доступен по адресу:
Он формируется классом WP_Sitemaps и управляется через фильтры. Никакой отдельный файл в теме не создаётся — всё генерируется динамически.
Ниже — основные способы управления содержимым.
1️⃣ Полностью отключить sitemap
2️⃣ Убрать конкретный тип записей (например, post)
Пример для кастомного типа:
3️⃣ Убрать таксономию (категории, теги и т.д.)
Убрать категории:
4️⃣ Убрать пользователей из sitemap
5️⃣ Изменить список ссылок (фильтрация конкретных URL)
Можно отфильтровать сами записи:
6️⃣ Изменить данные конкретной записи в sitemap
7️⃣ Добавить собственный sitemap
Можно зарегистрировать свой провайдер:
(для продакшена обычно создают отдельный класс-провайдер)
🔥 Где размещать код?
-
В
functions.phpтемы -
Или лучше — в собственном мини-плагине
⚠️ Важно
Если установлен SEO-плагин вроде:
-
Yoast SEO
-
Rank Math
-
All in One SEO
то встроенный sitemap WordPress обычно отключается, и управление происходит через плагин.
Работа с провайдерами sitemap
В контексте WordPress провайдер sitemap — это класс, который отвечает за генерацию определённого набора URL внутри XML-карты сайта.
Проще говоря:
Провайдер = источник данных для sitemap.
📌 Как это устроено внутри WordPress
Начиная с версии 5.5 в WordPress появился класс:
WP_Sitemaps
Он управляет всей системой sitemap и подключает провайдеры.
Каждый провайдер отвечает за свой тип данных.
По умолчанию есть три:
| Провайдер | Что генерирует |
|---|---|
posts |
записи, страницы, custom post types |
taxonomies |
категории, теги, кастомные таксономии |
users |
архивы авторов |
🔎 Пример структуры
Это индекс. В нём ссылки на:
Каждый из этих файлов формируется отдельным провайдером.
📦 Что делает провайдер технически
Каждый провайдер — это класс, наследующий:
Он обязан реализовать два метода:
🔹 get_url_list()
Возвращает массив URL для текущей страницы sitemap.
🔹 get_max_num_pages()
Возвращает количество страниц sitemap (если URL много).
🧠 Простая аналогия
Представьте sitemap как каталог:
-
Индекс — оглавление
-
Провайдер — глава каталога
-
get_url_list() — список страниц в главе
💡 Почему это удобно
Провайдеры позволяют:
-
отключить стандартные данные
-
добавить собственные URL
-
изменить логику выборки
-
добавить кастомный sitemap (например, для API или фильтров)
✅ Отключаем стандартные провайдеры
Добавьте в functions.php или в плагин:
add_filter('wp_sitemaps_add_provider', function ($provider, $name) {
// отключаем стандартные: posts, taxonomies, users
if (in_array($name, ['posts', 'taxonomies', 'users'])) {
return false;
}
return $provider;
}, 10, 2);
✅ Создаём свой провайдер
Создадим собственный класс.
class My_Custom_Sitemap_Provider extends WP_Sitemaps_Provider {
public function __construct() {
$this->name = 'custom';
$this->object_type = 'custom';
}
/**
* Количество страниц sitemap
*/
public function get_max_num_pages( $subtype = '' ) {
return 1;
}
/**
* Список URL для sitemap
*/
public function get_url_list( $page_num, $subtype = '' ) {
$urls = [];
$args = [
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => -1
];
$posts = get_posts($args);
foreach ($posts as $post) {
$urls[] = [
'loc' => get_permalink($post),
'lastmod' => get_the_modified_date('c', $post),
];
}
return $urls;
}
}
✅ Регистрируем провайдер
add_action('init', function () {
wp_register_sitemap_provider(
'custom',
new My_Custom_Sitemap_Provider()
);
});
🔎 Что получится
Теперь будет доступен:
И он будет содержать только ваши данные.
✅Основные способы управления содержимым провайдера
1️⃣ Полностью отключить sitemap
2️⃣ Убрать конкретный тип записей (например, post)
add_filter('wp_sitemaps_post_types', function ($post_types) {
unset($post_types['post']); // убираем записи return $post_types;
});
Пример для кастомного типа:
3️⃣ Убрать таксономию (категории, теги и т.д.)
dd_filter('wp_sitemaps_taxonomies', function ($taxonomies) {
unset($taxonomies['post_tag']); // убрать теги return $taxonomies;
});
Убрать категории:
4️⃣ Убрать пользователей из sitemap
add_filter('wp_sitemaps_add_provider', function ($provider, $name) {
if ($name === 'users') {
return false;
} return $provider;
}, 10, 2);
5️⃣ Изменить список ссылок (фильтрация конкретных URL)
Можно отфильтровать сами записи:
add_filter('wp_sitemaps_posts_query_args', function ($args, $post_type) {
if ($post_type === 'post') {
$args['meta_query'] = array( array( 'key' => 'exclude_from_sitemap', 'compare' => 'NOT EXISTS' ) );
}
return $args;
}, 10, 2);
6️⃣ Изменить данные конкретной записи в sitemap
add_filter('wp_sitemaps_posts_entry', function ($entry, $post, $post_type) {
$entry['priority'] = 0.8;
$entry['changefreq'] = 'weekly';
return $entry;
}, 10, 3);
Создание кастомного sitemap.xml
В статье рассматривается следующий сценарий:
- Сайт имеет только одну страницу - главную, остальные страницы это посты.
- На сайте установлен самописный плагин, который обрабатывает содержимое страницы или поста и можно получить либо русскую версию текста, либо английскую
- Русская версия текста вызывается обычной ссылкой на страницу или пост:
https://example.com/post-slug
- Английская версия вызывается добавлением к обычной ссылке окончания "/en":
https://example.com/post-slug/en
- Главная страница обязательно имеет английскую версию
- Для постов имеется поле ACF - "is_english", которое определяет, есть ли у страницы английская версия, если английской версии нет, то при ее запросе отображается 404 страница
- Для сайта формируется кастомный файл sitemap, который будет показан поисковикам через robots.txt или явным указанием в настройках индексирования.
- На сайте не используются плагины SEO
Отключение стандартного wp-sitemap.xml
add_filter('wp_sitemaps_enabled', '__return_false');
Регистрация кастомного sitemap.xml
Ядро wordpress
Как работают is_category() и is_tag()
Они становятся true, если текущий запрос определён как:
-
архив категории →
is_category() === true -
архив метки →
is_tag() === true
🔎 Что происходит при запросе архива
Например URL:
После rewrite WordPress превращает это во внутренний запрос:
или
Далее WP_Query анализирует параметры и выставляет флаги:
или
И только после этого начинают работать:
⚠ Важно: где они работают корректно
-
после того как WordPress разобрал запрос
-
внутри
pre_get_posts -
внутри
template_redirect -
внутри шаблонов
❌ Они НЕ работают корректно внутри init, потому что запрос ещё не разобран.
📌 Почему лучше проверять через $query
В pre_get_posts безопаснее писать так:
add_action('pre_get_posts', function ($query) {
if (is_admin() || !$query->is_main_query()) {
return;
}
if (
get_query_var('lang') === 'en' &&
( $query->is_category() || $query->is_tag() )
) {
...
}
});
Почему?
Потому что:
-
глобальная
is_category()работает через$wp_query -
внутри
pre_get_postsмы должны работать с конкретным$query
Это более правильно архитектурно.
📌 Что именно проверяет is_category()
По сути это проверка:
Если хотите проверить руками:
💡 Итог
is_category() и is_tag():
-
зависят от разобранного запроса
-
работают после
parse_query -
безопасны в
pre_get_postsи позже -
не зависят от вашего
lang, если rewrite правильный
Установка $wp_query->set_404()
После выполнения:
$wp_query->set_404();
WordPress:
-
устанавливает
$wp_query->is_404 = true -
сбрасывает другие флаги (is_single, is_page, is_archive и т.д.)
-
меняет внутреннее состояние запроса
✅ Как проверить, что запрос стал 404
🔹 1. Через условную функцию (рекомендуется)
if ( is_404() ) {
// это 404
}
Это самый правильный способ.
🔹 2. Через объект $wp_query
global $wp_query;
if ( $wp_query->is_404 ) {
// это 404
}
или
if ( $wp_query->is_404() ) {
// тоже работает
}
📌 Важно
После set_404():
$wp_query->is_single → false
$wp_query->is_page → false
$wp_query->is_archive → false
$wp_query->is_category → false
$wp_query->is_tag → false
$wp_query->is_404 → true
То есть WP "переквалифицирует" запрос полностью.
⚠ Частая ошибка
Если вы проверяете это внутри pre_get_posts, то is_404() работать не будет корректно, потому что:
-
pre_get_postsвыполняется ДО финального определения типа запроса -
404 логика применяется позже
Проверять 404 корректно в:
template_redirect
wp
template_include
🧠 Пример
add_action('template_redirect', function() {
if ( is_404() ) {
error_log('Это 404 страница');
}
});
🎯 Если нужно понять: вы сами установили 404 или WordPress
Можно добавить свой флаг:
global $wp_query;
$wp_query->set_404();
$wp_query->custom_en_404 = true;
И потом:
if ( !empty($wp_query->custom_en_404) ) {
// это именно ваша логика
}
Можно ли убрать link rel="shortlink" из блока <head> страницы?
Да, shortlink можно и лучше убрать, для SEO он не нужен и иногда мешает диагностике.
🔎 Что такое rel="shortlink"
<link rel="shortlink" href="https://voronkov.org/?p=108" />
Это:
-
техническая «короткая ссылка» WordPress
-
нужна была для старых мобильных клиентов и API
-
не используется поисковиками для SEO
Google и Яндекс:
-
игнорируют
shortlink -
не считают его каноническим
❌ Почему его лучше убрать
1️⃣ Засоряет <head>
2️⃣ Может сбивать инструменты SEO-анализа
3️⃣ Указывает на некрасивый URL с ?p=
4️⃣ Может использоваться как альтернативный URL в отчётах
5️⃣ Пользы — 0
✅ Безопасно ли удалять
✔ Да
✔ WordPress продолжит работать
✔ API не ломается
✔ SEO — только лучше
🔧 Как убрать shortlink в WordPress
Добавь в functions.php темы или в свой плагин:
remove_action('wp_head', 'wp_shortlink_wp_head', 10);
remove_action('template_redirect', 'wp_shortlink_header', 11);
После этого:
<link rel="canonical" href="https://voronkov.org/production-planning/" />
— и больше ничего лишнего.
⚠️ Важный момент
Если используются SEO-плагины (Yoast / RankMath):
-
они могут возвращать shortlink
-
тогда отключать нужно в настройках плагина
✅ Итог
✔ rel="shortlink" не нужен
✔ Можно удалять без последствий
✔ Упростит SEO-картину
✔ Каноникал остаётся главным
Архитектура поддержки переводов интерфейсов на другие языки для wordpress в целом и в связке с React
Ниже — полная архитектура переводов в WordPress:
1️⃣ как это работает в целом
2️⃣ как это работает в PHP
3️⃣ как это работает в React / Gutenberg
4️⃣ как соединяются PHP + JS переводы
5️⃣ как это обычно делают профессиональные плагины.
1. Общая архитектура переводов в WordPress
Система переводов в WordPress состоит из 4 уровней:
код → POT → PO → MO / JSON
1️⃣ Код
В коде используются функции:
PHP:
__()
_e()
_n()
_x()
JS:
wp.i18n.__()
wp.i18n._n()
wp.i18n._x()
2️⃣ POT (template)
.pot — шаблон переводов.
Пример:
nv-seo.pot
Он содержит все строки из кода.
3️⃣ PO
Файл конкретного языка:
ru_RU.po
de_DE.po
fr_FR.po
Пример:
msgid "Save settings"
msgstr "Сохранить настройки"
4️⃣ MO / JSON
WordPress использует:
| тип | для |
|---|---|
| MO | PHP |
| JSON | JavaScript |
2. Архитектура переводов для PHP
структура плагина
plugin
├─ languages
│ ├─ nv-seo.pot
│ ├─ nv-seo-ru_RU.po
│ ├─ nv-seo-ru_RU.mo
│
├─ nv-seo.php
подключение textdomain
add_action('plugins_loaded', function () {
load_plugin_textdomain(
'nv-seo',
false,
dirname(plugin_basename(__FILE__)) . '/languages'
);
});
использование
__('Settings saved', 'nv-seo')
или
echo __('Save', 'nv-seo');
3. Архитектура переводов для React / JS
В React используется пакет:
@wordpress/i18n
(глобально это wp.i18n)
использование
import { __ } from '@wordpress/i18n';
__('Save settings', 'nv-seo')
пример React
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
export default function SaveButton() {
return (
<Button variant="primary">
{__('Save settings', 'nv-seo')}
</Button>
);
}
4. Как WordPress переводит JS
WordPress не использует MO для JS.
Он использует:
JSON
пример:
languages/nv-seo-ru_RU-3a4f2.json
5. Связка PHP и JS
Очень важная функция:
wp_set_script_translations()
Она говорит WordPress:
"подключи JSON переводы для этого JS файла"
пример
wp_enqueue_script(
'nv-seo-admin',
plugins_url('build/index.js', __FILE__),
['wp-i18n', 'wp-element', 'wp-components'],
'1.0',
true
);
wp_set_script_translations(
'nv-seo-admin',
'nv-seo',
plugin_dir_path(__FILE__) . 'languages'
);
6. Как формируется JSON
WordPress CLI генерирует его из PO.
wp i18n make-json languages
Результат:
nv-seo-ru_RU-xxxx.json
7. Полная структура современного плагина
nv-seo
│
├─ src
│ └─ index.js
│
├─ build
│ └─ index.js
│
├─ languages
│ ├─ nv-seo.pot
│ ├─ nv-seo-ru_RU.po
│ ├─ nv-seo-ru_RU.mo
│ ├─ nv-seo-ru_RU-xxxx.json
│
├─ nv-seo.php
└─ package.json
8. Поток разработки
1️⃣ пишем код
PHP:
__('Save settings', 'nv-seo')
JS:
__('Save settings', 'nv-seo')
2️⃣ генерируем POT
wp i18n make-pot . languages/nv-seo.pot
3️⃣ создаём переводы
ru_RU.po
через:
-
Poedit
-
Loco Translate
4️⃣ компилируем
PO → MO
5️⃣ генерируем JSON
wp i18n make-json languages
9. Как делают крупные плагины
Например:
-
Yoast SEO
-
Rank Math
-
WooCommerce
используют:
languages/
build/
src/
и WordPress i18n CLI.
10. Лучшие практики
всегда указывайте textdomain
__('Save', 'nv-seo')
не используйте динамические строки
❌ плохо
__(text, 'nv-seo')
используйте sprintf
sprintf(
__('Found %d errors', 'nv-seo'),
count
)
используйте контекст
_x('Post', 'noun', 'nv-seo')
11. Очень важная оптимизация "Code splitting"
Если ваш интерфейс React большой, лучше:
code splitting
тогда JSON будет загружаться по частям.
Code splitting — это разбиение большого JavaScript-бандла на несколько файлов, которые загружаются только тогда, когда они реально нужны.
В WordPress это особенно полезно для React-интерфейсов в админке, чтобы не грузить весь JS сразу.
В сборке через @wordpress/scripts code splitting уже поддерживается, потому что внутри используется Webpack.
1. Основная идея
Без code splitting:
index.js
↓
build/index.js (400 KB)
С code splitting:
index.js
↓
build/index.js
build/settings.js
build/schema.js
build/faq.js
Подгружается только нужный модуль.
2. Как это делается в React
Используется dynamic import().
обычный импорт
import SettingsPage from './settings';
всё попадает в один файл.
dynamic import
const SettingsPage = import('./settings');
Webpack создаст отдельный chunk.
Но в React правильнее использовать React.lazy.
3. Пример code splitting
структура
src
├─ index.js
├─ pages
│ ├─ dashboard.js
│ ├─ schema.js
│ └─ settings.js
index.js
import { lazy, Suspense } from '@wordpress/element';
import { Spinner } from '@wordpress/components';
const Dashboard = lazy(() => import('./pages/dashboard'));
const Settings = lazy(() => import('./pages/settings'));
export default function App() {
return (
<Suspense fallback={<Spinner />}>
<Dashboard />
</Suspense>
);
}
4. Что создаст сборка
После
npm run build
в папке build будет:
build
├─ index.js
├─ dashboard.js
├─ settings.js
5. Как WordPress загружает chunks
Важный момент.
WordPress должен знать путь:
wp_enqueue_script(
'nv-seo-admin',
plugins_url('build/index.js', __FILE__),
['wp-element','wp-components','wp-i18n'],
'1.0',
true
);
Webpack автоматически будет подгружать:
build/dashboard.js
build/settings.js
через
import()
6. Как это выглядит в браузере
Без splitting:
index.js 400KB
Со splitting:
index.js 80KB
settings.js 100KB
schema.js 120KB
faq.js 100KB
7. Где это особенно полезно
Например в SEO-плагине можно разделить:
src
├─ index.js
├─ pages
│ ├─ dashboard.js
│ ├─ schema.js
│ ├─ sitemap.js
│ └─ redirects.js
Пользователь открывает Schema → грузится только:
schema.js
8. Пример с роутингом
Если используется **React Router:
const Settings = lazy(() => import('./pages/settings'));
const Schema = lazy(() => import('./pages/schema'));
9. Очень полезная оптимизация
Можно разбить даже библиотеки.
Например:
schema-builder.js
грузить только при редактировании Schema.
10. Важный момент для WordPress
Чтобы chunks работали, build папка должна содержать все файлы:
build
├─ index.js
├─ index.asset.php
├─ 234.js
├─ 567.js
WordPress автоматически подхватывает зависимости из:
index.asset.php
11. Подключение через asset
Правильный способ:
$asset = include plugin_dir_path(__FILE__) . 'build/index.asset.php';
wp_enqueue_script(
'nv-seo-admin',
plugins_url('build/index.js', __FILE__),
$asset['dependencies'],
$asset['version'],
true
);
12. Итог по "Code splitting"
Code splitting позволяет:
✔ ускорить загрузку
✔ уменьшить первый бандл
✔ грузить код по требованию
Это стандартная практика для:
-
Yoast SEO
-
WooCommerce
-
Rank Math
12. Типичная проблема новичков
Они думают:
JSON перевод нужно писать вручную.
Нет.
Он генерируется автоматически.
13. Самая правильная архитектура для вашего SEO-плагина
nv-seo
├─ src
│ └─ admin
│ └─ index.js
│
├─ build
│ └─ admin.js
│
├─ languages
│ ├─ nv-seo.pot
│ ├─ nv-seo-ru_RU.po
│ ├─ nv-seo-ru_RU.mo
│ ├─ nv-seo-ru_RU.json
│
└─ nv-seo.php
Что такое TRANSIENT?
В WordPress transient — это механизм временного кэширования данных с автоматическим сроком жизни (TTL).
Проще:
transient = временное значение в базе (или кэше), которое само “протухает”
🧠 Зачем нужны transient
Чтобы не выполнять тяжёлые операции каждый раз:
-
сложные
WP_Query -
API-запросы
-
генерация sitemap
-
вычисления SEO-данных
📦 Как это выглядит
Установка
set_transient('my_key', $data, 3600);
👉 сохранить данные на 1 час
Получение
$data = get_transient('my_key');
Удаление
delete_transient('my_key');
🔁 Как это работает
$data = get_transient('my_key');
if ($data === false) {
// кэша нет или истёк
$data = expensive_function();
set_transient('my_key', $data, 3600);
}
return $data;
📍 Где хранятся transient
По умолчанию:
👉 в таблице:
wp_options
ключи выглядят так:
_transient_my_key
_transient_timeout_my_key
Если есть object cache (Redis/Memcached)
Тогда transient хранятся:
👉 в памяти (быстрее)
⏱ Время жизни
set_transient('key', $data, 3600);
| значение | смысл |
|---|---|
| 60 | 1 минута |
| 3600 | 1 час |
| 86400 | 1 день |
❗ Важно
TTL — это максимальное время, а не гарантия:
transient может исчезнуть раньше
(например при очистке кэша)
🔥 Примеры использования
1. Кэш WP_Query
$posts = get_transient('latest_posts');
if ($posts === false) {
$posts = get_posts([...]);
set_transient('latest_posts', $posts, 600);
}
2. Кэш API
$response = get_transient('api_data');
if ($response === false) {
$response = wp_remote_get('https://api.site.com');
set_transient('api_data', $response, 3600);
}
3. Sitemap
$xml = get_transient('sitemap');
if ($xml === false) {
$xml = generate_sitemap();
set_transient('sitemap', $xml, 3600);
}
⚠️ Важные особенности
1. Не для постоянных данных
❌ нельзя хранить:
-
настройки
-
важные данные
👉 transient может исчезнуть
2. Автоудаление
WordPress сам удаляет истёкшие transient не сразу, а при обращении к ним.
3. Может быть очищен полностью
При:
-
очистке кэша
-
обновлении плагинов
-
некоторых хостингах
🧠 Transient vs Options
| transient | option | |
|---|---|---|
| время жизни | есть | нет |
| может исчезнуть | да | нет |
| назначение | кэш | постоянные данные |
🚀 Когда использовать
Используй transient, если:
✔ данные тяжело получать
✔ можно пересчитать
✔ не критично потерять
❌ Когда НЕ использовать
❌ SEO мета
❌ важные даты
❌ пользовательские настройки
🏁 Итог
Transient — это встроенный кэш WordPress с TTL
Он:
✔ ускоряет сайт
✔ снижает нагрузку
✔ автоматически “протухает”
Ядро wordpress, hooks
Filter document_title_parts
document_title_parts — это низкоуровневый фильтр WordPress, который управляет частями заголовка <title>, до того как они будут склеены в одну строку.
Разберём подробно.
🧠 Как WordPress формирует <title>
Современный WordPress (4.4+) делает так:
-
Собирает массив частей заголовка
-
Пропускает его через фильтр
document_title_parts -
Склеивает части через разделитель
-
Пропускает итог через
pre_get_document_title -
Выводит
<title>
Схема:
document_title_parts → pre_get_document_title → <title>
🔎 Что такое document_title_parts
Это фильтр, который возвращает массив:
array(
'title' => 'Заголовок страницы',
'page' => 'Page 2',
'tagline' => 'Описание сайта',
'site' => 'Название сайта',
);
WordPress сам решает, какие части включать.
📌 Пример стандартного массива
Для поста:
array(
'title' => 'Название поста',
'site' => 'Название сайта',
);
Для главной:
array(
'site' => 'Название сайта',
'tagline' => 'Описание сайта',
);
✅ Как использовать document_title_parts
Пример: заменить только title
add_filter('document_title_parts', function ($parts) {
if (is_singular()) {
$seo_title = get_field('seo_title');
if ($seo_title) {
$parts['title'] = $seo_title;
}
}
return $parts;
});
✔ меняется только заголовок
✔ имя сайта остаётся
✔ слоган остаётся (если включён)
🔥 Пример: убрать название сайта
add_filter('document_title_parts', function ($parts) {
unset($parts['site']);
return $parts;
});
Результат:
<title>Заголовок страницы</title>
🔥 Пример: кастомный порядок
add_filter('document_title_parts', function ($parts) {
return array(
'title' => $parts['title'],
'site' => $parts['site'],
);
});
🆚 document_title_parts vs pre_get_document_title
| Фильтр | Что меняет | Когда использовать |
|---|---|---|
document_title_parts |
части title | тонкая настройка |
pre_get_document_title |
весь title строкой | полный контроль |
👉 SEO-плагины используют оба.
⚠ Важно помнить
-
document_title_partsне выводит<title> -
он НЕ строка, а массив
-
pre_get_document_titleсрабатывает позже
✅ Рекомендация для SEO
Для кастомного SEO:
-
использовать
document_title_parts→ управлять логикой -
использовать
pre_get_document_title→ крайний приоритет
🎯 Итог
✔ document_title_parts — управляет частями <title>
✔ Работает до склейки
✔ Позволяет убрать site/tagline/page
✔ Идеален для SEO без дублирования
Action wp_head
🔹 Добавление SEO Description
✅ Правильный способ в WordPress (через фильтр)
❌ НЕ делаем echo <meta> в шаблоне
✔ используем wp_head
🧩 Универсальный код (посты, страницы, таксономии)
add_action('wp_head', function () {
$description = '';
// Посты и страницы
if (is_singular()) {
$description = get_field('seo_desc');
}
// Категории и метки
elseif (is_category() || is_tag()) {
$term = get_queried_object();
$description = get_field('seo_desc', $term);
}
// Фолбэк — excerpt или описание термина
if (!$description) {
if (is_singular()) {
$description = get_the_excerpt();
}
elseif (is_category() || is_tag()) {
$term = get_queried_object();
$description = term_description($term);
}
}
if (!$description) {
return;
}
// Чистим HTML и лишние пробелы
$description = wp_strip_all_tags($description);
$description = trim(preg_replace('/\s+/', ' ', $description));
echo '<meta name="description" content="' . esc_attr($description) . '">' . "\n";
});
🔎 Почему именно так
✔ Работает для:
-
постов
-
страниц
-
категорий
-
меток
✔ Без дублей
✔ Без XSS
✔ Совместимо с темами и плагинами
🎯 Лучшие практики SEO Description
-
120–160 символов (RU)
-
70–155 (моб)
-
уникальный
-
без кавычек
-
не повторять title
-
коммерческий посыл (если уместно)
🧠 Частые ошибки
❌ несколько <meta description>
❌ HTML внутри
❌ слишком длинный текст
❌ одинаковый на всех страницах
🚀 Если нужно улучшить
Можно добавить:
-
seo_desc_enдля EN версии -
авто-обрезку до 160 символов
-
приоритет для главной
-
проверку
noindex -
фильтр
wpseo_metadesc(если используешь Yoast)
✅ Итог
✔ Meta description добавляется через wp_head
✔ Используется ACF
✔ Есть fallback
✔ Без конфликтов
Плагины
Advanced Custom Fields ( ACF )
Рассматривается обычная версия плагина, не PRO.
Хуки.
acf/init
Через него можно добавлять поля к различынм сущностям wordpress.
Добавление двух полей к категориям постов:
add_action('acf/init', function() {
acf_add_local_field_group(array(
'key' => 'group_category_fields',
'title' => 'Category Fields',
'fields' => array(
array(
'key' => 'seo_title_category',
'label' => 'SEO Title',
'name' => 'seo_title',
'type' => 'text',
),
array(
'key' => 'seo_desc_category',
'label' => 'SEO Description',
'name' => 'seo_desc',
'type' => 'textarea',
),
),
'location' => array(
array(
array(
'param' => 'taxonomy',
'operator' => '==',
'value' => 'category',
),
),
),
));
});
В этом примере добавляется текстовое поле и текстовый блок.
Admin-панель wordpress
Какие frameworks поставляются в коробочной версии wordpress, чтобы их можно было использовать для построения интерфейсов admin-панели?
В «коробочной» версии WordPress нет полноценного JS-фреймворка как Laravel + Vue из коробки,
но есть встроенные инструменты, которые можно использовать.
🥇 1️⃣ Gutenberg (React) — главный встроенный framework
Внутри админки WordPress используется React.
WP поставляется с:
-
React
-
ReactDOM
-
wp.element
-
wp.components
-
wp.data
-
wp.apiFetch
-
wp.i18n
-
wp.hooks
Это называется WordPress JavaScript Packages.
Пример подключения:
wp_enqueue_script(
'my-admin-script',
plugins_url('admin.js', __FILE__),
['wp-element', 'wp-components', 'wp-api-fetch'],
'1.0',
true
);
И в JS:
const { Button, TextControl } = wp.components;
const { useState } = wp.element;
Это официальный способ.
✔ современный
✔ нативный
✔ безопасный
✔ используется в Gutenberg
Если делать сложный интерфейс — это лучший путь.
🥈 2️⃣ jQuery (встроен по умолчанию)
WordPress до сих пор включает jQuery.
wp_enqueue_script('jquery');
Подходит для:
-
простых форм
-
динамических элементов
-
AJAX
Но:
❌ это уже устаревший подход
❌ не подходит для сложного SPA-интерфейса
🥉 3️⃣ jQuery UI (частично встроен)
В комплекте есть:
-
Tabs
-
Accordion
-
Dialog
-
Datepicker
Подключается так:
wp_enqueue_script('jquery-ui-tabs');
Но выглядит немного устаревшим визуально.
🧠 Как подключить "всё"
Технически можно перечислить всё вручную:
wp_enqueue_script('jquery');
wp_enqueue_script('jquery-ui-core');
wp_enqueue_script('jquery-ui-widget');
wp_enqueue_script('jquery-ui-mouse');
wp_enqueue_script('jquery-ui-position');
wp_enqueue_script('jquery-ui-draggable');
wp_enqueue_script('jquery-ui-droppable');
wp_enqueue_script('jquery-ui-resizable');
wp_enqueue_script('jquery-ui-selectable');
wp_enqueue_script('jquery-ui-sortable');
wp_enqueue_script('jquery-ui-accordion');
wp_enqueue_script('jquery-ui-autocomplete');
wp_enqueue_script('jquery-ui-button');
wp_enqueue_script('jquery-ui-datepicker');
wp_enqueue_script('jquery-ui-dialog');
wp_enqueue_script('jquery-ui-menu');
wp_enqueue_script('jquery-ui-progressbar');
wp_enqueue_script('jquery-ui-slider');
wp_enqueue_script('jquery-ui-spinner');
wp_enqueue_script('jquery-ui-tabs');
wp_enqueue_script('jquery-ui-tooltip');
});
⚠ Но это плохая практика.
❌ Почему не стоит подключать всё
-
Лишняя нагрузка
-
Замедление админки
-
Возможные конфликты
-
Большинство модулей тебе не понадобится
✅ Правильный способ
Подключать только то, что нужно.
Например:
Tabs:
Dialog:
wp_enqueue_style('wp-jquery-ui-dialog');
Datepicker:
🎨 Важно: CSS
WordPress не включает полный jQuery UI theme CSS.
Для dialog есть:
Но для tabs, accordion и других — стили придётся писать самому или подключать свою тему jQuery UI.
🧩 4️⃣ Dashicons
Встроенная иконная библиотека.
wp_enqueue_style('dashicons');
🎨 5️⃣ Стандартные стили админки
Можно использовать встроенные CSS-классы:
-
.wrap -
.form-table -
.button -
.notice -
.widefat -
.nav-tab-wrapper -
.postbox
Пример:
<div class="wrap">
<h1>SEO Settings</h1>
</div>
Это самый простой путь.
🚀 6️⃣ @wordpress/components (UI-библиотека)
Очень мощная вещь.
Компоненты:
Пример:
wp.element.render(
wp.element.createElement(
wp.components.Button,
{ isPrimary: true },
'Save'
),
document.getElementById('app')
);
Это нативный WP-UI на React.
❗ Чего нет в коробке
-
Vue
-
Angular
-
Bootstrap
-
Tailwind
-
Material UI
Их можно подключить вручную, но это уже не «из коробки».
Варианты:
🔹 Простой интерфейс
→ стандартные WP формы + PHP
(самый быстрый способ)
🔹 Средний уровень
→ jQuery + AJAX
🔹 Профессиональный
→ React + wp.components
🧠 Дополнительные API WordPress
Ты можешь использовать:
-
REST API (
register_rest_route) -
Settings API
-
Options API
-
AJAX (
admin-ajax.php) -
Nonce security