Таргеты и 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 движок не создаёт.