Skip to content

Commit 2552d14

Browse files
justinphstaylor
authored andcommitted
feat: prioritize tags for seo on server
1 parent 3875978 commit 2552d14

File tree

4 files changed

+162
-45
lines changed

4 files changed

+162
-45
lines changed

src/constants.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,30 @@ export const TAG_NAMES = {
3131
FRAGMENT: 'Symbol(react.fragment)',
3232
};
3333

34+
export const SEO_PRIORITY_TAGS = {
35+
link: { rel: ['amphtml', 'canonical', 'alternate'] },
36+
script: { type: ['application/ld+json'] },
37+
meta: {
38+
charset: '',
39+
name: ['robots', 'description'],
40+
property: [
41+
'og:type',
42+
'og:title',
43+
'og:url',
44+
'og:image',
45+
'og:image:alt',
46+
'og:description',
47+
'twitter:url',
48+
'twitter:title',
49+
'twitter:description',
50+
'twitter:image',
51+
'twitter:image:alt',
52+
'twitter:card',
53+
'twitter:site',
54+
],
55+
},
56+
};
57+
3458
export const VALID_TAG_NAMES = Object.keys(TAG_NAMES).map(name => TAG_NAMES[name]);
3559

3660
export const REACT_TAG_MAP = {

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export class Helmet extends Component {
2727
* @param {String} title: "Title"
2828
* @param {Object} titleAttributes: {"itemprop": "name"}
2929
* @param {String} titleTemplate: "MySite.com - %s"
30+
* @param {Boolean} prioritizeSeoTags: false
3031
*/
3132
/* eslint-disable react/forbid-prop-types, react/require-default-props */
3233
static propTypes = {
@@ -46,12 +47,14 @@ export class Helmet extends Component {
4647
title: PropTypes.string,
4748
titleAttributes: PropTypes.object,
4849
titleTemplate: PropTypes.string,
50+
prioritizeSeoTags: PropTypes.bool,
4951
};
5052
/* eslint-enable react/prop-types, react/forbid-prop-types, react/require-default-props */
5153

5254
static defaultProps = {
5355
defer: true,
5456
encodeSpecialCharacters: true,
57+
prioritizeSeoTags: false,
5558
};
5659

5760
static displayName = 'Helmet';

src/server.js

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import {
55
REACT_TAG_MAP,
66
TAG_PROPERTIES,
77
ATTRIBUTE_NAMES,
8+
SEO_PRIORITY_TAGS,
89
} from './constants';
9-
import { flattenArray } from './utils';
10+
import { flattenArray, prioritizer } from './utils';
1011

1112
const SELF_CLOSING_TAGS = [TAG_NAMES.NOSCRIPT, TAG_NAMES.SCRIPT, TAG_NAMES.STYLE];
1213

@@ -142,16 +143,62 @@ const mapStateOnServer = ({
142143
styleTags,
143144
title = '',
144145
titleAttributes,
145-
}) => ({
146-
base: getMethodsForTag(TAG_NAMES.BASE, baseTag, encode),
147-
bodyAttributes: getMethodsForTag(ATTRIBUTE_NAMES.BODY, bodyAttributes, encode),
148-
htmlAttributes: getMethodsForTag(ATTRIBUTE_NAMES.HTML, htmlAttributes, encode),
149-
link: getMethodsForTag(TAG_NAMES.LINK, linkTags, encode),
150-
meta: getMethodsForTag(TAG_NAMES.META, metaTags, encode),
151-
noscript: getMethodsForTag(TAG_NAMES.NOSCRIPT, noscriptTags, encode),
152-
script: getMethodsForTag(TAG_NAMES.SCRIPT, scriptTags, encode),
153-
style: getMethodsForTag(TAG_NAMES.STYLE, styleTags, encode),
154-
title: getMethodsForTag(TAG_NAMES.TITLE, { title, titleAttributes }, encode),
155-
});
146+
prioritizeSeoTags,
147+
}) => {
148+
// these methods will be noops if prioritizeSeoTags is not true
149+
let priorityMethods = {
150+
toComponent: () => {},
151+
toString: () => {},
152+
};
153+
if (prioritizeSeoTags) {
154+
const meta = prioritizer(metaTags, SEO_PRIORITY_TAGS.meta);
155+
const link = prioritizer(linkTags, SEO_PRIORITY_TAGS.link);
156+
const script = prioritizer(scriptTags, SEO_PRIORITY_TAGS.script);
157+
158+
// need to have toComponent() and toString()
159+
priorityMethods = {
160+
toComponent: () => {
161+
const components = [];
162+
Array.prototype.push.apply(
163+
components,
164+
generateTagsAsReactComponent(TAG_NAMES.META, meta.priority)
165+
);
166+
Array.prototype.push.apply(
167+
components,
168+
generateTagsAsReactComponent(TAG_NAMES.LINK, link.priority)
169+
);
170+
Array.prototype.push.apply(
171+
components,
172+
generateTagsAsReactComponent(TAG_NAMES.SCRIPT, script.priority)
173+
);
174+
return components;
175+
},
176+
toString: () =>
177+
// generate all the tags as strings and concatenate them
178+
`${getMethodsForTag(TAG_NAMES.META, meta.priority, encode)} ${getMethodsForTag(
179+
TAG_NAMES.LINK,
180+
link.priority,
181+
encode
182+
)} ${getMethodsForTag(TAG_NAMES.SCRIPT, script.priority, encode)}`,
183+
};
184+
185+
metaTags = meta.default;
186+
linkTags = link.default;
187+
scriptTags = script.default;
188+
}
189+
190+
return {
191+
priority: priorityMethods,
192+
base: getMethodsForTag(TAG_NAMES.BASE, baseTag, encode),
193+
bodyAttributes: getMethodsForTag(ATTRIBUTE_NAMES.BODY, bodyAttributes, encode),
194+
htmlAttributes: getMethodsForTag(ATTRIBUTE_NAMES.HTML, htmlAttributes, encode),
195+
link: getMethodsForTag(TAG_NAMES.LINK, linkTags, encode),
196+
meta: getMethodsForTag(TAG_NAMES.META, metaTags, encode),
197+
noscript: getMethodsForTag(TAG_NAMES.NOSCRIPT, noscriptTags, encode),
198+
script: getMethodsForTag(TAG_NAMES.SCRIPT, scriptTags, encode),
199+
style: getMethodsForTag(TAG_NAMES.STYLE, styleTags, encode),
200+
title: getMethodsForTag(TAG_NAMES.TITLE, { title, titleAttributes }, encode),
201+
};
202+
};
156203

157204
export default mapStateOnServer;

src/utils.js

Lines changed: 76 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const HELMET_PROPS = {
66
ENCODE_SPECIAL_CHARACTERS: 'encodeSpecialCharacters',
77
ON_CHANGE_CLIENT_STATE: 'onChangeClientState',
88
TITLE_TEMPLATE: 'titleTemplate',
9+
PRIORITIZE_SEO_TAGS: 'prioritizeSeoTags',
910
};
1011

1112
const getInnermostProperty = (propsList, property) => {
@@ -20,6 +21,11 @@ const getInnermostProperty = (propsList, property) => {
2021
return null;
2122
};
2223

24+
const getAnyTrueFromPropsList = (propsList, checkedTag) =>
25+
propsList.reduce((accumulator, currentPropList) =>
26+
currentPropList[checkedTag] ? true : accumulator
27+
);
28+
2329
const getTitleFromPropsList = propsList => {
2430
let innermostTitle = getInnermostProperty(propsList, TAG_NAMES.TITLE);
2531
const innermostTemplate = getInnermostProperty(propsList, HELMET_PROPS.TITLE_TEMPLATE);
@@ -170,39 +176,76 @@ const getTagsFromPropsList = (tagName, primaryAttributes, propsList) => {
170176
.reverse();
171177
};
172178

173-
const reducePropsToState = propsList => ({
174-
baseTag: getBaseTagFromPropsList([TAG_PROPERTIES.HREF], propsList),
175-
bodyAttributes: getAttributesFromPropsList(ATTRIBUTE_NAMES.BODY, propsList),
176-
defer: getInnermostProperty(propsList, HELMET_PROPS.DEFER),
177-
encode: getInnermostProperty(propsList, HELMET_PROPS.ENCODE_SPECIAL_CHARACTERS),
178-
htmlAttributes: getAttributesFromPropsList(ATTRIBUTE_NAMES.HTML, propsList),
179-
linkTags: getTagsFromPropsList(
180-
TAG_NAMES.LINK,
181-
[TAG_PROPERTIES.REL, TAG_PROPERTIES.HREF],
182-
propsList
183-
),
184-
metaTags: getTagsFromPropsList(
185-
TAG_NAMES.META,
186-
[
187-
TAG_PROPERTIES.NAME,
188-
TAG_PROPERTIES.CHARSET,
189-
TAG_PROPERTIES.HTTPEQUIV,
190-
TAG_PROPERTIES.PROPERTY,
191-
TAG_PROPERTIES.ITEM_PROP,
192-
],
193-
propsList
194-
),
195-
noscriptTags: getTagsFromPropsList(TAG_NAMES.NOSCRIPT, [TAG_PROPERTIES.INNER_HTML], propsList),
196-
onChangeClientState: getOnChangeClientState(propsList),
197-
scriptTags: getTagsFromPropsList(
198-
TAG_NAMES.SCRIPT,
199-
[TAG_PROPERTIES.SRC, TAG_PROPERTIES.INNER_HTML],
200-
propsList
201-
),
202-
styleTags: getTagsFromPropsList(TAG_NAMES.STYLE, [TAG_PROPERTIES.CSS_TEXT], propsList),
203-
title: getTitleFromPropsList(propsList),
204-
titleAttributes: getAttributesFromPropsList(ATTRIBUTE_NAMES.TITLE, propsList),
205-
});
179+
// helper to inspect for matching props on components
180+
export const checkIfPropsMatch = (props, toMatch) => {
181+
const pairs = Object.entries(props);
182+
for (let i = 0; i < pairs.length; i += 1) {
183+
const [propName, propVal] = pairs[i];
184+
if (toMatch[propName]) {
185+
// e.g. if rel exists in the list of allowed props
186+
const propWeAreCheckingOut = toMatch[propName]; // e.g. [amphtml, alternate, etc]
187+
if (propWeAreCheckingOut.includes(propVal)) {
188+
return true;
189+
}
190+
}
191+
}
192+
return false;
193+
};
194+
195+
// re-usable fn to prioritize tags by matching props
196+
export const prioritizer = (elementsList, propsToMatch) => {
197+
if (Array.isArray(elementsList)) {
198+
return elementsList.reduce(
199+
(acc, elementAttrs) => {
200+
if (checkIfPropsMatch(elementAttrs, propsToMatch)) {
201+
acc.priority.push(elementAttrs);
202+
} else {
203+
acc.default.push(elementAttrs);
204+
}
205+
return acc;
206+
},
207+
{ priority: [], default: [] }
208+
);
209+
}
210+
return { default: elementsList };
211+
};
212+
213+
const reducePropsToState = propsList => {
214+
return {
215+
baseTag: getBaseTagFromPropsList([TAG_PROPERTIES.HREF], propsList),
216+
bodyAttributes: getAttributesFromPropsList(ATTRIBUTE_NAMES.BODY, propsList),
217+
defer: getInnermostProperty(propsList, HELMET_PROPS.DEFER),
218+
encode: getInnermostProperty(propsList, HELMET_PROPS.ENCODE_SPECIAL_CHARACTERS),
219+
htmlAttributes: getAttributesFromPropsList(ATTRIBUTE_NAMES.HTML, propsList),
220+
linkTags: getTagsFromPropsList(
221+
TAG_NAMES.LINK,
222+
[TAG_PROPERTIES.REL, TAG_PROPERTIES.HREF],
223+
propsList
224+
),
225+
metaTags: getTagsFromPropsList(
226+
TAG_NAMES.META,
227+
[
228+
TAG_PROPERTIES.NAME,
229+
TAG_PROPERTIES.CHARSET,
230+
TAG_PROPERTIES.HTTPEQUIV,
231+
TAG_PROPERTIES.PROPERTY,
232+
TAG_PROPERTIES.ITEM_PROP,
233+
],
234+
propsList
235+
),
236+
noscriptTags: getTagsFromPropsList(TAG_NAMES.NOSCRIPT, [TAG_PROPERTIES.INNER_HTML], propsList),
237+
onChangeClientState: getOnChangeClientState(propsList),
238+
scriptTags: getTagsFromPropsList(
239+
TAG_NAMES.SCRIPT,
240+
[TAG_PROPERTIES.SRC, TAG_PROPERTIES.INNER_HTML],
241+
propsList
242+
),
243+
styleTags: getTagsFromPropsList(TAG_NAMES.STYLE, [TAG_PROPERTIES.CSS_TEXT], propsList),
244+
title: getTitleFromPropsList(propsList),
245+
titleAttributes: getAttributesFromPropsList(ATTRIBUTE_NAMES.TITLE, propsList),
246+
prioritizeSeoTags: getAnyTrueFromPropsList(propsList, HELMET_PROPS.PRIORITIZE_SEO_TAGS),
247+
};
248+
};
206249

207250
export const flattenArray = possibleArray =>
208251
Array.isArray(possibleArray) ? possibleArray.join('') : possibleArray;

0 commit comments

Comments
 (0)