Skip to content

Commit

Permalink
Add itemProp attributes for meta tag, titleAttributes for title (nfl#169
Browse files Browse the repository at this point in the history
)

* Add titleProps to support add more prop to title tag, Add itemProp attributes for meta to support SEO

* Fix itemprop lowercase when generating meta string, add test case for itemprop meta

* Update test case

* Change PropTypes of titleProps

* Rename property for title attributes

* Using itemprop instead of itemProp
  • Loading branch information
dattran92 authored and cwelch5 committed Dec 19, 2016
1 parent 9a3fbdc commit 6312e09
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 25 deletions.
63 changes: 48 additions & 15 deletions src/Helmet.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ const getTitleFromPropsList = (propsList) => {
return innermostTitle || innermostDefaultTitle || "";
};

const getTitleAttributesFromPropsList = (propsList) => {
const innermostTitleAttributes = getInnermostProperty(propsList, "titleAttributes");
return innermostTitleAttributes || [];
};

const getOnChangeClientState = (propsList) => {
return getInnermostProperty(propsList, "onChangeClientState") ||(() => {});
};
Expand Down Expand Up @@ -107,7 +112,7 @@ const getTagsFromPropsList = (tagName, primaryAttributes, propsList) => {
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)) {
if (primaryAttributes.indexOf(attributeKey) !== -1 && (attributeKey === TAG_PROPERTIES.INNER_HTML || attributeKey === TAG_PROPERTIES.CSS_TEXT || attributeKey === TAG_PROPERTIES.ITEM_PROP)) {
primaryAttributeKey = attributeKey;
}
}
Expand Down Expand Up @@ -156,8 +161,15 @@ const getTagsFromPropsList = (tagName, primaryAttributes, propsList) => {
return tagList;
};

