import type { I18nConfig, LocalizedSlugs } from '../i18n/index.js' import { buildLocalizedPath, getLocaleCodes } from '../i18n/index.js' type BuildHreflangArgs = { /** slug per locale for the current document, e.g. { pl: 'o-nas', en: 'about' } */ slugs: LocalizedSlugs config: I18nConfig /** Absolute site origin, e.g. 'https://example.com'. Omit for relative paths. */ baseUrl?: string /** Home slug that collapses to the locale root. Defaults to 'home'. */ homeSlug?: string | Record /** * Localized segment the document lives under (an archive page's slugs), * e.g. { pl: 'artykuly', en: 'articles' }. Locales missing from the prefix * are omitted — an entry with no archive in that language has no URL there. */ prefix?: LocalizedSlugs } /** * Builds a map of locale → URL for hreflang alternate links, suitable for * Next.js Metadata `alternates.languages`. * * Bridges SEO and i18n: for each configured locale that the document has a * slug in, it produces the locale-aware path (via buildLocalizedPath), * optionally prefixed with an absolute origin. * * @example * buildHreflangAlternates({ * slugs: { pl: 'o-nas', en: 'about' }, * config, * baseUrl: 'https://example.com', * }) * // → { pl: 'https://example.com/pl/o-nas', en: 'https://example.com/en/about' } */ export function buildHreflangAlternates({ slugs, config, baseUrl, homeSlug = 'home', prefix, }: BuildHreflangArgs): Record { const origin = baseUrl?.replace(/\/$/, '') ?? '' const alternates: Record = {} // Single-locale sites have no language alternatives — hreflang describes // relationships BETWEEN language versions, and there's only one. Emitting // hreflang (or x-default) here would be wrong, so return empty: the page keeps // its canonical, but no alternate-language links. if (config.locales.length === 1) { return alternates } for (const locale of getLocaleCodes(config)) { const path = buildLocalizedPath({ slugs, locale, config, homeSlug, prefix }) if (path) { alternates[locale] = `${origin}${path}` } } // x-default: the version Google serves when the user's language/region doesn't // match any hreflang — and, crucially here, the fallback when the root ('/') // redirect is ambiguous (Googlebot with no/foreign Accept-Language). Point it // at the default locale (the primary market) so search shows that version by // default instead of guessing. Only set when the default locale has a URL. const defaultLocalePath = alternates[config.defaultLocale] if (defaultLocalePath) { alternates['x-default'] = defaultLocalePath } return alternates }