diff --git a/src/HelmetConstants.js b/src/HelmetConstants.js index 37818463..77f1a50a 100644 --- a/src/HelmetConstants.js +++ b/src/HelmetConstants.js @@ -25,6 +25,7 @@ export const TAG_PROPERTIES = { CHARSET: "charset", CSS_TEXT: "cssText", HREF: "href", + HREFLANG: "hrefLang", HTTPEQUIV: "http-equiv", INNER_HTML: "innerHTML", ITEM_PROP: "itemprop", diff --git a/src/HelmetUtils.js b/src/HelmetUtils.js index 370b766a..ddd6f8e7 100644 --- a/src/HelmetUtils.js +++ b/src/HelmetUtils.js @@ -112,43 +112,35 @@ const getTagsFromPropsList = (tagName, primaryAttributes, propsList) => { instanceTags .filter(tag => { - let primaryAttributeKey; + const primaryAttributeKeys = []; const keys = Object.keys(tag); - for (let i = 0; i < keys.length; i++) { - const attributeKey = keys[i]; - const lowerCaseAttributeKey = attributeKey.toLowerCase(); - - // Special rule with link tags, since rel and href are both primary tags, rel takes priority - if ( - primaryAttributes.indexOf(lowerCaseAttributeKey) !== - -1 && - !( - primaryAttributeKey === TAG_PROPERTIES.REL && - tag[primaryAttributeKey].toLowerCase() === - "canonical" - ) && - !( + for (let i = 0; i < primaryAttributes.length; i++) { + const primaryAttributeKeyCandidate = + primaryAttributes[i]; + + for (let j = 0; j < keys.length; j++) { + const attributeKey = keys[j]; + const lowerCaseAttributeKey = attributeKey.toLowerCase(); + const attributeMatch = + primaryAttributeKeyCandidate === attributeKey || + primaryAttributeKeyCandidate === + lowerCaseAttributeKey; + const relStylesheet = lowerCaseAttributeKey === TAG_PROPERTIES.REL && - tag[lowerCaseAttributeKey].toLowerCase() === - "stylesheet" - ) - ) { - primaryAttributeKey = lowerCaseAttributeKey; - } - // Special case for innerHTML which doesn't work lowercased - if ( - primaryAttributes.indexOf(attributeKey) !== -1 && - (attributeKey === TAG_PROPERTIES.INNER_HTML || - attributeKey === TAG_PROPERTIES.CSS_TEXT || - attributeKey === TAG_PROPERTIES.ITEM_PROP) - ) { - primaryAttributeKey = attributeKey; + tag[attributeKey] === "stylesheet"; + + if (attributeMatch && !relStylesheet) { + if (!tag[attributeKey]) return false; + primaryAttributeKeys.push( + primaryAttributeKeyCandidate + ); + } } } - if (!primaryAttributeKey || !tag[primaryAttributeKey]) { - return false; - } + const primaryAttributeKey = primaryAttributeKeys[0]; + + if (!primaryAttributeKey) return false; const value = tag[primaryAttributeKey].toLowerCase(); @@ -211,7 +203,7 @@ const reducePropsToState = propsList => ({ htmlAttributes: getAttributesFromPropsList(ATTRIBUTE_NAMES.HTML, propsList), linkTags: getTagsFromPropsList( TAG_NAMES.LINK, - [TAG_PROPERTIES.REL, TAG_PROPERTIES.HREF], + [TAG_PROPERTIES.HREFLANG, TAG_PROPERTIES.REL, TAG_PROPERTIES.HREF], propsList ), metaTags: getTagsFromPropsList( diff --git a/test/HelmetDeclarativeTest.js b/test/HelmetDeclarativeTest.js index 145da4ea..c7a78032 100644 --- a/test/HelmetDeclarativeTest.js +++ b/test/HelmetDeclarativeTest.js @@ -1768,6 +1768,62 @@ describe("Helmet - Declarative API", () => { }); }); + it("tags with rel='alternate' uses the hrefLang as the primary identification of the tag, regardless of ordering", done => { + ReactDOM.render( +
+ + + + + + + + + +
, + container + ); + + requestAnimationFrame(() => { + const tagNodes = headElement.querySelectorAll( + `link[${HELMET_ATTRIBUTE}]` + ); + const existingTags = Array.prototype.slice.call(tagNodes); + const firstTag = existingTags[0]; + + expect(existingTags).to.not.equal(undefined); + + expect(existingTags.length).to.equal(1); + + expect(existingTags).to.have.deep + .property("[0]") + .that.is.an.instanceof(Element); + expect(firstTag).to.have.property("getAttribute"); + expect(firstTag.getAttribute("href")).to.equal( + "http://localhost/en/innermost" + ); + expect(firstTag.getAttribute("rel")).to.equal("alternate"); + expect(firstTag.getAttribute("hrefLang")).to.equal("en"); + expect(firstTag.outerHTML).to.equal( + `` + ); + + done(); + }); + }); + it("sets link tags based on deepest nested component", done => { ReactDOM.render(
diff --git a/test/HelmetTest.js b/test/HelmetTest.js index c472e2c8..d4ab71dd 100644 --- a/test/HelmetTest.js +++ b/test/HelmetTest.js @@ -1498,6 +1498,70 @@ describe("Helmet", () => { }); }); + it("tags with rel='alternate' uses the hrefLang as the primary identification of the tag, regardless of ordering", done => { + ReactDOM.render( +
+ + + + + +
, + container + ); + + requestAnimationFrame(() => { + const tagNodes = headElement.querySelectorAll( + `link[${HELMET_ATTRIBUTE}]` + ); + const existingTags = Array.prototype.slice.call(tagNodes); + const firstTag = existingTags[0]; + + expect(existingTags).to.not.equal(undefined); + + expect(existingTags.length).to.equal(1); + + expect(existingTags).to.have.deep + .property("[0]") + .that.is.an.instanceof(Element); + expect(firstTag).to.have.property("getAttribute"); + expect(firstTag.getAttribute("href")).to.equal( + "http://localhost/en/innermost" + ); + expect(firstTag.getAttribute("rel")).to.equal("alternate"); + expect(firstTag.getAttribute("hrefLang")).to.equal("en"); + expect(firstTag.outerHTML).to.equal( + `` + ); + + done(); + }); + }); + it("sets link tags based on deepest nested component", done => { ReactDOM.render(