# Wordpress

# Разработка темы

# Создание 404 страницы

Создайте файл `404.php` в папке темы.

##### Минимальный пример `404.php`

```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 доступен по адресу:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%2Fwp-sitemap.xml"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"></div></div><div class="overflow-y-auto p-4" dir="ltr">`/wp-sitemap.xml`</div></div>Он формируется классом `WP_Sitemaps` и управляется через фильтры. Никакой отдельный файл в теме не создаётся — всё генерируется динамически.

Ниже — основные способы управления содержимым.

---

#### 1️⃣ Полностью отключить sitemap

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-add_filter%28%27wp_sitem"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-title function_ invoke__">add_filter</span>(<span class="hljs-string">'wp_sitemaps_enabled'</span>, <span class="hljs-string">'__return_false'</span>);`</div></div>---

#### 2️⃣ Убрать конкретный тип записей (например, post)

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-add_filter%28%27wp_sitem-1"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-title function_ invoke__">add_filter</span>(<span class="hljs-string">'wp_sitemaps_post_types'</span>, function (<span class="hljs-variable">$post_types</span>) {    <span class="hljs-keyword">unset</span>(<span class="hljs-variable">$post_types</span>[<span class="hljs-string">'post'</span>]); <span class="hljs-comment">// убираем записи</span>    <span class="hljs-keyword">return</span> <span class="hljs-variable">$post_types</span>;});`</div></div>Пример для кастомного типа:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-unset%28%24post_types%5B%27p"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-keyword">unset</span>(<span class="hljs-variable">$post_types</span>[<span class="hljs-string">'product'</span>]);`</div></div>---

#### 3️⃣ Убрать таксономию (категории, теги и т.д.)

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-add_filter%28%27wp_sitem-2"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-title function_ invoke__">add_filter</span>(<span class="hljs-string">'wp_sitemaps_taxonomies'</span>, function (<span class="hljs-variable">$taxonomies</span>) {    <span class="hljs-keyword">unset</span>(<span class="hljs-variable">$taxonomies</span>[<span class="hljs-string">'post_tag'</span>]); <span class="hljs-comment">// убрать теги</span>    <span class="hljs-keyword">return</span> <span class="hljs-variable">$taxonomies</span>;});`</div></div>Убрать категории:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-unset%28%24taxonomies%5B%27c"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-keyword">unset</span>(<span class="hljs-variable">$taxonomies</span>[<span class="hljs-string">'category'</span>]);`</div></div>---

#### 4️⃣ Убрать пользователей из sitemap

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-add_filter%28%27wp_sitem-3"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-title function_ invoke__">add_filter</span>(<span class="hljs-string">'wp_sitemaps_add_provider'</span>, function (<span class="hljs-variable">$provider</span>, <span class="hljs-variable">$name</span>) {    <span class="hljs-keyword">if</span> (<span class="hljs-variable">$name</span> === <span class="hljs-string">'users'</span>) {        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;    }    <span class="hljs-keyword">return</span> <span class="hljs-variable">$provider</span>;}, <span class="hljs-number">10</span>, <span class="hljs-number">2</span>);`</div></div>---

#### 5️⃣ Изменить список ссылок (фильтрация конкретных URL)

Можно отфильтровать сами записи:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-add_filter%28%27wp_sitem-4"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-title function_ invoke__">add_filter</span>(<span class="hljs-string">'wp_sitemaps_posts_query_args'</span>, function (<span class="hljs-variable">$args</span>, <span class="hljs-variable">$post_type</span>) {        <span class="hljs-keyword">if</span> (<span class="hljs-variable">$post_type</span> === <span class="hljs-string">'post'</span>) {        <span class="hljs-variable">$args</span>[<span class="hljs-string">'meta_query'</span>] = <span class="hljs-keyword">array</span>(            <span class="hljs-keyword">array</span>(                <span class="hljs-string">'key'</span>     => <span class="hljs-string">'exclude_from_sitemap'</span>,                <span class="hljs-string">'compare'</span> => <span class="hljs-string">'NOT EXISTS'</span>            )        );    }    <span class="hljs-keyword">return</span> <span class="hljs-variable">$args</span>;}, <span class="hljs-number">10</span>, <span class="hljs-number">2</span>);`</div></div>---

#### 6️⃣ Изменить данные конкретной записи в sitemap

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-add_filter%28%27wp_sitem-5"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-title function_ invoke__">add_filter</span>(<span class="hljs-string">'wp_sitemaps_posts_entry'</span>, function (<span class="hljs-variable">$entry</span>, <span class="hljs-variable">$post</span>, <span class="hljs-variable">$post_type</span>) {        <span class="hljs-variable">$entry</span>[<span class="hljs-string">'priority'</span>] = <span class="hljs-number">0.8</span>;    <span class="hljs-variable">$entry</span>[<span class="hljs-string">'changefreq'</span>] = <span class="hljs-string">'weekly'</span>;    <span class="hljs-keyword">return</span> <span class="hljs-variable">$entry</span>;}, <span class="hljs-number">10</span>, <span class="hljs-number">3</span>);`</div></div>---

#### 7️⃣ Добавить собственный sitemap

Можно зарегистрировать свой провайдер:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-add_action%28%27init%27%2C-f"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-title function_ invoke__">add_action</span>(<span class="hljs-string">'init'</span>, function () {    <span class="hljs-variable">$provider</span> = <span class="hljs-keyword">new</span> <span class="hljs-title class_">WP_Sitemaps_Provider</span>(        <span class="hljs-string">'custom'</span>,        <span class="hljs-string">'custom'</span>    );    <span class="hljs-title function_ invoke__">wp_register_sitemap_provider</span>(<span class="hljs-string">'custom'</span>, <span class="hljs-variable">$provider</span>);});`</div></div>(для продакшена обычно создают отдельный класс-провайдер)

---

#### 🔥 Где размещать код?

- В `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 и подключает **провайдеры**.

Каждый провайдер отвечает за свой тип данных.

По умолчанию есть три:

<div class="TyagGW_tableContainer" id="bkmrk-%D0%9F%D1%80%D0%BE%D0%B2%D0%B0%D0%B9%D0%B4%D0%B5%D1%80-%D0%A7%D1%82%D0%BE-%D0%B3%D0%B5%D0%BD%D0%B5%D1%80%D0%B8"><div class="group TyagGW_tableWrapper flex flex-col-reverse w-fit" tabindex="-1"><table class="w-fit min-w-(--thread-content-width)" data-end="640" data-start="442"><thead data-end="472" data-start="442"><tr data-end="472" data-start="442"><th class="" data-col-size="sm" data-end="454" data-start="442">Провайдер</th><th class="" data-col-size="sm" data-end="472" data-start="454">Что генерирует</th></tr></thead><tbody data-end="640" data-start="505"><tr data-end="554" data-start="505"><td data-col-size="sm" data-end="515" data-start="505">`posts`</td><td data-col-size="sm" data-end="554" data-start="515">записи, страницы, custom post types</td></tr><tr data-end="611" data-start="555"><td data-col-size="sm" data-end="570" data-start="555">`taxonomies`</td><td data-col-size="sm" data-end="611" data-start="570">категории, теги, кастомные таксономии</td></tr><tr data-end="640" data-start="612"><td data-col-size="sm" data-end="622" data-start="612">`users`</td><td data-col-size="sm" data-end="640" data-start="622">архивы авторов</td></tr></tbody></table>

</div></div>---

#### 🔎 Пример структуры

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%2Fwp-sitemap.xml"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`/wp-sitemap.xml`</div></div>Это индекс. В нём ссылки на:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%2Fwp-sitemap-posts-po"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`/wp-sitemap-posts-post-1.xml/wp-sitemap-taxonomies-category-1.xml/wp-sitemap-users-1.xml`</div></div>Каждый из этих файлов формируется **отдельным провайдером**.

---

#### 📦 Что делает провайдер технически

Каждый провайдер — это класс, наследующий:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-wp_sitemaps_provider"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-attribute">WP_Sitemaps_Provider</span>`</div></div>Он обязан реализовать два метода:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-get_url_list%28-%24page_"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-title function_ invoke__">get_url_list</span>( <span class="hljs-variable">$page_num</span>, <span class="hljs-variable">$subtype</span> )<span class="hljs-title function_ invoke__">get_max_num_pages</span>( <span class="hljs-variable">$subtype</span> )`</div></div>##### 🔹 get\_url\_list()

Возвращает массив URL для текущей страницы sitemap.

##### 🔹 get\_max\_num\_pages()

Возвращает количество страниц sitemap (если URL много).

---

#### 🧠 Простая аналогия

Представьте sitemap как каталог:

- Индекс — оглавление
- Провайдер — глава каталога
- get\_url\_list() — список страниц в главе

---

#### 💡 Почему это удобно

Провайдеры позволяют:

- отключить стандартные данные
- добавить собственные URL
- изменить логику выборки
- добавить кастомный sitemap (например, для API или фильтров)

#### ✅ Отключаем стандартные провайдеры

Добавьте в `functions.php` или в плагин:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk--4"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">  
</div></div><div class="overflow-y-auto p-4" dir="ltr">  
</div></div>```
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); 
```

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%D0%A2%D0%B5%D0%BF%D0%B5%D1%80%D1%8C-%2Fwp-sitemap.x"><div class="overflow-y-auto p-4" dir="ltr">Теперь `/wp-sitemap.xml` станет пустым</div></div>#### ✅ Создаём свой провайдер

Создадим собственный класс.

```php
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;
    }
}

```

#### ✅ Регистрируем провайдер

```php
add_action('init', function () {

    wp_register_sitemap_provider(
        'custom',
        new My_Custom_Sitemap_Provider()
    );

});

```

#### 🔎 Что получится

Теперь будет доступен:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%2Fwp-sitemap-custom-1"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">  
</div></div><div class="overflow-y-auto p-4" dir="ltr">`/wp-sitemap-custom-1.xml`</div></div>И он будет содержать только ваши данные.

### ✅Основные способы управления содержимым провайдера

#### 1️⃣ Полностью отключить sitemap

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-add_filter%28%27wp_sitem-1"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-title function_ invoke__">add_filter</span>(<span class="hljs-string">'wp_sitemaps_enabled'</span>, <span class="hljs-string">'__return_false'</span>);`</div></div>---

#### 2️⃣ Убрать конкретный тип записей (например, post)

```php
add_filter('wp_sitemaps_post_types', function ($post_types) {
  unset($post_types['post']); // убираем записи return $post_types;
}); 
```

Пример для кастомного типа:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-unset%28%24post_types%5B%27p"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-keyword">unset</span>(<span class="hljs-variable">$post_types</span>[<span class="hljs-string">'product'</span>]);`</div></div>---

#### 3️⃣ Убрать таксономию (категории, теги и т.д.)

```php
dd_filter('wp_sitemaps_taxonomies', function ($taxonomies) {
  unset($taxonomies['post_tag']); // убрать теги return $taxonomies;
}); 
```

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk--7"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"></div></div>Убрать категории:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-unset%28%24taxonomies%5B%27c"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-keyword">unset</span>(<span class="hljs-variable">$taxonomies</span>[<span class="hljs-string">'category'</span>]);`</div></div>---

#### 4️⃣ Убрать пользователей из sitemap

```php
add_filter('wp_sitemaps_add_provider', function ($provider, $name) {
  if ($name === 'users') {
    return false;
  } return $provider;
}, 10, 2); 
```

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk--9"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">  
</div></div></div>#### 5️⃣ Изменить список ссылок (фильтрация конкретных URL)

Можно отфильтровать сами записи:

```php
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

```php
add_filter('wp_sitemaps_posts_entry', function ($entry, $post, $post_type) {
   $entry['priority'] = 0.8;
   $entry['changefreq'] = 'weekly';
   return $entry;
}, 10, 3);


```

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk--11"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">  
</div></div><div class="overflow-y-auto p-4" dir="ltr">  
</div></div>

# Создание кастомного sitemap.xml

В статье рассматривается следующий сценарий:

- Сайт имеет только одну страницу - главную, остальные страницы это посты.
- На сайте установлен самописный плагин, который обрабатывает содержимое страницы или поста и можно получить либо русскую версию текста, либо английскую
- Русская версия текста вызывается обычной ссылкой на страницу или пост:

https://example.com/post-slug

- Английская версия вызывается добавлением к обычной ссылке окончания "/en":

https://example.com/post-slug/en

- Главная страница обязательно имеет английскую версию
- Для постов имеется поле ACF - "is\_english", которое определяет, есть ли у страницы английская версия, если английской версии нет, то при ее запросе отображается 404 страница
- Для сайта формируется кастомный файл sitemap, который будет показан поисковикам через robots.txt или явным указанием в настройках индексирования.
- <span style="color: rgb(224, 62, 45);">**На сайте не используются плагины SEO**</span>

#### Отключение стандартного wp-sitemap.xml

`add_filter('wp_sitemaps_enabled', '__return_false');`

Регистрация кастомного sitemap.xml

# Ядро wordpress

# Как работают is_category() и is_tag()

Это **conditional tags**, которые проверяют состояние **главного запроса** (`WP_Query`) после того, как WordPress распарсил URL.

Они становятся `true`, если текущий запрос определён как:

- архив категории → `is_category() === true`
- архив метки → `is_tag() === true`

---

#### 🔎 Что происходит при запросе архива

Например URL:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%2Fportfolio%2Fcategory%2F"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-regexp">/portfolio/</span>category<span class="hljs-regexp">/contract/</span><span class="hljs-regexp">/portfolio/</span>topics<span class="hljs-regexp">/asp/</span>`</div></div>После rewrite WordPress превращает это во внутренний запрос:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-index.php%3Fcategory_n"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"></div></div><div class="overflow-y-auto p-4" dir="ltr">`index.php?category_name=contract `</div></div>или

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-index.php%3Fpost_tag%3Da"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"></div></div><div class="overflow-y-auto p-4" dir="ltr">`index.php?post_tag=asp `</div></div>Далее `WP_Query` анализирует параметры и выставляет флаги:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%24query-%3Eis_category-"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-variable">$query</span>->is_category = <span class="hljs-literal">true</span>;<span class="hljs-variable">$query</span>->is_archive  = <span class="hljs-literal">true</span>;`</div></div>или

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-%24query-%3Eis_tag-%3D-tru"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-variable">$query</span>->is_tag = <span class="hljs-literal">true</span>;<span class="hljs-variable">$query</span>->is_archive = <span class="hljs-literal">true</span>;`</div></div>И только после этого начинают работать:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-is_category%28%29-is_tag"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-title function_ invoke__">is_category</span>()<span class="hljs-title function_ invoke__">is_tag</span>()<span class="hljs-title function_ invoke__">is_archive</span>()`</div></div>---

#### ⚠ Важно: где они работают корректно

Conditional tags корректно работают:

- после того как WordPress разобрал запрос
- внутри `pre_get_posts`
- внутри `template_redirect`
- внутри шаблонов

❌ Они НЕ работают корректно внутри `init`, потому что запрос ещё не разобран.

#### 📌 Почему лучше проверять через `$query`

В `pre_get_posts` безопаснее писать так:

```php
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()`

По сути это проверка:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-return-%24wp_query-%3Eis"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"></div></div><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-keyword">return</span> <span class="hljs-variable">$wp_query</span>->is_category;`</div></div>Если хотите проверить руками:

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk-global-%24wp_query%3B-va"><div class="overflow-y-auto p-4" dir="ltr">`<span class="hljs-keyword">global</span> <span class="hljs-variable">$wp_query</span>;<span class="hljs-title function_ invoke__">var_dump</span>(<span class="hljs-variable">$wp_query</span>->is_category); `</div></div>#### 💡 Итог

`is_category()` и `is_tag()`:

<div class="overflow-y-auto p-4" dir="ltr" id="bkmrk-%D0%B7%D0%B0%D0%B2%D0%B8%D1%81%D1%8F%D1%82-%D0%BE%D1%82-%D1%80%D0%B0%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%BD%D0%BD">- зависят от разобранного запроса
- работают после `parse_query`
- безопасны в `pre_get_posts` и позже
- не зависят от вашего `lang`, если rewrite правильный

</div>

# Установка $wp_query->set_404()

После выполнения:

```php
$wp_query->set_404();
```

WordPress:

- устанавливает `$wp_query->is_404 = true`
- сбрасывает другие флаги (is\_single, is\_page, is\_archive и т.д.)
- меняет внутреннее состояние запроса

---

#### ✅ Как проверить, что запрос стал 404

#### 🔹 1. Через условную функцию (рекомендуется)

```php
if ( is_404() ) {
    // это 404
}

```

Это самый правильный способ.

---

#### 🔹 2. Через объект `$wp_query`

```php
global $wp_query;

if ( $wp_query->is_404 ) {
    // это 404
}

```

или

```php
if ( $wp_query->is_404() ) {
    // тоже работает
}

```

---

#### 📌 Важно

После `set_404()`:

```php
$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

```

---

#### 🧠 Пример

```php
add_action('template_redirect', function() {
    if ( is_404() ) {
        error_log('Это 404 страница');
    }
});

```

---

#### 🎯 Если нужно понять: вы сами установили 404 или WordPress

Можно добавить свой флаг:

```php
global $wp_query;

$wp_query->set_404();
$wp_query->custom_en_404 = true;

```

И потом:

```php
if ( !empty($wp_query->custom_en_404) ) {
    // это именно ваша логика
}

```

# Можно ли убрать link rel="shortlink" из блока <head> страницы?

**Да, `shortlink` можно и лучше убрать**, для SEO он **не нужен** и иногда мешает диагностике.

---

#### 🔎 Что такое `rel="shortlink"`

```html
<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` темы или в свой плагин:

```php
remove_action('wp_head', 'wp_shortlink_wp_head', 10);
remove_action('template_redirect', 'wp_shortlink_header', 11);

```

После этого:

```html
<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 использует:

<table id="bkmrk-%D1%82%D0%B8%D0%BF-%D0%B4%D0%BB%D1%8F-mo-php-json-"><thead><tr><th>тип</th><th>для</th></tr></thead><tbody><tr><td>MO</td><td>PHP</td></tr><tr><td>JSON</td><td>JavaScript</td></tr></tbody></table>

---

### 2. Архитектура переводов для PHP

### структура плагина

```
plugin
 ├─ languages
 │   ├─ nv-seo.pot
 │   ├─ nv-seo-ru_RU.po
 │   ├─ nv-seo-ru_RU.mo
 │
 ├─ nv-seo.php

```

---

#### подключение textdomain

```php
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

```javascript
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 файла"

---

#### пример

```php
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()**.

##### обычный импорт

```javascript
import SettingsPage from './settings';

```

всё попадает в один файл.

---

##### dynamic import

```javascript
const SettingsPage = import('./settings');

```

Webpack создаст отдельный chunk.

Но в React правильнее использовать **React.lazy**.

---

#### 3. Пример code splitting

#### структура

```
src
 ├─ index.js
 ├─ pages
 │   ├─ dashboard.js
 │   ├─ schema.js
 │   └─ settings.js

```

---

##### index.js

```javascript
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 должен знать путь:

```php
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:

```javascript
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

Правильный способ:

```php
$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
);

