<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Андрей Викулов (VProger)</title>
    <description>The latest articles on DEV Community by Андрей Викулов (VProger) (@_vproger_).</description>
    <link>https://dev.to/_vproger_</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3736842%2Fa729d334-348d-49b8-90df-eeca7e767b29.png</url>
      <title>DEV Community: Андрей Викулов (VProger)</title>
      <link>https://dev.to/_vproger_</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_vproger_"/>
    <language>en</language>
    <item>
      <title>MySQL не запускается в BitrixVM: ошибка Unix socket lock file is empty</title>
      <dc:creator>Андрей Викулов (VProger)</dc:creator>
      <pubDate>Fri, 26 Jun 2026 09:09:34 +0000</pubDate>
      <link>https://dev.to/_vproger_/mysql-nie-zapuskaietsia-v-bitrixvm-oshibka-unix-socket-lock-file-is-empty-14a2</link>
      <guid>https://dev.to/_vproger_/mysql-nie-zapuskaietsia-v-bitrixvm-oshibka-unix-socket-lock-file-is-empty-14a2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0isw7zmytlvtynlritvv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0isw7zmytlvtynlritvv.png" alt="MySQL не запускается в BitrixVM: ошибка Unix socket lock file is empty" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Недавно столкнулся с интересной проблемой на сервере с BitrixVM и Percona Server 5.7. На первый взгляд MySQL отказывался запускаться без каких-либо очевидных причин, а systemd выводил довольно бесполезное сообщение:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initialization of mysqld failed: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Разберёмся, как диагностировать подобную проблему и почему в итоге помогло удаление файлов &lt;code&gt;mysqld.sock&lt;/code&gt; и &lt;code&gt;mysqld.sock.lock&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Симптомы
&lt;/h2&gt;

&lt;p&gt;При попытке запуска MySQL сервис постоянно падал:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl restart mysqld
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Проверка статуса показывала:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status mysqld
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Результат:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initialization of mysqld failed: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;При этом в системном журнале не было подробностей, поэтому пришлось смотреть лог MySQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Анализ логов
&lt;/h2&gt;

&lt;p&gt;Открываем журнал ошибок:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-200&lt;/span&gt; /var/log/mysql/error.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;В логе обнаружилась настоящая причина:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ERROR] Unix socket lock file is empty /var/lib/mysqld/mysqld.sock.lock.
[ERROR] Unable to setup unix socket lock file.
[ERROR] Aborting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Самое интересное — InnoDB успешно проходил инициализацию:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;InnoDB: Percona XtraDB started
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;То есть проблема была не в таблицах, не в повреждении базы данных и не в InnoDB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Что такое mysqld.sock и mysqld.sock.lock
&lt;/h2&gt;

&lt;p&gt;MySQL использует Unix Socket для локальных подключений.&lt;/p&gt;

&lt;p&gt;На сервере обычно присутствуют файлы:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/var/lib/mysqld/mysqld.sock
/var/lib/mysqld/mysqld.sock.lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Это служебные файлы.&lt;/p&gt;

&lt;p&gt;Они не содержат данные базы и создаются автоматически при запуске сервера MySQL.&lt;/p&gt;

&lt;p&gt;Если сервис завершился аварийно или файловая система была переполнена, эти файлы могут остаться в некорректном состоянии.&lt;/p&gt;

&lt;h2&gt;
  
  
  Дополнительная проблема
&lt;/h2&gt;

&lt;p&gt;Параллельно обнаружилось, что корневой раздел практически полностью заполнен:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Результат:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/dev/vda1 158G 156G 2.3G 99%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Недостаток свободного места часто становится причиной подобных ошибок.&lt;/p&gt;

&lt;h2&gt;
  
  
  Решение
&lt;/h2&gt;

&lt;p&gt;Сначала убедился, что процесс MySQL полностью остановлен:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pkill mysqld
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;После этого удалил служебные файлы:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/lib/mysqld/mysqld.sock
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/lib/mysqld/mysqld.sock.lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Запустил сервис заново:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl start mysqld
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;После очистки повреждённого lock-файла сервер успешно стартовал.&lt;/p&gt;

&lt;h2&gt;
  
  
  Важно
&lt;/h2&gt;

&lt;p&gt;Удаление файлов:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mysqld.sock
mysqld.sock.lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;не приводит к потере данных.&lt;/p&gt;

&lt;p&gt;Это временные служебные файлы, которые MySQL создаёт автоматически при запуске.&lt;/p&gt;

&lt;p&gt;Однако выполнять удаление следует только после полной остановки сервиса.&lt;/p&gt;

&lt;h2&gt;
  
  
  Что проверить дополнительно
&lt;/h2&gt;

&lt;p&gt;Если проблема повторяется:&lt;/p&gt;

&lt;h3&gt;
  
  
  Свободное место
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Размер каталогов
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-xh&lt;/span&gt; / &lt;span class="nt"&gt;--max-depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-hr&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Логи MySQL
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-200&lt;/span&gt; /var/log/mysql/error.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Активные процессы
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pgrep &lt;span class="nt"&gt;-a&lt;/span&gt; mysqld
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Вывод
&lt;/h2&gt;

&lt;p&gt;Если MySQL в BitrixVM или CentOS неожиданно перестал запускаться, а в логах присутствует ошибка:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unix socket lock file is empty
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;не спешите восстанавливать базы или искать повреждение InnoDB.&lt;/p&gt;

&lt;p&gt;В большинстве случаев проблема связана с повреждённым socket lock-файлом или нехваткой места на диске. Проверка логов и очистка служебных файлов позволяют восстановить работу сервиса за несколько минут.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://viku-lov.ru/blog/mysql-unix-socket-lock-file-is-empty-bitrixvm" rel="noopener noreferrer"&gt;Read more on Viku-Lov Studio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>bash</category>
      <category>server</category>
      <category>database</category>
    </item>
    <item>
      <title>Личный диск Bitrix24 не открывается и не загружаются файлы: восстановление Storage пользователя</title>
      <dc:creator>Андрей Викулов (VProger)</dc:creator>
      <pubDate>Fri, 26 Jun 2026 09:09:08 +0000</pubDate>
      <link>https://dev.to/_vproger_/lichnyi-disk-bitrix24-nie-otkryvaietsia-i-nie-zaghruzhaiutsia-faily-vosstanovlieniie-storage-polzovatielia-21g0</link>
      <guid>https://dev.to/_vproger_/lichnyi-disk-bitrix24-nie-otkryvaietsia-i-nie-zaghruzhaiutsia-faily-vosstanovlieniie-storage-polzovatielia-21g0</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fviku-lov.ru%2Fuploads%2F2026%2F06%2F1781512762072-ChatGPT_Image_15%2520____._2026__.__11_40_02.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fviku-lov.ru%2Fuploads%2F2026%2F06%2F1781512762072-ChatGPT_Image_15%2520____._2026__.__11_40_02.png" alt="Личный диск Bitrix24 не открывается и не загружаются файлы: восстановление Storage пользователя" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Симптомы
&lt;/h2&gt;

&lt;p&gt;На одном из порталов Bitrix24 возникла странная ситуация.&lt;/p&gt;

&lt;p&gt;У администратора перестал открываться личный диск:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/company/personal/user/14254/disk/path/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Вместо интерфейса диска отображалась обычная лента активности.&lt;/p&gt;

&lt;p&gt;Дополнительно появилась ошибка при загрузке файлов через модуль Почта:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Недостаточно прав доступа для загрузки файла на сервер
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;При этом:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;пользователь являлся администратором;&lt;/li&gt;
&lt;li&gt;права доступа были корректны;&lt;/li&gt;
&lt;li&gt;свободное место на сервере присутствовало;&lt;/li&gt;
&lt;li&gt;другие сотрудники работали без проблем.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Первичная диагностика
&lt;/h2&gt;

&lt;p&gt;Первое подозрение падает на модуль Disk.&lt;/p&gt;

&lt;p&gt;Проверяем наличие пользовательского Storage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;14254&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;\Bitrix\Disk\Driver&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStorageByUserId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$storage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Результат:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NULL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Для сравнения проверяем рабочего пользователя:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;\Bitrix\Disk\Driver&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStorageByUserId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$storage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Результат:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;object(Bitrix\Disk\Storage)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Что означает NULL
&lt;/h2&gt;

&lt;p&gt;Каждый пользователь Bitrix24 имеет собственное хранилище файлов.&lt;/p&gt;

&lt;p&gt;Оно хранится в таблице:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;b_disk_storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Через него работают:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Личный диск;&lt;/li&gt;
&lt;li&gt;Почта;&lt;/li&gt;
&lt;li&gt;Чат;&lt;/li&gt;
&lt;li&gt;Комментарии;&lt;/li&gt;
&lt;li&gt;Задачи;&lt;/li&gt;
&lt;li&gt;Прикрепление файлов.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Если Storage отсутствует, пользователь теряет возможность работать с файлами.&lt;/p&gt;




&lt;h2&gt;
  
  
  Проверка базы данных
&lt;/h2&gt;

&lt;p&gt;Проверяем наличие записи:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;b_disk_storage&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ENTITY_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;14254&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Результат:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 rows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Запись отсутствовала.&lt;/p&gt;




&lt;h2&gt;
  
  
  Решение
&lt;/h2&gt;

&lt;p&gt;Для восстановления хранилища используем штатный API Bitrix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;14254&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;\Bitrix\Main\UserTable&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;\Bitrix\Disk\Driver&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addUserStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$storage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;После выполнения метода Bitrix автоматически:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;создал Storage;&lt;/li&gt;
&lt;li&gt;создал корневую папку;&lt;/li&gt;
&lt;li&gt;зарегистрировал права доступа;&lt;/li&gt;
&lt;li&gt;восстановил структуру пользовательского диска.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Результат
&lt;/h2&gt;

&lt;p&gt;После создания Storage:&lt;/p&gt;

&lt;p&gt;✅ открылся личный диск&lt;/p&gt;

&lt;p&gt;✅ заработала загрузка файлов&lt;/p&gt;

&lt;p&gt;✅ исчезла ошибка доступа&lt;/p&gt;

&lt;p&gt;✅ восстановилась работа почтовых вложений&lt;/p&gt;




&lt;h2&gt;
  
  
  Как найти других пользователей без Storage
&lt;/h2&gt;

&lt;p&gt;Полезный скрипт для аудита портала:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$rsUsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;\Bitrix\Main\UserTable&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getList&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
&lt;span class="s1"&gt;'select'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'ID'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'LOGIN'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$rsUsers&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nv"&gt;$storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;\Bitrix\Disk\Driver&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStorageByUserId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'ID'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$storage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'ID'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' | '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'LOGIN'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Почему это происходит
&lt;/h2&gt;

&lt;p&gt;На практике такое встречается после:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;восстановления резервной копии;&lt;/li&gt;
&lt;li&gt;миграции портала;&lt;/li&gt;
&lt;li&gt;ошибок обновления;&lt;/li&gt;
&lt;li&gt;некорректного импорта пользователей;&lt;/li&gt;
&lt;li&gt;ручного изменения таблиц базы данных.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Вывод
&lt;/h2&gt;

&lt;p&gt;Если в Bitrix24 одновременно:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;не открывается личный диск;&lt;/li&gt;
&lt;li&gt;не прикрепляются файлы;&lt;/li&gt;
&lt;li&gt;появляются ошибки доступа;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;то первым делом проверьте наличие пользовательского Storage через:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;\Bitrix\Disk\Driver&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStorageByUserId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Возвращаемый &lt;code&gt;NULL&lt;/code&gt; почти всегда указывает на отсутствие персонального хранилища пользователя.&lt;/p&gt;

&lt;p&gt;В большинстве случаев проблема решается штатным методом:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;\Bitrix\Disk\Driver&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addUserStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;без вмешательства в базу данных и без переустановки модуля Disk.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://viku-lov.ru/blog/bitrix24-user-disk-storage-recovery" rel="noopener noreferrer"&gt;Read more on Viku-Lov Studio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>bitrix</category>
      <category>php</category>
      <category>crm</category>
      <category>cms</category>
    </item>
    <item>
      <title>1С-Битрикс: кастомная страница настроек темы через Option API</title>
      <dc:creator>Андрей Викулов (VProger)</dc:creator>
      <pubDate>Fri, 26 Jun 2026 07:01:26 +0000</pubDate>
      <link>https://dev.to/_vproger_/1s-bitriks-kastomnaia-stranitsa-nastroiek-tiemy-chieriez-option-api-7ao</link>
      <guid>https://dev.to/_vproger_/1s-bitriks-kastomnaia-stranitsa-nastroiek-tiemy-chieriez-option-api-7ao</guid>
      <description>&lt;h1&gt;
  
  
  1С-Битрикс: кастомная страница настроек темы через Option API
&lt;/h1&gt;

&lt;p&gt;Задача: сделать свою страницу настроек темы и сохранить параметры так, чтобы они стабильно применялись в шаблоне сайта. В новой документации Битрикс для этой задачи есть все базовые элементы: работа с административной панелью, API опций модуля и стандартная защита формы от CSRF.&lt;/p&gt;

&lt;p&gt;Официальные источники:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.1c-bitrix.ru/pages/cms-basics/admin-panel.html" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/pages/cms-basics/admin-panel.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.1c-bitrix.ru/pages/security/csrf-ssrf.html#csrf-tokeny" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/pages/security/csrf-ssrf.html#csrf-tokeny&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.1c-bitrix.ru/api/classes/Bitrix-Main-Config-Option.html#method%5C_set" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/api/classes/Bitrix-Main-Config-Option.html#method\_set&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.1c-bitrix.ru/api/classes/Bitrix-Main-Config-Option.html#method%5C_get" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/api/classes/Bitrix-Main-Config-Option.html#method\_get&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method%5C_AddPanelButton" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method\_AddPanelButton&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method%5C_GetPopupLink" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method\_GetPopupLink&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  В чем проблема
&lt;/h2&gt;

&lt;p&gt;Частая ситуация: параметры темы хранят в файлах или случайных таблицах, а затем сложно понять, где именно меняется цвет, телефон, логотип или поведение блока. В итоге:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;нет единой точки хранения;&lt;/li&gt;
&lt;li&gt;настройки меняются небезопасно;&lt;/li&gt;
&lt;li&gt;после обновлений или миграций часть параметров теряется;&lt;/li&gt;
&lt;li&gt;сложно проверить, что именно применилось на фронте.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Для устойчивой схемы в Битрикс достаточно:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;хранить настройки в &lt;code&gt;Option&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;отправлять форму только с CSRF-токеном;&lt;/li&gt;
&lt;li&gt;читать настройки в шаблоне сайта в одном месте.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Рабочее решение
&lt;/h2&gt;

&lt;p&gt;Ниже минимальный рабочий сценарий без недокументированных зависимостей.&lt;/p&gt;

&lt;h3&gt;
  
  
  Шаг 1. Добавляем кнопку открытия страницы настройки
&lt;/h3&gt;

&lt;p&gt;Документация по панели администрирования показывает, что можно добавить свою кнопку через &lt;code&gt;AddPanelButton&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// Например, в шаблоне после подключения пролога&lt;/span&gt;
&lt;span class="nv"&gt;$APPLICATION&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;AddPanelButton&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
&lt;span class="s2"&gt;"ID"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"theme_settings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s2"&gt;"TEXT"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Настройки темы"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s2"&gt;"TYPE"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"BIG"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s2"&gt;"SORT"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s2"&gt;"HREF"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"/local/admin/theme_settings.php"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method%5C_AddPanelButton" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method\_AddPanelButton&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.1c-bitrix.ru/pages/cms-basics/admin-panel.html#dobavit-knopki" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/pages/cms-basics/admin-panel.html#dobavit-knopki&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Если хотите открыть страницу в popup, используйте &lt;code&gt;GetPopupLink&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method%5C_GetPopupLink" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method\_GetPopupLink&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Шаг 2. Делаем форму с CSRF-токеном
&lt;/h3&gt;

&lt;p&gt;В новой документации по безопасности явно указаны &lt;code&gt;bitrixsessidpost()&lt;/code&gt; и &lt;code&gt;checkbitrixsessid()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="k"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"DOCUMENT_ROOT"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/bitrix/modules/main/include/prolog_admin_before.php"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Bitrix\Main\Config\Option&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$moduleId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"REQUEST_METHOD"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;check_bitrix_sessid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;die&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Invalid session"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;$phone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"theme_phone"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nv"&gt;$accent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"theme_accent"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nc"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$moduleId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"theme_phone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$phone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$moduleId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"theme_accent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$accent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;$savedPhone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$moduleId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"theme_phone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$savedAccent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$moduleId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"theme_accent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"#0a7c66"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;?=&lt;/span&gt; &lt;span class="nf"&gt;bitrix_sessid_post&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Телефон в шапке&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&amp;lt;br&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"theme_phone"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;?=&lt;/span&gt; &lt;span class="nb"&gt;htmlspecialchars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$savedPhone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ENT_QUOTES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"UTF-8"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Акцентный цвет&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&amp;lt;br&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"theme_accent"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;?=&lt;/span&gt; &lt;span class="nb"&gt;htmlspecialchars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$savedAccent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ENT_QUOTES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"UTF-8"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Сохранить&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Документация:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.1c-bitrix.ru/pages/security/csrf-ssrf.html#csrf-tokeny" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/pages/security/csrf-ssrf.html#csrf-tokeny&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.1c-bitrix.ru/api/classes/Bitrix-Main-Config-Option.html#method%5C_set" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/api/classes/Bitrix-Main-Config-Option.html#method\_set&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.1c-bitrix.ru/api/classes/Bitrix-Main-Config-Option.html#method%5C_get" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/api/classes/Bitrix-Main-Config-Option.html#method\_get&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Шаг 3. Читаем настройки в шаблоне темы
&lt;/h3&gt;

&lt;p&gt;Дальше используем те же ключи &lt;code&gt;Option&lt;/code&gt; в шаблоне:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Bitrix\Main\Config\Option&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$moduleId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$themePhone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$moduleId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"theme_phone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"+7 (000) 000-00-00"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$themeAccent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$moduleId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"theme_accent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"#0a7c66"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--theme-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;?=&lt;/span&gt; &lt;span class="nb"&gt;htmlspecialchars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$themeAccent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ENT_QUOTES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"UTF-8"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"tel:&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;?=&lt;/span&gt; &lt;span class="nb"&gt;htmlspecialchars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$themePhone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ENT_QUOTES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"UTF-8"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;?=&lt;/span&gt; &lt;span class="nb"&gt;htmlspecialchars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$themePhone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ENT_QUOTES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"UTF-8"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Идея простая: одна точка сохранения и одна точка чтения. Это упрощает поддержку и перенос.&lt;/p&gt;

&lt;h2&gt;
  
  
  Проверка результата
&lt;/h2&gt;

&lt;p&gt;Проверяем по шагам:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Открыть страницу настроек через кнопку панели.&lt;/li&gt;
&lt;li&gt;Сохранить значения.&lt;/li&gt;
&lt;li&gt;Обновить публичную страницу и убедиться, что новые данные применились.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Быстрая проверка из HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.ru/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"theme-accent|tel:"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ожидаемый признак: в ответе видны обновленные значения цвета и телефона.&lt;/p&gt;

&lt;h2&gt;
  
  
  Типичные ошибки
&lt;/h2&gt;

&lt;p&gt;❌ Ошибка: сохраняют POST без проверки токена.&lt;/p&gt;

&lt;p&gt;Причина: нет &lt;code&gt;checkbitrixsessid()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Как исправить: добавить проверку и &lt;code&gt;bitrixsessidpost()&lt;/code&gt; в форму.&lt;/p&gt;

