diff --git a/.gitignore b/.gitignore
index 74ae5c0..0da3a13 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,4 @@ __diff_output__
# build
build/
+*.tgz
diff --git a/README.md b/README.md
index 13d8804..51b482a 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,11 @@ Want to get paid for your contributions to `react-seo`?
```bash
npm install @americanexpress/react-seo
```
+
+
+
+Let's start with a minimal example of basic usage:
+
```javascript
import React from 'react';
import SEO from '@americanexpress/react-seo';
@@ -32,19 +37,75 @@ import SEO from '@americanexpress/react-seo';
const MyModule = () => (
+
+);
+
+export default MyModule;
+```
+
+This will result in the following tags being added to the `head` element:
+
+```html
+
+ Lorem Ipsum
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Notice in the example above that the Open Graph and Twitter Card metadata is constructed from the `title`, `description`, and `image` props. To override these values or add additional tags not provided by default, you may use the `openGraph` and `twitterCard` props.
+
+```javascript
+import React from 'react';
+import SEO from '@americanexpress/react-seo';
+
+const MyModule = () => (
+
+
);
export default MyModule;
```
+
## 🎛️ API
@@ -53,19 +114,96 @@ The interface for `react-seo` is denoted below:
```javascript
SEO.propTypes = {
- article: PropTypes.bool,
- author: PropTypes.string,
- description: PropTypes.string,
- image: PropTypes.shape({
- src: PropTypes.string,
+ title: string,
+ description: string,
+ canonical: string,
+ image: shape({
+ src: string,
+ secureUrl: string,
+ type: string,
+ width: number,
+ height: number,
+ alt: string,
+ }),
+ video: shape({
+ src: string,
+ secureUrl: string,
+ type: string,
+ width: number,
+ height: number,
+ alt: string,
+ }),
+ openGraph: shape({
+ type: string,
+ url: string,
+ title: string,
+ description: string,
+ determiner: string,
+ locale: string,
+ localeAlternate: string,
+ siteName: string,
+ image: shape({
+ src: string,
+ secureUrl: string,
+ type: string,
+ width: number,
+ height: number,
+ alt: string,
+ }),
+ video: shape({
+ src: string,
+ secureUrl: string,
+ type: string,
+ width: number,
+ height: number,
+ alt: string,
+ }),
+ audio: shape({
+ src: string,
+ secureUrl: string,
+ type: string,
+ }),
+ }),
+ twitterCard: shape({
+ card: string,
+ title: string,
+ description: string,
+ image: shape({
+ src: string,
+ alt: string,
+ }),
+ site: string,
+ siteId: string,
+ creator: string,
+ creatorId: string,
+ app: shape({
+ country: string,
+ iphone: shape({
+ id: string,
+ url: string,
+ name: string,
+ }),
+ ipad: shape({
+ id: string,
+ url: string,
+ name: string,
+ }),
+ googlePlay: shape({
+ id: string,
+ url: string,
+ name: string,
+ }),
+ }),
+ player: shape({
+ src: string,
+ width: number,
+ height: number,
+ }),
}),
- keywords: PropTypes.arrayOf(PropTypes.string),
- locale: PropTypes.string,
- meta: PropTypes.arrayOf(PropTypes.object),
- pathname: PropTypes.string,
- siteUrl: PropTypes.string,
- title: PropTypes.string,
- canonical: PropTypes.string,
+ keywords: arrayOf(string),
+ locale: string,
+ meta: arrayOf(object),
+ siteUrl: string,
};
SEO.defaultProps = {
diff --git a/__tests__/index.spec.jsx b/__tests__/components/SEO.spec.jsx
similarity index 61%
rename from __tests__/index.spec.jsx
rename to __tests__/components/SEO.spec.jsx
index a072269..cca5088 100644
--- a/__tests__/index.spec.jsx
+++ b/__tests__/components/SEO.spec.jsx
@@ -14,81 +14,104 @@
import React from 'react';
import { shallow } from 'enzyme';
-import SEO from '../src';
+import SEO from '../../src';
jest.mock('react-helmet', () => ({ Helmet: 'Helmet' }));
describe('SEO', () => {
- it('should render the snapshot correctly', () => {
+ it('should render correctly with the minimal tags', () => {
+ const component = shallow(
+
+ );
+ expect(component).toMatchSnapshot();
+ });
+
+ it('should provide the canonical URL', () => {
const component = shallow(
);
- expect(component).toMatchSnapshot();
+
+ const helmet = component.find('Helmet');
+ const { link } = helmet.props();
+
+ expect(link).toEqual([{
+ rel: 'canonical', href: 'https://example.com/index.html',
+ }]);
});
- it('should render articles correctly', () => {
+ it('should render image tags correctly', () => {
const component = shallow(
);
expect(component).toMatchSnapshot();
});
- it('should render images correctly', () => {
+ it('should render video tags correctly', () => {
const component = shallow(
);
expect(component).toMatchSnapshot();
});
- it('should render images pathnames correctly', () => {
+ it('should render Open Graph tags correctly', () => {
const component = shallow(
);
expect(component).toMatchSnapshot();
});
- it('should render children correctly', () => {
+ it('should render Twitter Card tags correctly', () => {
const component = shallow(
-
-
+ twitterCard={{
+ title: 'Twitter Card Title',
+ }}
+ />
);
expect(component).toMatchSnapshot();
});
diff --git a/__tests__/__snapshots__/index.spec.jsx.snap b/__tests__/components/__snapshots__/SEO.spec.jsx.snap
similarity index 60%
rename from __tests__/__snapshots__/index.spec.jsx.snap
rename to __tests__/components/__snapshots__/SEO.spec.jsx.snap
index 3fe6293..ce09db0 100644
--- a/__tests__/__snapshots__/index.spec.jsx.snap
+++ b/__tests__/components/__snapshots__/SEO.spec.jsx.snap
@@ -1,38 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`SEO should render articles correctly 1`] = `
+exports[`SEO should render Open Graph tags correctly 1`] = `
+ titleTemplate=""
+>
+
+ Lorem Ipsum
+
+
`;
-exports[`SEO should render children correctly 1`] = `
+exports[`SEO should render Twitter Card tags correctly 1`] = `
-
+
+ Lorem Ipsum
+
`;
-exports[`SEO should render images correctly 1`] = `
+exports[`SEO should render correctly with the minimal tags 1`] = `
+ titleTemplate=""
+>
+
+ Lorem Ipsum
+
+
`;
-exports[`SEO should render images pathnames correctly 1`] = `
+exports[`SEO should render image tags correctly 1`] = `
+ titleTemplate=""
+>
+
+ Lorem Ipsum
+
+
`;
-exports[`SEO should render the snapshot correctly 1`] = `
+exports[`SEO should render video tags correctly 1`] = `
+ titleTemplate=""
+>
+
+ Lorem Ipsum
+
+
`;
diff --git a/__tests__/utils/getOpenGraphTags.spec.js b/__tests__/utils/getOpenGraphTags.spec.js
new file mode 100644
index 0000000..359caa7
--- /dev/null
+++ b/__tests__/utils/getOpenGraphTags.spec.js
@@ -0,0 +1,250 @@
+/*
+ * 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 getOpenGraphTags from '../../src/utils/getOpenGraphTags';
+
+describe('getOpenGraphTags', () => {
+ it('should provide the basic tags', () => {
+ const config = {
+ title: 'Some title',
+ url: 'http://example.com/',
+ type: 'website',
+ };
+
+ expect(getOpenGraphTags(config)).toEqual([
+ { property: 'og:title', content: 'Some title' },
+ { property: 'og:url', content: 'http://example.com/' },
+ { property: 'og:type', content: 'website' },
+ ]);
+ });
+
+ it('should provide the optional tags', () => {
+ const config = {
+ title: 'Some title',
+ url: 'http://example.com/',
+ type: 'website',
+ description: 'Some description',
+ determiner: 'the',
+ locale: 'en-US',
+ localeAlternate: 'en-GB',
+ siteName: 'Example',
+ };
+
+ expect(getOpenGraphTags(config)).toEqual([
+ { property: 'og:title', content: 'Some title' },
+ { property: 'og:url', content: 'http://example.com/' },
+ { property: 'og:type', content: 'website' },
+ { property: 'og:description', content: 'Some description' },
+ { property: 'og:determiner', content: 'the' },
+ { property: 'og:locale', content: 'en-US' },
+ { property: 'og:locale:alternate', content: 'en-GB' },
+ { property: 'og:site_name', content: 'Example' },
+ ]);
+ });
+
+ describe('when an image is defined', () => {
+ it('should provide the basic image tags', () => {
+ const config = {
+ title: 'Some title',
+ url: 'http://example.com/',
+ type: 'website',
+ image: {
+ src: 'http://example.com/ogp.jpg',
+ },
+ };
+
+ expect(getOpenGraphTags(config)).toEqual([
+ { property: 'og:title', content: 'Some title' },
+ { property: 'og:url', content: 'http://example.com/' },
+ { property: 'og:type', content: 'website' },
+ { property: 'og:image', content: 'http://example.com/ogp.jpg' },
+ ]);
+ });
+
+ it('should set the secure_url tag', () => {
+ const config = {
+ title: 'Some title',
+ url: 'http://example.com/',
+ type: 'website',
+ image: {
+ src: 'https://example.com/ogp.jpg',
+ },
+ };
+
+ expect(getOpenGraphTags(config)).toEqual([
+ { property: 'og:title', content: 'Some title' },
+ { property: 'og:url', content: 'http://example.com/' },
+ { property: 'og:type', content: 'website' },
+ { property: 'og:image', content: 'https://example.com/ogp.jpg' },
+ { property: 'og:image:secure_url', content: 'https://example.com/ogp.jpg' },
+ ]);
+ });
+
+ it('should provide all of the image tags', () => {
+ const config = {
+ title: 'Some title',
+ url: 'http://example.com/',
+ type: 'website',
+ image: {
+ src: 'http://example.com/ogp.jpg',
+ secureUrl: 'https://secure.example.com/ogp.jpg',
+ type: 'image/jpeg',
+ width: 500,
+ height: 300,
+ alt: 'A shiny red apple with a bite taken out',
+ },
+ };
+
+ expect(getOpenGraphTags(config)).toEqual([
+ { property: 'og:title', content: 'Some title' },
+ { property: 'og:url', content: 'http://example.com/' },
+ { property: 'og:type', content: 'website' },
+ { property: 'og:image', content: 'http://example.com/ogp.jpg' },
+ { property: 'og:image:secure_url', content: 'https://secure.example.com/ogp.jpg' },
+ { property: 'og:image:type', content: 'image/jpeg' },
+ { property: 'og:image:width', content: 500 },
+ { property: 'og:image:height', content: 300 },
+ { property: 'og:image:alt', content: 'A shiny red apple with a bite taken out' },
+ ]);
+ });
+ });
+
+ describe('when a video is defined', () => {
+ it('should provide the basic video tags', () => {
+ const config = {
+ title: 'Some title',
+ url: 'http://example.com/',
+ type: 'website',
+ video: {
+ src: 'http://example.com/movie.swf',
+ },
+ };
+
+ expect(getOpenGraphTags(config)).toEqual([
+ { property: 'og:title', content: 'Some title' },
+ { property: 'og:url', content: 'http://example.com/' },
+ { property: 'og:type', content: 'website' },
+ { property: 'og:video', content: 'http://example.com/movie.swf' },
+ ]);
+ });
+
+ it('should set the secure_url tag', () => {
+ const config = {
+ title: 'Some title',
+ url: 'http://example.com/',
+ type: 'website',
+ video: {
+ src: 'https://example.com/movie.swf',
+ },
+ };
+
+ expect(getOpenGraphTags(config)).toEqual([
+ { property: 'og:title', content: 'Some title' },
+ { property: 'og:url', content: 'http://example.com/' },
+ { property: 'og:type', content: 'website' },
+ { property: 'og:video', content: 'https://example.com/movie.swf' },
+ { property: 'og:video:secure_url', content: 'https://example.com/movie.swf' },
+ ]);
+ });
+
+ it('should provide all of the video tags', () => {
+ const config = {
+ title: 'Some title',
+ url: 'http://example.com/',
+ type: 'website',
+ video: {
+ src: 'http://example.com/movie.swf',
+ secureUrl: 'https://secure.example.com/movie.swf',
+ type: 'application/x-shockwave-flash',
+ width: 500,
+ height: 300,
+ alt: 'A shiny red apple with a bite taken out',
+ },
+ };
+
+ expect(getOpenGraphTags(config)).toEqual([
+ { property: 'og:title', content: 'Some title' },
+ { property: 'og:url', content: 'http://example.com/' },
+ { property: 'og:type', content: 'website' },
+ { property: 'og:video', content: 'http://example.com/movie.swf' },
+ { property: 'og:video:secure_url', content: 'https://secure.example.com/movie.swf' },
+ { property: 'og:video:type', content: 'application/x-shockwave-flash' },
+ { property: 'og:video:width', content: 500 },
+ { property: 'og:video:height', content: 300 },
+ { property: 'og:video:alt', content: 'A shiny red apple with a bite taken out' },
+ ]);
+ });
+ });
+
+ describe('when audio is defined', () => {
+ it('should provide the basic audio tags', () => {
+ const config = {
+ title: 'Some title',
+ url: 'http://example.com/',
+ type: 'website',
+ audio: {
+ src: 'http://example.com/sound.mp3',
+ },
+ };
+
+ expect(getOpenGraphTags(config)).toEqual([
+ { property: 'og:title', content: 'Some title' },
+ { property: 'og:url', content: 'http://example.com/' },
+ { property: 'og:type', content: 'website' },
+ { property: 'og:audio', content: 'http://example.com/sound.mp3' },
+ ]);
+ });
+
+ it('should set the secure_url tag', () => {
+ const config = {
+ title: 'Some title',
+ url: 'http://example.com/',
+ type: 'website',
+ audio: {
+ src: 'https://example.com/sound.mp3',
+ },
+ };
+
+ expect(getOpenGraphTags(config)).toEqual([
+ { property: 'og:title', content: 'Some title' },
+ { property: 'og:url', content: 'http://example.com/' },
+ { property: 'og:type', content: 'website' },
+ { property: 'og:audio', content: 'https://example.com/sound.mp3' },
+ { property: 'og:audio:secure_url', content: 'https://example.com/sound.mp3' },
+ ]);
+ });
+
+ it('should provide all of the audio tags', () => {
+ const config = {
+ title: 'Some title',
+ url: 'http://example.com/',
+ type: 'website',
+ audio: {
+ src: 'http://example.com/sound.mp3',
+ secureUrl: 'https://secure.example.com/sound.mp3',
+ type: 'audio/mpeg',
+ },
+ };
+
+ expect(getOpenGraphTags(config)).toEqual([
+ { property: 'og:title', content: 'Some title' },
+ { property: 'og:url', content: 'http://example.com/' },
+ { property: 'og:type', content: 'website' },
+ { property: 'og:audio', content: 'http://example.com/sound.mp3' },
+ { property: 'og:audio:secure_url', content: 'https://secure.example.com/sound.mp3' },
+ { property: 'og:audio:type', content: 'audio/mpeg' },
+ ]);
+ });
+ });
+});
diff --git a/__tests__/utils/getTwitterCardTags.spec.js b/__tests__/utils/getTwitterCardTags.spec.js
new file mode 100644
index 0000000..3e8962d
--- /dev/null
+++ b/__tests__/utils/getTwitterCardTags.spec.js
@@ -0,0 +1,123 @@
+/*
+ * 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 getTwitterCardTags from '../../src/utils/getTwitterCardTags';
+
+describe('getTwitterCardTags', () => {
+ describe('summary card', () => {
+ it('should provide the tags for a summary card', () => {
+ const config = {
+ card: 'summary',
+ site: '@Example',
+ title: 'Some title',
+ description: 'Some description',
+ image: {
+ src: 'http://example.com/ogp.jpg',
+ alt: 'A shiny red apple with a bite taken out',
+ },
+ irrelevantProperty: 'foo',
+ };
+
+ expect(getTwitterCardTags(config)).toEqual([
+ { name: 'twitter:card', content: 'summary' },
+ { name: 'twitter:site', content: '@Example' },
+ { name: 'twitter:title', content: 'Some title' },
+ { name: 'twitter:description', content: 'Some description' },
+ { name: 'twitter:image', content: 'http://example.com/ogp.jpg' },
+ { name: 'twitter:image:alt', content: 'A shiny red apple with a bite taken out' },
+ ]);
+ });
+ });
+
+ describe('player card', () => {
+ it('should provide the tags for a player card', () => {
+ const config = {
+ card: 'player',
+ site: '@Example',
+ title: 'Some title',
+ description: 'Some description',
+ player: {
+ src: 'http://example.com/movie.swf',
+ width: 500,
+ height: 300,
+ },
+ irrelevantProperty: 'foo',
+ };
+
+ expect(getTwitterCardTags(config)).toEqual([
+ { name: 'twitter:card', content: 'player' },
+ { name: 'twitter:site', content: '@Example' },
+ { name: 'twitter:title', content: 'Some title' },
+ { name: 'twitter:description', content: 'Some description' },
+ { name: 'twitter:player', content: 'http://example.com/movie.swf' },
+ { name: 'twitter:player:width', content: 500 },
+ { name: 'twitter:player:height', content: 300 },
+ ]);
+ });
+ });
+
+ describe('app card', () => {
+ it('should provide the tags for an app card', () => {
+ const config = {
+ card: 'app',
+ site: '@Example',
+ title: 'Some title',
+ description: 'Some description',
+ app: {
+ country: 'CA',
+ iphone: {
+ id: '929750075',
+ url: 'example://app/5149e249222f9e600a7540ef',
+ name: 'Example App',
+ },
+ ipad: {
+ id: '929750075',
+ url: 'example://app/5149e249222f9e600a7540ef',
+ name: 'Example App',
+ },
+ googlePlay: {
+ id: 'io.examples.example',
+ url: 'http://example.examples.io/5149e249222f9e600a7540ef',
+ name: 'Example App',
+ },
+ },
+ };
+
+ expect(getTwitterCardTags(config)).toEqual([
+ { name: 'twitter:card', content: 'app' },
+ { name: 'twitter:site', content: '@Example' },
+ { name: 'twitter:title', content: 'Some title' },
+ { name: 'twitter:description', content: 'Some description' },
+ { name: 'twitter:app:country', content: 'CA' },
+ { name: 'twitter:app:id:iphone', content: '929750075' },
+ { name: 'twitter:app:url:iphone', content: 'example://app/5149e249222f9e600a7540ef' },
+ { name: 'twitter:app:name:iphone', content: 'Example App' },
+ { name: 'twitter:app:id:ipad', content: '929750075' },
+ { name: 'twitter:app:url:ipad', content: 'example://app/5149e249222f9e600a7540ef' },
+ { name: 'twitter:app:name:ipad', content: 'Example App' },
+ { name: 'twitter:app:id:googleplay', content: 'io.examples.example' },
+ { name: 'twitter:app:url:googleplay', content: 'http://example.examples.io/5149e249222f9e600a7540ef' },
+ { name: 'twitter:app:name:googleplay', content: 'Example App' },
+ ]);
+ });
+ });
+
+ it('should throw if the card type is not supported', () => {
+ const config = {
+ card: 'foo',
+ };
+
+ expect(() => getTwitterCardTags(config)).toThrow();
+ });
+});
diff --git a/package-lock.json b/package-lock.json
index 96684d7..8ac3321 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,5 +1,5 @@
{
- "name": "@americanexpress/seo",
+ "name": "@americanexpress/react-seo",
"version": "0.0.0",
"lockfileVersion": 1,
"requires": true,
@@ -4184,24 +4184,29 @@
"dependencies": {
"abbrev": {
"version": "1.1.1",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true,
"optional": true
},
"ansi-regex": {
"version": "2.1.1",
- "bundled": true,
- "dev": true
+ "resolved": false,
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true,
+ "optional": true
},
"aproba": {
"version": "1.2.0",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true,
"optional": true
},
"are-we-there-yet": {
"version": "1.1.5",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
"dev": true,
"optional": true,
"requires": {
@@ -4211,13 +4216,17 @@
},
"balanced-match": {
"version": "1.0.0",
- "bundled": true,
- "dev": true
+ "resolved": false,
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true,
+ "optional": true
},
"brace-expansion": {
"version": "1.1.11",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
+ "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -4225,34 +4234,43 @@
},
"chownr": {
"version": "1.1.4",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"dev": true,
"optional": true
},
"code-point-at": {
"version": "1.1.0",
- "bundled": true,
- "dev": true
+ "resolved": false,
+ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
+ "dev": true,
+ "optional": true
},
"concat-map": {
"version": "0.0.1",
- "bundled": true,
- "dev": true
+ "resolved": false,
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true,
+ "optional": true
},
"console-control-strings": {
"version": "1.1.0",
- "bundled": true,
- "dev": true
+ "resolved": false,
+ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
+ "dev": true,
+ "optional": true
},
"core-util-is": {
"version": "1.0.2",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true,
"optional": true
},
"debug": {
"version": "3.2.6",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"optional": true,
"requires": {
@@ -4261,25 +4279,29 @@
},
"deep-extend": {
"version": "0.6.0",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"dev": true,
"optional": true
},
"delegates": {
"version": "1.0.0",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
"dev": true,
"optional": true
},
"detect-libc": {
"version": "1.0.3",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
"dev": true,
"optional": true
},
"fs-minipass": {
"version": "1.2.7",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
"dev": true,
"optional": true,
"requires": {
@@ -4288,13 +4310,15 @@
},
"fs.realpath": {
"version": "1.0.0",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true,
"optional": true
},
"gauge": {
"version": "2.7.4",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"dev": true,
"optional": true,
"requires": {
@@ -4310,7 +4334,8 @@
},
"glob": {
"version": "7.1.6",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"optional": true,
"requires": {
@@ -4324,13 +4349,15 @@
},
"has-unicode": {
"version": "2.0.1",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
"dev": true,
"optional": true
},
"iconv-lite": {
"version": "0.4.24",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"optional": true,
"requires": {
@@ -4339,7 +4366,8 @@
},
"ignore-walk": {
"version": "3.0.3",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
"dev": true,
"optional": true,
"requires": {
@@ -4348,7 +4376,8 @@
},
"inflight": {
"version": "1.0.6",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"optional": true,
"requires": {
@@ -4358,46 +4387,58 @@
},
"inherits": {
"version": "2.0.4",
- "bundled": true,
- "dev": true
+ "resolved": false,
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "optional": true
},
"ini": {
"version": "1.3.5",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"dev": true,
"optional": true
},
"is-fullwidth-code-point": {
"version": "1.0.0",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
+ "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
},
"isarray": {
"version": "1.0.0",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true,
"optional": true
},
"minimatch": {
"version": "3.0.4",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
+ "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "1.2.5",
- "bundled": true,
- "dev": true
+ "resolved": false,
+ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+ "dev": true,
+ "optional": true
},
"minipass": {
"version": "2.9.0",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"dev": true,
+ "optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -4405,7 +4446,8 @@
},
"minizlib": {
"version": "1.3.3",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
"dev": true,
"optional": true,
"requires": {
@@ -4414,21 +4456,25 @@
},
"mkdirp": {
"version": "0.5.3",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==",
"dev": true,
+ "optional": true,
"requires": {
"minimist": "^1.2.5"
}
},
"ms": {
"version": "2.1.2",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true,
"optional": true
},
"needle": {
"version": "2.3.3",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-EkY0GeSq87rWp1hoq/sH/wnTWgFVhYlnIkbJ0YJFfRgEFlz2RraCjBpFQ+vrEgEdp0ThfyHADmkChEhcb7PKyw==",
"dev": true,
"optional": true,
"requires": {
@@ -4439,7 +4485,8 @@
},
"node-pre-gyp": {
"version": "0.14.0",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==",
"dev": true,
"optional": true,
"requires": {
@@ -4457,7 +4504,8 @@
},
"nopt": {
"version": "4.0.3",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
"dev": true,
"optional": true,
"requires": {
@@ -4467,7 +4515,8 @@
},
"npm-bundled": {
"version": "1.1.1",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
"dev": true,
"optional": true,
"requires": {
@@ -4476,12 +4525,15 @@
},
"npm-normalize-package-bin": {
"version": "1.0.1",
- "bundled": true,
- "dev": true
+ "resolved": false,
+ "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==",
+ "dev": true,
+ "optional": true
},
"npm-packlist": {
"version": "1.4.8",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
"dev": true,
"optional": true,
"requires": {
@@ -4492,7 +4544,8 @@
},
"npmlog": {
"version": "4.1.2",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"dev": true,
"optional": true,
"requires": {
@@ -4504,38 +4557,46 @@
},
"number-is-nan": {
"version": "1.0.1",
- "bundled": true,
- "dev": true
+ "resolved": false,
+ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
+ "dev": true,
+ "optional": true
},
"object-assign": {
"version": "4.1.1",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true,
"optional": true
},
"once": {
"version": "1.4.0",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
+ "optional": true,
"requires": {
"wrappy": "1"
}
},
"os-homedir": {
"version": "1.0.2",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"dev": true,
"optional": true
},
"os-tmpdir": {
"version": "1.0.2",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"dev": true,
"optional": true
},
"osenv": {
"version": "0.1.5",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"dev": true,
"optional": true,
"requires": {
@@ -4545,19 +4606,22 @@
},
"path-is-absolute": {
"version": "1.0.1",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true,
"optional": true
},
"process-nextick-args": {
"version": "2.0.1",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true,
"optional": true
},
"rc": {
"version": "1.2.8",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"dev": true,
"optional": true,
"requires": {
@@ -4569,7 +4633,8 @@
},
"readable-stream": {
"version": "2.3.7",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"optional": true,
"requires": {
@@ -4584,7 +4649,8 @@
},
"rimraf": {
"version": "2.7.1",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"optional": true,
"requires": {
@@ -4593,43 +4659,52 @@
},
"safe-buffer": {
"version": "5.1.2",
- "bundled": true,
- "dev": true
+ "resolved": false,
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "optional": true
},
"safer-buffer": {
"version": "2.1.2",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"optional": true
},
"sax": {
"version": "1.2.4",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true,
"optional": true
},
"semver": {
"version": "5.7.1",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true,
"optional": true
},
"set-blocking": {
"version": "2.0.0",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"dev": true,
"optional": true
},
"signal-exit": {
"version": "3.0.2",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
"dev": true,
"optional": true
},
"string-width": {
"version": "1.0.2",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
+ "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -4638,7 +4713,8 @@
},
"string_decoder": {
"version": "1.1.1",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"optional": true,
"requires": {
@@ -4647,21 +4723,25 @@
},
"strip-ansi": {
"version": "3.0.1",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
+ "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
},
"strip-json-comments": {
"version": "2.0.1",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
"dev": true,
"optional": true
},
"tar": {
"version": "4.4.13",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
"dev": true,
"optional": true,
"requires": {
@@ -4676,13 +4756,15 @@
},
"util-deprecate": {
"version": "1.0.2",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true,
"optional": true
},
"wide-align": {
"version": "1.1.3",
- "bundled": true,
+ "resolved": false,
+ "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"dev": true,
"optional": true,
"requires": {
@@ -4691,13 +4773,17 @@
},
"wrappy": {
"version": "1.0.2",
- "bundled": true,
- "dev": true
+ "resolved": false,
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true,
+ "optional": true
},
"yallist": {
"version": "3.1.1",
- "bundled": true,
- "dev": true
+ "resolved": false,
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "optional": true
}
}
},
diff --git a/package.json b/package.json
index e399cf9..13b6459 100644
--- a/package.json
+++ b/package.json
@@ -63,6 +63,17 @@
"react": "^16.13.1",
"react-helmet": "^5.2.1"
},
+ "release": {
+ "plugins": [
+ "@semantic-release/commit-analyzer",
+ "@semantic-release/release-notes-generator",
+ "@semantic-release/changelog",
+ "@semantic-release/npm",
+ "@semantic-release/git",
+ "@semantic-release/github"
+ ],
+ "branch": "master"
+ },
"husky": {
"hooks": {
"pre-commit": "npm test",
diff --git a/src/components/SEO.jsx b/src/components/SEO.jsx
new file mode 100644
index 0000000..a04a203
--- /dev/null
+++ b/src/components/SEO.jsx
@@ -0,0 +1,123 @@
+/*
+ * 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 React from 'react';
+import PropTypes from 'prop-types';
+import { Helmet } from 'react-helmet';
+import { getOpenGraphTags, getTwitterCardTags, provideDefaults } from '../utils';
+import { openGraphShape, twitterCardShape } from '../shapes';
+
+const SEO = ({
+ siteUrl,
+ title,
+ description,
+ locale,
+ titleTemplate,
+ canonical,
+ keywords,
+ openGraph,
+ twitterCard,
+ image,
+ video,
+}) => {
+ const canonicalUrl = canonical || siteUrl;
+
+ const link = [{ rel: 'canonical', href: canonicalUrl }];
+
+ const openGraphConfig = provideDefaults(openGraph, {
+ title,
+ description,
+ image,
+ video,
+ locale,
+ url: canonicalUrl,
+ type: 'website',
+ });
+
+ const twitterCardConfig = provideDefaults(twitterCard, {
+ title,
+ description,
+ image,
+ video,
+ card: 'summary',
+ });
+
+ const meta = [
+ ...getOpenGraphTags(openGraphConfig),
+ ...getTwitterCardTags(twitterCardConfig),
+ ];
+
+ if (description) {
+ meta.push({ name: 'description', content: description });
+ }
+
+ if (keywords.length > 0) {
+ meta.push({ name: 'keywords', content: keywords.join(',') });
+ }
+
+ return (
+
+ {title && {title}}
+
+ );
+};
+
+SEO.propTypes = {
+ siteUrl: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
+ description: PropTypes.string,
+ locale: PropTypes.string,
+ titleTemplate: PropTypes.string,
+ canonical: PropTypes.string,
+ openGraph: openGraphShape,
+ twitterCard: twitterCardShape,
+ keywords: PropTypes.arrayOf(PropTypes.string),
+ image: PropTypes.shape({
+ url: PropTypes.string,
+ secureUrl: PropTypes.string,
+ width: PropTypes.number,
+ height: PropTypes.number,
+ type: PropTypes.string,
+ alt: PropTypes.string,
+ }),
+ video: PropTypes.shape({
+ url: PropTypes.string,
+ secureUrl: PropTypes.string,
+ type: PropTypes.string,
+ width: PropTypes.number,
+ height: PropTypes.number,
+ alt: PropTypes.string,
+ }),
+};
+
+SEO.defaultProps = {
+ description: '',
+ locale: 'en-US',
+ titleTemplate: '',
+ canonical: '',
+ keywords: [],
+ image: undefined,
+ video: undefined,
+ openGraph: undefined,
+ twitterCard: undefined,
+};
+
+export default SEO;
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..cc8f289
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,19 @@
+/*
+ * 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 SEO from './components/SEO';
+
+export * from './shapes';
+
+export default SEO;
diff --git a/src/index.jsx b/src/index.jsx
deleted file mode 100644
index 845d60c..0000000
--- a/src/index.jsx
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * 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 React from 'react';
-import PropTypes from 'prop-types';
-import { Helmet } from 'react-helmet';
-
-const SEO = ({
- article,
- author,
- children,
- description,
- image: metaImage,
- keywords,
- lang,
- meta,
- pathname,
- siteUrl,
- title,
- canonical: canonicalTag,
-}) => {
- const image = metaImage && metaImage.src ? `${siteUrl}${metaImage.src}` : null;
-
- const canonical = canonicalTag || (pathname ? `${siteUrl}${pathname}` : null);
-
- const link = canonical ? [{ rel: 'canonical', href: canonical }] : [];
-
- let metaTags = [
- {
- name: 'description',
- content: description,
- },
- {
- name: 'keywords',
- content: keywords.join(','),
- },
- {
- property: 'og:title',
- content: title,
- },
- {
- property: 'og:description',
- content: description,
- },
- {
- property: 'og:type',
- content: article ? 'article' : 'website',
- },
- {
- name: 'twitter:creator',
- content: author,
- },
- {
- name: 'twitter:title',
- content: title,
- },
- {
- name: 'twitter:description',
- content: description,
- },
- ...meta,
- ];
-
- if (metaImage) {
- metaTags = [
- ...metaTags,
- {
- property: 'og:image',
- content: image,
- },
- {
- property: 'og:image:width',
- content: metaImage.width,
- },
- {
- property: 'og:image:height',
- content: metaImage.height,
- },
- {
- name: 'twitter:card',
- content: 'summary_large_image',
- },
- ];
- } else {
- metaTags = [
- ...metaTags,
- {
- name: 'twitter:card',
- content: 'summary',
- },
- ];
- }
-
- return (
-
- {children}
-
- );
-};
-
-SEO.propTypes = {
- article: PropTypes.bool,
- author: PropTypes.string,
- children: PropTypes.node,
- description: PropTypes.string,
- image: PropTypes.shape({
- src: PropTypes.string,
- }),
- keywords: PropTypes.arrayOf(PropTypes.string),
- lang: PropTypes.string,
- meta: PropTypes.arrayOf(PropTypes.object),
- pathname: PropTypes.string,
- siteUrl: PropTypes.string,
- title: PropTypes.string,
- canonical: PropTypes.string,
-};
-
-SEO.defaultProps = {
- article: false,
- author: '',
- children: null,
- description: '',
- image: null,
- keywords: [],
- lang: 'en-US',
- meta: [],
- pathname: '',
- siteUrl: '',
- title: '',
- canonical: '',
-};
-
-export default SEO;
diff --git a/src/shapes.js b/src/shapes.js
new file mode 100644
index 0000000..d34f58d
--- /dev/null
+++ b/src/shapes.js
@@ -0,0 +1,81 @@
+/*
+ * 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 { string, number, shape } from 'prop-types';
+
+export const openGraphVisualShape = shape({
+ src: string,
+ secureUrl: string,
+ type: string,
+ width: number,
+ height: number,
+ alt: string,
+});
+
+export const openGraphAudioShape = shape({
+ src: string,
+ secureUrl: string,
+ type: string,
+});
+
+export const openGraphShape = shape({
+ type: string,
+ url: string,
+ title: string,
+ description: string,
+ determiner: string,
+ locale: string,
+ localeAlternate: string,
+ siteName: string,
+ image: openGraphVisualShape,
+ video: openGraphVisualShape,
+ audio: openGraphAudioShape,
+});
+
+export const twitterCardImageShape = shape({
+ src: string,
+ alt: string,
+});
+
+export const twitterCardAppShape = shape({
+ id: string,
+ url: string,
+ name: string,
+});
+
+export const twitterCardAppsShape = shape({
+ country: string,
+ iphone: twitterCardAppShape,
+ ipad: twitterCardAppShape,
+ googlePlay: twitterCardAppShape,
+});
+
+export const twitterCardPlayerShape = shape({
+ src: string,
+ width: number,
+ height: number,
+});
+
+export const twitterCardShape = shape({
+ card: string,
+ title: string,
+ description: string,
+ image: twitterCardImageShape,
+ site: string,
+ siteId: string,
+ creator: string,
+ creatorId: string,
+ app: twitterCardAppsShape,
+ player: twitterCardPlayerShape,
+});
diff --git a/src/utils/constants.js b/src/utils/constants.js
new file mode 100644
index 0000000..d912bbd
--- /dev/null
+++ b/src/utils/constants.js
@@ -0,0 +1,141 @@
+/*
+ * 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.
+ */
+
+export const OPEN_GRAPH_BASE_SCHEMA = {
+ properties: {
+ title: 'og:title',
+ url: 'og:url',
+ type: 'og:type',
+ description: 'og:description',
+ determiner: 'og:determiner',
+ locale: 'og:locale',
+ localeAlternate: 'og:locale:alternate',
+ siteName: 'og:site_name',
+ },
+ required: ['title', 'url', 'type'],
+};
+
+export const OPEN_GRAPH_VIDEO_SCHEMA = {
+ properties: {
+ src: 'og:video',
+ secureUrl: 'og:video:secure_url',
+ type: 'og:video:type',
+ width: 'og:video:width',
+ height: 'og:video:height',
+ alt: 'og:video:alt',
+ },
+ required: ['src'],
+};
+
+export const OPEN_GRAPH_AUDIO_SCHEMA = {
+ properties: {
+ src: 'og:audio',
+ secureUrl: 'og:audio:secure_url',
+ type: 'og:audio:type',
+ },
+ required: ['src'],
+};
+
+export const OPEN_GRAPH_IMAGE_SCHEMA = {
+ properties: {
+ src: 'og:image',
+ secureUrl: 'og:image:secure_url',
+ type: 'og:image:type',
+ width: 'og:image:width',
+ height: 'og:image:height',
+ alt: 'og:image:alt',
+ },
+ required: ['src'],
+};
+
+export const TWITTER_APP_CARD_SCHEMA = {
+ properties: {
+ card: 'twitter:card',
+ site: 'twitter:site',
+ title: 'twitter:title',
+ description: 'twitter:description',
+ app: {
+ properties: {
+ country: 'twitter:app:country',
+ iphone: {
+ properties: {
+ id: 'twitter:app:id:iphone',
+ url: 'twitter:app:url:iphone',
+ name: 'twitter:app:name:iphone',
+ },
+ required: ['id'],
+ },
+ ipad: {
+ properties: {
+ id: 'twitter:app:id:ipad',
+ url: 'twitter:app:url:ipad',
+ name: 'twitter:app:name:ipad',
+ },
+ required: ['id'],
+ },
+ googlePlay: {
+ properties: {
+ id: 'twitter:app:id:googleplay',
+ url: 'twitter:app:url:googleplay',
+ name: 'twitter:app:name:googleplay',
+ },
+ required: ['id'],
+ },
+ },
+ },
+ },
+ required: ['card', 'title', 'site', 'app'],
+};
+
+export const TWITTER_PLAYER_CARD_SCHEMA = {
+ properties: {
+ card: 'twitter:card',
+ site: 'twitter:site',
+ title: 'twitter:title',
+ description: 'twitter:description',
+ player: {
+ properties: {
+ src: 'twitter:player',
+ width: 'twitter:player:width',
+ height: 'twitter:player:height',
+ },
+ required: ['src', 'width', 'height'],
+ },
+ image: {
+ properties: {
+ src: 'twitter:image',
+ alt: 'twitter:image:alt',
+ },
+ required: ['src'],
+ },
+ },
+ required: ['card', 'title', 'site', 'player'],
+};
+
+export const TWITTER_SUMMARY_CARD_SCHEMA = {
+ properties: {
+ card: 'twitter:card',
+ site: 'twitter:site',
+ title: 'twitter:title',
+ description: 'twitter:description',
+ image: {
+ properties: {
+ src: 'twitter:image',
+ alt: 'twitter:image:alt',
+ },
+ required: ['src'],
+ },
+ },
+ required: ['card', 'title'],
+};
diff --git a/src/utils/getOpenGraphTags.js b/src/utils/getOpenGraphTags.js
new file mode 100644
index 0000000..f1ff09c
--- /dev/null
+++ b/src/utils/getOpenGraphTags.js
@@ -0,0 +1,46 @@
+/*
+ * 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 getTagsFromSchema from './getTagsFromSchema';
+import {
+ OPEN_GRAPH_BASE_SCHEMA,
+ OPEN_GRAPH_AUDIO_SCHEMA,
+ OPEN_GRAPH_IMAGE_SCHEMA,
+ OPEN_GRAPH_VIDEO_SCHEMA,
+} from './constants';
+
+const getOpenGraphTags = ({
+ image,
+ video,
+ audio,
+ ...data
+}) => {
+ const withSecureUrl = (data_) => {
+ const { secureUrl, src } = data_;
+
+ return {
+ ...data_,
+ secureUrl: secureUrl || (src.startsWith('https') ? src : ''),
+ };
+ };
+
+ return [
+ ...getTagsFromSchema(OPEN_GRAPH_BASE_SCHEMA, data, 'property'),
+ ...audio ? getTagsFromSchema(OPEN_GRAPH_AUDIO_SCHEMA, withSecureUrl(audio), 'property') : [],
+ ...image ? getTagsFromSchema(OPEN_GRAPH_IMAGE_SCHEMA, withSecureUrl(image), 'property') : [],
+ ...video ? getTagsFromSchema(OPEN_GRAPH_VIDEO_SCHEMA, withSecureUrl(video), 'property') : [],
+ ];
+};
+
+export default getOpenGraphTags;
diff --git a/src/utils/getTagsFromSchema.js b/src/utils/getTagsFromSchema.js
new file mode 100644
index 0000000..3c30dca
--- /dev/null
+++ b/src/utils/getTagsFromSchema.js
@@ -0,0 +1,37 @@
+/*
+ * 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 getTagsFromSchema(schema, data, key = 'name') {
+ return Object.entries(schema.properties).reduce((collection, property) => {
+ const [name, value] = property;
+
+ if (!data[name]) {
+ return collection;
+ }
+
+ if (typeof value === 'string') {
+ return [
+ ...collection,
+ { [key]: value, content: data[name] },
+ ];
+ }
+
+ return [
+ ...collection,
+ ...getTagsFromSchema(value, data[name]),
+ ];
+ }, []);
+}
+
+export default getTagsFromSchema;
diff --git a/src/utils/getTwitterCardTags.js b/src/utils/getTwitterCardTags.js
new file mode 100644
index 0000000..403ce10
--- /dev/null
+++ b/src/utils/getTwitterCardTags.js
@@ -0,0 +1,37 @@
+/*
+ * 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 getTagsFromSchema from './getTagsFromSchema';
+import {
+ TWITTER_APP_CARD_SCHEMA,
+ TWITTER_PLAYER_CARD_SCHEMA,
+ TWITTER_SUMMARY_CARD_SCHEMA,
+} from './constants';
+
+const schemas = {
+ player: TWITTER_PLAYER_CARD_SCHEMA,
+ app: TWITTER_APP_CARD_SCHEMA,
+ summary: TWITTER_SUMMARY_CARD_SCHEMA,
+ summary_with_large_image: TWITTER_SUMMARY_CARD_SCHEMA,
+};
+
+const getTwitterCardTags = (tags) => {
+ if (schemas[tags.card]) {
+ return getTagsFromSchema(schemas[tags.card], tags);
+ }
+
+ throw new Error(`Unsupported Twitter Card type: ${tags.card}`);
+};
+
+export default getTwitterCardTags;
diff --git a/src/utils/index.js b/src/utils/index.js
new file mode 100644
index 0000000..a1c5ec7
--- /dev/null
+++ b/src/utils/index.js
@@ -0,0 +1,17 @@
+/*
+ * 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.
+ */
+
+export getOpenGraphTags from './getOpenGraphTags';
+export getTwitterCardTags from './getTwitterCardTags';
+export provideDefaults from './provideDefaults';
diff --git a/src/utils/provideDefaults.js b/src/utils/provideDefaults.js
new file mode 100644
index 0000000..d69fdca
--- /dev/null
+++ b/src/utils/provideDefaults.js
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+const provideDefaults = (baseConfig = {}, defaults) => Object.entries(defaults)
+ .reduce((base, property) => {
+ const [name, value] = property;
+
+ if (!base[name]) {
+ return {
+ ...base,
+ [name]: value,
+ };
+ }
+
+ return base;
+ }, baseConfig);
+
+export default provideDefaults;