-
Notifications
You must be signed in to change notification settings - Fork 9.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
new_audit(hreflang): document has a valid hreflang code #3815
Changes from all commits
8b5f8d1
fdba9e4
c9f1afe
6860d84
6c3ff27
95699f5
a55fccd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
/** | ||
* @license Copyright 2017 Google Inc. All Rights Reserved. | ||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. | ||
*/ | ||
'use strict'; | ||
|
||
const Audit = require('../audit'); | ||
const LinkHeader = require('http-link-header'); | ||
const VALID_LANGS = importValidLangs(); | ||
const LINK_HEADER = 'link'; | ||
const NO_LANGUAGE = 'x-default'; | ||
|
||
/** | ||
* Import list of valid languages from axe core without including whole axe-core package | ||
* This is a huge array of language codes that can be stored more efficiently if we will need to | ||
* shrink the bundle size. | ||
*/ | ||
function importValidLangs() { | ||
const axeCache = global.axe; | ||
global.axe = {utils: {}}; | ||
require('axe-core/lib/commons/utils/valid-langs.js'); | ||
const validLangs = global.axe.utils.validLangs(); | ||
global.axe = axeCache; | ||
|
||
return validLangs; | ||
} | ||
|
||
/** | ||
* @param {string} hreflang | ||
* @returns {boolean} | ||
*/ | ||
function isValidHreflang(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 VALID_LANGS.includes(lang.toLowerCase()); | ||
} | ||
|
||
/** | ||
* @param {string} headerValue | ||
* @returns {boolean} | ||
*/ | ||
function headerHasValidHreflangs(headerValue) { | ||
const linkHeader = LinkHeader.parse(headerValue); | ||
|
||
return linkHeader.get('rel', 'alternate') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. always returns an array? strange api 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Single |
||
.every(link => link.hreflang && isValidHreflang(link.hreflang)); | ||
} | ||
|
||
class Hreflang extends Audit { | ||
/** | ||
* @return {!AuditMeta} | ||
*/ | ||
static get meta() { | ||
return { | ||
name: 'hreflang', | ||
description: 'Document has a valid `hreflang`', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. seems to be a property of an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point! @rviscomi WDYT? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's been a while since I wrote the text, but I think I was trying to strike a balance between the hreflang being used as both an HTTP header value and HTML attribute value. Other audits also use "document" as the owner of the thing being audited, for example:
So I'm ok with the text as it is currently written but open to suggestions. |
||
failureDescription: 'Document doesn\'t have a valid `hreflang`', | ||
helpText: 'hreflang allows crawlers to discover alternate translations of the ' + | ||
'page content. [Learn more]' + | ||
'(https://support.google.com/webmasters/answer/189077).', | ||
requiredArtifacts: ['Hreflang'], | ||
}; | ||
} | ||
|
||
/** | ||
* @param {!Artifacts} artifacts | ||
* @return {!AuditResult} | ||
*/ | ||
static audit(artifacts) { | ||
const devtoolsLogs = artifacts.devtoolsLogs[Audit.DEFAULT_PASS]; | ||
|
||
return artifacts.requestMainResource(devtoolsLogs) | ||
.then(mainResource => { | ||
/** @type {Array<{source: string|{type: string, snippet: string}}>} */ | ||
const invalidHreflangs = []; | ||
|
||
if (artifacts.Hreflang) { | ||
artifacts.Hreflang.forEach(({href, hreflang}) => { | ||
if (!isValidHreflang(hreflang)) { | ||
invalidHreflangs.push({ | ||
source: { | ||
type: 'node', | ||
snippet: `<link name="alternate" hreflang="${hreflang}" href="${href}" />`, | ||
}, | ||
}); | ||
} | ||
}); | ||
} | ||
|
||
mainResource.responseHeaders | ||
.filter(h => h.name.toLowerCase() === LINK_HEADER && !headerHasValidHreflangs(h.value)) | ||
.forEach(h => invalidHreflangs.push({source: `${h.name}: ${h.value}`})); | ||
|
||
const headings = [ | ||
{key: 'source', itemType: 'code', text: 'Source'}, | ||
]; | ||
const details = Audit.makeTableDetails(headings, invalidHreflangs); | ||
|
||
return { | ||
rawValue: invalidHreflangs.length === 0, | ||
details, | ||
}; | ||
}); | ||
} | ||
} | ||
|
||
module.exports = Hreflang; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/** | ||
* @license Copyright 2017 Google Inc. All Rights Reserved. | ||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. | ||
*/ | ||
'use strict'; | ||
|
||
const Gatherer = require('../gatherer'); | ||
|
||
class Hreflang extends Gatherer { | ||
/** | ||
* @param {{driver: !Object}} options Run options | ||
* @return {!Promise<!Array<{href: string, hreflang: string}>>} Array with hreflang and href values of all link[rel=alternate] nodes found in HEAD | ||
*/ | ||
afterPass(options) { | ||
const driver = options.driver; | ||
|
||
return driver.querySelectorAll('head link[rel="alternate" i][hreflang]') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My bad, it always returns an array. I've updated the code (and jsdoc) accordingly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh ok, no worries! |
||
.then(nodes => Promise.all(nodes.map(node => | ||
Promise.all([node.getAttribute('href'), node.getAttribute('hreflang')])) | ||
) | ||
).then(attributeValues => attributeValues && | ||
attributeValues.map(values => { | ||
const [href, hreflang] = values; | ||
return {href, hreflang}; | ||
}) | ||
); | ||
} | ||
} | ||
|
||
module.exports = Hreflang; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
new dependency - https://github.com/jhermsmeier/node-http-link-header. It's a simple (~300LOC), well tested, link header value parser with no dependencies.