&lt;p&gt;❌ Ошибка: ключи в &lt;code&gt;Option::set()&lt;/code&gt; и &lt;code&gt;Option::get()&lt;/code&gt; не совпадают.&lt;/p&gt;

&lt;p&gt;Причина: опечатки вроде &lt;code&gt;themephone&lt;/code&gt; vs &lt;code&gt;themetel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Как исправить: вынести ключи в константы или использовать одинаковые литералы.&lt;/p&gt;

&lt;p&gt;❌ Ошибка: выводят значение без экранирования.&lt;/p&gt;

&lt;p&gt;Причина: прямой вывод пользовательского текста в HTML.&lt;/p&gt;

&lt;p&gt;Как исправить: использовать &lt;code&gt;htmlspecialchars(..., ENT_QUOTES, &amp;amp;quot;UTF-8&amp;amp;quot;)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;❌ Ошибка: ожидают, что кнопка панели появится для всех пользователей.&lt;/p&gt;

&lt;p&gt;Причина: права и настройки панели зависят от пользователя/группы.&lt;/p&gt;

&lt;p&gt;Как исправить: проверить доступы и настройки панели администрирования:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.1c-bitrix.ru/pages/cms-basics/admin-panel.html" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/pages/cms-basics/admin-panel.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Где применять
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;В темах, где нужно управлять контактами, цветами и переключателями блоков.&lt;/li&gt;
&lt;li&gt;В проектах с контент-редакторами, которым нужен простой интерфейс без правок кода.&lt;/li&gt;
&lt;li&gt;В production, где важно централизованное хранение параметров.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Дополнительно по структуре проекта:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://viku-lov.ru/blog/bitrix-local-folder-project-structure" rel="noopener noreferrer"&gt;https://viku-lov.ru/blog/bitrix-local-folder-project-structure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://viku-lov.ru/blog/bitrix-simple-module-with-component" rel="noopener noreferrer"&gt;https://viku-lov.ru/blog/bitrix-simple-module-with-component&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://viku-lov.ru/blog/bitrix-custom-theme-settings-page-option" rel="noopener noreferrer"&gt;Read more on Viku-Lov Studio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>bitrix</category>
      <category>theme</category>
      <category>admin</category>
      <category>security</category>
    </item>
    <item>
      <title>1С-Битрикс: как настроить canonical через SetPageProperty и ShowMeta</title>
      <dc:creator>Андрей Викулов (VProger)</dc:creator>
      <pubDate>Fri, 26 Jun 2026 07:00:35 +0000</pubDate>
      <link>https://dev.to/_vproger_/1s-bitriks-kak-nastroit-canonical-chieriez-setpageproperty-i-showmeta-57mm</link>
      <guid>https://dev.to/_vproger_/1s-bitriks-kak-nastroit-canonical-chieriez-setpageproperty-i-showmeta-57mm</guid>
      <description>&lt;h1&gt;
  
  
  1С-Битрикс: как настроить canonical через SetPageProperty и ShowMeta
&lt;/h1&gt;

&lt;p&gt;Запрос, с которым обычно приходят: в индексе появляются дубли страниц, а canonical либо не выводится, либо выводится не там, где ожидается. В 1С-Битрикс это решается штатно через свойства страницы и раздела: &lt;code&gt;SetPageProperty&lt;/code&gt;, &lt;code&gt;SetDirProperty&lt;/code&gt;, &lt;code&gt;ShowMeta&lt;/code&gt;. Ниже только рабочая схема по официальной документации, без кастомной магии.&lt;/p&gt;

&lt;p&gt;Официальные источники:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.1c-bitrix.ru/pages/cms-basics/page-templates.html" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/pages/cms-basics/page-templates.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method%5C_SetPageProperty" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method\_SetPageProperty&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method%5C_SetDirProperty" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method\_SetDirProperty&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method%5C_ShowMeta" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method\_ShowMeta&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method%5C_GetProperty" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method\_GetProperty&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  В чем проблема
&lt;/h2&gt;

&lt;p&gt;Типовые симптомы:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;canonical вообще отсутствует в &lt;code&gt;&amp;amp;lt;head&amp;amp;gt;&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;canonical задается, но не на всех страницах раздела;&lt;/li&gt;
&lt;li&gt;в шаблоне есть вывод meta-тегов, но canonical не появляется;&lt;/li&gt;
&lt;li&gt;на разных URL с одинаковым контентом нет единого канонического адреса.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Почему это происходит в Битрикс:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;свойство canonical задано на странице, но в шаблоне не выведено через &lt;code&gt;ShowMeta&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;canonical ожидают из &lt;code&gt;.section.php&lt;/code&gt;, но используют &lt;code&gt;SetPageProperty&lt;/code&gt; вместо &lt;code&gt;SetDirProperty&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;свойство называется не так, как параметр в &lt;code&gt;ShowMeta&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;задают свойство поздно и не проверяют итоговый HTML.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Документация по шаблонам страниц прямо указывает, что поведение страницы определяется свойствами, которые можно задавать на уровне страницы и раздела: &lt;a href="https://docs.1c-bitrix.ru/pages/cms-basics/page-templates.html#svojstva-stranic" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/pages/cms-basics/page-templates.html#svojstva-stranic&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Рабочее решение
&lt;/h2&gt;

&lt;p&gt;Ниже схема из трех шагов: вывод свойства в шаблоне, установка на странице, установка на раздел.&lt;/p&gt;

&lt;h3&gt;
  
  
  Шаг 1. Выводим canonical в &lt;code&gt;head&lt;/code&gt; через &lt;code&gt;ShowMeta&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;В шаблоне сайта (обычно в служебной части шапки) добавляем вывод свойства canonical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// В head шаблона сайта&lt;/span&gt;
&lt;span class="nv"&gt;$APPLICATION&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ShowMeta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"canonical"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"canonical"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Метод &lt;code&gt;ShowMeta&lt;/code&gt; документирован в API &lt;code&gt;CAllMain&lt;/code&gt; и выводит мета-тег для указанного свойства:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method%5C_ShowMeta" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method\_ShowMeta&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Если canonical уже хранится в свойстве &lt;code&gt;canonical&lt;/code&gt;, второй параметр можно не дублировать. Но в практической настройке удобнее явно фиксировать имя meta-поля.&lt;/p&gt;

&lt;h3&gt;
  
  
  Шаг 2. Задаем canonical для конкретной страницы
&lt;/h3&gt;

&lt;p&gt;В нужной странице задаем свойство canonical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="k"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"DOCUMENT_ROOT"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/bitrix/header.php"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$APPLICATION&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;SetPageProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"canonical"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"https://example.ru/catalog/item-1/"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$APPLICATION&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;SetTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Карточка товара"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Контент страницы&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt; &lt;span class="k"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"DOCUMENT_ROOT"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/bitrix/footer.php"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Метод &lt;code&gt;SetPageProperty&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method%5C_SetPageProperty" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method\_SetPageProperty&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Шаг 3. Задаем canonical на уровень раздела
&lt;/h3&gt;

