Files
ipal-kit/src/modules/seo/hreflang.ts
T

73 lines
2.7 KiB
TypeScript

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<string, string>
/**
* 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<string, string> {
const origin = baseUrl?.replace(/\/$/, '') ?? ''
const alternates: Record<string, string> = {}
// 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
}