```

---

#### <span style="text-decoration: underline;">12. Итог по "Code splitting"</span>

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)**.

Проще:

```text
transient = временное значение в базе (или кэше), которое само “протухает”

```

---

### 🧠 Зачем нужны transient

Чтобы **не выполнять тяжёлые операции каждый раз**:

- сложные `WP_Query`
- API-запросы
- генерация sitemap
- вычисления SEO-данных

---

### 📦 Как это выглядит

#### Установка

```php
set_transient('my_key', $data, 3600);

```

👉 сохранить данные на **1 час**

---

#### Получение

```php
$data = get_transient('my_key');

```

---

#### Удаление

```php
delete_transient('my_key');

```

---

### 🔁 Как это работает

```php
$data = get_transient('my_key');

if ($data === false) {
    // кэша нет или истёк
    $data = expensive_function();

    set_transient('my_key', $data, 3600);
}

return $data;

```

---

### 📍 Где хранятся transient

По умолчанию:

👉 в таблице:

```text
wp_options

```

ключи выглядят так:

```text
_transient_my_key
_transient_timeout_my_key

```

---

#### Если есть object cache (Redis/Memcached)

Тогда transient хранятся:

👉 в памяти (быстрее)

---

### ⏱ Время жизни

```php
set_transient('key', $data, 3600);