&lt;p&gt;Когда нужно единое поведение для раздела, используем &lt;code&gt;SetDirProperty&lt;/code&gt; (обычно в &lt;code&gt;.section.php&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// /catalog/.section.php&lt;/span&gt;
&lt;span class="nv"&gt;$sSectionName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Каталог"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$arDirProperties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="nv"&gt;$APPLICATION&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;SetDirProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"canonical"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"https://example.ru/catalog/"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Метод &lt;code&gt;SetDirProperty&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method%5C_SetDirProperty" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method\_SetDirProperty&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Шаг 4. Проверяем текущее значение свойства в коде
&lt;/h3&gt;

&lt;p&gt;Для диагностики можно проверить, какое значение реально видит страница:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="nv"&gt;$canonical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$APPLICATION&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;GetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"canonical"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;pre&amp;gt;canonical: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;htmlspecialchars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$canonical&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ENT_QUOTES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"UTF-8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;/pre&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Метод &lt;code&gt;GetProperty&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method%5C_GetProperty" rel="noopener noreferrer"&gt;https://docs.1c-bitrix.ru/api/classes/CAllMain.html#method\_GetProperty&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Проверка результата
&lt;/h2&gt;

&lt;p&gt;Проверяем, что canonical реально попал в итоговый HTML страницы.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.ru/catalog/item-1/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; canonical
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ожидаемый результат:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"canonical"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"https://example.ru/catalog/item-1/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Если у вас в проекте принято использовать &lt;code&gt;&amp;amp;lt;link rel=&amp;amp;quot;canonical&amp;amp;quot;&amp;amp;gt;&lt;/code&gt;, это отдельная реализация в шаблоне. В этой статье рассматривается именно документированная схема через свойства и &lt;code&gt;ShowMeta&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Типичные ошибки
&lt;/h2&gt;

&lt;p&gt;❌ Ошибка: свойство задают, но не выводят.&lt;/p&gt;

&lt;p&gt;Причина: есть &lt;code&gt;SetPageProperty&lt;/code&gt;, но нет &lt;code&gt;ShowMeta&lt;/code&gt; в &lt;code&gt;head&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Как исправить: добавить вызов &lt;code&gt;ShowMeta(&amp;amp;quot;canonical&amp;amp;quot;, &amp;amp;quot;canonical&amp;amp;quot;)&lt;/code&gt; в шаблон.&lt;/p&gt;

&lt;p&gt;❌ Ошибка: ждут, что значение из раздела применится, но задают на странице.&lt;/p&gt;

&lt;p&gt;Причина: путают &lt;code&gt;SetPageProperty&lt;/code&gt; и &lt;code&gt;SetDirProperty&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Как исправить: для раздела использовать &lt;code&gt;SetDirProperty&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;❌ Ошибка: canonical «пустой» в HTML.&lt;/p&gt;

&lt;p&gt;Причина: разное имя свойства при установке и выводе (например, &lt;code&gt;canon_url&lt;/code&gt; и &lt;code&gt;canonical&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Как исправить: унифицировать идентификатор свойства во всех местах.&lt;/p&gt;

&lt;p&gt;❌ Ошибка: проверяют только в браузере и не видят реальный HTML после правок.&lt;/p&gt;

&lt;p&gt;Причина: не выполняют проверку через &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Как исправить: всегда проверять итоговую разметку командой из раздела выше.&lt;/p&gt;

&lt;h2&gt;
  
  
  Где применять
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SEO-настройка карточек и разделов каталога.&lt;/li&gt;
&lt;li&gt;Проекты с дублирующими URL в публичной части.&lt;/li&gt;
&lt;li&gt;Миграции и реструктуризация URL, где важно зафиксировать канонический адрес.&lt;/li&gt;
&lt;li&gt;Поддержка больших разделов, где свойства удобнее задавать на уровне &lt;code&gt;.section.php&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Полезно дополнительно:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://viku-lov.ru/blog/bitrix-local-folder-project-structure" rel="noopener noreferrer"&gt;https://viku-lov.ru/blog/bitrix-local-folder-project-structure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://viku-lov.ru/blog/migraciya-s-wordpress-na-bitrix-bez-poteri-seo" rel="noopener noreferrer"&gt;https://viku-lov.ru/blog/migraciya-s-wordpress-na-bitrix-bez-poteri-seo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://viku-lov.ru/blog/bitrix-canonical-setpageproperty-showmeta" rel="noopener noreferrer"&gt;Read more on Viku-Lov Studio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>production</category>
      <category>bitrix</category>
      <category>seo</category>
      <category>canonical</category>
    </item>
    <item>
      <title>Как устранить ошибку Class not found в PHP: настройка автозагрузки PSR-4</title>
      <dc:creator>Андрей Викулов (VProger)</dc:creator>
      <pubDate>Fri, 26 Jun 2026 07:00:11 +0000</pubDate>
      <link>https://dev.to/_vproger_/kak-ustranit-oshibku-class-not-found-v-php-nastroika-avtozaghruzki-psr-4-3fa1</link>
      <guid>https://dev.to/_vproger_/kak-ustranit-oshibku-class-not-found-v-php-nastroika-avtozaghruzki-psr-4-3fa1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw3orcp9lvhpjz9ly35ns.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw3orcp9lvhpjz9ly35ns.png" alt="Как устранить ошибку Class not found в PHP: настройка автозагрузки PSR-4" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Как устранить ошибку Class not found в PHP: настройка автозагрузки PSR-4&lt;/p&gt;

&lt;p&gt;При переходе от процедурного кода к ООП часто возникает фатальная ошибка «класс не найден»: PHP не подключает файл с классом. В итоге проект обрастает десятками require, структура путается, поддержка усложняется. Ниже — пошаговое внедрение PSR-4 через Composer: классы начнут подгружаться автозагрузкой, структура проекта станет предсказуемой.&lt;/p&gt;




&lt;p&gt;В чём проблема&lt;/p&gt;

&lt;p&gt;Классический симптом отсутствия стандартизированной структуры — постоянные падения скрипта при попытке создать объект нового класса или вызвать статический метод зависимого сервиса, который расположен в другом файле.&lt;/p&gt;

&lt;p&gt;Пример реальной ошибки в логах веб-сервера (или прямо на экране, если включён вывод ошибок):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Fatal error: Uncaught Error: Class 'Database\Connection' not found in /var/www/html/index.php:5

Stack trace:

#0 {main}

thrown in /var/www/html/index.php on line 5

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Эта ошибка возникает потому, что PHP-интерпретатор не знает, где физически лежит файл с описанием класса Connection из пространства имён Database. В старом, так называемом «legacy» коде это решалось исключительно ручным управлением зависимостями и путями:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;
&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="c1"&gt;// Антипаттерн: ручное подключение каждого файла при разрастании кодовой базы&lt;/span&gt;

&lt;span class="n"&gt;require\_once&lt;/span&gt; &lt;span class="n"&gt;\_\_DIR\_\_&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/Database/Connection.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;require\_once&lt;/span&gt; &lt;span class="n"&gt;\_\_DIR\_\_&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/Models/User.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;require\_once&lt;/span&gt; &lt;span class="n"&gt;\_\_DIR\_\_&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/Services/EmailSender.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;require\_once&lt;/span&gt; &lt;span class="n"&gt;\_\_DIR\_\_&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/Controllers/AuthController.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Database\Connection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Дальнейшая бизнес-логика...&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Когда файлов становится больше сотни, управлять зависимостями вручную физически невозможно. Возникают циклические зависимости, дублирование путей и критические ошибки на production-сервере из-за банально забытого вызова require. Решение этой проблемы регламентировано стандартом PSR-4 от группы PHP-FIG, который диктует чёткое соответствие виртуального пространства имён (namespace) физической структуре директорий на диске. Это фундаментальное правило любого современного фреймворка или модульной системы.&lt;/p&gt;




&lt;p&gt;Рабочее решение&lt;/p&gt;

&lt;p&gt;Для автозагрузки отказываемся от ручных require и подключаем менеджер пакетов Composer. Он создаёт автозагрузчик по стандарту PSR-4 и сам подхватывает нужные файлы по имени класса и namespace.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Инициализация Composer и конфигурация файлов&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Убедитесь, что вы находитесь в корне вашего проекта и у вас установлен консольный Composer. Создайте или обновите базовый конфигурационный файл composer.json для описания архитектуры вашего приложения:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vproger/php-oop-example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Пример настройки PSR-4 автозагрузки в чистом PHP проекте"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nl"&gt;"autoload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nl"&gt;"psr-4"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nl"&gt;"App\\"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Здесь блок autoload с ключом psr-4 напрямую указывает менеджеру пакетов, что все PHP-классы, расположенные в корневом пространстве имён App, будут физически находиться внутри директории src/ относительно корня проекта. Обратите внимание на двойной слэш \ — он обязателен в формате JSON для экранирования разделителя пространства имён.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Создание физической структуры директорий&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Создайте директорию src и воспроизведите в ней структуру, которая будет строго соответствовать вашим будущим пространствам имён. Написание директорий должно полностью совпадать с регистрами (Capitalized):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; src/Database

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; src/Services

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Теперь создадим класс для подключения к базе данных. Создайте файл по пути src/Database/Connection.php:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;
&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Database&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Connection&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;PDO&lt;/span&gt; &lt;span class="nv"&gt;$pdo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;\_\_construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$dsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="c1"&gt;// Инициализация соединения с базой&lt;/span&gt;

&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pdo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PDO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$dsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pdo&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ATTR\_ERRMODE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;PDO&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ERRMODE\_EXCEPTION&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getPdo&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;PDO&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pdo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Критически важно: пространство имён App\Database строго соответствует физическому пути src/Database. Имя файла Connection.php в точности совпадает с названием класса Connection.&lt;/p&gt;

&lt;p&gt;Создадим второй класс, который будет использовать наш коннект. Создадим сервис для работы с пользователями в файле src/Services/UserService.php:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;
&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Services&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Database\Connection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;Connection&lt;/span&gt; &lt;span class="nv"&gt;$db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;\_\_construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Connection&lt;/span&gt; &lt;span class="nv"&gt;$db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="c1"&gt;// В реальном проекте здесь был бы SQL-запрос через $this-&amp;gt;db-&amp;gt;getPdo()&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;

&lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Алексей'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="s1"&gt;'role'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;

&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Бинарная генерация автозагрузчика файлов и точка входа&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Выполните команду для генерации файлов автозагрузки в терминале. Эту операцию нужно запускать каждый раз, когда вы меняете секцию autoload или базовые правила путей в файле composer.json:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
composer dump-autoload &lt;span class="nt"&gt;-o&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Обязательный флаг -o (--optimize) создаёт оптимизированную карту классов, сканируя все директории. Это необходимо для обеспечения максимальной производительности, чтобы автозагрузчику не приходилось каждый раз использовать дорогостоящие операции файловой системы file_exists().&lt;/p&gt;

&lt;p&gt;Теперь создадим единую точку входа — файл index.php в корне проекта, который будет перехватывать логику исполнения:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;
&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="c1"&gt;// Единственный обязательный require во всём вашем проекте!&lt;/span&gt;

&lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="n"&gt;\_\_DIR\_\_&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/vendor/autoload.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Импортируем классы для использования&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Database\Connection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Services\UserService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="c1"&gt;// Теперь PHP автоматически найдёт и подключит нужные файлы "на лету"&lt;/span&gt;

&lt;span class="nv"&gt;$connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mysql:host=localhost;dbname=test'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'root'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'secret'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$userService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$connection&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$userService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Content-Type: application/json; charset=utf-8'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nf"&gt;json\_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JSON\_UNESCAPED\_UNICODE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Критическая ошибка: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Первый аргумент Connection — DSN (строка подключения к БД). Больше никаких ручных require_once. Код стал масштабируемым, полностью соответствует принципам ООП и готов к расширению. Добавление сотен новых сервисов, контроллеров или репозиториев не потребует никаких правок в логике подключения.&lt;/p&gt;




&lt;p&gt;Проверка результата&lt;/p&gt;

&lt;p&gt;Чтобы убедиться, что автозагрузка работает правильно и нигде не сломана маршрутизация классов, запустите встроенный сервер PHP прямо в корне проекта:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
php &lt;span class="nt"&gt;-S&lt;/span&gt; localhost:8080

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Затем в другом окне системного терминала (или через клиент вроде Postman/Insomnia) выполните HTTP-запрос для проверки:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
curl &lt;span class="nt"&gt;-i&lt;/span&gt; http://localhost:8080/index.php

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ожидаемый вывод, если всё сделано без ошибок:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
HTTP/1.1 200 OK

Host: localhost:8080

Date: Sun, 01 Mar 2026 20:00:00 GMT

Connection: close

X-Powered-By: PHP/8.2.0

Content-Type: application/json; charset=utf-8

{"id":1,"name":"Алексей","role":"admin"}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Как понять, что всё прошло успешно:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Вывод точно соответствует ожидаемому JSON, скрипт полностью отрабатывает и не обрывается фатальной ошибкой Class not found.&lt;/li&gt;
&lt;li&gt;В созданной директории vendor существует корневой файл autoload.php.&lt;/li&gt;
&lt;li&gt;В файле vendor/composer/autoload_psr4.php (его создаёт Composer) есть запись вида array($baseDir . '/src'), связывающая префикс пространства имён с директорией.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Типичные ошибки&lt;/p&gt;

&lt;p&gt;Чаще всего PSR-4 автозагрузка перестаёт работать по таким причинам:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ &lt;strong&gt;Ошибка регистров в именах файлов или директорий (Самая частая проблема)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Причина:&lt;/strong&gt; В операционной системе Windows (и при использовании локальных сред вроде XAMPP) файловые пути регистронезависимы, поэтому namespace App\database без проблем подключит папку src/Database. Но при деплое на Linux (например, Ubuntu/Debian на production-сервере) файловая система строго регистрозависима (case-sensitive). В результате рабочий локальный проект мгновенно падает в production с ошибкой загрузчика.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Как исправить:&lt;/strong&gt; Строго следите за абсолютным совпадением регистра. Имена директорий и файлов должны в точности, до заглавной буквы, повторять ваши неймспейсы и имена реальных классов. Файл Connection.php лежит строго в папке Database, а класс назван Connection.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Забыта команда перегенерации composer dump-autoload&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Причина:&lt;/strong&gt; Вы добавили новый префикс модуля или изменили базовую директорию в конфигурации composer.json, структура поменялась, но вы не перегенерировали кэш-файл автозагрузчика. Composer банально ещё не знает о ваших новых настройках.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Как исправить:&lt;/strong&gt; После любых инфраструктурных изменений в секции autoload выполняйте директиву composer dump-autoload -o. В CI/CD пайплайне или в Docker-контейнере эту команду нужно обязательно добавлять в сборочный скрипт перед стартом веб-приложения.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Несоответствие пространства имён физическому пути файла&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Причина:&lt;/strong&gt; Вы скопировали старый класс из другого проекта, где исторически был namespace OldProject\Models, положили его в новую папку src/Services, и автозагрузчик логично не может его найти, так как ожидает строгий namespace App\Services.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Как исправить:&lt;/strong&gt; Откройте целевой файл скопированного класса и исправьте самую первую строку объявления namespace на корректную для текущего приложения. В современных средах разработки, таких как PhpStorm, следует всегда использовать функцию рефакторинга: выделите класс, затем отправьте команду Refactor -&amp;gt; Move Namespace, чтобы IDE сама поменяла все пути и импорты зависимостей.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Где применять&lt;/p&gt;

&lt;p&gt;PSR-4 через Composer — стандарт для современного PHP. Его стоит использовать везде:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Работающие production окружения (в классической связке веб-сервера Nginx/PHP-FPM).&lt;/li&gt;
&lt;li&gt;Изолированные микросервисы в Docker-контейнерах в Kubernetes-кластерах.&lt;/li&gt;
&lt;li&gt;CI/CD пайплайны развёртывания (команда composer install --no-dev --optimize-autoloader строго обязательна перед деплоем кода на сервер).&lt;/li&gt;
&lt;li&gt;Разработка бизнес-модулей в системах, так как полезно применять &lt;a href="https://dev.to/_vproger_/papka-local-v-1c-bitriks-struktura-proiekta-biez-boli-i-kostyliei-1cf"&gt;правильную файловую структуру в 1С-Битрикс&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Модернизация старых систем и правильное &lt;a href="https://dev.to/_vproger_/php-83-84-dlia-bitrix-i-wordpress-tipizatsiia-atributy-pattierny-24l8"&gt;использование новых возможностей и типизации в PHP 8.3/8.4&lt;/a&gt;, чтобы архитектура не отставала от языка.&lt;/li&gt;
&lt;li&gt;Рефакторинг старых legacy-приложений перед &lt;a href="https://dev.to/_vproger_/php-85-novyie-vozmozhnosti-apghrieid-i-riealnyie-riski-4hh8"&gt;обновлением на будущий PHP 8.5 и старше&lt;/a&gt;, где требования к чистоте кода возрастают экспоненциально.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Сниппеты по статье:&lt;/strong&gt; Composer: composer.json PSR-4 · composer dump-autoload -o · Точка входа require vendor/autoload · Проверка php -S и curl&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Термины словаря:&lt;/strong&gt; Composer · PHP-FIG · Fatal error · require/include · Case-sensitive · DSN · Менеджер пакетов · Зависимость&lt;/p&gt;

&lt;p&gt;&lt;a href="https://viku-lov.ru/blog/php-psr4-autoload-class-not-found" rel="noopener noreferrer"&gt;Read more on viku-lov.ru&lt;/a&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>psr4</category>
      <category>composer</category>
      <category>backend</category>
    </item>
    <item>
      <title>Закон об англицизмах 2026: что изменилось на самом деле</title>
      <dc:creator>Андрей Викулов (VProger)</dc:creator>
      <pubDate>Fri, 26 Jun 2026 06:59:42 +0000</pubDate>
      <link>https://dev.to/_vproger_/zakon-ob-anghlitsizmakh-2026-chto-izmienilos-na-samom-dielie-3521</link>
      <guid>https://dev.to/_vproger_/zakon-ob-anghlitsizmakh-2026-chto-izmienilos-na-samom-dielie-3521</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmrd0cqsct7dveimncb42.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmrd0cqsct7dveimncb42.png" alt="Закон об англицизмах 2026: что изменилось на самом деле" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Что означает закон об англицизмах в России с 2026 года на самом деле&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Коротко:&lt;/strong&gt; запрета английских слов нет. Требование одно — информация для потребителя должна быть доступна на русском. Вывеска «Coffee» допустима, если рядом есть «Кофе» того же размера. Бренды, слова из словарей и техобозначения не трогают.&lt;/p&gt;

&lt;p&gt;С марта 2026 года начали действовать обновлённые требования к использованию русского языка в публичной информации. В новостях это часто подают как «запрет английских слов», что звучит громко, но не совсем точно.&lt;/p&gt;

&lt;p&gt;Разберём спокойно, без эмоций: что именно изменилось, кому это важно и где преувеличения.&lt;/p&gt;




&lt;p&gt;В чём суть изменений&lt;/p&gt;

&lt;p&gt;Главный принцип закона простой:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;информация для потребителя должна быть доступна на русском языке.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;То есть человек не обязан знать иностранный язык, чтобы понять вывеску, меню или описание услуги. Речь идёт именно о сфере взаимодействия бизнеса с клиентом: вывески, меню, реклама, объявления в местах оказания услуг. Личные блоги, переписка и разговорная речь законом не регулируются.&lt;/p&gt;

&lt;p&gt;Важно: закон не предписывает «убрать всё английское». Он требует, чтобы информация была доступна на русском. Как именно это оформить (две строки на вывеске, табличка рядом, дублирование в меню) — вопрос практики и дизайна, в рамках требований к заметности и читаемости русского текста.&lt;/p&gt;




&lt;p&gt;Кого это касается&lt;/p&gt;

&lt;p&gt;В первую очередь:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;магазины&lt;/li&gt;
&lt;li&gt;кафе и рестораны&lt;/li&gt;
&lt;li&gt;сервисные компании&lt;/li&gt;
&lt;li&gt;застройщики&lt;/li&gt;
&lt;li&gt;реклама услуг&lt;/li&gt;
&lt;li&gt;публичная навигация&lt;/li&gt;
&lt;li&gt;точки общепита и розницы с вывесками на иностранном языке&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Обычных людей закон не касается.&lt;/p&gt;

&lt;p&gt;Разговорная речь, переписка, соцсети и личные блоги под регулирование не попадают. То есть писать посты или комментарии на любом языке по-прежнему можно; закон касается только той информации, которую бизнес адресует потребителю в момент оказания услуг или продажи товаров.&lt;/p&gt;




&lt;p&gt;Что изменится на практике&lt;/p&gt;

&lt;p&gt;Если используется иностранное слово в публичной информации, рядом должен быть русский вариант.&lt;/p&gt;

&lt;p&gt;Например:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Coffee&lt;/strong&gt; → Кофе&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sale&lt;/strong&gt; → Распродажа&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beauty Studio&lt;/strong&gt; → Студия красоты&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery&lt;/strong&gt; → Доставка&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open 24/7&lt;/strong&gt; → Круглосуточно&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;При этом русский текст должен быть:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;сопоставим по размеру&lt;/li&gt;
&lt;li&gt;читаемым&lt;/li&gt;
&lt;li&gt;понятным&lt;/li&gt;
&lt;li&gt;не менее заметным&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Фактически речь идёт не о запрете английского языка, а о дублировании информации. То есть вывеска «Coffee &amp;amp; Cake» допустима, если рядом или под ней тем же размером написано «Кофе и выпечка» (или аналогичный понятный вариант). Проверяющие смотрят не на наличие иностранных слов, а на то, может ли потребитель понять предложение без знания другого языка.&lt;/p&gt;




&lt;p&gt;Можно ли использовать английский&lt;/p&gt;

&lt;p&gt;Да.&lt;/p&gt;

&lt;p&gt;Запрета иностранного языка нет.&lt;/p&gt;

&lt;p&gt;Можно писать:&lt;/p&gt;

&lt;p&gt;Кофейня — Coffee&lt;/p&gt;

&lt;p&gt;Coffee — Кофейня&lt;/p&gt;

&lt;p&gt;Оба варианта допустимы. Порядок («сначала русский» или «сначала английский») законом не задаётся — важно наличие обоих вариантов и соблюдение требований к размеру и заметности русского текста.&lt;/p&gt;




&lt;p&gt;Когда ничего менять не нужно&lt;/p&gt;

&lt;p&gt;Есть важные исключения.&lt;/p&gt;

&lt;p&gt;Бренды и товарные знаки&lt;/p&gt;

&lt;p&gt;Если название зарегистрировано как бренд:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apple&lt;/li&gt;
&lt;li&gt;Nike&lt;/li&gt;
&lt;li&gt;Ozon&lt;/li&gt;
&lt;li&gt;Wildberries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Перевод не требуется.&lt;/p&gt;

&lt;p&gt;Это прямо предусмотрено законодательством. То есть сеть кофеен или магазин одежды с иностранным названием бренда в вывеске не обязан добавлять русский перевод названия самого бренда — достаточно, чтобы тип деятельности (например, «кофейня», «одежда») был понятен потребителю, если это требуется по смыслу нормы.&lt;/p&gt;




&lt;p&gt;Слова, ставшие частью русского языка&lt;/p&gt;

&lt;p&gt;Например:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;интернет&lt;/li&gt;
&lt;li&gt;маркетинг&lt;/li&gt;
&lt;li&gt;менеджер&lt;/li&gt;
&lt;li&gt;роутер&lt;/li&gt;
&lt;li&gt;офис&lt;/li&gt;
&lt;li&gt;сайт&lt;/li&gt;
&lt;li&gt;онлайн&lt;/li&gt;
&lt;li&gt;офлайн&lt;/li&gt;
&lt;li&gt;дилер&lt;/li&gt;
&lt;li&gt;дистрибьютор&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Если слово зафиксировано в нормативных словарях русского языка, оно считается допустимым без обязательного дублирования.&lt;/p&gt;




&lt;p&gt;Технические обозначения&lt;/p&gt;

&lt;p&gt;Не требуют перевода общепринятые технические обозначения и аббревиатуры:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wi-Fi&lt;/li&gt;
&lt;li&gt;USB&lt;/li&gt;
&lt;li&gt;QR&lt;/li&gt;
&lt;li&gt;Bluetooth&lt;/li&gt;
&lt;li&gt;HDMI&lt;/li&gt;
&lt;li&gt;4G / 5G&lt;/li&gt;
&lt;li&gt;LED&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Их использование в вывесках и рекламе без русского дублирования не считается нарушением.&lt;/p&gt;




&lt;p&gt;Можно ли сделать две вывески&lt;/p&gt;

&lt;p&gt;Да. Это один из самых безопасных вариантов: например, «Coffee» и «Кофе» рядом, или две строки — «Beauty Studio» и «Студия красоты». Главное условие — русский текст должен присутствовать и быть не менее заметным, чем иностранный (размер, контраст, читаемость). Убирать английский не обязательно.&lt;/p&gt;




&lt;p&gt;Штрафы&lt;/p&gt;

&lt;p&gt;Отдельного штрафа «за английские слова» не существует.&lt;/p&gt;

&lt;p&gt;Нарушение может квалифицироваться как предоставление неполной информации потребителю. Размеры штрафов сопоставимы с обычными административными нарушениями в торговле и не выглядят экстремальными. Конкретные суммы и формулировки лучше уточнять в актуальных редакциях КоАП и разъяснениях Роспотребнадзора по вашему региону. Отдельных «мегаштрафов» за одно иностранное слово на вывеске закон не вводит.&lt;/p&gt;




&lt;p&gt;Почему возникло ощущение запрета&lt;/p&gt;

&lt;p&gt;Причина — формулировки в новостях. Когда говорят «запрет англицизмов», создаётся впечатление масштабных ограничений.&lt;/p&gt;

&lt;p&gt;Фактически речь идёт о более узкой норме: обеспечить информацию на государственном языке там, где бизнес обращается к потребителю. Подобные требования (обязательное дублирование или приоритет государственного языка в вывесках и рекламе) существуют во многих странах — от Франции до Казахстана. Цель не в том, чтобы запретить иностранные слова, а в том, чтобы человек мог понять предложение без знания другого языка. Поэтому в новостях и стоит различать: «запрет англицизмов» — это упрощение; по факту речь о норме доступности информации на государственном языке в сфере потребления.&lt;/p&gt;




&lt;p&gt;Что важно понимать&lt;/p&gt;

&lt;p&gt;Закон:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;не регулирует речь людей&lt;/li&gt;
&lt;li&gt;не запрещает иностранные слова&lt;/li&gt;
&lt;li&gt;не заставляет менять бренды&lt;/li&gt;
&lt;li&gt;не влияет на IT-термины&lt;/li&gt;
&lt;li&gt;касается только публичной информации для потребителей&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Иными словами: личные соцсети, переписка в мессенджерах, внутренние документы компании и разговорная речь остаются без изменений. Регулируется только то, что видит или слышит потребитель в момент выбора товара или услуги — вывески, реклама, меню, объявления в местах продаж.&lt;/p&gt;




&lt;p&gt;Что сделать предпринимателю&lt;/p&gt;

&lt;p&gt;Если у вас офлайн-точка, вывеска или реклама с иностранными словами:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Проверить вывеску и витрину&lt;/strong&gt; — есть ли рядом с иностранным текстом русский вариант. Если нет, добавить: тот же размер, читаемость, заметность.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Бренды не трогать&lt;/strong&gt; — зарегистрированные товарные знаки (названия компаний, продуктов) переводить не нужно.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Меню и прайсы&lt;/strong&gt; — дублировать на русском ключевые пункты (названия блюд, услуг, акции). Не обязательно переводить каждое слово, если контекст понятен.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;При сомнениях&lt;/strong&gt; — уточнить в юридической консультации или в территориальном управлении Роспотребнадзора, как норму применяют в вашей сфере.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Для интернет-магазинов и соцсетей требования те же: информация для потребителя должна быть доступна на русском. Обычно достаточно описаний товаров и условий на сайте на русском языке.&lt;/p&gt;




&lt;p&gt;Где смотреть точные требования&lt;/p&gt;

&lt;p&gt;Официальные разъяснения даёт Роспотребнадзор; нормы опираются на закон о государственном языке РФ и подзаконные акты. Точные формулировки и практику применения по регионам лучше уточнять на сайте управления Роспотребнадзора по вашему субъекту или у юриста, который специализируется на защите прав потребителей и рекламе. Это поможет понять, как трактуют «сопоставимость» размера шрифта и «доступность» информации в вашем конкретном случае.&lt;/p&gt;




&lt;p&gt;Итог&lt;/p&gt;

&lt;p&gt;Самое точное описание изменений звучит так: если написано SALE, должно быть написано РАСПРОДАЖА. Без драматизации. Английский можно оставлять, бренды не переименовывать, техобозначения и слова из словарей не трогать. Меняются только правила оформления публичной информации для потребителя: русский вариант должен быть доступен и заметен. Для большинства предпринимателей это сводится к проверке вывесок и рекламы и при необходимости добавлению дублирования.&lt;/p&gt;




&lt;p&gt;Вывод&lt;/p&gt;

&lt;p&gt;Пока рано говорить о серьёзных последствиях. Скорее всего изменения будут точечными и затронут в первую очередь офлайн-вывески и рекламу. Для повседневной жизни большинства людей ничего не меняется; основные изменения касаются оформления офлайн-точек и рекламы.&lt;/p&gt;

&lt;p&gt;Главное: закон не запрещает английский, а требует, чтобы информация для потребителя была доступна на русском. Соблюдение сводится к дублированию — например, «SALE» и «Распродажа» рядом, без драмы и без переписывания брендов.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Полный текст статьи на сайте; в Дзене — анонс и ссылка.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://viku-lov.ru/blog/zakon-anglicizmov-rossiya-2026-chto-izmenilos" rel="noopener noreferrer"&gt;Read more on viku-lov.ru&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blog</category>
    </item>
    <item>
      <title>Как настроить canonical, sitemap и RSS в Astro для индексации</title>
      <dc:creator>Андрей Викулов (VProger)</dc:creator>
      <pubDate>Fri, 26 Jun 2026 06:58:22 +0000</pubDate>
      <link>https://dev.to/_vproger_/kak-nastroit-canonical-sitemap-i-rss-v-astro-dlia-indieksatsii-eo3</link>
      <guid>https://dev.to/_vproger_/kak-nastroit-canonical-sitemap-i-rss-v-astro-dlia-indieksatsii-eo3</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcaonxsr75wjx9xflbwq8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcaonxsr75wjx9xflbwq8.png" alt="Как настроить canonical, sitemap и RSS в Astro для индексации" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Как настроить canonical, sitemap и RSS в Astro для индексации&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблема:&lt;/strong&gt; страницы блога на Astro не индексируются в Яндексе, попадают в LOW_DEMAND или дублируются в выдаче.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Решение:&lt;/strong&gt; canonical из origin (не из Astro.url.href), sitemap.xml с фильтрацией черновиков, robots.txt с Sitemap, RSS с автообнаружением, meta description до 155 символов, Open Graph с абсолютными URL.&lt;/p&gt;




&lt;p&gt;В чём проблема: почему страницы не индексируются&lt;/p&gt;

&lt;p&gt;Типичные ошибки в Astro-проектах:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Canonical из Astro.url.href&lt;/strong&gt; — на localhost и production получаются разные URL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Нет site в конфиге&lt;/strong&gt; — sitemap и RSS не знают домен&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Домен в &amp;lt;/strong&amp;gt; — занимает место в выдаче&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Одинаковый description&amp;lt;/strong&amp;gt; на всех страницах — слабый сигнал для поисковиков&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Относительный og:image&amp;lt;/strong&amp;gt; — соцсети не подхватывают превью&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;В sitemap попадают черновики&amp;lt;/strong&amp;gt; — индексируются незавершённые страницы&amp;lt;/li&amp;gt;
&amp;lt;/ol&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Результат:&amp;lt;/strong&amp;gt; LOW_DEMAND в Яндекс Вебмастере, дубли страниц, плохие сниппеты в выдаче.&amp;lt;/p&amp;gt;

&amp;lt;hr&amp;gt;

&amp;lt;p&amp;gt;Рабочее решение: пошаговая настройка&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Шаг 1: Задаём site в astro.config.mjs&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight javascript"&amp;gt;&amp;lt;code&amp;gt;
&amp;lt;span class="c1"&amp;gt;// astro.config.mjs&amp;lt;/span&amp;gt;

&amp;lt;span class="k"&amp;gt;import&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;defineConfig&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;}&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;from&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;astro/config&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;

&amp;lt;span class="k"&amp;gt;export&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;default&amp;lt;/span&amp;gt; &amp;lt;span class="nf"&amp;gt;defineConfig&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;({&amp;lt;/span&amp;gt;

&amp;lt;span class="na"&amp;gt;site&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;https://example.com&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="c1"&amp;gt;// Полный URL с протоколом&amp;lt;/span&amp;gt;

&amp;lt;span class="c1"&amp;gt;// ...&amp;lt;/span&amp;gt;

&amp;lt;span class="p"&amp;gt;});&amp;lt;/span&amp;gt;

&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Важно:&amp;lt;/strong&amp;gt; указывай полный URL с протоколом (https://). Без site интеграция @astrojs/sitemap не заработает.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Шаг 2: Canonical только из origin + pathname&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight typescript"&amp;gt;&amp;lt;code&amp;gt;
&amp;lt;span class="c1"&amp;gt;// В компоненте SeoHead.astro&amp;lt;/span&amp;gt;

&amp;lt;span class="kd"&amp;gt;const&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;origin&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;https://example.com&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt; &amp;lt;span class="c1"&amp;gt;// Из astro.config.mjs site&amp;lt;/span&amp;gt;

&amp;lt;span class="kd"&amp;gt;const&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;pathname&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;Astro&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;url&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;pathname&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;

&amp;lt;span class="kd"&amp;gt;const&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;search&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;Astro&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;url&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;search&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;

&amp;lt;span class="kd"&amp;gt;const&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;canonical&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;$&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;origin&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;}&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;$&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;pathname&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;}&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;$&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;search&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;||&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;""&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;};&amp;lt;/span&amp;gt;

