Skip to content

Commit cd932e8

Browse files
s00dcursoragent
andcommitted
fix(seo): emit hreflang from iso, not routing code
One alternate per locale (iso || code); optional hreflangBaseLanguage claims bare language by list order (#243). Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 08dd21e commit cd932e8

42 files changed

Lines changed: 437 additions & 194 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/api/module-options.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ Parts of the module you can switch off.
123123

124124
| Option | Type | Default | Description |
125125
| --- | --- | --- | --- |
126+
| `hreflangBaseLanguage` | `boolean` | `false` | Also emit a bare-language `hreflang` derived from each locale's `iso` (e.g. `es-ES` → also `es`). The first regional locale in `locales` claims the bare tag for that language. Routing `code` is never used — only `iso \|\| code`. |
126127
| `localizedRouteNamePrefix` | `string` | `'localized-'` | Prefix prepended to localized route names (e.g. `'localized-index'`). Used internally to distinguish original routes from generated locale variants. |
127128
| `routeDisableMeta` | `Record<string, boolean \| string[]>` || Per-route meta tag disabling, extracted from `defineI18nRoute()` calls. Maps a route path to `true` (disable all meta) or an array of locale codes for which meta should be disabled. |
128129
| `missingWarn` | `boolean` | `true` | Show console warnings when a translation key is missing. |

docs/api/packages.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ or CI failing.
2323
| [`@i18n-micro/devtools-ui`](/api/packages/devtools-ui) | `@i18n-micro/devtools-ui`, `@i18n-micro/devtools-ui/bridge`, `@i18n-micro/devtools-ui/bridge/create`, `@i18n-micro/devtools-ui/vite` | 25 |
2424
| [`@i18n-micro/hmr`](/api/packages/hmr) | `@i18n-micro/hmr/cache-keys`, `@i18n-micro/hmr/generate-plugin`, `@i18n-micro/hmr/watcher` | 8 |
2525
| [`@i18n-micro/node`](/api/packages/node) | `@i18n-micro/node` | 15 |
26-
| [`@i18n-micro/path-strategy`](/api/packages/path-strategy) | `@i18n-micro/path-strategy`, `@i18n-micro/path-strategy/no-prefix`, `@i18n-micro/path-strategy/prefix`, `@i18n-micro/path-strategy/prefix-and-default`, `@i18n-micro/path-strategy/prefix-except-default`, `@i18n-micro/path-strategy/types` | 37 |
26+
| [`@i18n-micro/path-strategy`](/api/packages/path-strategy) | `@i18n-micro/path-strategy`, `@i18n-micro/path-strategy/no-prefix`, `@i18n-micro/path-strategy/prefix`, `@i18n-micro/path-strategy/prefix-and-default`, `@i18n-micro/path-strategy/prefix-except-default`, `@i18n-micro/path-strategy/types` | 38 |
2727
| [`@i18n-micro/preact`](/api/packages/preact) | `@i18n-micro/preact` | 39 |
2828
| [`@i18n-micro/route-strategy`](/api/packages/route-strategy) | `@i18n-micro/route-strategy` | 18 |
2929
| [`@i18n-micro/solid`](/api/packages/solid) | `@i18n-micro/solid` | 33 |
3030
| [`@i18n-micro/test-utils`](/api/packages/test-utils) | `@i18n-micro/test-utils`, `@i18n-micro/test-utils/publish-smoke` | 36 |
3131
| [`@i18n-micro/types`](/api/packages/types) | `@i18n-micro/types` | 26 |
3232
| [`@i18n-micro/types-generator`](/api/packages/types-generator) | `@i18n-micro/types-generator`, `@i18n-micro/types-generator/nuxt` | 7 |
33-
| [`@i18n-micro/utils`](/api/packages/utils) | `@i18n-micro/utils/accept-language`, `@i18n-micro/utils/active-locales`, `@i18n-micro/utils/app-path`, `@i18n-micro/utils/build`, `@i18n-micro/utils/cache-control`, `@i18n-micro/utils/cookie`, `@i18n-micro/utils/deep-merge`, `@i18n-micro/utils/merge-i18n-head`, `@i18n-micro/utils/merge-source`, `@i18n-micro/utils/normalize`, `@i18n-micro/utils/parse-path`, `@i18n-micro/utils/payload-config`, `@i18n-micro/utils/payload-fetch`, `@i18n-micro/utils/payload-stats`, `@i18n-micro/utils/payload-url`, `@i18n-micro/utils/resolve-locale`, `@i18n-micro/utils/resolve-og-locale`, `@i18n-micro/utils/route`, `@i18n-micro/utils/route-pattern`, `@i18n-micro/utils/runtime-config`, `@i18n-micro/utils/source-loader` | 93 |
33+
| [`@i18n-micro/utils`](/api/packages/utils) | `@i18n-micro/utils/accept-language`, `@i18n-micro/utils/active-locales`, `@i18n-micro/utils/app-path`, `@i18n-micro/utils/build`, `@i18n-micro/utils/cache-control`, `@i18n-micro/utils/cookie`, `@i18n-micro/utils/deep-merge`, `@i18n-micro/utils/merge-i18n-head`, `@i18n-micro/utils/merge-source`, `@i18n-micro/utils/normalize`, `@i18n-micro/utils/parse-path`, `@i18n-micro/utils/payload-config`, `@i18n-micro/utils/payload-fetch`, `@i18n-micro/utils/payload-stats`, `@i18n-micro/utils/payload-url`, `@i18n-micro/utils/resolve-hreflang`, `@i18n-micro/utils/resolve-locale`, `@i18n-micro/utils/resolve-og-locale`, `@i18n-micro/utils/route`, `@i18n-micro/utils/route-pattern`, `@i18n-micro/utils/runtime-config`, `@i18n-micro/utils/source-loader` | 97 |
3434
| [`@i18n-micro/vue`](/api/packages/vue) | `@i18n-micro/vue` | 27 |
3535

3636
<!-- /generated:packages-index -->

docs/api/packages/astro.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,17 @@ import { /* … */ } from '@i18n-micro/astro'
3636
| `Getter` | type | `(key: TranslationKey, params?: Record<string, string \| number \| boolean>, defaultValue?: string) => unknown` |
3737
| `I18nClientProps` | interface | 4 members |
3838
| `i18nIntegration` | function | `(options: I18nIntegrationOptions) => AstroIntegration` |
39-
| `I18nIntegrationOptions` | interface | 51 members |
39+
| `I18nIntegrationOptions` | interface | 52 members |
4040
| `I18nMiddlewareOptions` | interface | 7 members |
4141
| `I18nRoutingStrategy` | interface | 10 members |
4242
| `interpolate` | function | `(template: string, params: Params) => string` |
4343
| `LoadedTranslations` | interface | 2 members |
4444
| `loadTranslationsFromDir` | function | `(options: LoadTranslationsOptions) => LoadedTranslations` |
4545
| `loadTranslationsIntoI18n` | function | `(i18n: { addTranslations: (locale: string, translations: Translations, merge?: boolean) => void; addRouteTranslations: (locale: string, routeName: string, translations: Translations, merge?: boolean) => void; }, options: LoadTranslationsOptions) => void` |
4646
| `LoadTranslationsOptions` | interface | 3 members |
47-
| `Locale` | interface | 11 members |
47+
| `Locale` | interface | 12 members |
4848
| `LocaleCode` | type | `string` |
49-
| `LocaleHeadOptions` | interface | 3 members |
49+
| `LocaleHeadOptions` | interface | 4 members |
5050
| `LocaleHeadResult` | interface | 3 members |
5151
| `Params` | type | `Record<string, string \| number \| boolean>` |
5252
| `PluralFunc` | type | `(key: TranslationKey, count: number, params: Params, locale: string, getter: Getter) => string \| null` |
@@ -165,7 +165,7 @@ import { /* … */ } from '@i18n-micro/astro'
165165

166166
</details>
167167
<details>
168-
<summary><code>I18nIntegrationOptions</code> — 51 members</summary>
168+
<summary><code>I18nIntegrationOptions</code> — 52 members</summary>
169169

170170
| Member | Type |
171171
| --- | --- |
@@ -193,6 +193,7 @@ import { /* … */ } from '@i18n-micro/astro'
193193
| `globalLocaleRoutes?` | `GlobalLocaleRoutes` |
194194
| `hmr?` | `boolean \| undefined` |
195195
| `hooks?` | `boolean \| undefined` |
196+
| `hreflangBaseLanguage?` | `boolean \| undefined` |
196197
| `httpCacheDuration?` | `number \| undefined` |
197198
| `locale` | `string` |
198199
| `localeCookie?` | `string \| null \| undefined` |
@@ -272,15 +273,16 @@ import { /* … */ } from '@i18n-micro/astro'
272273
| `translationDir` | `string` |
273274

274275
</details>
275-
<code>Locale</code> — 11 members, identical to [`Locale`](/api/packages/types).
276+
<code>Locale</code> — 12 members, identical to [`Locale`](/api/packages/types).
276277
<details>
277-
<summary><code>LocaleHeadOptions</code> — 3 members</summary>
278+
<summary><code>LocaleHeadOptions</code> — 4 members</summary>
278279

279280
| Member | Type |
280281
| --- | --- |
281282
| `addDirAttribute?` | `boolean \| undefined` |
282283
| `addSeoAttributes?` | `boolean \| undefined` |
283284
| `baseUrl?` | `string \| undefined` |
285+
| `hreflangBaseLanguage?` | `boolean \| undefined` |
284286

285287
</details>
286288
<details>

docs/api/packages/node.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { /* … */ } from '@i18n-micro/node'
2828
| `LoadedTranslations` | interface | 2 members |
2929
| `loadRootTranslations` | function | `(dir: string, disablePageLocales?: boolean) => Promise<Record<string, Translations>>` |
3030
| `loadTranslations` | function | `(dir: string, disablePageLocales?: boolean) => Promise<LoadedTranslations>` |
31-
| `Locale` | interface | 11 members |
31+
| `Locale` | interface | 12 members |
3232
| `LocaleCode` | type | `string` |
3333
| `Params` | type | `Record<string, string \| number \| boolean>` |
3434
| `PluralFunc` | type | `(key: TranslationKey, count: number, params: Params, locale: string, getter: Getter) => string \| null` |
@@ -103,7 +103,7 @@ import { /* … */ } from '@i18n-micro/node'
103103

104104
</details>
105105
<code>LoadedTranslations</code> — 2 members, identical to [`LoadedTranslations`](/api/packages/astro).
106-
<code>Locale</code> — 11 members, identical to [`Locale`](/api/packages/types).
106+
<code>Locale</code> — 12 members, identical to [`Locale`](/api/packages/types).
107107
<code>Translations</code> — 1 members, identical to [`Translations`](/api/packages/types).
108108

109109
Back to [all packages](/api/packages) · [Integration guides](/integrations/)

docs/api/packages/path-strategy.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ outline: 'deep'
66

77
# `@i18n-micro/path-strategy`
88

9-
37 exports across 6 entry points.
9+
38 exports across 6 entry points.
1010
Generated from the API snapshot that [`pnpm run api:surface`](/guide/maintenance-commands#api-surface)
1111
checks against the TypeScript sources.
1212

@@ -35,6 +35,7 @@ import { /* … */ } from '@i18n-micro/path-strategy'
3535
| `PrefixExceptDefaultPathStrategy` | class | 44 members |
3636
| `PrefixPathStrategy` | class | 36 members |
3737
| `ResolvedRouteLike` | interface | 6 members |
38+
| `resolvePathWithParams` | function | `(path: string, params?: Record<string, unknown>) => string` |
3839
| `RouteAnalysis` | interface | 2 members |
3940
| `RouteLike` | interface | 6 members |
4041
| `RouterAdapter` | interface | 3 members |

docs/api/packages/preact.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ import { /* … */ } from '@i18n-micro/preact'
4141
| `I18nT` | const | `(props: I18nTProps) => JSX.Element \| null` |
4242
| `I18nTProps` | interface | 311 members |
4343
| `interpolate` | function | `(template: string, params: Params) => string` |
44-
| `Locale` | interface | 11 members |
44+
| `Locale` | interface | 12 members |
4545
| `LocaleCode` | type | `string` |
46-
| `ModuleOptions` | interface | 45 members |
46+
| `ModuleOptions` | interface | 46 members |
4747
| `Params` | type | `Record<string, string \| number \| boolean>` |
4848
| `PluralFunc` | type | `(key: TranslationKey, count: number, params: Params, locale: string, getter: Getter) => string \| null` |
4949
| `PreactI18n` | class | 44 members |
@@ -1353,7 +1353,7 @@ import { /* … */ } from '@i18n-micro/preact'
13531353

13541354
</details>
13551355
<details>
1356-
<summary><code>Locale</code> — 11 members</summary>
1356+
<summary><code>Locale</code> — 12 members</summary>
13571357

13581358
| Member | Type |
13591359
| --- | --- |
@@ -1365,12 +1365,13 @@ import { /* … */ } from '@i18n-micro/preact'
13651365
| `disabled?` | `boolean \| undefined` |
13661366
| `displayName?` | `string \| undefined` |
13671367
| `fallbackLocale?` | `string \| undefined` |
1368+
| `isCatchallLocale?` | `boolean \| undefined` |
13681369
| `iso?` | `string \| undefined` |
13691370
| `og?` | `string \| undefined` |
13701371
| `seo?` | `boolean \| undefined` |
13711372

13721373
</details>
1373-
<code>ModuleOptions</code> — 45 members, identical to [`ModuleOptions`](/api/packages/types).
1374+
<code>ModuleOptions</code> — 46 members, identical to [`ModuleOptions`](/api/packages/types).
13741375
<details>
13751376
<summary><code>PreactI18n</code> — 44 members</summary>
13761377

docs/api/packages/solid.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ import { /* … */ } from '@i18n-micro/solid'
3636
| `I18nSwitcher` | const | `Component<I18nSwitcherProps>` |
3737
| `I18nT` | const | `Component<I18nTProps>` |
3838
| `interpolate` | function | `(template: string, params: Params) => string` |
39-
| `Locale` | interface | 11 members |
39+
| `Locale` | interface | 12 members |
4040
| `LocaleCode` | type | `string` |
41-
| `ModuleOptions` | interface | 45 members |
41+
| `ModuleOptions` | interface | 46 members |
4242
| `Params` | type | `Record<string, string \| number \| boolean>` |
4343
| `PluralFunc` | type | `(key: TranslationKey, count: number, params: Params, locale: string, getter: Getter) => string \| null` |
4444
| `SolidI18n` | class | 53 members |
@@ -78,8 +78,8 @@ import { /* … */ } from '@i18n-micro/solid'
7878
| `resolvePath?` | `((to: string \| { path?: string; }, locale: string) => string \| { path?: string; }) \| undefined` |
7979

8080
</details>
81-
<code>Locale</code> — 11 members, identical to [`Locale`](/api/packages/types).
82-
<code>ModuleOptions</code> — 45 members, identical to [`ModuleOptions`](/api/packages/types).
81+
<code>Locale</code> — 12 members, identical to [`Locale`](/api/packages/types).
82+
<code>ModuleOptions</code> — 46 members, identical to [`ModuleOptions`](/api/packages/types).
8383
<details>
8484
<summary><code>SolidI18n</code> — 53 members</summary>
8585

docs/api/packages/types.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ import { /* … */ } from '@i18n-micro/types'
2929
| `I18nHeadMeta` | interface | 1 members |
3030
| `I18nRouteParams` | type | `Record<LocaleCode, Record<string, string>> \| null` |
3131
| `init` | const | `() => void` |
32-
| `Locale` | interface | 11 members |
32+
| `Locale` | interface | 12 members |
3333
| `LocaleCode` | type | `string` |
3434
| `MissingHandler` | type | `(locale: string, key: TranslationKey, routeName: string, instance?: unknown, type?: string) => void` |
35-
| `ModuleOptions` | interface | 45 members |
36-
| `ModuleOptionsExtend` | interface | 48 members |
37-
| `ModulePrivateOptionsExtend` | interface | 48 members |
35+
| `ModuleOptions` | interface | 46 members |
36+
| `ModuleOptionsExtend` | interface | 49 members |
37+
| `ModulePrivateOptionsExtend` | interface | 49 members |
3838
| `Params` | type | `Record<string, string \| number \| boolean>` |
3939
| `PluralFunc` | type | `(key: TranslationKey, count: number, params: Params, locale: string, getter: Getter) => string \| null` |
4040
| `PluralTranslations` | interface | 2 members |
@@ -95,7 +95,7 @@ import { /* … */ } from '@i18n-micro/types'
9595

9696
</details>
9797
<details>
98-
<summary><code>Locale</code> — 11 members</summary>
98+
<summary><code>Locale</code> — 12 members</summary>
9999

100100
| Member | Type |
101101
| --- | --- |
@@ -107,13 +107,14 @@ import { /* … */ } from '@i18n-micro/types'
107107
| `disabled?` | `boolean \| undefined` |
108108
| `displayName?` | `string \| undefined` |
109109
| `fallbackLocale?` | `string \| undefined` |
110+
| `isCatchallLocale?` | `boolean \| undefined` |
110111
| `iso?` | `string \| undefined` |
111112
| `og?` | `string \| undefined` |
112113
| `seo?` | `boolean \| undefined` |
113114

114115
</details>
115116
<details>
116-
<summary><code>ModuleOptions</code> — 45 members</summary>
117+
<summary><code>ModuleOptions</code> — 46 members</summary>
117118

118119
| Member | Type |
119120
| --- | --- |
@@ -140,6 +141,7 @@ import { /* … */ } from '@i18n-micro/types'
140141
| `globalLocaleRoutes?` | `GlobalLocaleRoutes` |
141142
| `hmr?` | `boolean \| undefined` |
142143
| `hooks?` | `boolean \| undefined` |
144+
| `hreflangBaseLanguage?` | `boolean \| undefined` |
143145
| `httpCacheDuration?` | `number \| undefined` |
144146
| `localeCookie?` | `string \| null \| undefined` |
145147
| `locales?` | `Locale[] \| undefined` |
@@ -165,7 +167,7 @@ import { /* … */ } from '@i18n-micro/types'
165167

166168
</details>
167169
<details>
168-
<summary><code>ModuleOptionsExtend</code> — 48 members</summary>
170+
<summary><code>ModuleOptionsExtend</code> — 49 members</summary>
169171

170172
| Member | Type |
171173
| --- | --- |
@@ -193,6 +195,7 @@ import { /* … */ } from '@i18n-micro/types'
193195
| `hashMode` | `boolean` |
194196
| `hmr?` | `boolean \| undefined` |
195197
| `hooks?` | `boolean \| undefined` |
198+
| `hreflangBaseLanguage?` | `boolean \| undefined` |
196199
| `httpCacheDuration?` | `number \| undefined` |
197200
| `isSSG` | `boolean` |
198201
| `localeCookie?` | `string \| null \| undefined` |
@@ -220,7 +223,7 @@ import { /* … */ } from '@i18n-micro/types'
220223

221224
</details>
222225
<details>
223-
<summary><code>ModulePrivateOptionsExtend</code> — 48 members</summary>
226+
<summary><code>ModulePrivateOptionsExtend</code> — 49 members</summary>
224227

225228
| Member | Type |
226229
| --- | --- |
@@ -247,6 +250,7 @@ import { /* … */ } from '@i18n-micro/types'
247250
| `globalLocaleRoutes?` | `GlobalLocaleRoutes` |
248251
| `hmr?` | `boolean \| undefined` |
249252
| `hooks?` | `boolean \| undefined` |
253+
| `hreflangBaseLanguage?` | `boolean \| undefined` |
250254
| `httpCacheDuration?` | `number \| undefined` |
251255
| `localeCookie?` | `string \| null \| undefined` |
252256
| `locales?` | `Locale[] \| undefined` |

docs/api/packages/utils.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ outline: 'deep'
66

77
# `@i18n-micro/utils`
88

9-
93 exports across 21 entry points.
9+
97 exports across 22 entry points.
1010
Generated from the API snapshot that [`pnpm run api:surface`](/guide/maintenance-commands#api-surface)
1111
checks against the TypeScript sources.
1212

@@ -369,6 +369,36 @@ import { /* … */ } from '@i18n-micro/utils/payload-url'
369369
| `routeName?` | `string \| undefined` |
370370
| `routesLocaleLinks?` | `Record<string, string> \| undefined` |
371371

372+
</details>
373+
## `@i18n-micro/utils/resolve-hreflang`
374+
375+
```ts
376+
import { /**/ } from '@i18n-micro/utils/resolve-hreflang'
377+
```
378+
379+
| Export | Kind | Signature |
380+
| --- | --- | --- |
381+
| `HreflangAlternate` | interface | 2 members |
382+
| `HreflangLocaleInput` | type | ``Pick<Locale, 'code' \| 'iso'> & { /** When true and `hreflangBaseLanguage` is on, this locale claims the bare language tag even if another locale already did. */ isCatchallLocale?: boolean }`` |
383+
| `resolveHreflangAlternates` | function | `(locales: HreflangLocaleInput[], options?: ResolveHreflangAlternatesOptions) => HreflangAlternate[]` |
384+
| `ResolveHreflangAlternatesOptions` | interface | 1 members |
385+
386+
<details>
387+
<summary><code>HreflangAlternate</code> — 2 members</summary>
388+
389+
| Member | Type |
390+
| --- | --- |
391+
| `hreflang` | `string` |
392+
| `localeCode` | `string` |
393+
394+
</details>
395+
<details>
396+
<summary><code>ResolveHreflangAlternatesOptions</code> — 1 members</summary>
397+
398+
| Member | Type |
399+
| --- | --- |
400+
| `hreflangBaseLanguage?` | `boolean \| undefined` |
401+
372402
</details>
373403
## `@i18n-micro/utils/resolve-locale`
374404

0 commit comments

Comments
 (0)