Руководство: как правильно настроить hreflang-теги для мультиязычного сайта на Cotonti

Типовые решения и инструкция разработчикам: динамическая генерация rel="alternate" hreflang и x-default с поддержкой IETF‑стандарта для многоязычных страниц в Cotonti (плагин i18n) на примере сайта для ОАЭ

 

Оглавление


1. Проблема: недостаточность стандартного функционала i18n и важность правильной языковой разметки

1.1 Актуальность задачи для многоязычного сайта в одной стране

Современные поисковые системы (Google, Яндекс, Bing) активно используют атрибут hreflang и значение lang тега <html> для определения языковой и региональной принадлежности страницы. Для сайта, обслуживающего аудиторию в пределах одной страны, но на нескольких языках (например, Объединённые Арабские Эмираты с арабским, английским, русским, украинским, французским и другими), корректная разметка критически важна. Она позволяет:

  • Исключить дублирование контента между языковыми версиями.
  • Показать пользователю в поисковой выдаче версию на его родном языке, даже если он физически находится в ОАЭ.
  • Чётко обозначить поисковикам, что все языковые версии относятся к одному региону (ОАЭ) и не конкурируют с аналогичными версиями для других стран (например, русскоязычный сайт для России).

Стандартный плагин i18n для Cotonti отлично справляется с хранением переводов и переключением языков, но не генерирует теги <link rel="alternate" hreflang="..."> и не устанавливает полный IETF‑атрибут lang. Вместо этого он оперирует короткими внутренними кодами (например, ua, en, ru), что не соответствует требованиям HTML5 и SEO.

1.2 Типичные проблемы при ручном добавлении разметки

Попытки вручную прописать статические теги в шаблоне header.tpl приводят к ряду проблем:

  • Негибкость – при добавлении нового языка или изменении алиаса страницы требуется правка шаблона.
  • Ошибки в URL – легко забыть обновить ссылку, что ведёт к битым альтернативным версиям.
  • Некорректный x-default – не учитывается настройка omitmain, в результате страница по умолчанию может указывать не на ту версию.
  • Отсутствие проверки существования перевода – теги генерируются для всех языков, даже если страница не переведена, что вводит поисковики в заблуждение.

1.3 Требования к автоматическому решению

Необходимо динамическое решение, которое:

  1. Генерирует теги <link rel="alternate" hreflang="xx-XX" href="URL"> только для языков, на которые страница действительно переведена.
  2. Формирует корректный тег x-default, указывающий на основную языковую версию с учётом настройки omitmain.
  3. Устанавливает атрибут lang тега <html> в полном IETF‑формате (язык-РЕГИОН), например uk-AE, en-AE, ru-AE.
  4. Учитывает настройки ЧПУ (rewrite) и генерирует абсолютные URL с доменом.
  5. Не требует ручного вмешательства при добавлении/удалении языков или переводов.
  6. Работает на страницах модуля page (с возможностью расширения).

2. Ключевые понятия: внутренние коды плагина и международный стандарт IETF (RFC 5646)

2.1 Внутренние коды i18n

Плагин i18n использует в настройке locales собственные короткие идентификаторы, например:

ua|Українська
en|English
ru|Русский
pl|Polski

Эти коды (ua, en, ru, pl) применяются:

  • в параметре URL ?l=ua или префиксе /ua/;
  • в cookie i18n_locale;
  • в поле ipage_locale таблицы переводов cot_i18n_pages.

Они удобны для внутренней логики, но не являются стандартными языковыми тегами для HTML и поисковиков.

2.2 Стандарт IETF BCP 47 (RFC 5646)

Согласно стандарту, языковой тег должен иметь формат язык-РЕГИОН, где:

  • язык – двухбуквенный код ISO 639-1 (например, uk – украинский, ru – русский, en – английский);
  • РЕГИОН – двухбуквенный код страны ISO 3166-1 alpha-2 (например, AE – Объединённые Арабские Эмираты, UA – Украина, US – США).

Примеры корректных IETF‑тегов для сайта, ориентированного на ОАЭ:

  • uk-AE – украинский язык, аудитория в ОАЭ;
  • ru-AE – русский язык, аудитория в ОАЭ;
  • en-AE – английский язык, аудитория в ОАЭ;
  • fr-AE – французский язык, аудитория в ОАЭ.

