From af7b5a397f0629da424185ca0764017f0e2e1af7 Mon Sep 17 00:00:00 2001 From: Patrick Cooney Date: Wed, 21 Oct 2020 15:30:04 -0400 Subject: [PATCH] feat(seo): add support for alternate links (#26) - add support for tags - can be used to point to pages in alternate languages and locales - https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel#attr-alternate --- README.md | 9 +++ __tests__/components/SEO.spec.jsx | 14 +++++ .../__snapshots__/SEO.spec.jsx.snap | 61 +++++++++++++++++++ __tests__/utils/getAlternateLinks.spec.js | 41 +++++++++++++ src/components/SEO.jsx | 12 +++- src/shapes.js | 5 ++ src/utils/getAlternateLinks.js | 23 +++++++ src/utils/index.js | 1 + 8 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 __tests__/utils/getAlternateLinks.spec.js create mode 100644 src/utils/getAlternateLinks.js diff --git a/README.md b/README.md index c4f8f53..423240e 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,10 @@ const MyModule = () => ( alt: 'Lorem ipsum', } }} + alternateLinks={[ + { hreflang: 'en-CA', href: 'http://example.com/en-CA' }, + { hreflang: 'fr-CA', href: 'http://example.com/fr-CA' }, + ]} /> ); @@ -206,6 +210,10 @@ SEO.propTypes = { locale: string, meta: arrayOf(object), siteUrl: string, + alternateLinks: arrayOf(shape({ + hreflang: string, + href: string, + })) }; SEO.defaultProps = { @@ -221,6 +229,7 @@ SEO.defaultProps = { siteUrl: '', title: '', canonical: '', + alternateLinks: [], }; ``` diff --git a/__tests__/components/SEO.spec.jsx b/__tests__/components/SEO.spec.jsx index d0011ff..cba537d 100644 --- a/__tests__/components/SEO.spec.jsx +++ b/__tests__/components/SEO.spec.jsx @@ -157,4 +157,18 @@ describe('SEO', () => { ); expect(component).toMatchSnapshot(); }); + + it('should render alternate links correctly', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); }); diff --git a/__tests__/components/__snapshots__/SEO.spec.jsx.snap b/__tests__/components/__snapshots__/SEO.spec.jsx.snap index ee755c7..76350e6 100644 --- a/__tests__/components/__snapshots__/SEO.spec.jsx.snap +++ b/__tests__/components/__snapshots__/SEO.spec.jsx.snap @@ -201,6 +201,67 @@ exports[`SEO should render Twitter Card tags correctly 1`] = ` `; +exports[`SEO should render alternate links correctly 1`] = ` + + + Lorem Ipsum + + +`; + exports[`SEO should render correctly with the minimal tags 1`] = ` { + it('should add the rel: alternate key value pair to entires', () => { + const mockData = [ + { hreflang: 'en-CA', href: 'https://example.com/en-CA' }, + { hreflang: 'fr-CA', href: 'https://example.com/fr-CA' }, + ]; + + expect(getAlternateLinks(mockData)).toEqual([ + { rel: 'alternate', hreflang: 'en-CA', href: 'https://example.com/en-CA' }, + { rel: 'alternate', hreflang: 'fr-CA', href: 'https://example.com/fr-CA' }, + ]); + }); + + it('should return an empty array if incorrect inputs are supplied', () => { + expect(getAlternateLinks(null)).toEqual([]); + expect(getAlternateLinks(undefined)).toEqual([]); + expect(getAlternateLinks('string')).toEqual([]); + expect(getAlternateLinks(1)).toEqual([]); + expect(getAlternateLinks({ hreflang: 'en-CA', href: 'https://example.com/en-CA' })).toEqual([]); + }); + + it('should return an empty array if an empty array is supplied', () => { + expect(getAlternateLinks([])).toEqual([]); + }); +}); diff --git a/src/components/SEO.jsx b/src/components/SEO.jsx index 57ae557..449134e 100644 --- a/src/components/SEO.jsx +++ b/src/components/SEO.jsx @@ -15,8 +15,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; -import { getOpenGraphTags, getTwitterCardTags, provideDefaults } from '../utils'; -import { openGraphShape, twitterCardShape } from '../shapes'; +import { getAlternateLinks, getOpenGraphTags, getTwitterCardTags, provideDefaults } from '../utils'; +import { alternateLinkShape, openGraphShape, twitterCardShape } from '../shapes'; const SEO = ({ siteUrl, @@ -31,10 +31,14 @@ const SEO = ({ twitterCard, image, video, + alternateLinks, }) => { const canonicalUrl = canonical || siteUrl; - const link = [{ rel: 'canonical', href: canonicalUrl }]; + const link = [ + { rel: 'canonical', href: canonicalUrl }, + ...getAlternateLinks(alternateLinks), + ]; const openGraphConfig = provideDefaults(openGraph, { title, @@ -112,6 +116,7 @@ SEO.propTypes = { height: PropTypes.number, alt: PropTypes.string, }), + alternateLinks: PropTypes.arrayOf(alternateLinkShape), }; SEO.defaultProps = { @@ -125,6 +130,7 @@ SEO.defaultProps = { video: undefined, openGraph: undefined, twitterCard: undefined, + alternateLinks: [], }; export default SEO; diff --git a/src/shapes.js b/src/shapes.js index d34f58d..2324545 100644 --- a/src/shapes.js +++ b/src/shapes.js @@ -79,3 +79,8 @@ export const twitterCardShape = shape({ app: twitterCardAppsShape, player: twitterCardPlayerShape, }); + +export const alternateLinkShape = shape({ + hreflang: string, + href: string, +}); diff --git a/src/utils/getAlternateLinks.js b/src/utils/getAlternateLinks.js new file mode 100644 index 0000000..4596def --- /dev/null +++ b/src/utils/getAlternateLinks.js @@ -0,0 +1,23 @@ +/* + * Copyright 2020 American Express Travel Related Services Company, Inc. + * + * 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. + */ + +function getAlternateLinks(alternateLinks) { + if (!alternateLinks || !Array.isArray(alternateLinks)) { + return []; + } + + return alternateLinks.map((x) => ({ rel: 'alternate', ...x })); +} + +export default getAlternateLinks; diff --git a/src/utils/index.js b/src/utils/index.js index a1c5ec7..84e0995 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -12,6 +12,7 @@ * under the License. */ +export getAlternateLinks from './getAlternateLinks'; export getOpenGraphTags from './getOpenGraphTags'; export getTwitterCardTags from './getTwitterCardTags'; export provideDefaults from './provideDefaults';