```

<table id="bkmrk-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5-%D1%81%D0%BC%D1%8B%D1%81%D0%BB-60-1-"><thead><tr><th>значение</th><th>смысл</th></tr></thead><tbody><tr><td>60</td><td>1 минута</td></tr><tr><td>3600</td><td>1 час</td></tr><tr><td>86400</td><td>1 день</td></tr></tbody></table>

---

### ❗ Важно

TTL — это **максимальное время**, а не гарантия:

```text
transient может исчезнуть раньше

```

(например при очистке кэша)

---

### 🔥 Примеры использования

#### 1. Кэш WP\_Query

```php
$posts = get_transient('latest_posts');

if ($posts === false) {
    $posts = get_posts([...]);
    set_transient('latest_posts', $posts, 600);
}

```

---

#### 2. Кэш API

```php
$response = get_transient('api_data');

if ($response === false) {
    $response = wp_remote_get('https://api.site.com');
    set_transient('api_data', $response, 3600);
}

```

---

#### 3. Sitemap

```php
$xml = get_transient('sitemap');

if ($xml === false) {
    $xml = generate_sitemap();
    set_transient('sitemap', $xml, 3600);
}

```

---

### ⚠️ Важные особенности

#### 1. Не для постоянных данных

❌ нельзя хранить:

- настройки
- важные данные

👉 transient может исчезнуть

---

#### 2. Автоудаление

WordPress сам удаляет истёкшие transient **не сразу**, а при обращении к ним.

---

#### 3. Может быть очищен полностью

При:

- очистке кэша
- обновлении плагинов
- некоторых хостингах

---

### 🧠 Transient vs Options

<table id="bkmrk-%C2%A0-transient-option-%D0%B2"><thead><tr><th> </th><th>transient</th><th>option</th></tr></thead><tbody><tr><td>время жизни</td><td>есть</td><td>нет</td></tr><tr><td>может исчезнуть</td><td>да</td><td>нет</td></tr><tr><td>назначение</td><td>кэш</td><td>постоянные данные</td></tr></tbody></table>

---

### 🚀 Когда использовать

Используй transient, если:

✔ данные тяжело получать  
✔ можно пересчитать  
✔ не критично потерять

---

### ❌ Когда НЕ использовать

❌ SEO мета  
❌ важные даты  
❌ пользовательские настройки

---

### 🏁 Итог

**Transient — это встроенный кэш WordPress с TTL**

Он:

✔ ускоряет сайт  
✔ снижает нагрузку  
✔ автоматически “протухает”

# Ядро wordpress, hooks

# Filter document_title_parts

`document_title_parts` — это **низкоуровневый фильтр WordPress**, который управляет **частями заголовка `<title>`**, *до* того как они будут склеены в одну строку.

Разберём подробно.

---

#### 🧠 Как WordPress формирует `<title>`

Современный WordPress (4.4+) делает так:

1. Собирает массив частей заголовка
2. Пропускает его через фильтр `document_title_parts`
3. Склеивает части через разделитель
4. Пропускает итог через `pre_get_document_title`
5. Выводит `<title>`

Схема:

```text
document_title_parts  →  pre_get_document_title  →  <title>