&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Почему не Astro.url.href:&amp;lt;/strong&amp;gt; в dev-режиме Astro.url может быть &amp;lt;a href="http://localhost:4321"&amp;gt;http://localhost:4321&amp;lt;/a&amp;gt;, а в проде — твой домен. Поисковики хотят видеть &amp;lt;strong&amp;gt;один стабильный каноничный адрес&amp;lt;/strong&amp;gt;.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Шаг 3: Создаём компонент SeoHead.astro&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Создай файл src/components/SeoHead.astro:&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight plaintext"&amp;gt;&amp;lt;code&amp;gt;
---

const {

title = "Сайт",

description = "",

canonicalUrl,

ogImage = "/assets/og-default.png",

noIndex = false,

} = Astro.props;

// Origin из конфига (один источник правды)

const origin = "https://example.com";

const pathname = Astro.url.pathname;

const search = Astro.url.search;

const canonical = canonicalUrl ?? ${origin}${pathname}${search || ""};

// Убираем домен из title (если есть)

const titleWithoutDomain = title.replace(/\s_[|\-—]\s_example\.com\s\*$/i, "").trim() || title;

const safeDescription = description.slice(0, 155).trim();

---

&amp;lt;title&amp;gt;{titleWithoutDomain}&amp;lt;/title&amp;gt;

&amp;lt;meta name="description" content={safeDescription} /&amp;gt;

&amp;lt;link rel="canonical" href={canonical} /&amp;gt;

{noIndex ? (

&amp;lt;meta name="robots" content="noindex, nofollow" /&amp;gt;

) : (

&amp;lt;meta name="robots" content="index, follow" /&amp;gt;

)}

&amp;lt;!-- Open Graph --&amp;gt;

&amp;lt;meta property="og:title" content={titleWithoutDomain} /&amp;gt;

&amp;lt;meta property="og:description" content={safeDescription} /&amp;gt;

&amp;lt;meta property="og:type" content="website" /&amp;gt;

&amp;lt;meta property="og:url" content={canonical} /&amp;gt;

&amp;lt;meta property="og:image" content={${origin}${ogImage}} /&amp;gt;

&amp;lt;meta property="og:locale" content="ru\_RU" /&amp;gt;

&amp;lt;!-- Twitter Card --&amp;gt;

&amp;lt;meta name="twitter:card" content="summary\_large\_image" /&amp;gt;

&amp;lt;meta name="twitter:title" content={titleWithoutDomain} /&amp;gt;

&amp;lt;meta name="twitter:description" content={safeDescription} /&amp;gt;

&amp;lt;meta name="twitter:image" content={${origin}${ogImage}} /&amp;gt;

&amp;lt;slot /&amp;gt;

&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Шаг 4: Используем SeoHead в layout&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight plaintext"&amp;gt;&amp;lt;code&amp;gt;
---

import SeoHead from "../components/SeoHead.astro";

const { title = "Сайт", description = "", canonicalUrl, ogImage, noIndex } = Astro.props;

---

&amp;lt;head&amp;gt;

&amp;lt;meta charset="utf-8" /&amp;gt;

&amp;lt;meta name="viewport" content="width=device-width, initial-scale=1" /&amp;gt;

&amp;lt;SeoHead

title={title}

description={description}

canonicalUrl={canonicalUrl}

ogImage={ogImage}

noIndex={noIndex}

/&amp;gt;

&amp;lt;/head&amp;gt;

&amp;lt;body&amp;gt;

&amp;lt;slot /&amp;gt;

&amp;lt;/body&amp;gt;

&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Шаг 5: Создаём robots.txt&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Положи файл в public/robots.txt:&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight plaintext"&amp;gt;&amp;lt;code&amp;gt;
User-agent: \*

Allow: /

Disallow: /admin/

Disallow: /api/

User-agent: Yandex

Allow: /

Disallow: /admin/

Disallow: /api/

Sitemap: https://example.com/sitemap.xml

&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Шаг 6: Генерация sitemap.xml&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Вариант A: интеграция @astrojs/sitemap (SSG)&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight shell"&amp;gt;&amp;lt;code&amp;gt;
pnpm astro add sitemap

&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;В astro.config.mjs должен быть задан site. Интеграция сгенерирует sitemap.xml в dist.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Ограничение:&amp;lt;/strong&amp;gt; в sitemap попадут только статические страницы. Для блога на Content Collections этого обычно достаточно.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Вариант B: кастомный endpoint (SSR или полный контроль)&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Создай файл src/pages/sitemap.xml.ts:&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight typescript"&amp;gt;&amp;lt;code&amp;gt;
&amp;lt;span class="k"&amp;gt;import&amp;lt;/span&amp;gt; &amp;lt;span class="kd"&amp;gt;type&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;APIRoute&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;}&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;from&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;astro&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;

&amp;lt;span class="k"&amp;gt;import&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;getCollection&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;}&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;from&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;astro:content&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;

&amp;lt;span class="kd"&amp;gt;const&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;SITE&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;https://example.com&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;

&amp;lt;span class="kd"&amp;gt;function&amp;lt;/span&amp;gt; &amp;lt;span class="nf"&amp;gt;formatLastmod&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;d&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="nb"&amp;gt;Date&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;|&amp;lt;/span&amp;gt; &amp;lt;span class="kc"&amp;gt;undefined&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;):&amp;lt;/span&amp;gt; &amp;lt;span class="kr"&amp;gt;string&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;|&amp;lt;/span&amp;gt; &amp;lt;span class="kc"&amp;gt;undefined&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt;

&amp;lt;span class="k"&amp;gt;return&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;d&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;?&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;d&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nf"&amp;gt;toISOString&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;().&amp;lt;/span&amp;gt;&amp;lt;span class="nf"&amp;gt;split&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;T&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)[&amp;lt;/span&amp;gt;&amp;lt;span class="mi"&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;]&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="kc"&amp;gt;undefined&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;

&amp;lt;span class="p"&amp;gt;}&amp;lt;/span&amp;gt;

&amp;lt;span class="k"&amp;gt;export&amp;lt;/span&amp;gt; &amp;lt;span class="kd"&amp;gt;const&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;prerender&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="kc"&amp;gt;false&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;

&amp;lt;span class="k"&amp;gt;export&amp;lt;/span&amp;gt; &amp;lt;span class="kd"&amp;gt;const&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;GET&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;APIRoute&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;async &amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;gt;&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt;

&amp;lt;span class="kd"&amp;gt;const&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;posts&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;await&amp;lt;/span&amp;gt; &amp;lt;span class="nf"&amp;gt;getCollection&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;blog&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;({&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;data&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;})&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;gt;&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;!&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;data&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;draft&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;);&amp;lt;/span&amp;gt;

&amp;lt;span class="kd"&amp;gt;const&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;urls&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;[&amp;lt;/span&amp;gt;

&amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt; &amp;lt;span class="na"&amp;gt;url&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;SITE&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="na"&amp;gt;changefreq&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;daily&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="na"&amp;gt;priority&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;1.0&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;},&amp;lt;/span&amp;gt;

&amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt; &amp;lt;span class="na"&amp;gt;url&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;$&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;SITE&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;}&amp;lt;/span&amp;gt;&amp;lt;span class="sr"&amp;gt;/blog, changefreq: "weekly", priority: "0.9" }&amp;lt;/span&amp;gt;&amp;lt;span class="err"&amp;gt;,
&amp;lt;/span&amp;gt;
&amp;lt;span class="p"&amp;gt;...&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;posts&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nf"&amp;gt;map&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;((&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;p&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;gt;&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;({&amp;lt;/span&amp;gt;

&amp;lt;span class="na"&amp;gt;url&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;$&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;SITE&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;}&amp;lt;/span&amp;gt;&amp;lt;span class="sr"&amp;gt;/blog/&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;$&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;p&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;data&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;slug&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;??&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;p&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;slug&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;},&amp;lt;/span&amp;gt;

&amp;lt;span class="na"&amp;gt;lastmod&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="nf"&amp;gt;formatLastmod&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;p&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;data&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;updatedDate&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;??&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;p&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;data&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;pubDate&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;),&amp;lt;/span&amp;gt;

&amp;lt;span class="na"&amp;gt;changefreq&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;monthly&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;as&amp;lt;/span&amp;gt; &amp;lt;span class="kd"&amp;gt;const&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt;

&amp;lt;span class="na"&amp;gt;priority&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;0.8&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt;

&amp;lt;span class="p"&amp;gt;})),&amp;lt;/span&amp;gt;

&amp;lt;span class="p"&amp;gt;];&amp;lt;/span&amp;gt;

&amp;lt;span class="kd"&amp;gt;const&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;xml&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="s2"&amp;gt;`&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;

&amp;lt;urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"&amp;gt;

&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;${&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;urls&amp;lt;/span&amp;gt;

&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nf"&amp;gt;map&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;

&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;u&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;gt;&amp;lt;/span&amp;gt; &amp;lt;span class="s2"&amp;gt;` &amp;lt;url&amp;gt;

&amp;lt;loc&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;${&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;u&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;url&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;}&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;&amp;lt;/loc&amp;gt;

&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;${&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;u&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;lastmod&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;?&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;&amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;lastmod&amp;lt;/span&amp;gt;&amp;lt;span class="o"&amp;gt;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;$&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;u&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;lastmod&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;}&amp;lt;/span&amp;gt;&amp;lt;span class="o"&amp;gt;&amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class="sr"&amp;gt;/lastmod&amp;gt; : ""&amp;lt;/span&amp;gt;&amp;lt;span class="err"&amp;gt;}
&amp;lt;/span&amp;gt;
&amp;lt;span class="o"&amp;gt;&amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;changefreq&amp;lt;/span&amp;gt;&amp;lt;span class="o"&amp;gt;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;$&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;u&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;changefreq&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;}&amp;lt;/span&amp;gt;&amp;lt;span class="o"&amp;gt;&amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class="sr"&amp;gt;/changefreq&amp;lt;/span&amp;gt;&amp;lt;span class="err"&amp;gt;&amp;gt;
&amp;lt;/span&amp;gt;
&amp;lt;span class="o"&amp;gt;&amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;priority&amp;lt;/span&amp;gt;&amp;lt;span class="o"&amp;gt;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;$&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;u&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;priority&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;}&amp;lt;/span&amp;gt;&amp;lt;span class="o"&amp;gt;&amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class="sr"&amp;gt;/priority&amp;lt;/span&amp;gt;&amp;lt;span class="err"&amp;gt;&amp;gt;
&amp;lt;/span&amp;gt;
&amp;lt;span class="o"&amp;gt;&amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class="sr"&amp;gt;/url&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class="err"&amp;gt;`
&amp;lt;/span&amp;gt;
&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;

&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nf"&amp;gt;join&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="se"&amp;gt;\n&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)}&amp;lt;/span&amp;gt;

&amp;lt;span class="o"&amp;gt;&amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class="sr"&amp;gt;/urlset&amp;gt;`&amp;lt;/span&amp;gt;&amp;lt;span class="err"&amp;gt;;
&amp;lt;/span&amp;gt;
&amp;lt;span class="k"&amp;gt;return&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;new&amp;lt;/span&amp;gt; &amp;lt;span class="nc"&amp;gt;Response&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;xml&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt;

&amp;lt;span class="na"&amp;gt;headers&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;Content-Type&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;application/xml; charset=utf-8&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;},&amp;lt;/span&amp;gt;

&amp;lt;span class="p"&amp;gt;});&amp;lt;/span&amp;gt;

&amp;lt;span class="p"&amp;gt;};&amp;lt;/span&amp;gt;

&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Шаг 7: Настраиваем RSS&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight shell"&amp;gt;&amp;lt;code&amp;gt;
pnpm add @astrojs/rss

&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Создай файл src/pages/rss.xml.ts:&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight typescript"&amp;gt;&amp;lt;code&amp;gt;
&amp;lt;span class="k"&amp;gt;import&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;rss&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;from&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;@astrojs/rss&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;

&amp;lt;span class="k"&amp;gt;import&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;getCollection&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;}&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;from&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;astro:content&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;;&amp;lt;/span&amp;gt;

&amp;lt;span class="k"&amp;gt;export&amp;lt;/span&amp;gt; &amp;lt;span class="k"&amp;gt;async&amp;lt;/span&amp;gt; &amp;lt;span class="kd"&amp;gt;function&amp;lt;/span&amp;gt; &amp;lt;span class="nf"&amp;gt;GET&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;context&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="kr"&amp;gt;any&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt;

&amp;lt;span class="kd"&amp;gt;const&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;posts&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="k"&amp;gt;await&amp;lt;/span&amp;gt; &amp;lt;span class="nf"&amp;gt;getCollection&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;blog&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;))&amp;lt;/span&amp;gt;

&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nf"&amp;gt;filter&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;((&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;p&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;gt;&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;!&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;p&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;data&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;draft&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt;

&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nf"&amp;gt;sort&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;((&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;a&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;b&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;gt;&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;b&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;data&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;pubDate&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nf"&amp;gt;valueOf&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;()&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;-&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;a&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;data&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;pubDate&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nf"&amp;gt;valueOf&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;());&amp;lt;/span&amp;gt;

&amp;lt;span class="k"&amp;gt;return&amp;lt;/span&amp;gt; &amp;lt;span class="nf"&amp;gt;rss&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;({&amp;lt;/span&amp;gt;

&amp;lt;span class="na"&amp;gt;title&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;Блог Example&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt;

&amp;lt;span class="na"&amp;gt;description&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;Заметки о разработке&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt;

&amp;lt;span class="na"&amp;gt;site&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;context&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;site&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;?.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;href&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;??&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;https://example.com&amp;lt;/span&amp;gt;&amp;lt;span class="dl"&amp;gt;"&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt;

&amp;lt;span class="na"&amp;gt;items&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;posts&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nf"&amp;gt;map&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;((&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;p&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;=&amp;gt;&amp;lt;/span&amp;gt; &amp;lt;span class="p"&amp;gt;({&amp;lt;/span&amp;gt;

&amp;lt;span class="na"&amp;gt;title&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;p&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;data&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;title&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt;

&amp;lt;span class="na"&amp;gt;description&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;p&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;data&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;description&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;??&amp;lt;/span&amp;gt; &amp;lt;span class="dl"&amp;gt;""&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt;

&amp;lt;span class="na"&amp;gt;pubDate&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;p&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;data&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;pubDate&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;,&amp;lt;/span&amp;gt;

&amp;lt;span class="na"&amp;gt;link&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class="sr"&amp;gt;/blog/&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;$&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;{&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;p&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;data&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;slug&amp;lt;/span&amp;gt; &amp;lt;span class="o"&amp;gt;??&amp;lt;/span&amp;gt; &amp;lt;span class="nx"&amp;gt;p&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class="nx"&amp;gt;slug&amp;lt;/span&amp;gt;&amp;lt;span class="p"&amp;gt;}&amp;lt;/span&amp;gt;&amp;lt;span class="sr"&amp;gt;/&amp;lt;/span&amp;gt;&amp;lt;span class="err"&amp;gt;,
&amp;lt;/span&amp;gt;
&amp;lt;span class="p"&amp;gt;})),&amp;lt;/span&amp;gt;

&amp;lt;span class="p"&amp;gt;});&amp;lt;/span&amp;gt;

&amp;lt;span class="p"&amp;gt;}&amp;lt;/span&amp;gt;

&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;В &amp;lt;head&amp;gt; основного layout добавь:&amp;lt;br&amp;gt;
&amp;lt;/p&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight html"&amp;gt;&amp;lt;code&amp;gt;
&amp;lt;span class="nt"&amp;gt;&amp;lt;link&amp;lt;/span&amp;gt; &amp;lt;span class="na"&amp;gt;rel=&amp;lt;/span&amp;gt;&amp;lt;span class="s"&amp;gt;"alternate"&amp;lt;/span&amp;gt; &amp;lt;span class="na"&amp;gt;type=&amp;lt;/span&amp;gt;&amp;lt;span class="s"&amp;gt;"application/rss+xml"&amp;lt;/span&amp;gt; &amp;lt;span class="na"&amp;gt;title=&amp;lt;/span&amp;gt;&amp;lt;span class="s"&amp;gt;"RSS"&amp;lt;/span&amp;gt; &amp;lt;span class="na"&amp;gt;href=&amp;lt;/span&amp;gt;&amp;lt;span class="s"&amp;gt;"/rss.xml"&amp;lt;/span&amp;gt; &amp;lt;span class="nt"&amp;gt;/&amp;gt;&amp;lt;/span&amp;gt;

&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;hr&amp;gt;

&amp;lt;p&amp;gt;Проверка результата&amp;lt;/p&amp;gt;

&amp;lt;ol&amp;gt;
&amp;lt;li&amp;gt;Проверка canonical
&amp;lt;/li&amp;gt;
&amp;lt;/ol&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight shell"&amp;gt;&amp;lt;code&amp;gt;
Проверить заголовок canonical

curl &amp;lt;span class="nt"&amp;gt;-s&amp;lt;/span&amp;gt; https://example.com/blog/astro-part-1/ | &amp;lt;span class="nb"&amp;gt;grep &amp;lt;/span&amp;gt;canonical

Должно быть:

&amp;lt;&amp;lt;span class="nb"&amp;gt;link &amp;lt;/span&amp;gt;&amp;lt;span class="nv"&amp;gt;rel&amp;lt;/span&amp;gt;&amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;"canonical"&amp;lt;/span&amp;gt; &amp;lt;span class="nv"&amp;gt;href&amp;lt;/span&amp;gt;&amp;lt;span class="o"&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class="s2"&amp;gt;"https://example.com/blog/astro-part-1/"&amp;lt;/span&amp;gt;&amp;lt;span class="o"&amp;gt;&amp;gt;&amp;lt;/span&amp;gt;

&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;ol&amp;gt;
&amp;lt;li&amp;gt;Проверка robots.txt
&amp;lt;/li&amp;gt;
&amp;lt;/ol&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight shell"&amp;gt;&amp;lt;code&amp;gt;
Проверить наличие Sitemap

curl &amp;lt;span class="nt"&amp;gt;-s&amp;lt;/span&amp;gt; https://example.com/robots.txt | &amp;lt;span class="nb"&amp;gt;grep &amp;lt;/span&amp;gt;Sitemap

&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;ol&amp;gt;
&amp;lt;li&amp;gt;Проверка sitemap.xml
&amp;lt;/li&amp;gt;
&amp;lt;/ol&amp;gt;
&amp;lt;div class="highlight"&amp;gt;&amp;lt;pre class="highlight shell"&amp;gt;&amp;lt;code&amp;gt;
Проверить наличие URL постов

curl &amp;lt;span class="nt"&amp;gt;-s&amp;lt;/span&amp;gt; https://example.com/sitemap.xml | &amp;lt;span class="nb"&amp;gt;grep&amp;lt;/span&amp;gt; &amp;lt;span class="s2"&amp;gt;"blog/"&amp;lt;/span&amp;gt;

&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;ol&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;p&amp;gt;Проверка в Яндекс Вебмастере&amp;lt;/p&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;p&amp;gt;Открыть &amp;lt;a href="https://webmaster.yandex.ru/"&amp;gt;Яндекс Вебмастер&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;p&amp;gt;Добавить сайт&amp;lt;/p&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;p&amp;gt;Проверить разделы:&amp;lt;/p&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;/ol&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Индексирование → Sitemap&amp;lt;/strong&amp;gt; — должна быть загружена&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Диагностика → Страницы в поиске&amp;lt;/strong&amp;gt; — проверить наличие страниц&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Поведение в выдаче&amp;lt;/strong&amp;gt; — посмотреть CTR и позиции&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;hr&amp;gt;

&amp;lt;p&amp;gt;Типичные ошибки&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;❌ Нет site в конфиге&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Проблема:&amp;lt;/strong&amp;gt; sitemap и RSS не знают домен, canonical собирается неправильно.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Решение:&amp;lt;/strong&amp;gt; задать site в astro.config.mjs.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;❌ Canonical из Astro.url.href&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Проблема:&amp;lt;/strong&amp;gt; на localhost и production получаются разные каноничные URL.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Решение:&amp;lt;/strong&amp;gt; использовать origin из конфига + pathname.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;❌ Домен в &amp;lt;title&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Проблема:&amp;lt;/strong&amp;gt; занимает место в выдаче.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Решение:&amp;lt;/strong&amp;gt; убрать домен из title или использовать короткое имя сайта.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;❌ Одинаковый description на всех страницах&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Проблема:&amp;lt;/strong&amp;gt; слабый сигнал для поисковиков.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Решение:&amp;lt;/strong&amp;gt; делать уникальные описания до ~155 символов.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;❌ Относительный og:image&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Проблема:&amp;lt;/strong&amp;gt; соцсети не подхватывают превью.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Решение:&amp;lt;/strong&amp;gt; использовать абсолютный URL (origin + путь).&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;❌ В sitemap попадают черновики&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Проблема:&amp;lt;/strong&amp;gt; индексируются незавершённые страницы.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Решение:&amp;lt;/strong&amp;gt; фильтровать по draft: false при генерации sitemap.&amp;lt;/p&amp;gt;

&amp;lt;hr&amp;gt;

&amp;lt;p&amp;gt;Где применять&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Блоги на Astro:&amp;lt;/strong&amp;gt; индексация статей в Яндексе и Google&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Документация:&amp;lt;/strong&amp;gt; правильное отображение в поиске&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Маркетинговые сайты:&amp;lt;/strong&amp;gt; улучшенные сниппеты в выдаче&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Портфолио:&amp;lt;/strong&amp;gt; индексация проектов&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Связанные статьи:&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;Как создать блог на Astro&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Actions, API Routes, SSR и деплой&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Документация:&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href="https://docs.astro.build/en/reference/configuration-reference/"&amp;gt;Astro Configuration Reference&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href="https://docs.astro.build/en/guides/integrations-guide/sitemap/"&amp;gt;Sitemap Integration&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href="https://docs.astro.build/en/guides/rss/"&amp;gt;RSS Guide&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;hr&amp;gt;

&amp;lt;p&amp;gt;Чек-лист для Яндекс Вебмастера&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;[] &amp;lt;strong&amp;gt;site&amp;lt;/strong&amp;gt; в astro.config.mjs задан и совпадает с основным зеркалом&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;[] На каждой странице есть &amp;lt;strong&amp;gt;один&amp;lt;/strong&amp;gt; &amp;lt;link rel="canonical"&amp;gt; с абсолютным URL&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;[] &amp;lt;strong&amp;gt;meta description&amp;lt;/strong&amp;gt; уникален и длиной до ~155 символов&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;[] &amp;lt;strong&amp;gt;&amp;lt;title&amp;gt;&amp;lt;/strong&amp;gt; без домена, осмысленный для каждой страницы&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;[] &amp;lt;strong&amp;gt;robots.txt&amp;lt;/strong&amp;gt; доступен с Sitemap:&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;[] &amp;lt;strong&amp;gt;sitemap.xml&amp;lt;/strong&amp;gt; открывается по URL из robots.txt&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;[] &amp;lt;strong&amp;gt;Open Graph&amp;lt;/strong&amp;gt; : og:title, og:description, og:url, og:image (абсолютный URL)&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;[] Служебные разделы (/admin/, /api/) закрыты в robots.txt&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;[] &amp;lt;strong&amp;gt;RSS&amp;lt;/strong&amp;gt; доступен и объявлен в &amp;lt;head&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;[] Нет дублей страниц (одинаковый контент по разным URL без canonical)&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;a href="https://viku-lov.ru/blog/astro-seo-sitemap-rss-setup"&amp;gt;Read more on viku-lov.ru&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>astro</category>
      <category>seo</category>
      <category>sitemap</category>
      <category>rss</category>
    </item>
    <item>
      <title>Как создать блог на Astro: установка, MDX, Content Collections</title>
      <dc:creator>Андрей Викулов (VProger)</dc:creator>
      <pubDate>Tue, 21 Apr 2026 18:21:53 +0000</pubDate>
      <link>https://dev.to/_vproger_/kak-sozdat-blogh-na-astro-ustanovka-mdx-content-collections-3000</link>
      <guid>https://dev.to/_vproger_/kak-sozdat-blogh-na-astro-ustanovka-mdx-content-collections-3000</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fymgh3nec0ehg518w1np8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fymgh3nec0ehg518w1np8.png" alt="Как создать блог на Astro: установка, MDX, Content Collections" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Как создать блог на Astro: полная инструкция&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблема:&lt;/strong&gt; нужно быстро поднять быстрый блог или контентный сайт с хорошим SEO, но без сложностей Next.js и без перегруженного JavaScript.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Решение:&lt;/strong&gt; Astro — статический генератор (SSG), который рендерит HTML по умолчанию и добавляет интерактивность точечно (islands architecture). В итоге получается лёгкая страница, понятная поисковикам и пользователям.&lt;/p&gt;

&lt;p&gt;В этой статье — пошаговая настройка блога на Astro: установка, структура проекта, маршруты, &lt;strong&gt;MDX&lt;/strong&gt; (Markdown с компонентами) и &lt;strong&gt;Content Collections&lt;/strong&gt; со схемой на Zod для валидации контента.&lt;/p&gt;




&lt;p&gt;В чём проблема: выбор фреймворка для контентного сайта&lt;/p&gt;

&lt;p&gt;Если нужен блог, документация или маркетинговый сайт, есть три основных варианта:&lt;/p&gt;

&lt;p&gt;| Критерий | Astro | Next.js | Gatsby |&lt;/p&gt;

&lt;p&gt;|----------|-------|---------|--------|&lt;/p&gt;

&lt;p&gt;| &lt;strong&gt;Модель по умолчанию&lt;/strong&gt; | SSG, HTML-first | SSR/SSG, JS-heavy | SSG (React) |&lt;/p&gt;

&lt;p&gt;| &lt;strong&gt;JS в браузере&lt;/strong&gt; | Минимум (islands) | Много (гидрация всего) | Много (React-дерево) |&lt;/p&gt;

&lt;p&gt;| &lt;strong&gt;Контент из коробки&lt;/strong&gt; | MDX, Markdown, Collections | Нужны решения | GraphQL + плагины |&lt;/p&gt;

&lt;p&gt;| &lt;strong&gt;Скорость контент-сайта&lt;/strong&gt; | Очень высокая | Зависит от настроек | Хорошая после билда |&lt;/p&gt;

&lt;p&gt;| &lt;strong&gt;Кривая обучения&lt;/strong&gt; | Низкая (HTML + островки) | Выше (React, App Router) | Выше (GraphQL, плагины) |&lt;/p&gt;

&lt;p&gt;| &lt;strong&gt;Лучше всего под&lt;/strong&gt; | Блог, доки, лендинг, портфолио | SPA, приложения, сложный роутинг | Контент на React + CMS |&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Вывод:&lt;/strong&gt; для блога, документации или маркетингового сайта Astro часто даёт самый лёгкий результат при минимальном JavaScript.&lt;/p&gt;




&lt;p&gt;Рабочее решение: пошаговая настройка&lt;/p&gt;

&lt;p&gt;Шаг 1: Создание проекта Astro&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
Официальный способ: интерактивный мастер

npm create astro@latest

или

pnpm create astro@latest

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Мастер спросит:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Шаблон (выбери blog для блога или minimal для чистого старта)&lt;/li&gt;
&lt;li&gt;TypeScript (рекомендуется включить — пригодится для Content Collections)&lt;/li&gt;
&lt;li&gt;ESLint и форматирование (по желанию)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Шаг 2: Проверка системных требований&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
Проверить Node.js &lt;span class="o"&gt;(&lt;/span&gt;должно быть &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; 18&lt;span class="o"&gt;)&lt;/span&gt;

node &lt;span class="nt"&gt;-v&lt;/span&gt;

Проверить менеджер пакетов

npm &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="c"&gt;# или pnpm -v / yarn -v&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Требования:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 18+ (рекомендуется LTS: 20 или 22)&lt;/li&gt;
&lt;li&gt;npm, pnpm или yarn&lt;/li&gt;
&lt;li&gt;Редактор кода (VS Code + плагин Astro)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Шаг 3: Запуск dev-сервера&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
Перейти в папку проекта

&lt;span class="nb"&gt;cd&lt;/span&gt; &amp;lt;имя-проекта&amp;gt;

Запустить dev-сервер

npm run dev

или

pnpm dev

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Открой &lt;a href="http://localhost:4321" rel="noopener noreferrer"&gt;http://localhost:4321&lt;/a&gt; — должен открыться стартовый сайт.&lt;/p&gt;

&lt;p&gt;Шаг 4: Проверка сборки перед деплоем&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
Собрать проект в папку dist/

npm run build

или

pnpm build

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Если сборка прошла без ошибок — проект готов к деплою.&lt;/p&gt;




&lt;p&gt;Структура проекта Astro&lt;/p&gt;

&lt;p&gt;Типичная структура блога на Astro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
/

├─ astro.config.mjs # Конфигурация Astro

├─ package.json # Зависимости и скрипты

├─ src/

│ ├─ pages/ # Маршруты (file-based routing)

│ │ ├─ index.astro # Главная страница

│ │ ├─ about.astro # /about

│ │ └─ blog/

│ │ ├─ index.astro # /blog (список статей)

│ │ └─ [slug].astro # /blog/:slug (страница статьи)

│ ├─ layouts/ # Layouts (общая обвязка)

│ │ └─ BaseLayout.astro

│ ├─ components/ # UI компоненты

│ │ └─ Callout.astro

│ ├─ content/ # Контент (Content Collections)

│ │ └─ blog/ # Статьи блога

│ └─ styles/ # Глобальные стили

└─ public/ # Статика (копируется как есть)

└─ favicon.svg

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Правило маршрутизации:&lt;/strong&gt; файл в src/pages/ = маршрут.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;src/pages/index.astro → /&lt;/li&gt;
&lt;li&gt;src/pages/about.astro → /about&lt;/li&gt;
&lt;li&gt;src/pages/blog/[slug].astro → /blog/:slug&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Первый layout: BaseLayout.astro&lt;/p&gt;

&lt;p&gt;Создай файл src/layouts/BaseLayout.astro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
---

// Пропсы приходят со страницы; задаём значения по умолчанию

const {

title = "Site",

description = "Astro site",

} = Astro.props;

---

&amp;lt;!doctype html&amp;gt;

&amp;lt;html lang="ru"&amp;gt;

&amp;lt;head&amp;gt;

&amp;lt;meta charset="utf-8" /&amp;gt;

&amp;lt;meta name="viewport" content="width=device-width, initial-scale=1" /&amp;gt;

&amp;lt;title&amp;gt;{title}&amp;lt;/title&amp;gt;

&amp;lt;meta name="description" content={description} /&amp;gt;

&amp;lt;/head&amp;gt;

&amp;lt;body&amp;gt;

&amp;lt;header style="padding:16px; border-bottom:1px solid rgba(255,255,255,.1)"&amp;gt;

[Главная](/)

&amp;lt;span style="opacity:.6; margin-left:8px"&amp;gt;|&amp;lt;/span&amp;gt;

[Блог](/blog)

&amp;lt;/header&amp;gt;

&amp;lt;main style="max-width: 860px; margin: 0 auto; padding: 24px;"&amp;gt;

&amp;lt;slot /&amp;gt;

&amp;lt;/main&amp;gt;

&amp;lt;footer style="padding:16px; border-top:1px solid rgba(255,255,255,.1); opacity:.7"&amp;gt;

© {new Date().getFullYear()}

&amp;lt;/footer&amp;gt;

&amp;lt;/body&amp;gt;

&amp;lt;/html&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt; — это "сюда вставится содержимое страницы".&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Первая страница: index.astro&lt;/p&gt;

&lt;p&gt;Создай файл src/pages/index.astro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
---

import BaseLayout from "../layouts/BaseLayout.astro";

---

&amp;lt;BaseLayout title="Главная" description="Стартовый проект на Astro"&amp;gt;

# Привет, Astro

Если страница открылась — ты уже в деле.

&amp;lt;/BaseLayout&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Открой &lt;a href="http://localhost:4321" rel="noopener noreferrer"&gt;http://localhost:4321&lt;/a&gt; — должна отобразиться главная страница.&lt;/p&gt;




&lt;p&gt;Подключаем MDX: Markdown с компонентами&lt;/p&gt;

&lt;p&gt;Установка MDX&lt;/p&gt;

&lt;p&gt;MDX — это Markdown, который умеет &lt;strong&gt;встраивать компоненты&lt;/strong&gt; и JS-выражения.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
npx astro add mdx

или

pnpm astro add mdx

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;После установки в astro.config.mjs появится интеграция @astrojs/mdx.&lt;/p&gt;

&lt;p&gt;Первая MDX-страница&lt;/p&gt;

&lt;p&gt;Создай файл src/pages/guide.mdx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
---

title: "Гайд по Astro"

description: "Пробная MDX-страница"

---

MDX в Astro работает ✅

Это обычный Markdown… но с бонусами.

- списки
- код
- и компоненты ниже

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Открой /guide — должна отрендериться страница.&lt;/p&gt;




&lt;p&gt;Встраиваем компоненты в MDX&lt;/p&gt;

&lt;p&gt;Создаём компонент Callout.astro&lt;/p&gt;

&lt;p&gt;Создай файл src/components/Callout.astro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
---

const { type = "info", title = "Важно" } = Astro.props;

const styles = {

info: "background: rgba(0, 180, 255, .08); border: 1px solid rgba(0, 180, 255, .35);",

warn: "background: rgba(255, 180, 0, .08); border: 1px solid rgba(255, 180, 0, .35);",

danger: "background: rgba(255, 80, 80, .08); border: 1px solid rgba(255, 80, 80, .35);",

};

---

&amp;lt;section style={padding: 14px 16px; border-radius: 12px; ${styles[type] ?? styles.info}}&amp;gt;

&amp;lt;strong style="display:block; margin-bottom: 6px"&amp;gt;{title}&amp;lt;/strong&amp;gt;

&amp;lt;div&amp;gt;&amp;lt;slot /&amp;gt;&amp;lt;/div&amp;gt;

&amp;lt;/section&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Используем компонент в MDX&lt;/p&gt;

&lt;p&gt;Обнови src/pages/guide.mdx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
---

title: "Гайд по Astro"

description: "Пробная MDX-страница"

---

import Callout from "../components/Callout.astro";

MDX + компоненты

&amp;lt;Callout type="info" title="Смысл MDX"&amp;gt;

Ты пишешь статью как Markdown, но можешь вставлять "живые" компоненты.

&amp;lt;/Callout&amp;gt;

&amp;lt;Callout type="warn" title="Пара слов о порядке"&amp;gt;

Не превращай статью в React-приложение. Это всё ещё контент.

&amp;lt;/Callout&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Content Collections: контент с валидацией&lt;/p&gt;

&lt;p&gt;Зачем нужны collections&lt;/p&gt;

&lt;p&gt;Чтобы:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Описать &lt;strong&gt;схему фронтматтера&lt;/strong&gt; (какие поля обязательны)&lt;/li&gt;
&lt;li&gt;Ловить ошибки &lt;strong&gt;на этапе билда&lt;/strong&gt; (а не в проде)&lt;/li&gt;
&lt;li&gt;Получать &lt;strong&gt;типизацию&lt;/strong&gt; (особенно приятно в TS)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Настраиваем src/content.config.ts&lt;/p&gt;

&lt;p&gt;Создай файл src/content.config.ts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;
&lt;span class="c1"&gt;// src/content.config.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineCollection&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;astro:content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;astro/zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineCollection&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;

&lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;

&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="na"&gt;pubDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;

&lt;span class="na"&gt;updatedDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;

&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;([]),&lt;/span&gt;

&lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Frontend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="na"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="na"&gt;cover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;

&lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="p"&gt;}),&lt;/span&gt;

&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;collections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;blog&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;astro/zod — это реэкспорт Zod для схем коллекций.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Первая статья в коллекции&lt;/p&gt;

&lt;p&gt;Создай файл src/content/blog/astro-part-1.mdx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
---

title: "Astro: старт (часть 1)"

description: "Статья в content collection, чтобы потом красиво собрать список и страницы."

pubDate: 2026-01-05T16:00:00.000Z

tags:

- astro
- mdx
- frontend

category: Frontend

draft: false

slug: "astro-part-1"

---

import Callout from "../../components/Callout.astro";

Astro: старт (часть 1)

&amp;lt;Callout type="info" title="Это пост из коллекции"&amp;gt;

Он лежит в src/content/blog и валидируется схемой.

&amp;lt;/Callout&amp;gt;

Здесь начинается содержание статьи…

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Выводим список статей: /blog&lt;/p&gt;

