-
Notifications
You must be signed in to change notification settings - Fork 9.5k
/
Copy pathhreflang.js
148 lines (126 loc) · 5.14 KB
/
hreflang.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/** @typedef {string|LH.Audit.Details.NodeValue|undefined} Source */
/** @typedef {{source: Source, subItems: {type: 'subitems', items: SubItem[]}}} InvalidHreflang */
/** @typedef {{reason: LH.IcuMessage}} SubItem */
import {Audit} from '../audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
import {isValidLang} from '../../../third-party/axe/valid-langs.js';
const NO_LANGUAGE = 'x-default';
const UIStrings = {
/** Title of a Lighthouse audit that provides detail on the `hreflang` attribute on a page. This descriptive title is shown when the page's `hreflang` attribute is configured correctly. "hreflang" is an HTML attribute and should not be translated. */
title: 'Document has a valid `hreflang`',
/** Title of a Lighthouse audit that provides detail on the `hreflang` attribute on a page. This descriptive title is shown when the page's `hreflang` attribute is not valid and needs to be fixed. "hreflang" is an HTML attribute and should not be translated. */
failureTitle: 'Document doesn\'t have a valid `hreflang`',
/** Description of a Lighthouse audit that tells the user *why* they need to have an hreflang link on their page. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. "hreflang" is an HTML attribute and should not be translated. */
description: 'hreflang links tell search engines what version of a page they should ' +
'list in search results for a given language or region. [Learn more about `hreflang`]' +
'(https://developer.chrome.com/docs/lighthouse/seo/hreflang/).',
/** A failure reason for a Lighthouse audit that flags incorrect use of the `hreflang` attribute on `link` elements. This failure reason is shown when the hreflang language code is unexpected. */
unexpectedLanguage: 'Unexpected language code',
/** A failure reason for a Lighthouse audit that flags incorrect use of the `hreflang` attribute on `link` elements. This failure reason is shown when the `href` attribute value is not fully-qualified. */
notFullyQualified: 'Relative href value',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
/**
* @param {string} href
* @return {boolean}
*/
function isFullyQualified(href) {
return href.startsWith('http:') || href.startsWith('https:');
}
/**
* @param {string} hreflang
* @return {boolean}
*/
function isExpectedLanguageCode(hreflang) {
if (hreflang.toLowerCase() === NO_LANGUAGE) {
return true;
}
// hreflang can consist of language-script-region, we are validating only language
const [lang] = hreflang.split('-');
return isValidLang(lang.toLowerCase());
}
class Hreflang extends Audit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'hreflang',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
supportedModes: ['navigation'],
requiredArtifacts: ['LinkElements', 'URL'],
};
}
/**
* @param {LH.Artifacts} artifacts
* @return {LH.Audit.Product}
*/
static audit({LinkElements}) {
/** @type {InvalidHreflang[]} */
const invalidHreflangs = [];
const auditableLinkElements = LinkElements.filter(linkElement => {
const isAlternate = linkElement.rel === 'alternate';
const hasHreflang = linkElement.hreflang;
const isInBody = linkElement.source === 'body';
return isAlternate && hasHreflang && !isInBody;
});
for (const link of auditableLinkElements) {
const reasons = [];
/** @type {Source} */
let source;
if (!isExpectedLanguageCode(link.hreflang)) {
reasons.push(str_(UIStrings.unexpectedLanguage));
}
if (!isFullyQualified(link.hrefRaw.toLowerCase())) {
reasons.push(str_(UIStrings.notFullyQualified));
}
if (link.source === 'head') {
if (link.node) {
source = {
...Audit.makeNodeItem(link.node),
snippet: `<link rel="alternate" hreflang="${link.hreflang}" href="${link.hrefRaw}" />`,
};
} else {
source = {
type: 'node',
snippet: `<link rel="alternate" hreflang="${link.hreflang}" href="${link.hrefRaw}" />`,
};
}
} else if (link.source === 'headers') {
source = `Link: <${link.hrefRaw}>; rel="alternate"; hreflang="${link.hreflang}"`;
}
if (!source || !reasons.length) continue;
invalidHreflangs.push({
source,
subItems: {
type: 'subitems',
items: reasons.map(reason => ({reason})),
},
});
}
/** @type {LH.Audit.Details.Table['headings']} */
const headings = [{
key: 'source',
valueType: 'code',
subItemsHeading: {
key: 'reason',
valueType: 'text',
},
label: '',
}];
const details = Audit.makeTableDetails(headings, invalidHreflangs);
return {
score: Number(invalidHreflangs.length === 0),
details,
};
}
}
export default Hreflang;
export {UIStrings};