Canonical, hreflang и карта языковых версий
У «Параллели» есть самостоятельные адреса переводов и обычные ссылки между ними. Теперь опишем эти отношения в метаданных. Русская и английская статьи связаны одним ID, но каждая представляет готовый документ на своём языке. Нужно согласовать canonical, языковые альтернативы и sitemap, не превратив английскую страницу в случайную копию, которую мы сами просим исключить.
Все данные возьмём из того же реестра, который использует переключатель. Если писать hreflang вручную в каждом Markdown-файле, после добавления перевода придётся исправлять сразу несколько документов. Сборка из общей карты уменьшает число мест, где может возникнуть противоречие.
Canonical конкретной версии
Для опубликованной английской страницы зададим canonical на её собственный окончательный адрес. Русская версия также укажет на себя. Перевод основного содержания является самостоятельной языковой версией; в нашем примере нет причины объявлять все английские документы дублями русского оригинала.
В build.py добавим помощник абсолютного адреса. Схема URL уже проверена до вызова функции, а origin хранится в конфигурации:
def absolute_url(site, route):
return site["origin"].rstrip("/") + route
def canonical_tag(site, variant):
url = escape(absolute_url(site, variant["url"]), quote=True)
return f'<link rel="canonical" href="{url}">'
Учебная английская страница получит https://parallel.example/en/articles/stable-urls.html. Не подставляйте адрес локального предпросмотра вместо производственного origin. Сборщик и сервер чтения могут работать на 127.0.0.1, но это не меняет договор будущих публичных адресов.
Canonical выражает предпочтение адреса и не гарантирует решение поисковой системы. Он также не является перенаправлением. Пользователь, открывший URL, останется на этой странице, пока сервер или браузер явно не выполнят другое действие. В учебной библиотеке такие переходы не нужны: версии доступны по своим адресам.
Взаимный набор альтернатив
Для первой статьи сформируем на обеих страницах одинаковый набор альтернатив: русский оригинал и английский перевод. В набор включается и сама текущая версия. У второй статьи английский статус draft, поэтому её адрес в метаданные не попадает.
def alternate_tags(site, article):
tags = []
for language, variant in published_variants(article).items():
url = escape(absolute_url(site, variant["url"]), quote=True)
lang = escape(language, quote=True)
tags.append(f'<link rel="alternate" hreflang="{lang}" href="{url}">')
return "\n".join(tags)
Условия для взаимных ссылок, включения собственной версии и абсолютных URL описаны в документации Google по языковым версиям. В нашем договоре generic en соответствует английскому тексту без отдельного регионального предложения. Локаль en-GB, выбранная для форматирования интерфейса, сама по себе не требует региональной цели en-GB в поисковых метаданных.
Это важное различие. Если статья полезна любому англоязычному читателю, нет необходимости ограничивать её назначение страной только из-за формата даты. Региональные версии появляются, когда меняется реальное предложение или содержание. Тогда реестр потребуется расширить: один язык может иметь несколько осмысленных вариантов.
Когда нужен x-default
Значение x-default может указывать на страницу выбора языка или на другую согласованную запасную версию. Оно не означает «главная версия для всех поисковых запросов». В нашем маленьком проекте нет отдельной страницы выбора перевода конкретной статьи, поэтому не добавляем это значение автоматически.
Если позднее создадим нейтральный селектор, его адрес должен действительно существовать и быть полезным. Назначать каждой статье x-default на главную сайта только ради заполнения ещё одного тега не следует: читатель потеряет исходную задачу. Объяснение запасного назначения относится к структуре проекта, а не к формальной обязанности заполнить все возможные атрибуты.
Атрибут lang в HTML продолжает отвечать за язык документа для браузера и средств чтения. Поисковые системы имеют собственные механизмы определения языка основного текста. Поэтому перевод только меню при русском теле не превращает страницу в качественную английскую версию. Согласованное содержание важнее заполненного набора тегов.
Карта сайта из публичных маршрутов
Для sitemap нам нужен точный список выдаваемых страниц. В уроке используем обычный XML urlset и канонические URL. Языковые альтернативы уже описаны в HTML, поэтому дублировать тот же набор вторым способом не обязательно. Один понятный механизм легче сопровождать.
import xml.etree.ElementTree as ET
def write_sitemap(site, articles, output):
namespace = "http://www.sitemaps.org/schemas/sitemap/0.9"
ET.register_namespace("", namespace)
root = ET.Element(f"{{{namespace}}}urlset")
for article in articles:
for variant in published_variants(article).values():
item = ET.SubElement(root, f"{{{namespace}}}url")
ET.SubElement(item, f"{{{namespace}}}loc").text = (
absolute_url(site, variant["url"]))
ET.ElementTree(root).write(output / "sitemap.xml",
encoding="utf-8", xml_declaration=True)
XML-библиотека экранирует содержимое элементов. Строковая склейка вручную особенно неприятна, когда в адресах появляются специальные символы. Наши учебные пути ограничены простым форматом, но генератору всё равно полезно использовать корректный сериализатор.
Не записываем lastmod как время каждой сборки. Пересборка после изменения CSS не доказывает обновление содержания каждой статьи. Если добавим поле даты, оно должно отражать реальное существенное изменение соответствующей языковой версии. У оригинала и перевода это могут быть разные даты.
Согласованное состояние выпуска
Для stable-urls ожидаются два canonical, взаимный набор альтернатив и два адреса в sitemap. Для reading-list ожидаются только русский документ и русский URL. Если английский перевод опубликовали, один переход статуса в реестре должен изменить сборку страницы, переключатель и метаданные одновременно.
Ссылка на отсутствующий файл, противоречивый canonical и черновик в sitemap — разные ошибки, но у них часто одна причина: несколько независимых списков страниц. Наш общий реестр связывает эти операции. Перед публикацией потребуется отдельная техническая проверка полученных файлов и HTTP-выдачи; в текущей подготовке таких запусков нет.
Разделы и страницы списка
Описанная карта относится к одной статье. У каталога собственные адреса и содержание: русская версия перечисляет два материала, английская — один. Не следует автоматически считать любую страницу английского списка переводом страницы русского списка с тем же номером. После фильтрации и пагинации на них могут оказаться разные наборы записей.
Сначала определите назначение каждой страницы. Для главного оглавления языковые альтернативы обычно понятны. Для подробной страницы результата фильтра или поиска договор может быть другим. Если индексировать такие состояния не планируется, не включайте их в sitemap только ради количества адресов. Наш учебный генератор перечисляет канонические страницы статей, а не все возможные параметры интерфейса.
При росте библиотеки разносите карту сайта по согласованным частям и сохраняйте единый источник опубликованных версий. Добавление отдельного XML-файла не меняет необходимость готового содержания и доступной выдачи. Взаимные альтернативы и старые адреса должны оставаться правильными после каждого расширения.
Мы получили поисковое описание языковых версий без изменения старого русского адреса. В следующем уроке рассмотрим редакционную проблему: оригинал обновился, а перевод остался прежним. Теги не могут определить смысловую актуальность текста, поэтому зафиксируем её отдельно.