&lt;p&gt;Создай файл src/pages/blog/index.astro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
---

import BaseLayout from "../../layouts/BaseLayout.astro";

import { getCollection } from "astro:content";

// Загружаем все посты, скрываем черновики, сортируем по дате

const posts = (await getCollection("blog"))

.filter(p =&amp;gt; !p.data.draft)

.sort((a, b) =&amp;gt; b.data.pubDate.valueOf() - a.data.pubDate.valueOf());

---

&amp;lt;BaseLayout title="Блог" description="Список статей на Astro"&amp;gt;

# Блог

{posts.map((post) =&amp;gt; (

- [{post.data.title}]({/blog/${post.data.slug}/})

))}

&amp;lt;/BaseLayout&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Страница статьи: /blog/:slug&lt;/p&gt;

&lt;p&gt;Создай файл src/pages/blog/[slug].astro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
---

import BaseLayout from "../../layouts/BaseLayout.astro";

import { getCollection } from "astro:content";

// Для SSG Astro нужен список всех путей

export async function getStaticPaths() {

const posts = await getCollection("blog");

return posts

.filter(p =&amp;gt; !p.data.draft)

.map((post) =&amp;gt; ({

params: { slug: post.data.slug },

props: { post },

}));

}

const { post } = Astro.props;

// У коллекционных MDX/MD есть render() — получаем компонент контента

const { Content } = await post.render();

---

&amp;lt;BaseLayout title={post.data.title} description={post.data.description}&amp;gt;

&amp;lt;article&amp;gt;

# {post.data.title}

{post.data.pubDate.toLocaleDateString("ru-RU")}

&amp;lt;Content /&amp;gt;

&amp;lt;/article&amp;gt;

&amp;lt;/BaseLayout&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Проверка результата&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Проверка списка статей
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
Запустить dev-сервер

npm run dev

Открыть http://localhost:4321/blog

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Должен отобразиться список статей из src/content/blog/.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Проверка страницы статьи
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
Открыть http://localhost:4321/blog/astro-part-1/

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Должна отобразиться страница статьи с содержимым MDX.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Проверка сборки
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
Собрать проект

npm run build

Проверить dist/

&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; dist/

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Если сборка прошла без ошибок — все маршруты и коллекции настроены верно.&lt;/p&gt;




&lt;p&gt;Типичные ошибки&lt;/p&gt;

&lt;p&gt;❌ Ошибка: getCollection("blog") — коллекция не найдена&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Причина:&lt;/strong&gt; Нет src/content.config.ts или папки src/content/blog/.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Решение:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
Создать конфиг

&lt;span class="nb"&gt;touch &lt;/span&gt;src/content.config.ts

Создать папку для статей

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; src/content/blog

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;❌ Ошибка валидации frontmatter при билде&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Причина:&lt;/strong&gt; Поля не совпадают со схемой (дата, обязательные поля).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Решение:&lt;/strong&gt; Проверить title, description, pubDate, slug в .mdx. Даты в формате ISO: 2026-01-05T16:00:00.000Z.&lt;/p&gt;

&lt;p&gt;❌ MDX-страница не открывается / 404&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Причина:&lt;/strong&gt; Файл не в src/pages/ или опечатка в имени.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Решение:&lt;/strong&gt; Роут = путь от pages: guide.mdx → /guide.&lt;/p&gt;

&lt;p&gt;❌ Компонент в MDX не найден&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Причина:&lt;/strong&gt; Неверный путь импорта (относительно файла MDX).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Решение:&lt;/strong&gt; Импорт от корня файла: ../../components/Callout.astro.&lt;/p&gt;

&lt;p&gt;❌ post.render is not a function&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Причина:&lt;/strong&gt; Используется сырой объект поста вместо результата getCollection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Решение:&lt;/strong&gt; В [slug].astro брать post из getStaticPaths → props и вызывать post.render().&lt;/p&gt;




&lt;p&gt;Где применять&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Блоги:&lt;/strong&gt; контентные сайты с MDX и Collections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Документация:&lt;/strong&gt; техническая документация с компонентами&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Лендинги:&lt;/strong&gt; маркетинговые страницы с быстрым SEO&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Портфолио:&lt;/strong&gt; сайты-визитки с минимальным JS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Связанные статьи:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SEO, sitemap и RSS в Astro&lt;/li&gt;
&lt;li&gt;Actions, API Routes, SSR и деплой&lt;/li&gt;
&lt;li&gt;TypeScript: основы и философия&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Документация:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.astro.build/" rel="noopener noreferrer"&gt;Astro Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astro.build/en/guides/markdown-content/" rel="noopener noreferrer"&gt;MDX Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astro.build/en/guides/content-collections/" rel="noopener noreferrer"&gt;Content Collections&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;a href="https://viku-lov.ru/blog/astro-blog-setup-mdx-content-collections" rel="noopener noreferrer"&gt;Read more on viku-lov.ru&lt;/a&gt;&lt;/p&gt;

</description>
      <category>astro</category>
      <category>frontend</category>
      <category>blog</category>
      <category>mdx</category>
    </item>
    <item>
      <title>Как конвертировать CHM в один PDF на Linux: без мусора и битых ссылок</title>
      <dc:creator>Андрей Викулов (VProger)</dc:creator>
      <pubDate>Sat, 28 Feb 2026 08:04:06 +0000</pubDate>
      <link>https://dev.to/_vproger_/kak-konviertirovat-chm-v-odin-pdf-na-linux-biez-musora-i-bitykh-ssylok-3edd</link>
      <guid>https://dev.to/_vproger_/kak-konviertirovat-chm-v-odin-pdf-na-linux-biez-musora-i-bitykh-ssylok-3edd</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwl7mqlbqlo3b8s0gxi78.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwl7mqlbqlo3b8s0gxi78.png" alt="Как конвертировать CHM в один PDF на Linux: без мусора и битых ссылок" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Как конвертировать CHM в один PDF на Linux: без мусора и битых ссылок&lt;/p&gt;

&lt;p&gt;Если у тебя есть bsm_api.chm и нужно получить &lt;strong&gt;один нормальный PDF&lt;/strong&gt; , а не «папку из 900 HTML + слёзы», вот рабочий пайплайн: &lt;strong&gt;CHM → HTML → (сборка) → PDF&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;В конце будет:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;один manual.pdf&lt;/li&gt;
&lt;li&gt;без битых внутренних ссылок (насколько позволяет исходник)&lt;/li&gt;
&lt;li&gt;с нормальной навигацией/оглавлением (если CHM адекватный)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;В чём проблема&lt;/p&gt;

&lt;p&gt;CHM — это не “один файл с текстом”, а контейнер: HTML + картинки + оглавление + внутренние ссылки.&lt;/p&gt;

&lt;p&gt;Типовые боли:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;конвертеры делают &lt;strong&gt;1000 страниц&lt;/strong&gt; вместо одного PDF&lt;/li&gt;
&lt;li&gt;ломают ссылки и кодировки&lt;/li&gt;
&lt;li&gt;получаешь PDF, который выглядит как скриншоты из 2007&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Рабочее решение&lt;/p&gt;

&lt;p&gt;1) Установка инструментов&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ubuntu/Debian:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update

&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;

extractchm &lt;span class="se"&gt;\&lt;/span&gt;

wkhtmltopdf &lt;span class="se"&gt;\&lt;/span&gt;

python3 &lt;span class="se"&gt;\&lt;/span&gt;

python3-bs4 &lt;span class="se"&gt;\&lt;/span&gt;

python3-lxml

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Если extractchm нет (редко, но бывает), ставим из chmlib:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; chmlib-tools

потом будет утилита extract&lt;span class="se"&gt;\_&lt;/span&gt;chmLib

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Проверка:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
extractchm &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true

&lt;/span&gt;wkhtmltopdf &lt;span class="nt"&gt;--version&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;2) Распаковать CHM в HTML&lt;/p&gt;

&lt;p&gt;Создай рабочую папку:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/work/chm2pdf/&lt;span class="o"&gt;{&lt;/span&gt;src,html,out&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;cp &lt;/span&gt;bsm&lt;span class="se"&gt;\_&lt;/span&gt;api.chm ~/work/chm2pdf/src/

&lt;span class="nb"&gt;cd&lt;/span&gt; ~/work/chm2pdf

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Распаковка:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Вариант A (extractchm):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
extractchm &lt;span class="nt"&gt;-o&lt;/span&gt; html src/bsm&lt;span class="se"&gt;\_&lt;/span&gt;api.chm

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Вариант B (extract_chmLib):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
extract&lt;span class="se"&gt;\_&lt;/span&gt;chmLib src/bsm&lt;span class="se"&gt;\_&lt;/span&gt;api.chm html

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Проверка что распаковалось:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; html | &lt;span class="nb"&gt;head

&lt;/span&gt;find html &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 2 &lt;span class="nt"&gt;-type&lt;/span&gt; f | &lt;span class="nb"&gt;head&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;3) Найти “точку входа” (главную страницу)&lt;/p&gt;

&lt;p&gt;Обычно это index.html, default.html, start.html или что-то из оглавления.&lt;/p&gt;

&lt;p&gt;Быстрый поиск кандидатов:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;ls &lt;/span&gt;html | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'index|default|start|main'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Если не очевидно — ищем самый “толстый” HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
find html &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-iname&lt;/span&gt; &lt;span class="s1"&gt;'\*.html'&lt;/span&gt; &lt;span class="nt"&gt;-printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\t%p\n'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;4) Собрать единый HTML (важный шаг)&lt;/p&gt;

&lt;p&gt;wkhtmltopdf лучше работает, когда ты отдаёшь ему &lt;strong&gt;одну HTML-страницу&lt;/strong&gt; , а не тысячу.&lt;/p&gt;

&lt;p&gt;Сделаем “склейку”: берём список страниц и собираем в out/merged.html.&lt;/p&gt;

&lt;p&gt;Вот минимальный Python-скрипт, который:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;читает оглавление (если найдём файл типа .hhc/toc)&lt;/li&gt;
&lt;li&gt;если оглавления нет — склеивает HTML по алфавиту (хуже, но работает)&lt;/li&gt;
&lt;li&gt;вырезает мусорные script/iframe&lt;/li&gt;
&lt;li&gt;приводит относительные ссылки к локальным путям&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Создай файл tools/merge.py:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;
&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;

&lt;span class="n"&gt;ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;OUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;OUT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist&lt;/span&gt;\&lt;span class="n"&gt;_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;\&lt;span class="n"&gt;_html&lt;/span&gt;\&lt;span class="nf"&gt;_files&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;

&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Если&lt;/span&gt; &lt;span class="n"&gt;есть&lt;/span&gt; &lt;span class="n"&gt;оглавление&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;можно&lt;/span&gt; &lt;span class="n"&gt;допилить&lt;/span&gt; &lt;span class="n"&gt;парсер&lt;/span&gt; &lt;span class="n"&gt;под&lt;/span&gt; &lt;span class="n"&gt;конкретный&lt;/span&gt; &lt;span class="n"&gt;CHM&lt;/span&gt;

&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Универсальный&lt;/span&gt; &lt;span class="n"&gt;fallback&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;склеим&lt;/span&gt; &lt;span class="n"&gt;HTML&lt;/span&gt; &lt;span class="n"&gt;по&lt;/span&gt; &lt;span class="n"&gt;имени&lt;/span&gt;

&lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\*.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;выкинем&lt;/span&gt; &lt;span class="n"&gt;дубли&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;служебное&lt;/span&gt;

&lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="ow"&gt;is&lt;/span&gt;\&lt;span class="nf"&gt;_file&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;\&lt;span class="n"&gt;_size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;clean&lt;/span&gt;\&lt;span class="nf"&gt;_body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;\&lt;span class="n"&gt;_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;\&lt;span class="n"&gt;_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;\&lt;span class="nf"&gt;_bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;грубая&lt;/span&gt; &lt;span class="n"&gt;попытка&lt;/span&gt; &lt;span class="n"&gt;с&lt;/span&gt; &lt;span class="n"&gt;кодировкой&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cp1251&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;windows-1251&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latin-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;break&lt;/span&gt;

&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;UnicodeDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="k"&gt;continue&lt;/span&gt;

&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ignore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lxml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;выкидываем&lt;/span&gt; &lt;span class="n"&gt;потенциальный&lt;/span&gt; &lt;span class="n"&gt;мусор&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;script&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iframe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noscript&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;

&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decompose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;

&lt;span class="n"&gt;нормализуем&lt;/span&gt; &lt;span class="n"&gt;якоря&lt;/span&gt; &lt;span class="n"&gt;и&lt;/span&gt; &lt;span class="n"&gt;локальные&lt;/span&gt; &lt;span class="n"&gt;ресурсы&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt;\&lt;span class="nf"&gt;_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

&lt;span class="n"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;убираем&lt;/span&gt; &lt;span class="n"&gt;внешние&lt;/span&gt; &lt;span class="n"&gt;и&lt;/span&gt; &lt;span class="n"&gt;mailto&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;href&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mailto:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;

&lt;span class="k"&gt;continue&lt;/span&gt;

&lt;span class="n"&gt;normalize&lt;/span&gt; &lt;span class="n"&gt;windows&lt;/span&gt; &lt;span class="n"&gt;slashes&lt;/span&gt;

&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;href&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;добавим&lt;/span&gt; &lt;span class="n"&gt;заголовок&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;разделитель&lt;/span&gt;

&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;\&lt;span class="nf"&gt;_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;\&lt;span class="n"&gt;_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;

&lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;

# &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;

&lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;\&lt;span class="n"&gt;_html&lt;/span&gt;\&lt;span class="nf"&gt;_files&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;\&lt;span class="nf"&gt;_body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;skip &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;!doctype html&amp;gt;

&amp;lt;html&amp;gt;

&amp;lt;head&amp;gt;

&amp;lt;meta charset=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; /&amp;gt;

&amp;lt;title&amp;gt;CHM merged&amp;lt;/title&amp;gt;

&amp;lt;style&amp;gt;

body {{ font-family: Arial, sans-serif; line-height: 1.45; }}

pre, code {{ white-space: pre-wrap; word-wrap: break-word; }}

h1 {{ page-break-before: always; }}

&amp;lt;/style&amp;gt;

&amp;lt;/head&amp;gt;

&amp;lt;body&amp;gt;

&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

&amp;lt;/body&amp;gt;

&amp;lt;/html&amp;gt;

&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OUT&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;merged.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;\&lt;span class="nf"&gt;_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK -&amp;gt; out/merged.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; \&lt;span class="n"&gt;_&lt;/span&gt;\&lt;span class="n"&gt;_name&lt;/span&gt;\&lt;span class="n"&gt;_&lt;/span&gt;\&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\_\_main\_\_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Запуск:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; tools out

python3 tools/merge.py

&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; out/merged.html

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;5) Конвертировать merged.html → PDF&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
wkhtmltopdf &lt;span class="se"&gt;\&lt;/span&gt;

&lt;span class="nt"&gt;--enable-local-file-access&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;

&lt;span class="nt"&gt;--encoding&lt;/span&gt; utf-8 &lt;span class="se"&gt;\&lt;/span&gt;

&lt;span class="nt"&gt;--page-size&lt;/span&gt; A4 &lt;span class="se"&gt;\&lt;/span&gt;

&lt;span class="nt"&gt;--margin-top&lt;/span&gt; 12 &lt;span class="nt"&gt;--margin-bottom&lt;/span&gt; 12 &lt;span class="nt"&gt;--margin-left&lt;/span&gt; 10 &lt;span class="nt"&gt;--margin-right&lt;/span&gt; 10 &lt;span class="se"&gt;\&lt;/span&gt;

out/merged.html out/manual.pdf

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Проверка результата:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lah&lt;/span&gt; out/manual.pdf

file out/manual.pdf

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Проверка результата&lt;/p&gt;

&lt;p&gt;Минимум:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PDF открывается, вес адекватный (не 2 KB и не 2 GB)&lt;/li&gt;
&lt;li&gt;есть текст (не “картинки страниц”)&lt;/li&gt;
&lt;li&gt;код/таблицы читаются&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Быстрый sanity-check: извлечь пару строк текста:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
python3 - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'

import subprocess, sys

p = subprocess.run(["pdftotext", "out/manual.pdf", "-"], capture&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="sh"&gt;output=True, text=True)

print(p.stdout[:800])
&lt;/span&gt;&lt;span class="no"&gt;
PY

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Если pdftotext нет:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; poppler-utils

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Типичные ошибки&lt;/p&gt;

&lt;p&gt;❌ 1) “Blocked access to file” / не видит картинки&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Причина:&lt;/strong&gt; wkhtmltopdf по умолчанию режет локальные ресурсы.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Решение:&lt;/strong&gt; добавь --enable-local-file-access (в примере уже есть).&lt;/p&gt;




&lt;p&gt;❌ 2) Кракозябры вместо русского&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Причина:&lt;/strong&gt; HTML в CP1251, а ты склеил как UTF-8.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Решение:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;в merge.py мы пробуем cp1251/windows-1251&lt;/li&gt;
&lt;li&gt;если всё равно плохо — найди реальную кодировку исходников:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
file &lt;span class="nt"&gt;-i&lt;/span&gt; html/&lt;span class="se"&gt;\*&lt;/span&gt;.html | &lt;span class="nb"&gt;head&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;❌ 3) “Оглавление” нет, порядок глав сломан&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Причина:&lt;/strong&gt; CHM хранит структуру в .hhc/.hhk, а мы склеили “по алфавиту”.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Решение:&lt;/strong&gt; дописать парсер оглавления под конкретный CHM (реально 20–40 строк, если структура нормальная).&lt;/p&gt;

&lt;p&gt;Если хочешь — кидай список файлов из html/ (без содержимого), я подскажу точечно.&lt;/p&gt;




&lt;p&gt;Где применять&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Локально:&lt;/strong&gt; быстро получить PDF, отдать в LLM/коллегам/заказчику.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD:&lt;/strong&gt; автогенерация документации в PDF (если CHM как артефакт).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPS:&lt;/strong&gt; можно крутить без GUI, чисто консолью.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Смотри также&lt;/p&gt;

&lt;p&gt;Если будешь ковыряться с файлами и поиском по дереву — пригодится:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Поиск файлов в Linux: команды для Ubuntu/CentOS — диагностика и find&lt;/li&gt;
&lt;li&gt;find: поиск больших файлов — когда PDF внезапно 900MB&lt;/li&gt;
&lt;li&gt;rsync: безопасное копирование с прогрессом — перенос распакованного CHM или готового PDF&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://viku-lov.ru/blog/kak-konvertirovat-chm-v-odin-pdf-linux" rel="noopener noreferrer"&gt;Read more on viku-lov.ru&lt;/a&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>documentation</category>
      <category>pdf</category>
      <category>chm</category>
    </item>
    <item>
      <title>Миграция с WordPress на Bitrix без потери SEO</title>
      <dc:creator>Андрей Викулов (VProger)</dc:creator>
      <pubDate>Mon, 23 Feb 2026 19:00:40 +0000</pubDate>
      <link>https://dev.to/_vproger_/mighratsiia-s-wordpress-na-bitrix-biez-potieri-seo-2l45</link>
      <guid>https://dev.to/_vproger_/mighratsiia-s-wordpress-na-bitrix-biez-potieri-seo-2l45</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fam3d01p5lcaterkncknu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fam3d01p5lcaterkncknu.png" alt="Миграция с WordPress на Bitrix без потери SEO" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Миграция с WordPress на Bitrix без потери SEO&lt;/p&gt;