```

---

#### 🔎 Что такое `document_title_parts`

Это фильтр, который возвращает **массив**:

```php
array(
    'title'   => 'Заголовок страницы',
    'page'    => 'Page 2',
    'tagline' => 'Описание сайта',
    'site'    => 'Название сайта',
);

```

WordPress сам решает, какие части включать.

---

#### 📌 Пример стандартного массива

##### Для поста:

```php
array(
    'title' => 'Название поста',
    'site'  => 'Название сайта',
);

```

##### Для главной:

```php
array(
    'site'    => 'Название сайта',
    'tagline' => 'Описание сайта',
);

```

---

#### ✅ Как использовать `document_title_parts`

##### Пример: заменить только title

```php
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;
});

```

✔ меняется только заголовок  
✔ имя сайта остаётся  
✔ слоган остаётся (если включён)

---

#### 🔥 Пример: убрать название сайта

```php
add_filter('document_title_parts', function ($parts) {

    unset($parts['site']);

    return $parts;
});

```

Результат:

```html
<title>Заголовок страницы</title>

```

---

#### 🔥 Пример: кастомный порядок

```php
add_filter('document_title_parts', function ($parts) {

    return array(
        'title' => $parts['title'],
        'site'  => $parts['site'],
    );
});

```

---

#### 🆚 document\_title\_parts vs pre\_get\_document\_title

<table id="bkmrk-%D0%A4%D0%B8%D0%BB%D1%8C%D1%82%D1%80-%D0%A7%D1%82%D0%BE-%D0%BC%D0%B5%D0%BD%D1%8F%D0%B5%D1%82-%D0%9A%D0%BE"><thead><tr><th>Фильтр</th><th>Что меняет</th><th>Когда использовать</th></tr></thead><tbody><tr><td>`document_title_parts`</td><td>части title</td><td>тонкая настройка</td></tr><tr><td>`pre_get_document_title`</td><td>весь title строкой</td><td>полный контроль</td></tr></tbody></table>

👉 **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`