Почему важно указывать регион AE для всех языков? Поисковые системы учитывают региональную привязку. Если для русскоязычной версии указать ru-RU, Google может решить, что страница предназначена для России, и не показывать её пользователям, находящимся в ОАЭ. Тег ru-AE явно сообщает: «это русскоязычный контент, актуальный для аудитории в ОАЭ». Аналогично, en-AE предпочтительнее, чем en-US или en-GB, если сайт не ориентирован исключительно на экспатов из конкретной страны.

2.3 Необходимость преобразования внутренних кодов в IETF

В нашем решении вводится массив-карта $i18n_ietf_map, который связывает короткий внутренний код плагина (например, 'ua') с полным IETF‑тегом ('uk-AE'). Это позволяет:

  • корректно установить lang="uk-AE" для украинской версии;
  • сгенерировать hreflang="uk-AE" для альтернативных ссылок.

При этом внутренняя логика плагина продолжает использовать короткие коды – они остаются в URL и в базе данных. Такое разделение обеспечивает совместимость с существующей структурой и полное соответствие современным стандартам.


3. Конфигурация плагина i18n и особенности многоязычного сайта в ОАЭ

Рассмотрим фактические настройки плагина, используемые в проекте (из таблицы cot_config):

('plug', 'i18n', '', '02', 'locales', 0, 'ua|Українська\r\nen|English\r\nru|Русский\r\npl|Polski', ...);
('plug', 'i18n', '', '03', 'omitmain', 3, '1', '1', ...);
('plug', 'i18n', '', '04', 'rewrite', 3, '1', '0', ...);
('plug', 'i18n', '', '05', 'cookie', 3, '0', '0', ...);

Пояснение параметров:

  • locales – список поддерживаемых языков с внутренними кодами (ua, en, ru, pl). Внимание: в нашем проекте для украинского используется код ua, а не стандартный uk. Это важно для правильной настройки карты преобразования.
  • omitmain – значение 1 означает, что для основного языка (в $cfg['defaultlang'] = 'ua') языковой префикс в URL не добавляется. Украинская версия будет доступна по адресу /e-vehicles/article, а альтернативные – /en/e-vehicles/article.
  • rewrite1 включает ЧПУ, и плагин формирует URL с префиксом языка (например, /en/page).
  • cookie0 – выбор языка не сохраняется в cookie (можно изменить при необходимости).

В файле config.php проекта должны быть определены:

$cfg['defaultlang'] = 'ua';
$cfg['mainurl'] = 'https://cotonti.local';

Такая конфигурация идеально подходит для сайта, ориентированного на ОАЭ, где основной язык (украинский) используется без префикса, а дополнительные языки добавляются через префикс.


4. Механика формирования URL с языковым префиксом (связка .htaccess и i18n)

4.1 Правила в .htaccess

В корневом .htaccess содержится правило, которое преобразует ЧПУ‑префиксы в GET‑параметр l:

# Language selector
RewriteRule ^(en|ru|de|pl|ua)/(.*) $2?l=$1 [QSA,NC,NE,DPI]

Это правило работает так: при запросе /en/e-vehicles/article сервер внутренне перенаправляет на /e-vehicles/article?l=en. Далее Cotonti, получив параметр l, активирует соответствующую локаль и загружает перевод страницы.

Важно: список языков в правиле должен совпадать с внутренними кодами из настройки locales (ua, en, ru, pl). В примере выше также присутствует de – его можно убрать или оставить, если немецкий язык добавлен.

4.2 Функция cot_url() и параметр force_no_lang

Функция cot_url() – стандартный инструмент Cotonti для генерации ЧПУ‑ссылок. Её сигнатура:

cot_url($area, $params, $anchor = '', $escape_html = false, $force_no_lang = false)

Параметр $force_no_lang играет ключевую роль. Если он установлен в true, функция никогда не добавит языковой префикс, даже если в массиве $params присутствует элемент 'l' и настройка rewrite включена. Это даёт нам полный контроль над формированием URL для x-default при omitmain = 1.

