# Ядро 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**

Он:

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