Skip to content

Commit ebd2213

Browse files
feat(seo): additional catchall hreflang tags (#597)
Implements additional hreflang tags that are not specific to a country, if not provided explicitly. Resolves #522
1 parent deeb607 commit ebd2213

File tree

5 files changed

+318
-67
lines changed

5 files changed

+318
-67
lines changed

docs/seo.md

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,92 @@
11
# SEO
22

3-
With `seo` option enabled, **nuxt-i18n** attempts to add some metadata to improve your pages SEO. Here's what it does:
43

5-
* Add a _lang_ attribute containing current locale's ISO code to the `<html>` tag.
6-
* Generate `<link rel="alternate" hreflang="x">` tags for every language configured in `nuxt.config.js`. For each language, the ISO code is used as `hreflang` attribute's value. [More on hreflang](https://support.google.com/webmasters/answer/189077)
7-
* Generate `og:locale` and `og:locale:alternate` meta tags as defined in the [Open Graph protocol](http://ogp.me/#optional)
8-
* When using `prefix_and_default` strategy, generate `rel="canonical"` link on the default language routes containing the
4+
## Benefits
5+
6+
When the `seo` option is enabled, **nuxt-i18n** attempts to add some metadata to improve your pages SEO. Here's what it does.
7+
8+
### `lang` attribute for `<html>` tag.
9+
10+
With `seo` enabled, nuxt-i18n will set the correct `lang` attribute, equivalent to the current locale's ISO code, in the `<html>` tag.
11+
12+
### Automatic hreflang generation
13+
14+
nuxt-i18n will generate `<link rel="alternate" hreflang="x">` tags for every language configured in `nuxt.config.js`.
15+
The language's ISO codes are used as `hreflang` values.
16+
17+
Since version [v6.6.0](https://github.com/nuxt-community/nuxt-i18n/releases/tag/v6.6.0), a catchall locale hreflang link
18+
is provided for each language group (e.g. `en-*`) as well.
19+
By default, it is the first language provided but another language can be selected by setting `isCatchallLocale` to `true`
20+
on that specific language object in your `nuxt.config.js`. [More on hreflang](https://support.google.com/webmasters/answer/189077)
21+
22+
An example without selected catchall locale:
23+
```js
24+
// nuxt.config.js
25+
26+
['nuxt-i18n', {
27+
locales: [
28+
{
29+
code: 'en',
30+
iso: 'en-US' // Will be used as catchall locale by default
31+
},
32+
{
33+
code: 'gb',
34+
iso: 'en-GB'
35+
}
36+
]
37+
}]
38+
```
39+
40+
Here is how you'd use `isCatchallLocale` to selected another language:
41+
```js
42+
// nuxt.config.js
43+
44+
['nuxt-i18n', {
45+
locales: [
46+
{
47+
code: 'en',
48+
iso: 'en-US'
49+
},
50+
{
51+
code: 'gb',
52+
iso: 'en-GB',
53+
isCatchallLocale: true // This one will be used as catchall locale
54+
}
55+
]
56+
}]
57+
```
58+
59+
In case you already have an `en` language iso set, it'll be used as the catchall without doing anything
60+
61+
```js
62+
// nuxt.config.js
63+
64+
['nuxt-i18n', {
65+
locales: [
66+
{
67+
code: 'gb',
68+
iso: 'en-GB'
69+
},
70+
{
71+
code: 'en',
72+
iso: 'en' // will be used as catchall locale
73+
}
74+
]
75+
}]
76+
```
77+
78+
### OpenGraph Locale tag generation
79+
80+
nuxt-i18n will also generate `og:locale` and `og:locale:alternate` meta tags as defined in the [Open Graph protocol](http://ogp.me/#optional).
81+
82+
### Canonical links for `prefix_and_default`
83+
84+
When using the `prefix_and_default` strategy, `rel="canonical"` links on the default language routes will be generated and contain the
985
prefix to avoid duplicate indexation. [More on canonical](https://support.google.com/webmasters/answer/182192#dup-content)
1086

87+
## Requirements
1188

12-
For this feature to work, you must configure `locales` option as an array of objects, where each object has an `iso` option set to the language ISO code:
89+
To leverage the SEO benefits of nuxt-i18n, you must configure the `locales` option as an array of objects, where each object has an `iso` option set to the language's ISO code:
1390

1491
```js
1592
// nuxt.config.js
@@ -32,7 +109,7 @@ For this feature to work, you must configure `locales` option as an array of obj
32109
}]
33110
```
34111

35-
You should also set the `baseUrl` option to your production domain in order to make alternate URLs fully-qualified:
112+
You must also set the `baseUrl` option to your production domain in order to make alternate URLs fully-qualified:
36113

37114
```js
38115
// nuxt.config.js
@@ -43,7 +120,8 @@ You should also set the `baseUrl` option to your production domain in order to m
43120
```
44121

45122

46-
To enable this feature everywhere in your app, set `seo` option to `true`:
123+
To enable this feature everywhere in your app, set `seo` option to `true`.
124+
**This comes with a performance drawback though**. More information below.
47125

48126
```js
49127
// nuxt.config.js

src/templates/seo-head.js

Lines changed: 109 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -21,71 +21,127 @@ export const nuxtI18nSeo = function () {
2121
return {}
2222
}
2323
// Prepare html lang attribute
24-
const currentLocaleData = this.$i18n.locales.find(l => l[LOCALE_CODE_KEY] === this.$i18n.locale)
24+
const currentLocale = this.$i18n.locales.find(l => codeFromLocale(l) === this.$i18n.locale)
25+
const currentLocaleIso = isoFromLocale(currentLocale)
26+
2527
const htmlAttrs = {}
26-
if (currentLocaleData && currentLocaleData[LOCALE_ISO_KEY]) {
27-
htmlAttrs.lang = currentLocaleData[LOCALE_ISO_KEY]
28+
29+
if (currentLocale && currentLocaleIso) {
30+
htmlAttrs.lang = currentLocaleIso // TODO: simple lang or "specific" lang with territory?
2831
}
2932

3033
const link = []
31-
// hreflang tags
32-
if (strategy !== STRATEGIES.NO_PREFIX) {
33-
link.push(...this.$i18n.locales
34-
.map(locale => {
35-
if (locale[LOCALE_ISO_KEY]) {
36-
return {
37-
hid: 'alternate-hreflang-' + locale[LOCALE_ISO_KEY],
38-
rel: 'alternate',
39-
href: baseUrl + this.switchLocalePath(locale.code),
40-
hreflang: locale[LOCALE_ISO_KEY]
41-
}
42-
} else {
43-
// eslint-disable-next-line no-console
44-
console.warn(`[${MODULE_NAME}] Locale ISO code is required to generate alternate link`)
45-
return null
46-
}
47-
})
48-
.filter(item => !!item))
49-
}
5034

51-
// canonical links
52-
if (strategy === STRATEGIES.PREFIX_AND_DEFAULT) {
53-
const canonicalPath = this.switchLocalePath(currentLocaleData[LOCALE_CODE_KEY])
54-
if (canonicalPath && canonicalPath !== this.$route.path) {
55-
// Current page is not the canonical one -- add a canonical link
56-
link.push({
57-
hid: 'canonical-lang-' + currentLocaleData[LOCALE_CODE_KEY],
58-
rel: 'canonical',
59-
href: baseUrl + canonicalPath
60-
})
61-
}
62-
}
35+
addHreflangLinks.bind(this)(currentLocale, this.$i18n.locales, link)
36+
37+
addCanonicalLinks.bind(this)(currentLocale, link)
6338

6439
// og:locale meta
6540
const meta = []
66-
// og:locale - current
67-
if (currentLocaleData && currentLocaleData[LOCALE_ISO_KEY]) {
68-
meta.push({
69-
hid: 'og:locale',
70-
property: 'og:locale',
71-
// Replace dash with underscore as defined in spec: language_TERRITORY
72-
content: currentLocaleData[LOCALE_ISO_KEY].replace(/-/g, '_')
73-
})
74-
}
75-
// og:locale - alternate
76-
meta.push(
77-
...this.$i18n.locales
78-
.filter(l => l[LOCALE_ISO_KEY] && l[LOCALE_ISO_KEY] !== currentLocaleData[LOCALE_ISO_KEY])
79-
.map(locale => ({
80-
hid: 'og:locale:alternate-' + locale[LOCALE_ISO_KEY],
81-
property: 'og:locale:alternate',
82-
content: locale[LOCALE_ISO_KEY].replace(/-/g, '_')
83-
}))
84-
)
41+
addCurrentOgLocale.bind(this)(currentLocale, meta)
42+
addAlternateOgLocales.bind(this)(this.$i18n.locales, currentLocale, meta)
8543

8644
return {
8745
htmlAttrs,
8846
link,
8947
meta
9048
}
9149
}
50+
51+
function addHreflangLinks (currentLocale, locales, link) {
52+
if (strategy === STRATEGIES.NO_PREFIX) {
53+
return
54+
}
55+
56+
const localeMap = new Map()
57+
58+
for (const locale of locales) {
59+
const localeIso = isoFromLocale(locale)
60+
61+
if (!localeIso) {
62+
// eslint-disable-next-line no-console
63+
console.warn(`[${MODULE_NAME}] Locale ISO code is required to generate alternate link`)
64+
continue
65+
}
66+
67+
const [language, region] = localeIso.split('-')
68+
69+
if (language && region && (locale.isCatchallLocale || !localeMap.has(language))) {
70+
localeMap.set(language, locale)
71+
}
72+
73+
localeMap.set(localeIso, locale)
74+
}
75+
76+
for (const [iso, locale] of localeMap.entries()) {
77+
link.push({
78+
hid: `alternate-hreflang-${iso}`,
79+
rel: 'alternate',
80+
href: baseUrl + this.switchLocalePath(locale.code),
81+
hreflang: iso
82+
})
83+
}
84+
}
85+
86+
function addCanonicalLinks (currentLocale, link) {
87+
if (strategy !== STRATEGIES.PREFIX_AND_DEFAULT) {
88+
return
89+
}
90+
91+
const currentLocaleCode = codeFromLocale(currentLocale)
92+
93+
const canonicalPath = this.switchLocalePath(currentLocaleCode)
94+
95+
const canonicalPathIsDifferentFromCurrent = canonicalPath !== this.$route.path
96+
const shouldAddCanonical = canonicalPath && canonicalPathIsDifferentFromCurrent
97+
if (!shouldAddCanonical) {
98+
return
99+
}
100+
101+
link.push({
102+
hid: `canonical-lang-${currentLocaleCode}`,
103+
rel: 'canonical',
104+
href: baseUrl + canonicalPath
105+
})
106+
}
107+
108+
function addCurrentOgLocale (currentLocale, meta) {
109+
const hasCurrentLocaleAndIso = currentLocale && isoFromLocale(currentLocale)
110+
111+
if (!hasCurrentLocaleAndIso) {
112+
return
113+
}
114+
115+
meta.push({
116+
hid: 'og:locale',
117+
property: 'og:locale',
118+
// Replace dash with underscore as defined in spec: language_TERRITORY
119+
content: underscoreIsoFromLocale(currentLocale)
120+
})
121+
}
122+
123+
function addAlternateOgLocales (locales, currentLocale, meta) {
124+
const localesWithoutCurrent = l => isoFromLocale(l) && isoFromLocale(l) !== isoFromLocale(currentLocale)
125+
126+
const alternateLocales = locales
127+
.filter(localesWithoutCurrent)
128+
.map(locale => ({
129+
hid: `og:locale:alternate-${isoFromLocale(locale)}`,
130+
property: 'og:locale:alternate',
131+
content: underscoreIsoFromLocale(locale)
132+
}))
133+
134+
meta.push(...alternateLocales)
135+
}
136+
137+
function isoFromLocale (locale) {
138+
return locale[LOCALE_ISO_KEY]
139+
}
140+
141+
function underscoreIsoFromLocale (locale) {
142+
return isoFromLocale(locale).replace(/-/g, '_')
143+
}
144+
145+
function codeFromLocale (locale) {
146+
return locale[LOCALE_CODE_KEY]
147+
}

test/fixture/base.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ module.exports = {
3131
locales: [
3232
{
3333
code: 'en',
34-
iso: 'en-US',
34+
iso: 'en',
3535
name: 'English'
3636
},
3737
{

0 commit comments

Comments
 (0)