4.3 Влияние omitmain на URL основного языка

  • omitmain = 1: для основного языка префикс не добавляется. Украинская версия: /e-vehicles/article. Альтернативные: /en/e-vehicles/article.
  • omitmain = 0: даже для основного языка добавляется префикс. Украинская версия: /ua/e-vehicles/article.

Наше решение автоматически учитывает эту настройку при генерации x-default и тегов альтернативных языков.


5. Разработанное решение: интеграция улучшенной логики в i18n.header.tags.php

Мы расширяем стандартный файл плагина plugins/i18n/i18n.header.tags.php, добавляя три ключевых блока:

  1. Карта преобразования внутренних кодов в IETF‑теги ($i18n_ietf_map).
  2. Установка переменной HTML_LANG для использования в шаблоне <html lang="{HTML_LANG}">.
  3. Генерация тегов hreflang для переведённых страниц модуля page с учётом всех настроек.

Ниже представлен итоговый код с подробными комментариями. Оригинальная логика переключателя языков сохранена, исправлен только баг с формированием URL в админ‑панели.

5.1 Полный код файла i18n.header.tags.php

<?php
/* ====================
[BEGIN_COT_EXT]
Hooks=header.tags
Tags=header.tpl:{I18N_LANG_ROW_URL},{I18N_LANG_ROW_CODE},{I18N_LANG_ROW_TITLE},{I18N_LANG_ROW_CLASS},{I18N_LANG_ROW_SELECTED}
[END_COT_EXT]
==================== */

/**
 * Renders language selector
 * Отрисовка переключателя языков и генерация тегов hreflang.
 * Фиксы и дополнительные плюшки от https://github.com/webitproff 
 * @package I18n
 * @copyright (c) Cotonti Team
 * @license https://github.com/Cotonti/Cotonti/blob/master/License.txt
 */

defined('COT_CODE') or die('Wrong URL');

// ------------------------------------------------------------------------------
// IETF (Internet Engineering Task Force) – организация, разрабатывающая стандарты
// интернета, в том числе формат языковых тегов вида "язык-РЕГИОН" (RFC 5646).
// Карта преобразования коротких внутренних кодов плагина i18n в полные IETF-теги.
// Для сайта, ориентированного на ОАЭ, регион указывается как AE.
// ------------------------------------------------------------------------------
$i18n_ietf_map = [
    'ua' => 'uk-AE',   // Украинский язык для ОАЭ (основной)
    'ru' => 'ru-AE',   // Русский язык для ОАЭ
    'en' => 'en-AE',   // Английский язык для ОАЭ
    'pl' => 'pl-AE'    // Польский язык для ОАЭ
];

// ========== 1. ПОСТРОЕНИЕ ПЕРЕКЛЮЧАТЕЛЯ ЯЗЫКОВ (с исправлением для админки) ==========
if (count($i18n_locales) > 0) {
    foreach ($i18n_locales as $lc => $lc_title) {
        $lc_class = ($lc == $i18n_locale) ? 'selected' : '';
        $lc_selected = ($lc == $i18n_locale) ? 'selected="selected"' : '';
        $i18n_urlparams = $_GET;

        if ($cfg['plugin']['i18n']['omitmain'] && $lc == $i18n_fallback && (!$cfg['plugin']['i18n']['cookie'] || !cot_import('i18n_locale', 'COOKIE', 'ALP'))) {
            unset($i18n_urlparams['l']);
        } else {
            $i18n_urlparams['l'] = $lc;
        }

        if (defined('COT_PLUG')) {
            $i18n_ext = 'plug';
        } else {
            $i18n_ext = $env['ext'];
            unset($i18n_urlparams['e']);
        }
        if (isset($i18n_urlparams['rwr'])) {
            unset($i18n_urlparams['rwr']);
        }

        // Исправление для админ-панели: URL должен содержать /admin/ после языкового префикса
        if (defined('COT_ADMIN') && COT_ADMIN) {
            $url = cot_url('admin', $i18n_urlparams, '', false, true);
        } else {
            $url = cot_url($i18n_ext, $i18n_urlparams, '', false, true);
        }

        $t->assign([
            'I18N_LANG_ROW_URL'      => $url,
            'I18N_LANG_ROW_CODE'     => $lc,
            'I18N_LANG_ROW_FLAG'     => $lc === 'en' ? 'gb' : $lc,
            'I18N_LANG_ROW_TITLE'    => htmlspecialchars($lc_title),
            'I18N_LANG_ROW_CLASS'    => $lc_class,
            'I18N_LANG_ROW_SELECTED' => $lc_selected,
        ]);
        $t->parse('HEADER.I18N_LANG.I18N_LANG_ROW');
    }
    $t->parse('HEADER.I18N_LANG');
}