---

#### 🧩 Универсальный код (посты, страницы, таксономии)

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk--1"><div class="sticky top-[calc(var(--sticky-padding-top)+9*var(--spacing))]"><div class="absolute end-0 bottom-0 flex h-9 items-center pe-2"><div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">  
</div></div></div><div class="overflow-y-auto p-4" dir="ltr">  
</div></div>```php
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";

});

```

<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary" id="bkmrk--2"><div class="overflow-y-auto p-4" dir="ltr">  
</div></div>---

#### 🔎 Почему именно так

✔ Работает для:

- постов
- страниц
- категорий
- меток

✔ Без дублей  
✔ Без 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.

#### Добавление двух полей к категориям постов:

```php
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**.

##### Пример подключения:

```php
wp_enqueue_script(
    'my-admin-script',
    plugins_url('admin.js', __FILE__),
    ['wp-element', 'wp-components', 'wp-api-fetch'],
    '1.0',
    true
);

```

И в JS:

```js
const { Button, TextControl } = wp.components;
const { useState } = wp.element;

```

Это официальный способ.

✔ современный  
✔ нативный  
✔ безопасный  
✔ используется в Gutenberg

Если делать сложный интерфейс — это лучший путь.

---

### 🥈 2️⃣ jQuery (встроен по умолчанию)

WordPress до сих пор включает jQuery.

```php
wp_enqueue_script('jquery');

