nuckty
EN
Все разделы →

Таргеты и hreflang

Из одного репозитория nuckty может собрать несколько таргетов — сайтов с отдельными языками, доменами и выходными каталогами. Для родственных таргетов движок готовит данные alternates; разметку hreflang создаёт обработчик документа.

Что такое таргет

Таргеты задают в config-nuckty.ts:

import { defineNucktyConfig } from 'nuckty';

export default defineNucktyConfig({
    targets: {
        'site-ru': { configDir: 'config/ru/', outDir: 'site-ru/', lang: 'ru', host: 'example.ru', group: 'site' },
        'site-en': { configDir: 'config/en/', outDir: 'site-en/', lang: 'en', host: 'example.com', group: 'site' },
    },
    ext: [/* ... */],
});

Поле lang задаёт язык документа, но не переводит Markdown и не выбирает файл автоматически. В примере подготовьте отдельные конфигурации в config/ru/ и config/en/: их карты сайта должны указывать на нужные исходники, например index.ru.md и index.md. Если configDir одинаков, оба таргета читают одну карту сайта и один набор страниц. Глобальный config/config-nuckty.ts остаётся общим.

ПолеНазначение
configDirКаталог конфигурации таргета: site.json, sitemap.yml, levels.json и external.json.
outDirКуда пишется артефакт сборки.
langЯзык, который обработчик документа может вывести в <html lang>.
dataПроизвольные настройки сайта, доступные через DocumentData.target.data.
hostКанонический домен.
groupИмя группы родственных таргетов. Нужно именно для hreflang: без group движок не строит альтернативы.

Корень контента задаётся не в таргете: движок читает .md из первого уровня levels.json (по умолчанию content/). Своих полей на этот счёт у таргета нет.

nuckty build site-ru соберёт русскую версию, nuckty build site-en — английскую. nuckty build без аргумента собирает все.

Родственные таргеты и hreflang

Родственными считаются таргеты с одинаковым group и разными lang. Таргеты без group ни с кем не сопоставляются. Движок строит тот же маршрут на родственных доменах и передаёт эти адреса (alternates) в контекст рендеринга. Sitemap другого таргета он не читает и наличие страницы не проверяет. Объединяйте в группу только таргеты с соответствующими маршрутами; заголовки пусты, если явно не переданы функции сопоставления:

type Alternates = Map<string, { url: string; title: string; host: string }>;

Например, страница /catalog есть и в site-ru, и в site-en (оба таргета в одной группе). При сборке site-ru движок строит адрес /catalog на site-en и передаёт в контекст:

const alternates = new Map([
    ['en', { url: 'https://example.com/catalog', title: '', host: 'example.com' }],
]);

Текущий таргет в карту не входит — в ней только родственные страницы. При сравнении языковых тегов регистр не учитывается: en-US и EN-us обозначают один тег, а en-GB остаётся отдельным региональным вариантом. В возвращаемых ключах сохраняется авторское написание.

Когда нужен group

group — это переключатель hreflang, а не ограничитель. Правило простое:

  • Нужны ссылки между языковыми версиями — всем таргетам одного сайта задайте одинаковый group. Без group движок не строит альтернативы вообще.
  • hreflang не нужен — group не задавайте. Таргет без группы ни с кем не сопоставляется.

Одинаковый group и означает «это языковые версии одного сайта». Разные группы — независимые сайты, между которыми сопоставление не нужно.

Пример: репозиторий компании держит основной сайт на двух языках и отдельный мини-сайт конференции:

targets: {
    // Основной сайт — две языковые версии, связаны hreflang
    'main-ru': { configDir: 'config/main-ru/', outDir: 'site/main-ru/', lang: 'ru', host: 'company.ru', group: 'main' },
    'main-en': { configDir: 'config/main-en/', outDir: 'site/main-en/', lang: 'en', host: 'company.com', group: 'main' },
    // Конференция — отдельный сайт, своя группа, чтобы не связываться с основным
    'event': { configDir: 'config/event/', outDir: 'site/event/', lang: 'ru', host: 'event.company.ru', group: 'event' },
}

Если сайт один и на одном языке — group не нужен: альтернатив нет и без него.

Движок готовит данные, блок — разметку

Движок передаёт alternates обработчику документа. Стандартный шаблон root использует эти данные для ссылок <link rel=alternate> внутри head; собственный обработчик или шаблон может оформить их иначе.

Пример разметки head по карте alternates:

{ tag: 'head', content: [
    // ...meta...
    ...[...alternates.entries()].map(([lang, a]) => ({
        tag: 'link',
        attrs: {
            rel: 'alternate',
            hreflang: lang,
            href: a.url,
        },
    })),
]}

Обязанности разделены: движок не закладывает поисковую разметку в код, а блок сам решает, как её оформить. hreflang="x-default", перевод title — задача блока.

Когда это нужно

Несколько таргетов полезны, когда одни и те же блоки, шаблоны и конвейер переиспользуются для нескольких языковых или доменных версий. Если сайт один и на одном языке — хватает одного таргета, а ссылки hreflang движок не создаёт.