// ========== 2. УСТАНОВКА ПОЛНОГО IETF-ТЕГА ДЛЯ АТРИБУТА lang ==========
$current_short_locale = $i18n_locale ?: Cot::$cfg['defaultlang'];
$html_lang = isset($i18n_ietf_map[$current_short_locale]) 
    ? $i18n_ietf_map[$current_short_locale] 
    : $current_short_locale;
$t->assign('HTML_LANG', $html_lang);

// ========== 3. ГЕНЕРАЦИЯ ТЕГОВ hreflang ДЛЯ ПЕРЕВЕДЁННЫХ СТРАНИЦ ==========
if ($env['ext'] == 'page' && isset($id) && $id > 0) {
    $translated_locales = cot_i18n_list_page_locales($id);
    if (!empty($translated_locales)) {
        $page_data = Cot::$db->query("SELECT page_cat, page_alias FROM " . Cot::$db->pages . " WHERE page_id = ?", array($id))->fetch();
        if ($page_data) {
            $url_params = empty($page_data['page_alias']) 
                ? array('c' => $page_data['page_cat'], 'id' => $id)
                : array('c' => $page_data['page_cat'], 'al' => $page_data['page_alias']);
            $alternate_tags = '';
            $main_url = rtrim(Cot::$cfg['mainurl'], '/');

            // Теги для переведённых языков (кроме основного)
            foreach ($translated_locales as $lc) {
                if ($lc == Cot::$cfg['defaultlang']) continue;
                $params = $url_params;
                $params['l'] = $lc;
                $url = $main_url . '/' . cot_url('page', $params, '', false, false);
                $hreflang = isset($i18n_ietf_map[$lc]) ? $i18n_ietf_map[$lc] : $lc;
                $alternate_tags .= '' . "\n";
            }

            // Тег x-default
            $default_params = $url_params;
            if ((int)Cot::$cfg['plugin']['i18n']['omitmain'] === 1) {
                unset($default_params['l']);
                $default_url = $main_url . '/' . cot_url('page', $default_params, '', false, true);
            } else {
                $default_params['l'] = Cot::$cfg['defaultlang'];
                $default_url = $main_url . '/' . cot_url('page', $default_params, '', false, false);
            }
            $alternate_tags .= '' . "\n";

            $t->assign('ALTERNATE_TAGS', $alternate_tags);
            $t->parse('HEADER.ALTERNATE_TAGS');
        }
    }
}

Этот код полностью заменяет оригинальный файл i18n.header.tags.php.

5.2 Что нужно добавить в шаблон header.tpl

В файле темы header.tpl необходимо использовать переменные {HTML_LANG} и {ALTERNATE_TAGS}. Фрагмент шаблона:

<!-- BEGIN: HEADER -->
<!DOCTYPE html>
<!-- IF {HTML_LANG} -->
<html lang="{HTML_LANG}" data-bs-theme="light">
<!-- ELSE -->
<html lang="{PHP.usr.lang}" data-bs-theme="light">
<!-- ENDIF -->
<head>
    ...
    <!-- IF {ALTERNATE_TAGS} -->
    {ALTERNATE_TAGS}
    <!-- ENDIF -->
    ...
</head>

Важно: переменная {ALTERNATE_TAGS} должна располагаться внутри <head> до закрывающего тега.


6. Детальный разбор кода: построчное объяснение каждого блока

6.1 Карта преобразования $i18n_ietf_map

Массив связывает внутренние коды плагина ('ua', 'ru', 'en', 'pl') с полными IETF‑тегами для ОАЭ. Если в будущем добавится новый язык (например, французский с кодом 'fr'), достаточно дополнить карту записью 'fr' => 'fr-AE'.