```

Подходит для:

- простых форм
- динамических элементов
- AJAX

Но:

❌ это уже устаревший подход  
❌ не подходит для сложного SPA-интерфейса

---

### 🥉 3️⃣ jQuery UI (частично встроен)

В комплекте есть:

- Tabs
- Accordion
- Dialog
- Datepicker

Подключается так:

```php
wp_enqueue_script('jquery-ui-tabs');

```

Но выглядит немного устаревшим визуально.

#### 🧠 Как подключить "всё"

Технически можно перечислить всё вручную:

<div class="relative w-full my-4" id="bkmrk-add_action%28%27admin_en"><div class=""><div class="relative"><div class="h-full min-h-0 min-w-0"><div class="h-full min-h-0 min-w-0"><div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl"><div class="h-full w-full border-radius-3xl bg-token-bg-elevated-secondary corner-superellipse/1.1 overflow-clip rounded-3xl lxnfua_clipPathFallback"><div class=""><div class="relative z-0 flex max-w-full"><div class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼ5 ͼj" dir="ltr" id="bkmrk-add_action%28%27admin_en-1"><div class="cm-scroller"><div class="cm-content q9tKkq_readonly">add_action('admin_enqueue_scripts', function () {  
  
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');  
  
});</div></div></div></div></div></div></div></div></div><div class=""><div class="">  
</div></div></div></div></div>⚠ Но это плохая практика.

---

#### ❌ Почему не стоит подключать всё

1. Лишняя нагрузка
2. Замедление админки
3. Возможные конфликты
4. Большинство модулей тебе не понадобится

---

#### ✅ Правильный способ

Подключать только то, что нужно.

Например:

##### Tabs:

<div class="relative w-full my-4" id="bkmrk-wp_enqueue_script%28%27j-2"><div class=""><div class="relative"><div class="h-full min-h-0 min-w-0"><div class="h-full min-h-0 min-w-0"><div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl"><div class="h-full w-full border-radius-3xl bg-token-bg-elevated-secondary corner-superellipse/1.1 overflow-clip rounded-3xl lxnfua_clipPathFallback"><div class=""><div class="relative z-0 flex max-w-full"><div class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼ5 ͼj" dir="ltr" id="bkmrk-wp_enqueue_script%28%27j-3"><div class="cm-scroller"><div class="cm-content q9tKkq_readonly">wp_enqueue_script('jquery-ui-tabs');</div></div></div></div></div></div></div></div></div><div class=""></div></div></div></div>##### Dialog:

<div class="relative w-full my-4" id="bkmrk-wp_enqueue_script%28%27j-4"><div class=""><div class="relative"><div class="h-full min-h-0 min-w-0"><div class="h-full min-h-0 min-w-0"><div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl"><div class="h-full w-full border-radius-3xl bg-token-bg-elevated-secondary corner-superellipse/1.1 overflow-clip rounded-3xl lxnfua_clipPathFallback"><div class=""><div class="relative z-0 flex max-w-full"><div class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼ5 ͼj" dir="ltr" id="bkmrk-wp_enqueue_script%28%27j-5"><div class="cm-scroller"><div class="cm-content q9tKkq_readonly">wp_enqueue_script('jquery-ui-dialog');  
wp_enqueue_style('wp-jquery-ui-dialog');</div></div></div></div></div></div></div></div></div><div class=""></div></div></div></div>##### Datepicker:

<div class="relative w-full my-4" id="bkmrk-wp_enqueue_script%28%27j-6"><div class=""><div class="relative"><div class="h-full min-h-0 min-w-0"><div class="h-full min-h-0 min-w-0"><div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl"><div class="h-full w-full border-radius-3xl bg-token-bg-elevated-secondary corner-superellipse/1.1 overflow-clip rounded-3xl lxnfua_clipPathFallback"><div class=""><div class="relative z-0 flex max-w-full"><div class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼ5 ͼj" dir="ltr" id="bkmrk-wp_enqueue_script%28%27j-7"><div class="cm-scroller"><div class="cm-content q9tKkq_readonly">wp_enqueue_script('jquery-ui-datepicker');</div></div></div></div></div></div></div></div></div><div class=""><div class=""></div></div></div></div></div>#### 🎨 Важно: CSS

WordPress **не включает полный jQuery UI theme CSS**.

Для dialog есть:

<div class="relative w-full my-4" id="bkmrk-wp_enqueue_style%28%27wp"><div class=""><div class="relative"><div class="h-full min-h-0 min-w-0"><div class="h-full min-h-0 min-w-0"><div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl"><div class="h-full w-full border-radius-3xl bg-token-bg-elevated-secondary corner-superellipse/1.1 overflow-clip rounded-3xl lxnfua_clipPathFallback"><div class=""><div class="relative z-0 flex max-w-full"><div class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼ5 ͼj" dir="ltr" id="bkmrk-wp_enqueue_style%28%27wp-1"><div class="cm-scroller"><div class="cm-content q9tKkq_readonly">wp_enqueue_style('wp-jquery-ui-dialog');</div></div></div></div></div></div></div></div></div><div class=""></div></div></div></div>Но для tabs, accordion и других — стили придётся писать самому или подключать свою тему jQuery UI.

---

### 🧩 4️⃣ Dashicons

Встроенная иконная библиотека.

```php
wp_enqueue_style('dashicons');

```

---

### 🎨 5️⃣ Стандартные стили админки

Можно использовать встроенные CSS-классы:

- `.wrap`
- `.form-table`
- `.button`
- `.notice`
- `.widefat`
- `.nav-tab-wrapper`
- `.postbox`

Пример:

```html
<div class="wrap">
    <h1>SEO Settings</h1>
</div>

```

Это самый простой путь.

---

### 🚀 6️⃣ @wordpress/components (UI-библиотека)

Очень мощная вещь.

Компоненты:

- Button
- TextControl
- SelectControl
- ToggleControl
- Panel
- TabPanel
- Notice
- Modal

Пример:

```js
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