const updateTitle = title => {
const updateTitle = (title, attributes) => {
document.title = title || document.title;
const htmlTag = document.getElementsByTagName("title")[0];
const attributeKeys = Object.keys(attributes);
for (let i = 0; i < attributeKeys.length; i++) {
const attribute = attributeKeys[i];
const value = attributes[attribute] || "";
htmlTag.setAttribute(attribute, value);
}
};

const updateHtmlAttributes = (attributes) => {
Expand Down Expand Up @@ -258,8 +270,18 @@ const generateHtmlAttributesAsString = (attributes) => {
return attributeString.trim();
};

const generateTitleAsString = (type, title) => {
const stringifiedMarkup = `<${type} ${HELMET_ATTRIBUTE}="true">${encodeSpecialCharacters(title)}</${type}>`;
const generateTitleAsString = (type, title, attributes) => {
let attributeString = "";
const attributeKeys = Object.keys(attributes);
for (let i = 0; i < attributeKeys.length; i++) {
const attribute = attributeKeys[i];
const attr = typeof attributes[attribute] !== "undefined" ? `${attribute.toLowerCase()}="${attributes[attribute]}"` : `${attribute.toLowerCase()}`;
attributeString += `${attr} `;
}

const stringifiedMarkup = attributeString
? `<${type} ${HELMET_ATTRIBUTE}="true" ${attributeString.trim()}>${encodeSpecialCharacters(title)}</${type}>`
: `<${type} ${HELMET_ATTRIBUTE}="true">${encodeSpecialCharacters(title)}</${type}>`;

return stringifiedMarkup;
};
Expand Down Expand Up @@ -288,15 +310,21 @@ const generateTagsAsString = (type, tags) => {
return stringifiedMarkup;
};

const generateTitleAsReactComponent = (type, title) => {
const generateTitleAsReactComponent = (type, title, attributes) => {
// assigning into an array to define toString function on it
const props = {
key: title,
[HELMET_ATTRIBUTE]: true
};
Object.keys(attributes).forEach((attribute) => {
const mappedAttribute = REACT_TAG_MAP[attribute] || attribute;
props[mappedAttribute] = attributes[attribute];
});

const component = [
React.createElement(
TAG_NAMES.TITLE,
{
key: title,
[HELMET_ATTRIBUTE]: true
},
props,
title
)
];
Expand Down Expand Up @@ -334,8 +362,8 @@ const getMethodsForTag = (type, tags) => {
switch (type) {
case TAG_NAMES.TITLE:
return {
toComponent: () => generateTitleAsReactComponent(type, tags),
toString: () => generateTitleAsString(type, tags)
toComponent: () => generateTitleAsReactComponent(type, tags.title, tags.titleAttributes),
toString: () => generateTitleAsString(type, tags.title, tags.titleAttributes)
};
case TAG_NAMES.HTML:
return {
Expand All @@ -350,9 +378,9 @@ const getMethodsForTag = (type, tags) => {
}
};

const mapStateOnServer = ({htmlAttributes, title, baseTag, metaTags, linkTags, scriptTags, noscriptTags, styleTags}) => ({
const mapStateOnServer = ({htmlAttributes, title, titleAttributes, baseTag, metaTags, linkTags, scriptTags, noscriptTags, styleTags}) => ({
htmlAttributes: getMethodsForTag(TAG_NAMES.HTML, htmlAttributes),
title: getMethodsForTag(TAG_NAMES.TITLE, title),
title: getMethodsForTag(TAG_NAMES.TITLE, {title, titleAttributes}),
base: getMethodsForTag(TAG_NAMES.BASE, baseTag),
meta: getMethodsForTag(TAG_NAMES.META, metaTags),
link: getMethodsForTag(TAG_NAMES.LINK, linkTags),
Expand All @@ -369,6 +397,7 @@ const Helmet = (Component) => {
* @param {String} title: "Title"
* @param {String} defaultTitle: "Default Title"
* @param {String} titleTemplate: "MySite.com - %s"
* @param {Object} titleAttributes: {"itemprop": "name"}
* @param {Object} base: {"target": "_blank", "href": "http://mysite.com/"}
* @param {Array} meta: [{"name": "description", "content": "Test description"}]
* @param {Array} link: [{"rel": "canonical", "href": "http://mysite.com/example"}]
Expand All @@ -382,6 +411,7 @@ const Helmet = (Component) => {
title: React.PropTypes.string,
defaultTitle: React.PropTypes.string,
titleTemplate: React.PropTypes.string,
titleAttributes: React.PropTypes.object,
base: React.PropTypes.object,
meta: React.PropTypes.arrayOf(React.PropTypes.object),
link: React.PropTypes.arrayOf(React.PropTypes.object),
Expand All @@ -404,6 +434,7 @@ const Helmet = (Component) => {
mappedState = mapStateOnServer({
htmlAttributes: {},
title: "",
titleAttributes: {},
baseTag: [],
metaTags: [],
linkTags: [],
Expand Down Expand Up @@ -436,8 +467,9 @@ const Helmet = (Component) => {
const reducePropsToState = (propsList) => ({
htmlAttributes: getHtmlAttributesFromPropsList(propsList),
title: getTitleFromPropsList(propsList),
titleAttributes: getTitleAttributesFromPropsList(propsList),
baseTag: getBaseTagFromPropsList([TAG_PROPERTIES.HREF], propsList),
metaTags: getTagsFromPropsList(TAG_NAMES.META, [TAG_PROPERTIES.NAME, TAG_PROPERTIES.CHARSET, TAG_PROPERTIES.HTTPEQUIV, TAG_PROPERTIES.PROPERTY], propsList),
metaTags: getTagsFromPropsList(TAG_NAMES.META, [TAG_PROPERTIES.NAME, TAG_PROPERTIES.CHARSET, TAG_PROPERTIES.HTTPEQUIV, TAG_PROPERTIES.PROPERTY, TAG_PROPERTIES.ITEM_PROP], propsList),
linkTags: getTagsFromPropsList(TAG_NAMES.LINK, [TAG_PROPERTIES.REL, TAG_PROPERTIES.HREF], propsList),
scriptTags: getTagsFromPropsList(TAG_NAMES.SCRIPT, [TAG_PROPERTIES.SRC, TAG_PROPERTIES.INNER_HTML], propsList),
noscriptTags: getTagsFromPropsList(TAG_NAMES.NOSCRIPT, [TAG_PROPERTIES.INNER_HTML], propsList),
Expand All @@ -449,6 +481,7 @@ const handleClientStateChange = (newState) => {
const {
htmlAttributes,
title,
titleAttributes,
baseTag,
metaTags,
linkTags,
Expand All @@ -460,7 +493,7 @@ const handleClientStateChange = (newState) => {

updateHtmlAttributes(htmlAttributes);

updateTitle(title);
updateTitle(title, titleAttributes);

const tagUpdates = {
baseTag: updateTags(TAG_NAMES.BASE, baseTag),
Expand Down
6 changes: 4 additions & 2 deletions src/HelmetConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ export const TAG_PROPERTIES = {
PROPERTY: "property",
SRC: "src",
INNER_HTML: "innerHTML",
CSS_TEXT: "cssText"
CSS_TEXT: "cssText",
ITEM_PROP: "itemprop"
};

export const REACT_TAG_MAP = {
"charset": "charSet",
"http-equiv": "httpEquiv"
"http-equiv": "httpEquiv",
"itemprop": "itemProp"
};
80 changes: 72 additions & 8 deletions src/test/HelmetTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,21 @@ describe("Helmet", () => {

expect(document.title).to.equal(chineseTitle);
});

it("page tite with prop itemprop", () => {
ReactDOM.render(
<Helmet
defaultTitle={"Fallback"}
title={"Test Title with itemProp"}
titleAttributes={{itemprop: "name"}}
/>,
container
);

const titleTag = document.getElementsByTagName("title")[0];
expect(document.title).to.equal("Test Title with itemProp");
expect(titleTag.getAttribute("itemprop")).to.equal("name");
});
});

describe("html attributes", () => {
Expand Down Expand Up @@ -471,7 +486,8 @@ describe("Helmet", () => {
{"charset": "utf-8"},
{"name": "description", "content": "Test description"},
{"http-equiv": "content-type", "content": "text/html"},
{"property": "og:type", "content": "article"}
{"property": "og:type", "content": "article"},
{"itemprop": "name", "content": "Test name itemprop"}
]}
/>,
container
Expand All @@ -485,10 +501,11 @@ describe("Helmet", () => {
const filteredTags = [].slice.call(existingTags).filter((tag) => {
return tag.getAttribute("charset") === "utf-8" ||
(tag.getAttribute("name") === "description" && tag.getAttribute("content") === "Test description") ||
(tag.getAttribute("http-equiv") === "content-type" && tag.getAttribute("content") === "text/html");
(tag.getAttribute("http-equiv") === "content-type" && tag.getAttribute("content") === "text/html") ||
(tag.getAttribute("itemprop") === "name" && tag.getAttribute("content") === "Test name itemprop");
});

expect(filteredTags.length).to.be.at.least(3);
expect(filteredTags.length).to.be.at.least(4);
});

it("will clear all meta tags if none are specified", () => {
Expand All @@ -510,7 +527,7 @@ describe("Helmet", () => {
expect(existingTags.length).to.equal(0);
});

it("tags without 'name', 'http-equiv', 'property', or 'charset' will not be accepted", () => {
it("tags without 'name', 'http-equiv', 'property', 'charset', or 'itemprop' will not be accepted", () => {
ReactDOM.render(
<Helmet
meta={[{"href": "won't work"}]}
Expand Down Expand Up @@ -1399,13 +1416,15 @@ describe("Helmet", () => {
describe("server", () => {
const stringifiedHtmlAttribute = `lang="ga"`;
const stringifiedTitle = `<title ${HELMET_ATTRIBUTE}="true">Dangerous &lt;script&gt; include</title>`;
const stringifiedTitleWithItemprop = `<title ${HELMET_ATTRIBUTE}="true" itemprop="name">Title with Itemprop</title>`;
const stringifiedBaseTag = `<base ${HELMET_ATTRIBUTE}="true" target="_blank" href="http://localhost/"/>`;

const stringifiedMetaTags = [
`<meta ${HELMET_ATTRIBUTE}="true" charset="utf-8"/>`,
`<meta ${HELMET_ATTRIBUTE}="true" name="description" content="Test description &amp; encoding of special characters like &#x27; &quot; &gt; &lt; \`"/>`,
`<meta ${HELMET_ATTRIBUTE}="true" http-equiv="content-type" content="text/html"/>`,
`<meta ${HELMET_ATTRIBUTE}="true" property="og:type" content="article"/>`
`<meta ${HELMET_ATTRIBUTE}="true" property="og:type" content="article"/>`,
`<meta ${HELMET_ATTRIBUTE}="true" itemprop="name" content="Test name itemprop"/>`
].join("");

const stringifiedLinkTags = [
Expand Down Expand Up @@ -1486,6 +1505,49 @@ describe("Helmet", () => {
}</div>`);
});

it("will render title with itemprop name", () => {
ReactDOM.render(
<Helmet
title={"Title with Itemprop"}
titleAttributes={{itemprop: "name"}}
/>,
container
);

const head = Helmet.rewind();

expect(head.title).to.exist;
expect(head.title).to.respondTo("toComponent");

const titleComponent = head.title.toComponent();
const titleString = head.title.toString();
expect(titleString)
.to.be.a("string")
.that.equals(stringifiedTitleWithItemprop);

expect(titleComponent)
.to.be.an("array")
.that.has.length.of(1);

titleComponent.forEach(title => {
expect(title)
.to.be.an("object")
.that.contains.property("type", "title");
});

const markup = ReactServer.renderToStaticMarkup(
<div>
{titleComponent}
</div>
);

expect(markup)
.to.be.a("string")
.that.equals(`<div>${
stringifiedTitleWithItemprop
}</div>`);
});

it("will render base tag as React component", () => {
ReactDOM.render(
<Helmet
Expand Down Expand Up @@ -1531,7 +1593,8 @@ describe("Helmet", () => {
{"charset": "utf-8"},
{"name": "description", "content": "Test description & encoding of special characters like ' \" > < `"},
{"http-equiv": "content-type", "content": "text/html"},
{"property": "og:type", "content": "article"}
{"property": "og:type", "content": "article"},
{"itemprop": "name", "content": "Test name itemprop"}
]}
/>,
container
Expand All @@ -1546,7 +1609,7 @@ describe("Helmet", () => {

expect(metaComponent)
.to.be.an("array")
.that.has.length.of(4);
.that.has.length.of(5);

metaComponent.forEach(meta => {
expect(meta)
Expand Down Expand Up @@ -1774,7 +1837,8 @@ describe("Helmet", () => {
{"charset": "utf-8"},
{"name": "description", "content": "Test description & encoding of special characters like ' \" > < `"},
{"http-equiv": "content-type", "content": "text/html"},
{"property": "og:type", "content": "article"}
{"property": "og:type", "content": "article"},
{"itemprop": "name", "content": "Test name itemprop"}
]}
/>,
container
Expand Down

0 comments on commit 6312e09

Please sign in to comment.