&lt;p&gt;Поисковый запрос «миграция с WordPress на Bitrix» обычно означает одно: нужно перенести проект без падения трафика и без пересборки индекса с нуля. Статья для тех, кто уже решил перейти на Bitrix и кому важно не потерять позиции и переходы из поиска.&lt;/p&gt;

&lt;p&gt;Цель — сохранить структуру URL, метаданные (title, description, canonical), пользователей и поведенческие факторы. Ниже — конкретные шаги: экспорт из БД WordPress, импорт в Bitrix с сохранением slug, настройка ЧПУ и 301, проверка и типичные ошибки.&lt;/p&gt;

&lt;p&gt;Сниппеты по статье&lt;/p&gt;

&lt;p&gt;Готовые проверенные фрагменты по шагам миграции:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WordPress: экспорт постов и SEO-меты для миграции (SQL) — выгрузка из wp_posts и wp_postmeta&lt;/li&gt;
&lt;li&gt;Bitrix: SEF и шаблон URL для блога — настройка ЧПУ с ELEMENT_CODE&lt;/li&gt;
&lt;li&gt;Nginx: 301 rewrite при смене структуры URL — постоянный редирект ветки адресов&lt;/li&gt;
&lt;li&gt;Bitrix: UrlRewriter — правило 301 редиректа — точечные редиректы через API&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;В чём проблема&lt;/p&gt;

&lt;p&gt;Типичные симптомы при кривой миграции: старые ссылки из поиска и закладок перестают открываться, в панелях вебмастеров растёт число ошибок доступа, трафик из органики падает.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ 404 на старых URL&lt;/li&gt;
&lt;li&gt;❌ Потеря meta title / description&lt;/li&gt;
&lt;li&gt;❌ Слетевшие canonical&lt;/li&gt;
&lt;li&gt;❌ Обнулённые позиции в Яндексе и Google&lt;/li&gt;
&lt;li&gt;❌ Пользователи не могут авторизоваться&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Подробнее про работу WordPress «под капотом» и настройку окружения — в статьях &lt;a href="https://viku-lov.ru/blog/wordpress-wp-cron-ne-vypolnyaetsya-nastroit-systemnyj-cron/" rel="noopener noreferrer"&gt;«WordPress: системный cron вместо WP-Cron»&lt;/a&gt; и &lt;a href="https://viku-lov.ru/blog/wordpress-dolgo-zagruzhaetsya-nginx-php-fpm-ttfb/" rel="noopener noreferrer"&gt;«WordPress: долгая загрузка, TTFB, nginx и PHP-FPM»&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;WordPress хранит данные в:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;wp_posts&lt;/li&gt;
&lt;li&gt;wp_users&lt;/li&gt;
&lt;li&gt;wp_usermeta&lt;/li&gt;
&lt;li&gt;wp_terms&lt;/li&gt;
&lt;li&gt;wp_term_relationships&lt;/li&gt;
&lt;li&gt;wp_postmeta&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bitrix работает через инфоблоки и таблицы:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;b_iblock_element&lt;/li&gt;
&lt;li&gt;b_iblock_section&lt;/li&gt;
&lt;li&gt;b_user&lt;/li&gt;
&lt;li&gt;b_utm_*&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Если просто «залить HTML» или перенести только текст без сохранения адресов и метаданных, поисковики перестают считать страницы теми же самыми: меняется URL — теряется история ссылок и поведенческие факторы. В итоге позиции обнуляются, трафик падает. Поэтому миграция без потери SEO — это всегда экспорт структуры (slug, meta, категории), настройка ЧПУ в Bitrix и обязательные 301-редиректы со старых адресов.&lt;/p&gt;




&lt;p&gt;Рабочее решение&lt;/p&gt;

&lt;p&gt;Миграция делится на 5 этапов: экспорт из WordPress, импорт в Bitrix, настройка URL и ЧПУ, настройка редиректов, перенос пользователей. Каждый этап можно выполнять на копии БД, чтобы не трогать прод до финальной проверки.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Экспорт данных из WordPress&lt;/li&gt;
&lt;li&gt;Импорт в Bitrix&lt;/li&gt;
&lt;li&gt;Сохранение структуры URL&lt;/li&gt;
&lt;li&gt;Настройка 301&lt;/li&gt;
&lt;li&gt;Перенос пользователей&lt;/li&gt;
&lt;/ol&gt;




&lt;ol&gt;
&lt;li&gt;Экспорт постов и страниц из WordPress&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Нужны посты и страницы из wp_posts и метаданные SEO из wp_postmeta (плагины вроде Yoast или Rank Math хранят там title, description и canonical). Прямой доступ к БД быстрее и надёжнее, чем выгрузка через админку. Оба запроса ниже можно выполнить в phpMyAdmin или из консоли MySQL; результат сохраняем в JSON для следующего шага. Готовый сниппет с пояснениями и ссылками на документацию: WordPress: экспорт постов и SEO-меты для миграции (SQL).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;
&lt;span class="c1"&gt;-- Посты и страницы&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_status&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_posts&lt;/span&gt;

&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_type&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'page'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'publish'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Мета SEO (title, description, canonical)&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_value&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_postmeta&lt;/span&gt;

&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_key&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="s1"&gt;yoast&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="s1"&gt;wpseo&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="s1"&gt;title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="s1"&gt;yoast&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="s1"&gt;wpseo&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="s1"&gt;metadesc'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="s1"&gt;yoast&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="s1"&gt;wpseo&lt;/span&gt;&lt;span class="se"&gt;\_&lt;/span&gt;&lt;span class="s1"&gt;canonical'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;ol&gt;
&lt;li&gt;Импорт в Bitrix через Python&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Пример скрипта (через REST или прямой PHP endpoint):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wp\_export.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IBLOCK\_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;post\_title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CODE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;post\_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;# важно для URL
&lt;/span&gt;
&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ACTIVE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Y&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PREVIEW\_TEXT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;post\_content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;

&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://staging-site.ru/api/import.php&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;

&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ключевой момент — поле CODE в Bitrix должно совпадать с post_name из WordPress. Тогда адрес вида /blog/post-name/ останется тем же, и поисковики увидят тот же URL. Импорт можно делать через REST API Bitrix или через свой PHP-скрипт на временном endpoint; в примере выше используется один общий endpoint для всех записей.&lt;/p&gt;




&lt;ol&gt;
&lt;li&gt;Сохранение структуры URL&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;В WordPress адреса часто выглядят как /blog/post-name/ или /category/design/. В Bitrix нужно явно включить ЧПУ (SEF) и задать шаблон так, чтобы символьный код элемента подставлялся в URL. Создаём раздел с псевдонимом blog, в настройках сайта включаем SEF и указываем папку, например /blog/. Шаблон детальной страницы — #ELEMENT_CODE#/, тогда адрес будет /blog/имя-element-code/. В компоненте списка новостей (например, news) задаём (подробнее — в сниппете Bitrix: SEF и шаблон URL для блога):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;
&lt;span class="s2"&gt;"SEF\_MODE"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="s2"&gt;"SEF\_FOLDER"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"/blog/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="s2"&gt;"SEF\_URL\_TEMPLATES"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;

&lt;span class="s2"&gt;"detail"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"#ELEMENT\_CODE#/"&lt;/span&gt;

&lt;span class="p"&gt;]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;ol&gt;
&lt;li&gt;301 редиректы&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Если часть URL изменилась (например, был /category/, стал /blog/), все старые адреса должны отдавать 301 с новым Location. Иначе поисковики будут считать страницы удалёнными или дублями. Редиректы можно настроить в nginx для массовых правил (один rewrite на целую ветку старых URL) или в Bitrix через UrlRewriter для точечных перенаправлений. Готовые сниппеты: Nginx: 301 rewrite при смене структуры URL, Bitrix: UrlRewriter — правило 301 редиректа. Базовые приёмы nginx — в &lt;a href="https://viku-lov.ru/blog/nginx-basics-web-server-fundamentals-2026/" rel="noopener noreferrer"&gt;«Nginx: основы веб-сервера»&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;
&lt;span class="k"&gt;rewrite&lt;/span&gt; &lt;span class="s"&gt;^/old-category/(.&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;*)&lt;/span&gt;$ &lt;span class="n"&gt;/blog/&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="s"&gt;permanent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Или массово через таблицу редиректов Bitrix (модуль «Управление структурой» или API UrlRewriter в том же проекте, где настраивали SEF):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Bitrix\Main\UrlRewriter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;UrlRewriter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;

&lt;span class="s2"&gt;"CONDITION"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"#^/old-url/$#"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="s2"&gt;"RULE"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="s2"&gt;"ID"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="s2"&gt;"PATH"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"/new-url/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;ol&gt;
&lt;li&gt;Перенос пользователей&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Из WordPress: SELECT ID, user_login, user_email, user_pass FROM wp_users;. В Bitrix создаём пользователей так:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CUser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$arFields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;

&lt;span class="s2"&gt;"LOGIN"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$wpUser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"user\_login"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

&lt;span class="s2"&gt;"EMAIL"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$wpUser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"user\_email"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

&lt;span class="s2"&gt;"PASSWORD"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bin2hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;random\_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;

&lt;span class="s2"&gt;"ACTIVE"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Y"&lt;/span&gt;

&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$arFields&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Пароли из WordPress (хеш в user_pass) нельзя подставить в Bitrix: алгоритмы и соль разные. Поэтому при переносе пользователей задаём временный пароль (в коде выше — случайная строка) и при первом входе отправляем пользователю сброс пароля по email либо даём инструкцию сменить пароль в личном кабинете. Так вы сохраните учётки и доступ без взлома старых хешей.&lt;/p&gt;




&lt;p&gt;Проверка результата (диагностика)&lt;/p&gt;

&lt;p&gt;Диагностика после миграции сводится к трём проверкам: новый URL отдаёт 200, старые адреса отдают 301 с правильным Location, в sitemap перечислены те же URL, что и раньше (или новые, если вы сознательно сменили структуру и настроили редиректы). Команды ниже выполняйте с подставленным вашим доменом.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
URL страницы — ожидаем HTTP/1.1 200 OK

curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://site.ru/blog/post-name/

Старый URL — ожидаем 301 Moved Permanently и Location: https://site.ru/new-url/

curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://site.ru/old-url/

Sitemap: URL в файле должны совпадать со старыми

curl https://site.ru/sitemap.xml

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Проверка индексации&lt;/p&gt;

&lt;p&gt;После переключения прод-домена на Bitrix в Яндекс.Вебмастер и Google Search Console нужно заново отправить sitemap и при необходимости запросить переобход важных URL. Раздел «Покрытие» (coverage) покажет, какие страницы в индексе и нет ли массовых 404. Первые дни возможны временные просадки; если 301 настроены и URL совпадают, через 1–2 недели индекс стабилизируется.&lt;/p&gt;




&lt;p&gt;Если не работает&lt;/p&gt;

&lt;p&gt;Ниже — частые причины сбоев и что проверить в первую очередь. Если после миграции часть страниц отдаёт 404, трафик падает или пользователи не могут войти — пройдите по пунктам ниже и сверьтесь с разделом «Проверка результата (диагностика)». Чаще всего проблема в пропущенном этапе: забыли мету, поменяли slug или не настроили редиректы. Проверка по чек-листу выше обычно выявляет причину.&lt;/p&gt;




&lt;p&gt;Типичные ошибки&lt;/p&gt;

&lt;p&gt;❌ Не перенесли meta description&lt;/p&gt;

&lt;p&gt;Причина: выгрузили только wp_posts и не учли wp_postmeta, где лежат title, description и canonical от Yoast/Rank Math. Решение: выгрузить мету (запрос выше) и при импорте в Bitrix записывать её в отдельные свойства инфоблока или в отдельные поля; в шаблоне детальной страницы выводить эти поля в &lt;/p&gt; и мета-тегах.



&lt;p&gt;❌ Изменили структуру URL&lt;/p&gt;

&lt;p&gt;Причина: в Bitrix включили SEF, но символьный код элемента (CODE) задали произвольно или из названия, а не из post_name WordPress. В итоге адреса отличаются, старые ссылки ведут в никуда. Решение: при импорте строго задавать CODE = post_name для каждой записи и проверять по списку, что URL совпадают.&lt;/p&gt;




&lt;p&gt;❌ Не сделали 301&lt;/p&gt;

&lt;p&gt;Причина: надеялись, что поисковик «переиндексирует» или что достаточно обновить sitemap. Без 301 старые URL считаются мёртвыми, ссылочный вес и поведенческие факторы не передаются на новый адрес. Решение: для каждого старого адреса настроить 301 на новый (nginx или UrlRewriter в Bitrix), затем проверить через curl -I.&lt;/p&gt;




&lt;p&gt;❌ Потеряли теги&lt;/p&gt;

&lt;p&gt;Причина: в WordPress теги и категории хранятся в wp_terms и wp_term_relationships; если их не выгрузить и не завести в Bitrix (разделы инфоблока или HighloadBlock), на новом сайте пропадут рубрики и теги. Решение: экспортировать термины и связи, при импорте создавать разделы или записи в HighloadBlock и привязывать элементы к ним.&lt;/p&gt;




&lt;p&gt;Где применять&lt;/p&gt;

&lt;p&gt;Подход подходит для продакшена при полной смене CMS с WordPress на Bitrix, в том числе в Docker или на &lt;a href="https://viku-lov.ru/blog/bitrixvm-centos-ssl-letsencrypt-create-renew/" rel="noopener noreferrer"&gt;BitrixVM&lt;/a&gt; (после переноса имеет смысл проверить SSL и при необходимости обновить сертификаты). Особенно важно сохранять URL и 301 для проектов с существенным SEO-трафиком и для интернет-магазинов, у которых блог или статьи переносятся в Bitrix отдельно от каталога. Перед выкатом на прод стоит прогнать все шаги на копии сайта и убедиться, что количество записей, разделов и пользователей совпадает с выгрузкой из WordPress.&lt;/p&gt;




&lt;p&gt;Итог&lt;/p&gt;

&lt;p&gt;Миграция с WordPress на Bitrix — это не «экспорт-импорт» одной кнопкой, а последовательность: выгрузка постов и меты, импорт с сохранением символьного кода, настройка ЧПУ, 301 и проверка. Если сохранить slug, структуру URL, meta-поля, настроить 301 и актуальный sitemap, позиции не падают. Если хоть один пункт пропустить — получите просадку трафика на 30–70% в первые две недели. SEO здесь сводится к структуре, предсказуемым URL и корректным редиректам: нарушил — заплатил трафиком.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://viku-lov.ru/blog/migraciya-s-wordpress-na-bitrix-bez-poteri-seo" rel="noopener noreferrer"&gt;Read more on viku-lov.ru&lt;/a&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>bitrix</category>
      <category>seo</category>
      <category>migration</category>
    </item>
    <item>
      <title>Почему миграции с WordPress на Bitrix рушат SEO</title>
      <dc:creator>Андрей Викулов (VProger)</dc:creator>
      <pubDate>Mon, 23 Feb 2026 18:59:44 +0000</pubDate>
      <link>https://dev.to/_vproger_/pochiemu-mighratsii-s-wordpress-na-bitrix-rushat-seo-12k7</link>
      <guid>https://dev.to/_vproger_/pochiemu-mighratsii-s-wordpress-na-bitrix-rushat-seo-12k7</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6gep90r59bmaup7efv4p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6gep90r59bmaup7efv4p.png" alt="Почему миграции с WordPress на Bitrix рушат SEO" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Почему миграции с WordPress на Bitrix рушат SEO&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Коротко:&lt;/strong&gt; SEO падает не из-за смены CMS. Оно падает из-за неправильной архитектуры миграции и игнорирования структуры URL.&lt;/p&gt;

&lt;p&gt;За последние годы я участвовал в нескольких переносах проектов с WordPress на Bitrix с трафиком от 50 000 до 300 000 визитов в месяц. В 80% случаев просадка происходила не из-за «плохого Bitrix», а из-за управленческих и архитектурных ошибок. В оставшихся 20% причиной оказывались технические просчёты: не те редиректы, смена адресов «для красоты» или перенос только контента без метаданных. Ниже — о том, в чём именно спор, что происходит в реальности и при каких условиях миграция не бьёт по трафику.&lt;/p&gt;

&lt;p&gt;В чём спор&lt;/p&gt;

&lt;p&gt;Обычно говорят:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;«CMS не влияет на SEO»&lt;/li&gt;
&lt;li&gt;«Главное — контент, поисковик разберётся»&lt;/li&gt;
&lt;li&gt;«Редиректы можно сделать потом»&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Формально доля правды в этом есть: сама по себе система управления контентом не ранжируется. Но то, что из неё следует — URL-модель, структура разделов, скорость ответа сервера и поведение при переезде — уже влияет на индексацию и ранжирование. Поисковые системы реагируют не на название CMS, а на то, остались ли прежними адреса страниц, не посылаете ли вы пользователей и роботов на 404 и не подменили ли вы один документ другим без явного сигнала (301). Игнорировать это — значит заранее закладывать просадку. То же касается тезиса «редиректы потом»: после переключения домена старые URL начинают отдавать 404, ссылочный вес и поведенческие факторы теряются, и «доделать потом» уже не восстанавливает прежнюю картину в глазах поисковика.&lt;/p&gt;

&lt;p&gt;Что происходит в реальности&lt;/p&gt;

&lt;p&gt;При типичной миграции с WordPress на Bitrix я сталкиваюсь с одними и теми же проблемами:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Меняется структура URL: было «/blog/post-name/», стало «/news/123/» или «/katalog/stati/post-name/»&lt;/li&gt;
&lt;li&gt;Теряются slug: в WordPress адрес задаётся полем post_name, в Bitrix — полем CODE; при некорректном маппинге адреса расходятся&lt;/li&gt;
&lt;li&gt;Мета-поля не переносятся: title, description и canonical остаются в старой базе или подставляются шаблоном по умолчанию&lt;/li&gt;
&lt;li&gt;Не настроены 301-редиректы: старые ссылки из выдачи и с других сайтов оказываются битыми или ведут на временный редирект (302), что не передаёт вес&lt;/li&gt;
&lt;li&gt;Sitemap публикуется с задержкой или содержит уже неактуальные URL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;В результате поисковик фиксирует: часть страниц пропала, часть появилась по новым адресам, часть ссылок ведёт на 404, canonical изменился. Он не «понимает», что вы переехали; он видит разрыв целостности сайта и переоценивает его. Типичный пример из практики: проект с примерно 120 000 визитов в месяц после «технического переноса» без сохранения slug через три недели снизил трафик примерно до 47 000. Причина — массовые 404 на старых URL и частичные редиректы вместо сквозных 301. В другом кейсе заказчик настаивал на смене структуры каталога «под новую логику»; через два месяца органический трафик упал почти вдвое, и возврат к старой схеме URL с 301 уже не вернул позиции в прежний вид — пришлось выводить сайт из просадки месяцами. Оба случая объединяет одно: решение о миграции принималось без жёсткого требования сохранить адреса и без плана по редиректам.&lt;/p&gt;

&lt;p&gt;Когда миграция не убивает SEO&lt;/p&gt;