6.2 Генерация переключателя языков (блок 1)

Этот код повторяет оригинальную логику, за исключением исправления для админ‑панели. Раньше при переключении языка в админке URL формировался некорректно (например, /en/page?m=page вместо /en/admin/page). Теперь при обнаружении константы COT_ADMIN вызывается cot_url('admin', ...), что гарантирует правильный путь.

6.3 Установка HTML_LANG (блок 2)

$current_short_locale = $i18n_locale ?: Cot::$cfg['defaultlang'];
$html_lang = isset($i18n_ietf_map[$current_short_locale]) ? $i18n_ietf_map[$current_short_locale] : $current_short_locale;
$t->assign('HTML_LANG', $html_lang);

Определяется текущий язык (из переменной плагина $i18n_locale или из конфига). Затем через карту он преобразуется в полный IETF‑тег. Если для языка нет записи в карте, используется оригинальный короткий код (на случай временного отсутствия). Результат передаётся в шаблон.

6.4 Генерация тегов hreflang (блок 3)

Проверка контекста

if ($env['ext'] == 'page' && isset($id) && $id > 0) {

Код выполняется только на страницах модуля page, когда известен идентификатор.

Получение списка переведённых языков

$translated_locales = cot_i18n_list_page_locales($id);

Функция плагина возвращает массив внутренних кодов языков, для которых существует запись в cot_i18n_pages. Например, ['en', 'ru', 'pl']. Основной язык (ua) в этот массив обычно не входит, если для него нет явного перевода.

Получение данных страницы

$page_data = Cot::$db->query("SELECT page_cat, page_alias FROM " . Cot::$db->pages . " WHERE page_id = ?", array($id))->fetch();

Из таблицы pages извлекаются категория и алиас, необходимые для построения ЧПУ.

Формирование базовых параметров URL

$url_params = empty($page_data['page_alias']) 
    ? array('c' => $page_data['page_cat'], 'id' => $id)
    : array('c' => $page_data['page_cat'], 'al' => $page_data['page_alias']);

Если у страницы есть алиас, используем его (al), иначе – идентификатор (id).

Цикл по переведённым языкам

foreach ($translated_locales as $lc) {
    if ($lc == Cot::$cfg['defaultlang']) continue;
    $params = $url_params;
    $params['l'] = $lc;
    $url = $main_url . '/' . cot_url('page', $params, '', false, false);
    $hreflang = isset($i18n_ietf_map[$lc]) ? $i18n_ietf_map[$lc] : $lc;
    $alternate_tags .= '' . "\n";
}
  • Пропускаем основной язык – он будет представлен через x-default.
  • Добавляем параметр l с внутренним кодом языка.
  • Вызываем cot_url() с force_no_lang = false, чтобы получить относительный путь с языковым префиксом (если rewrite включён).
  • Присоединяем основной URL сайта, получая абсолютный адрес.
  • Преобразуем внутренний код в IETF‑тег через карту и формируем тег <link>.

Генерация x-default

$default_params = $url_params;
if ((int)Cot::$cfg['plugin']['i18n']['omitmain'] === 1) {
    unset($default_params['l']);
    $default_url = $main_url . '/' . cot_url('page', $default_params, '', false, true);
} else {
    $default_params['l'] = Cot::$cfg['defaultlang'];
    $default_url = $main_url . '/' . cot_url('page', $default_params, '', false, false);
}
  • Если omitmain = 1, удаляем параметр l и вызываем cot_url() с force_no_lang = true. Это гарантирует URL без языкового префикса.
  • Если omitmain = 0, добавляем параметр l со значением основного языка и вызываем cot_url() с force_no_lang = false – получим URL с префиксом (например, /ua/...).

Передача в шаблон

$t->assign('ALTERNATE_TAGS', $alternate_tags);
$t->parse('HEADER.ALTERNATE_TAGS');

Собранная строка с тегами помещается в переменную шаблона и парсится (даже если блок не объявлен, это безопасно).


7. Настройка шаблона header.tpl

Полный пример актуального шаблона header.tpl из темы Index36:

<!-- BEGIN: HEADER -->
<!DOCTYPE html>
<!-- IF {HTML_LANG} -->
<html lang="{HTML_LANG}" data-bs-theme="light">
<!-- ELSE -->
<html lang="{PHP.usr.lang}" data-bs-theme="light">
<!-- ENDIF -->
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{HEADER_TITLE}</title>
    <!-- IF {HEADER_META_DESCRIPTION} -->
    <meta name="description" content="{HEADER_META_DESCRIPTION}" />
    <!-- ENDIF -->
    <!-- IF {HEADER_BASEHREF} -->
    {HEADER_BASEHREF}
    <!-- ENDIF -->
    <!-- IF {HEADER_CANONICAL_URL} -->
    <link rel="canonical" href="{HEADER_CANONICAL_URL}" />
    <!-- ENDIF -->
    <!-- IF {ALTERNATE_TAGS} -->
    {ALTERNATE_TAGS}
    <!-- ENDIF -->
    <link rel="shortcut icon" href="favicon.ico" />
    <link rel="icon" href="{PHP.cfg.themes_dir}/{PHP.theme}/img/icon.webp" type="image/svg+xml">
    <link rel="apple-touch-icon" href="apple-touch-icon.png" />
    <!-- IF {PHP.out.meta} -->
    {PHP.out.meta}
    <!-- ENDIF -->
    <script>
        (function () {
            const storedTheme = localStorage.getItem('theme');
            const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
            const defaultTheme = storedTheme || (prefersDark ? 'dark' : 'light');
            document.documentElement.setAttribute('data-bs-theme', defaultTheme);
        })();
    </script>
    {HEADER_HEAD}
</head>

Здесь:

  • {HTML_LANG} подставляет значение uk-AE, en-AE и т.д.
  • {ALTERNATE_TAGS} выводит сгенерированные <link> теги.

8. Проверка и отладка: ожидаемый результат в исходном коде страницы

8.1 Украинская (основная) версия

URL: https://cotonti.local/catname/page-alias-123

Исходный код (фрагмент):

<!DOCTYPE html>
<html lang="uk-AE" data-bs-theme="light">
<head>
    ...
    <link rel="canonical" href="https://cotonti.local/catname/page-alias-123" />
    <link rel="alternate" hreflang="en-AE" href="https://cotonti.local/en/catname/page-alias-123">
    <link rel="alternate" hreflang="pl-AE" href="https://cotonti.local/pl/catname/page-alias-123">
    <link rel="alternate" hreflang="ru-AE" href="https://cotonti.local/ru/catname/page-alias-123">
    <link rel="alternate" hreflang="x-default" href="https://cotonti.local/catname/page-alias-123">
    ...
</head>

Анализ:

  • lang="uk-AE" – полный IETF‑тег для украинского языка в ОАЭ.
  • hreflang содержат коды en-AE, pl-AE, ru-AE (если есть переводы).
  • x-default указывает на украинскую версию без префикса (благодаря omitmain=1).

8.2 Русская версия

URL: https://cotonti.local/ru/catname/page-alias-123

<!DOCTYPE html>
<html lang="ru-AE" data-bs-theme="light">
<head>
    ...
    <link rel="canonical" href="https://cotonti.local/ru/catname/page-alias-123" />
    <link rel="alternate" hreflang="en-AE" href="https://cotonti.local/en/catname/page-alias-123">
    <link rel="alternate" hreflang="pl-AE" href="https://cotonti.local/pl/catname/page-alias-123">
    <link rel="alternate" hreflang="ru-AE" href="https://cotonti.local/ru/catname/page-alias-123">
    <link rel="alternate" hreflang="x-default" href="https://cotonti.local/catname/page-alias-123">
    ...
</head>

Обратите внимание: x-default по‑прежнему ведёт на URL без префикса – украинскую версию. Это правильное поведение, так как основная версия является страницей по умолчанию.

8.3 Ситуация с omitmain = 0

Если бы параметр omitmain был равен 0, то x-default выглядел бы так:

<link rel="alternate" hreflang="x-default" href="https://cotonti.local/ua/catname/page-alias-123">

А основной язык также мог бы иметь собственный тег hreflang (если убрать условие пропуска в цикле).


9. Типичные ошибки и способы их устранения

9.1 Теги hreflang не выводятся на странице

  • Причина: страница не относится к модулю page, либо переменная $id не определена.
  • Решение: проверьте, что URL действительно ведёт на страницу модуля page. Для других модулей расширьте условие (см. раздел 10).

9.2 x-default содержит префикс языка при omitmain=1

  • Причина: неверно передан параметр force_no_lang или в массиве $default_params остался элемент l.
  • Решение: убедитесь, что для omitmain=1 используется unset($default_params['l']) и вызов cot_url(..., true).

9.3 В тегах hreflang отображаются короткие коды вместо IETF

  • Причина: отсутствует запись для языка в массиве $i18n_ietf_map.
  • Решение: добавьте соответствующую пару в карту, например 'fr' => 'fr-AE'.

9.4 Атрибут lang содержит короткий код

  • Причина: переменная {HTML_LANG} не передана в шаблон или текущий язык не найден в карте.
  • Решение: проверьте наличие блока 2 в коде и правильность карты.

9.5 Ссылки в hreflang относительные (без домена)

  • Причина: Cot::$cfg['mainurl'] не задан или содержит слеш в конце, который был удалён функцией rtrim, но при склейке возникла ошибка.
  • Решение: в config.php пропишите $cfg['mainurl'] = 'https://cotonti.local'; (без завершающего слеша).

9.6 Дублирование тега для основного языка

  • Причина: в цикле не пропущен основной язык.
  • Рекомендация: оставьте условие if ($lc == Cot::$cfg['defaultlang']) continue;. Дублирование hreflang для одной и той же версии может запутать поисковики.

9.7 Проблемы с кешированием

Если используется статический кеш страниц, изменения могут проявиться не сразу. Очистите кеш Cotonti или временно отключите кеширование для проверки.


10. Расширение решения на другие модули Cotonti

Представленный код ориентирован на модуль page. Если ваш сайт использует мультиязычность в других модулях (например, market для товаров), адаптация потребует нескольких шагов.

10.1 Пример для модуля market

Предположим, у вас есть модуль market с товарами, и переводы хранятся в таблице cot_i18n_market. Алгоритм расширения:

  1. В условии if ($env['ext'] == 'page' && isset($id) && $id > 0) замените на if (in_array($env['ext'], ['page', 'market']) && isset($id) && $id > 0).
  2. Получите ID товара (может быть в $id или $al).
  3. Создайте функцию, аналогичную cot_i18n_list_page_locales(), которая возвращает массив переведённых локалей для товара.
  4. Сформируйте $url_params в соответствии с правилами маршрутизации модуля market (например, array('c' => $cat, 'id' => $id)).
  5. Используйте ту же логику генерации тегов.

Принцип остаётся неизменным: получить список языков перевода, для каждого сформировать URL с параметром l, преобразовать короткий код в IETF и добавить тег.


11. Заключение и лучшие практики

Мы разработали и внедрили полностью автоматическое решение для генерации корректной языковой разметки в Cotonti с плагином i18n. Оно:

  • Динамически формирует теги hreflang и x-default только для существующих переводов.
  • Устанавливает атрибут lang в соответствии со стандартом IETF BCP 47.
  • Учитывает настройки omitmain и rewrite, гарантируя правильные абсолютные URL.
  • Поддерживает специфику многоязычного сайта в пределах одной страны (ОАЭ) благодаря региональной привязке -AE.
  • Не требует ручного вмешательства при изменении списка языков или добавлении переводов.
  • Включает исправление для корректной работы переключателя языков в админ‑панели.

Рекомендации по дальнейшему обслуживанию:

  • При обновлении плагина i18n файл i18n.header.tags.php может быть перезаписан. Рекомендуется создать резервную копию или вынести кастомную логику в отдельный мини‑плагин, подключающийся к хуку header.tags после i18n.
  • Регулярно проверяйте актуальность карты $i18n_ietf_map при добавлении новых языков.
  • Используйте инструменты Google Search Console для проверки корректности разметки hreflang.

Данное руководство основано на реальном внедрении в проект cotonti.local (демонстрационный домен). Код распространяется под лицензией MIT, вы можете свободно использовать и модифицировать его.


12. Приложения

12.1 SQL‑запрос для проверки переводов конкретной страницы

SELECT ipage_locale FROM cot_i18n_pages WHERE ipage_id = 123;

Замените 123 на ID вашей страницы. Результат покажет внутренние коды языков, на которые страница переведена.

12.2 Полная карта IETF‑тегов для ОАЭ (пример с часто используемыми языками)

Внутренний код (i18n)IETF‑тегОписание
uauk-AEУкраинский (ОАЭ)
ruru-AEРусский (ОАЭ)
enen-AEАнглийский (ОАЭ)
frfr-AEФранцузский (ОАЭ)
arar-AEАрабский (ОАЭ)
hihi-AEХинди (ОАЭ)
urur-AEУрду (ОАЭ)

12.3 Правила .htaccess для языковых префиксов (адаптировано под ОАЭ)

# Language selector (перечислите все используемые внутренние коды)
RewriteRule ^(en|ru|pl|ua|fr|ar)/(.*) $2?l=$1 [QSA,NC,NE,DPI]

Убедитесь, что список совпадает с кодами в настройке locales плагина i18n.

12.4 Полезные ссылки


Статья подготовлена на основе реального опыта внедрения и отладки. Автор выражает благодарность сообществу Cotonti (кроме отдельных динозавров-педиков) и разработчикам плагина i18n. При возникновении вопросов обращайтесь в соответствующие разделы форума поддержки.

Версия документа: 2.0
Дата: 22 апреля 2026 г.
Автор: Практикующий разработчик (webitproff)
Целевая CMS: Cotonti Siena (v0.9.26 и выше)
Задействованный плагин: i18n (Content Internationalization)
Особенности: полная поддержка региональной привязки (ОАЭ), стандарт IETF BCP 47, автоматическая установка атрибута lang и генерация hreflang
 


Важные изменения – современный формат языковых тегов требует использования дефиса (-)


В HTML правильно писать <html lang="en-GB"> (с дефисом).
Почему?

   Стандарт BCP 47 / RFC 5646 – современный формат языковых тегов требует использования дефиса (-) в качестве разделителя между языком и регионом.

       en – язык (английский)

       GB – регион (Великобритания)

   Подчёркивание (en_GB) – устаревший синтаксис (из POSIX, Java, некоторых старых систем). В HTML, XML, HTTP-заголовке Accept-Language используется только дефис.

   Валидация и SEO – браузеры и поисковые системы корректно парсят только теги с дефисами. Например, Google и Яндекс при анализе lang ожидают en-GB, а en_GB может быть проигнорирован или воспринят как неизвестный.

Пример
 

<!DOCTYPE html>
<html lang="en-GB">
...

Если нужно указать регион без дефиса – неверно.
Дефис – единственный допустимый разделитель по спецификации W3C.

16 минут чтения Sodium Carbonate

Комментарии (0)

Комментарии отсутствуют
Добавление комментариев доступно только зарегистрированным пользователям

Автор контента

webitproff

Оффлайн

Sodium Carbonate

Последняя авторизация: 11.07.2026 19:44

  • Страница размещена: 22.04.2026 06:52
  • Последнее обновление: 28.04.2026 06:40

Похожие страницы

Шаблон сайта, верстка сайта и тема сайта: профессиональный гид
1 Профессиональный гид: шаблон сайта, верстка сайта и тема сайтаВ контексте использования Cotonti Siena v0.9.261.
Cotonti Siena CMF • 22.02.2026 20:04 webitproff
Страница 404 ошибки для сайта и как сделать ее правильно
2 Качественные сайты разрабатываются в поддержку проектов – коммерческих и не только. Они служат для того, чтобы донести
Как настроить оплату на фриланс бирже услуг
3 Как настроить оплату на бирже фриланса и услуг. 💵 Плагины онлайн платежей для сайта на Cotonti: 1. — QIWI КИВИ –
Мета теги title и description для сайта
4 Мета теги Title и Description являются далеко не единственными meta-данными в разделе head HTML-документа. Однако в
ARIA в интерактивных элементах. Как правильно писать?.
5   Атрибуты ARIA позволяют изменять состояния и свойства элемента, определенные в дереве специальных