Skip to content

Commit

Permalink
feat(seo): add support for alternate links (#26)
Browse files Browse the repository at this point in the history
- add support for <link rel=alternate /> 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
  • Loading branch information
0xpatrickdev authored Oct 21, 2020
1 parent 531e9d5 commit af7b5a3
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 3 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
]}
/>
</div>
);
Expand Down Expand Up @@ -206,6 +210,10 @@ SEO.propTypes = {
locale: string,
meta: arrayOf(object),
siteUrl: string,
alternateLinks: arrayOf(shape({
hreflang: string,
href: string,
}))
};

SEO.defaultProps = {
Expand All @@ -221,6 +229,7 @@ SEO.defaultProps = {
siteUrl: '',
title: '',
canonical: '',
alternateLinks: [],
};
```

Expand Down
14 changes: 14 additions & 0 deletions __tests__/components/SEO.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,18 @@ describe('SEO', () => {
);
expect(component).toMatchSnapshot();
});

it('should render alternate links correctly', () => {
const component = shallow(
<SEO
title="Lorem Ipsum"
siteUrl="https://example.com"
alternateLinks={[
{ hreflang: 'en-CA', href: 'https://example.com/en-CA' },
{ hreflang: 'fr-CA', href: 'https://example.com/fr-CA' },
]}
/>
);
expect(component).toMatchSnapshot();
});
});
61 changes: 61 additions & 0 deletions __tests__/components/__snapshots__/SEO.spec.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,67 @@ exports[`SEO should render Twitter Card tags correctly 1`] = `
</Helmet>
`;

exports[`SEO should render alternate links correctly 1`] = `
<Helmet
htmlAttributes={
Object {
"lang": "en-US",
}
}
link={
Array [
Object {
"href": "https://example.com",
"rel": "canonical",
},
Object {
"href": "https://example.com/en-CA",
"hreflang": "en-CA",
"rel": "alternate",
},
Object {
"href": "https://example.com/fr-CA",
"hreflang": "fr-CA",
"rel": "alternate",
},
]
}
meta={
Array [
Object {
"content": "Lorem Ipsum",
"property": "og:title",
},
Object {
"content": "https://example.com",
"property": "og:url",
},
Object {
"content": "website",
"property": "og:type",
},
Object {
"content": "en-US",
"property": "og:locale",
},
Object {
"content": "summary",
"name": "twitter:card",
},
Object {
"content": "Lorem Ipsum",
"name": "twitter:title",
},
]
}
titleTemplate=""
>
<title>
Lorem Ipsum
</title>
</Helmet>
`;

exports[`SEO should render correctly with the minimal tags 1`] = `
<Helmet
htmlAttributes={
Expand Down
41 changes: 41 additions & 0 deletions __tests__/utils/getAlternateLinks.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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.
*/

import getAlternateLinks from '../../src/utils/getAlternateLinks';

describe('getAlternateLinks', () => {
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([]);
});
});
12 changes: 9 additions & 3 deletions src/components/SEO.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -112,6 +116,7 @@ SEO.propTypes = {
height: PropTypes.number,
alt: PropTypes.string,
}),
alternateLinks: PropTypes.arrayOf(alternateLinkShape),
};

SEO.defaultProps = {
Expand All @@ -125,6 +130,7 @@ SEO.defaultProps = {
video: undefined,
openGraph: undefined,
twitterCard: undefined,
alternateLinks: [],
};

export default SEO;
5 changes: 5 additions & 0 deletions src/shapes.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,8 @@ export const twitterCardShape = shape({
app: twitterCardAppsShape,
player: twitterCardPlayerShape,
});

export const alternateLinkShape = shape({
hreflang: string,
href: string,
});
23 changes: 23 additions & 0 deletions src/utils/getAlternateLinks.js
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* under the License.
*/

export getAlternateLinks from './getAlternateLinks';
export getOpenGraphTags from './getOpenGraphTags';
export getTwitterCardTags from './getTwitterCardTags';
export provideDefaults from './provideDefaults';

0 comments on commit af7b5a3

Please sign in to comment.