&lt;p&gt;Перенос безопасен для трафика, если соблюдены условия:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slug полностью совпадает со старым: адрес страницы после переноса тот же, что и в WordPress&lt;/li&gt;
&lt;li&gt;Структура вложенности не изменилась: если были разделы и подразделы, они сохраняются в том же виде&lt;/li&gt;
&lt;li&gt;Meta title и description перенесены и отображаются в выдаче так же, как раньше (или осознанно улучшены)&lt;/li&gt;
&lt;li&gt;Все старые URL отдают 301 permanent на новый адрес, без 302 и без 404&lt;/li&gt;
&lt;li&gt;Sitemap обновлён в день релиза и содержит актуальные адреса&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;В такой постановке CMS вторична: поисковику важна стабильная структура и предсказуемое поведение. Структура и дисциплина первичны; платформа — лишь инструмент. Отсюда практический вывод: перед переездом нужен чёткий чек-лист (выгрузка всех URL, маппинг slug → CODE, перенос мета-полей, настройка 301, обновление sitemap и проверка на staging), и ни один пункт нельзя откладывать «на потом».&lt;/p&gt;

&lt;p&gt;Когда я бы не стал переносить проект&lt;/p&gt;

&lt;p&gt;Я бы отложил или пересмотрел миграцию в таких случаях:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Если 80% трафика — органика, а бизнес не готов к просадке на 1–2 месяца. Восстановление индекса и доверие после сбоев занимает время; это нужно закладывать в планы&lt;/li&gt;
&lt;li&gt;Если нет staging или тестового окружения для полной проверки до переключения прод-домена&lt;/li&gt;
&lt;li&gt;Если нет выгрузки всех URL старого сайта: без полного списка адресов невозможно настроить 301 и проверить покрытие&lt;/li&gt;
&lt;li&gt;Если команда путает 302 и 301 или не понимает разницы между временным и постоянным редиректом&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Миграция с сохранением SEO — это инфраструктурная операция с чёткими критериями приёмки, а не просто «перевёрстка» или «перенос контента». Руководству и заказчику важно заранее принять возможность временной просадки даже при идеальной технике (переобход индекса, обновление сниппетов) и не давить на команду «сделать быстрее» в ущерб проверкам. Сокращение сроков за счёт пропуска этапов почти всегда оборачивается многомесячным восстановлением трафика.&lt;/p&gt;

&lt;p&gt;Технический аспект&lt;/p&gt;

&lt;p&gt;Без эмоций, только факты:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;URL-модель:&lt;/strong&gt; в WordPress slug хранится в поле post_name, в Bitrix — в поле CODE элемента инфоблока. Ошибка в маппинге при переносе ломает адреса и делает старые ссылки нерабочими&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO-поля:&lt;/strong&gt; в WordPress метаданные страниц часто лежат в wp_postmeta (в том числе при использовании Yoast, Rank Math и аналогов), в Bitrix — в свойствах инфоблока или отдельных полях; перенос должен быть явно спроектирован&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Редиректы:&lt;/strong&gt; эффективнее и надёжнее настраивать на уровне веб-сервера (nginx или аналог), а не только средствами CMS, особенно при массовых правилах&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Производительность:&lt;/strong&gt; «голый» Bitrix без кеша и оптимизаций обычно тяжелее типового WordPress; это может отразиться на Core Web Vitals и косвенно на ранжировании&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Проверка после миграции сводится к простым шагам: убедиться, что запрос к старому URL возвращает ответ 301 с заголовком Location на новый адрес, а запрос к новому URL — ответ 200. Если вместо этого вы видите 404 или 302, позиции и трафик будут падать, пока ситуация не исправлена. Имеет смысл заранее подготовить список ключевых страниц (топ по трафику и по ссылочному весу) и после переключения пройтись по нему вручную или с помощью скриптов, проверяя статус-коды и заголовки. То же касается sitemap и раздела «Покрытие» в панелях вебмастеров: расхождения между старым и новым списком URL нужно устранять сразу, а не «в следующем спринте».&lt;/p&gt;

&lt;p&gt;Типичные ошибки при принятии решения&lt;/p&gt;

&lt;p&gt;Чаще всего встречаются такие просчёты:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Переносят только HTML-контент, не заботясь о slug, мета-тегах и редиректах&lt;/li&gt;
&lt;li&gt;Меняют структуру URL «чтобы стало красивее» или «логичнее», не оценивая последствия для индекса&lt;/li&gt;
&lt;li&gt;Игнорируют категории и теги: в WordPress они живут в своей модели данных, в Bitrix их нужно явно смоделировать (разделы, Highload и т.п.)&lt;/li&gt;
&lt;li&gt;Забывают снять запрет индексации или открыть сайт для роботов после переноса&lt;/li&gt;
&lt;li&gt;Не проверяют robots.txt и настройки в панелях вебмастеров после переключения&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Самая дорогая ошибка — считать, что SEO восстановится само собой. Не восстановится: поисковику нужны однозначные сигналы (те же URL или корректные 301), актуальный sitemap и стабильная работа сайта. Не менее опасно поручать миграцию команде, которая не различает постоянный и временный редирект или не умеет выгружать полный список URL из старой CMS: без этого любые обещания «сохранить SEO» остаются декларацией. Имеет смысл зафиксировать критерии приёмки до старта работ и привязывать этапы оплаты или релиза к их выполнению.&lt;/p&gt;

&lt;p&gt;Моя позиция&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SEO рушит не Bitrix и не WordPress. Его рушит небрежная миграция.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Если при переносе сохраняется структура URL, корректно настроены 301, перенесены метаданные и проведено тестирование на staging до выката на прод — трафик остаётся стабильным. Если хотя бы один из этих пунктов пропущен — просадка практически гарантирована.&lt;/p&gt;

&lt;p&gt;CMS — инструмент. Структура и дисциплина — основа. Понимание этого и отличает миграцию, после которой сайт продолжает приносить трафик, от той, после которой месяцами разгребают последствия. Резюмируя: винить Bitrix или WordPress в просадке трафика после переезда — значит смотреть не туда. Смотреть нужно на то, сохранили ли вы адреса и сигналы для поисковика; если да — смена платформы остаётся прозрачной и для пользователей, и для роботов. Если нет — просадка почти неизбежна, и исправлять придётся уже по факту, с потерями во времени и в деньгах.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://viku-lov.ru/blog/pochemu-migracii-s-wordpress-na-bitrix-rushat-seo" rel="noopener noreferrer"&gt;Read more on viku-lov.ru&lt;/a&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>bitrix</category>
      <category>seo</category>
      <category>migration</category>
    </item>
    <item>
      <title>WordPress: JSON-LD для CPT — дубли и ошибки</title>
      <dc:creator>Андрей Викулов (VProger)</dc:creator>
      <pubDate>Mon, 23 Feb 2026 18:56:45 +0000</pubDate>
      <link>https://dev.to/_vproger_/wordpress-json-ld-dlia-cpt-dubli-i-oshibki-181j</link>
      <guid>https://dev.to/_vproger_/wordpress-json-ld-dlia-cpt-dubli-i-oshibki-181j</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2k0u86yn3simfkwad4u5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2k0u86yn3simfkwad4u5.png" alt="WordPress: JSON-LD для CPT — дубли и ошибки" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;WordPress: JSON-LD для CPT — дубли и ошибки&lt;/p&gt;

&lt;h1&gt;
  
  
  WordPress: JSON-LD для CPT — дубли и ошибки
&lt;/h1&gt;

&lt;p&gt;Владельцы сайтов на WordPress часто ловят одну и ту же боль: добавили Custom Post Type (например, &lt;code&gt;articles&lt;/code&gt; или &lt;code&gt;news&lt;/code&gt;), а JSON-LD разметка либо &lt;strong&gt;не появляется&lt;/strong&gt;, либо &lt;strong&gt;ломается синтаксисом&lt;/strong&gt;, либо &lt;strong&gt;дублируется в &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;&lt;/strong&gt;. В итоге Google Search Console/ Rich Results Test ругаются на невалидный Structured Data — и красивые сниппеты в выдаче не прилетают.&lt;/p&gt;

&lt;p&gt;Ниже — рабочий вариант генерации &lt;code&gt;BlogPosting&lt;/code&gt;/&lt;code&gt;Article&lt;/code&gt; через &lt;code&gt;wp_head&lt;/code&gt;, с безопасной сериализацией JSON (&lt;code&gt;wp_json_encode&lt;/code&gt;) и защитой от дублей.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Сниппеты по статье:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON-LD класс для CPT&lt;/li&gt;
&lt;li&gt;JSON-LD для CPT без класса&lt;/li&gt;
&lt;li&gt;curl: проверка JSON-LD в head&lt;/li&gt;
&lt;li&gt;Диагностика JSON-LD для CPT&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  В чём проблема
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Реальный симптом
&lt;/h3&gt;

&lt;p&gt;Вы регистрируете Custom Post Type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;register_post_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'articles'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s1"&gt;'public'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'has_archive'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Но на странице &lt;code&gt;/articles/my-post/&lt;/code&gt; происходит одно из трёх:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;В исходнике нет блока &lt;code&gt;&amp;lt;script type="application/ld+json"&amp;gt;…&amp;lt;/script&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;JSON ломается (в консоли/валидаторе): &lt;code&gt;Unexpected token &amp;lt; in JSON&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Разметка дублируется: 2–3 одинаковых блока в &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Пример из Rich Results Test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: Missing required field "author"
ERROR: Missing required field "datePublished"
WARNING: Multiple BlogPosting detected on page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Почему это происходит
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Тема не генерирует JSON-LD для CPT&lt;/strong&gt; — часто разметка делается только для стандартного &lt;code&gt;post&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO-плагин добавляет свою разметку&lt;/strong&gt; — и вы “добавили ещё одну такую же”.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Неправильный хук&lt;/strong&gt; — вставили в &lt;code&gt;template_redirect&lt;/code&gt;/шаблон, а не в &lt;code&gt;wp_head&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON собирается руками строкой&lt;/strong&gt; — кавычки, спецсимволы, HTML в excerpt → привет, сломанный JSON. Нужен &lt;code&gt;wp_json_encode()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Рабочее решение
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Шаг 1: Класс генерации JSON-LD для CPT
&lt;/h3&gt;

&lt;p&gt;Создайте файл:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wp-content/themes/your-theme/includes/class-jsonld-cpt.php&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(или в небольшом плагине — логика та же).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="cd"&gt;/**
 * JSON-LD Generator for Custom Post Types
 * Place in:
 * - wp-content/themes/your-theme/includes/class-jsonld-cpt.php
 * Or:
 * - wp-content/plugins/your-plugin/includes/class-jsonld-cpt.php
 */&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JSONLD_CPT_Generator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$target_post_types&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'articles'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'news'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'publications'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wp_head'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'output_jsonld'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;output_jsonld&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Только single страницы нужных CPT&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;is_singular&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;target_post_types&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="n"&gt;WP_Post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Защита от дублей: если уже выводили — выходим&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;did_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'jsonld_cpt_output'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$jsonld&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;build_blogposting_schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$jsonld&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;script type="application/ld+json"&amp;gt;'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nf"&gt;wp_json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jsonld&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;JSON_UNESCAPED_UNICODE&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;JSON_PRETTY_PRINT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/script&amp;gt;'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Маркер, что мы уже вывели JSON-LD&lt;/span&gt;
        &lt;span class="nf"&gt;do_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'jsonld_cpt_output'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;build_blogposting_schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;WP_Post&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$author_id&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post_author&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$author_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_the_author_meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'display_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$author_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$author_url&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_author_posts_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$author_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$publish_date&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_the_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DATE_W3C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$modified_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_the_modified_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DATE_W3C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Изображение: featured image или заглушка&lt;/span&gt;
        &lt;span class="nv"&gt;$image_id&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;get_post_thumbnail_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$image_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$image_id&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;wp_get_attachment_image_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$image_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'full'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get_stylesheet_directory_uri&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/assets/images/og-default.png'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Категория (первая)&lt;/span&gt;
        &lt;span class="nv"&gt;$categories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_the_terms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$section&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$categories&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;is_wp_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$categories&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;$categories&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'General'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'@context'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://schema.org'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'@type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'BlogPosting'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'mainEntityOfPage'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'@type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'WebPage'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'@id'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_permalink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'headline'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_the_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'description'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_the_excerpt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'image'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'@type'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'ImageObject'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'url'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$image_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'width'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'height'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;630&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'datePublished'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$publish_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'dateModified'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$modified_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'author'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'@type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Person'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'name'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$author_name&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nf"&gt;get_bloginfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'url'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$author_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'publisher'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'@type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Organization'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'name'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_bloginfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'logo'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="s1"&gt;'@type'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'ImageObject'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'url'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_stylesheet_directory_uri&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/assets/images/logo.png'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'width'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'height'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'articleSection'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$section&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'wordCount'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str_word_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;wp_strip_all_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post_content&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="c1"&gt;// keywords из тегов&lt;/span&gt;
        &lt;span class="nv"&gt;$tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_the_terms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'post_tag'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tags&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;is_wp_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tags&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'keywords'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;wp_list_pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Инициализация&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;JSONLD_CPT_Generator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Шаг 2: Подключение в &lt;code&gt;functions.php&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// wp-content/themes/your-theme/functions.php&lt;/span&gt;

&lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="nf"&gt;get_stylesheet_directory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/includes/class-jsonld-cpt.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Шаг 3: Альтернатива без класса (быстро и грубо, но работает)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wp_head'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'custom_cpt_jsonld_output'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;custom_cpt_jsonld_output&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$target_types&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'articles'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'news'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;is_singular&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$target_types&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;did_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'jsonld_cpt_output'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="n"&gt;WP_Post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'@context'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://schema.org'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'@type'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'BlogPosting'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'headline'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_the_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'datePublished'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_the_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DATE_W3C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'dateModified'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_the_modified_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DATE_W3C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'author'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'@type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Person'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'name'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_the_author_meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'display_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post_author&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'publisher'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'@type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Organization'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'name'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_bloginfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;script type="application/ld+json"&amp;gt;'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nf"&gt;wp_json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;JSON_UNESCAPED_UNICODE&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;JSON_PRETTY_PRINT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/script&amp;gt;'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;do_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'jsonld_cpt_output'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Проверка результата
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Проверка в исходнике
&lt;/h3&gt;

&lt;p&gt;Откройте страницу CPT и найдите в &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;должен быть &lt;strong&gt;один&lt;/strong&gt; блок &lt;code&gt;&amp;lt;script type="application/ld+json"&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;внутри — валидный JSON (без HTML, без “поехавших” кавычек)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) Быстрая проверка через &lt;code&gt;curl&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://yoursite.com/articles/my-post/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 60 &lt;span class="s1"&gt;'application/ld+json'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3) Проверка на дубли
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://yoursite.com/articles/my-post/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'application/ld+json'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ожидаемо: &lt;code&gt;1&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4) Google Rich Results Test
&lt;/h3&gt;

&lt;p&gt;Вбейте URL в Rich Results Test и смотрите:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;code&gt;BlogPosting&lt;/code&gt;/&lt;code&gt;Article detected&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ критических ошибок нет&lt;/li&gt;
&lt;li&gt;⚠️ warnings допустимы (например, &lt;code&gt;articleBody&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Типичные ошибки
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ❌ Ошибка 1: JSON ломается из-за кавычек в заголовке
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Симптом:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Uncaught SyntaxError: Unexpected token " in JSON
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Причина:&lt;/strong&gt; JSON собирается строкой или через &lt;code&gt;json_encode&lt;/code&gt; без нормальной сериализации.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Исправление:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ плохо&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"headline":"'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;get_the_title&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'"}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ нормально&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nf"&gt;wp_json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'headline'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_the_title&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  ❌ Ошибка 2: Разметка дублируется
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Симптом:&lt;/strong&gt; 2–3 блока JSON-LD в &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Причина:&lt;/strong&gt; тема + SEO-плагин (Yoast/RankMath) или несколько &lt;code&gt;add_action('wp_head', ...)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Исправление:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;выключить structured data для CPT в SEO-плагине (если есть такая настройка),&lt;/li&gt;
&lt;li&gt;оставить один источник разметки,&lt;/li&gt;
&lt;li&gt;использовать маркер &lt;code&gt;did_action('jsonld_cpt_output')&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ❌ Ошибка 3: &lt;code&gt;Missing required field "author"&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Причина:&lt;/strong&gt; неверная структура &lt;code&gt;author&lt;/code&gt; или он пустой.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Исправление (правильная структура):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'author'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s1"&gt;'@type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Person'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'name'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_the_author_meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'display_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post_author&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="s1"&gt;'url'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get_author_posts_url&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post_author&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  ❌ Ошибка 4: JSON-LD не появляется на CPT
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Причина:&lt;/strong&gt; &lt;code&gt;is_singular()&lt;/code&gt; проверяет не тот post type (или CPT другой).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Исправление:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$target_types&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'articles'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'news'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'publications'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;is_singular&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$target_types&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Если не работает: чеклист
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;CPT реально существует:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;post_type_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'articles'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// должно вернуть true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Хук вообще вызывается (временно):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'JSONLD: output called'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Кэш мешает:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;очистить кэш плагина/сервера,&lt;/li&gt;
&lt;li&gt;отключить page cache на время проверки.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Посмотреть PHP-логи:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/php/error.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Где применять
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Среда&lt;/th&gt;
&lt;th&gt;Применимость&lt;/th&gt;
&lt;th&gt;Примечание&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;✅ Да&lt;/td&gt;
&lt;td&gt;основной сценарий&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dev/Staging&lt;/td&gt;
&lt;td&gt;✅ Да&lt;/td&gt;
&lt;td&gt;тест перед деплоем&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;✅ Да&lt;/td&gt;
&lt;td&gt;без изменений&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BitrixVM&lt;/td&gt;
&lt;td&gt;✅ Да&lt;/td&gt;
&lt;td&gt;только проверь права на логи&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nginx/Apache&lt;/td&gt;
&lt;td&gt;✅ Да&lt;/td&gt;
&lt;td&gt;на генерацию не влияет&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD&lt;/td&gt;
&lt;td&gt;⚠️ Частично&lt;/td&gt;
&lt;td&gt;удобно валидировать через CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Связанные материалы
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Сниппеты по статье:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON-LD класс для CPT&lt;/li&gt;
&lt;li&gt;JSON-LD для CPT без класса&lt;/li&gt;
&lt;li&gt;curl: проверка JSON-LD в head&lt;/li&gt;
&lt;li&gt;Диагностика JSON-LD для CPT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Другие сниппеты и справочник:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CLI валидатор JSON-LD&lt;/li&gt;
&lt;li&gt;CLI валидатор микроразметки&lt;/li&gt;
&lt;li&gt;Термин: JSON-LD&lt;/li&gt;
&lt;li&gt;Термин: Schema.org&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Итоги
&lt;/h2&gt;

&lt;p&gt;Если JSON-LD для CPT в WordPress &lt;strong&gt;не появляется&lt;/strong&gt;, &lt;strong&gt;ломается&lt;/strong&gt; или &lt;strong&gt;дублируется&lt;/strong&gt;, почти всегда виноваты: неправильный хук, ручная сборка JSON и конфликт с SEO-плагином. Решение простое и скучное (значит хорошее): &lt;code&gt;wp_head&lt;/code&gt; + &lt;code&gt;wp_json_encode()&lt;/code&gt; + маркер от дублей через &lt;code&gt;did_action()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Read more on viku-lov.ru: &lt;a href="https://viku-lov.ru/blog/wordpress-json-ld-blogposting-custom-post-type" rel="noopener noreferrer"&gt;https://viku-lov.ru/blog/wordpress-json-ld-blogposting-custom-post-type&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://viku-lov.ru/blog/wordpress-json-ld-blogposting-custom-post-type" rel="noopener noreferrer"&gt;Read more on viku-lov.ru&lt;/a&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>jsonld</category>
      <category>customposttype</category>
      <category>schemaorg</category>
    </item>
  </channel>
</rss>
