Skip to content

Commit

Permalink
Implement SiteMetadata component for SEO & social media representation
Browse files Browse the repository at this point in the history
Implemented the core atom `SiteMetadata` that injects global metadata
like documented in the "SEO & Social Media Representation" design
concept (1). Next to general data like the page title and canonical URL
it includes data for the Open Graph Protocol (2) and Twitter Cards (3).
The component doesn't render any UI, but injects the elements into the
`<head>` using React Helmet (4).

For more details read the great documentation about SEO with Gatsby (5).

References:
  (1) #100
  (2) http://ogp.me
  (3) https://developer.twitter.com/en/docs/tweets/optimize-with-cards/guides/getting-started.html
  (4) https://github.com/nfl/react-helmet
  (5) https://www.gatsbyjs.org/docs/seo

Associated epic: GH-100
GH-101
  • Loading branch information
arcticicestudio committed Dec 22, 2018
1 parent b09a09c commit 42bc8d0
Show file tree
Hide file tree
Showing 7 changed files with 453 additions and 3 deletions.
78 changes: 78 additions & 0 deletions src/components/atoms/core/SiteMetadata/JsonLd.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (C) 2018-present Arctic Ice Studio <[email protected]>
* Copyright (C) 2018-present Sven Greb <[email protected]>
*
* Project: Nord Docs
* Repository: https://github.com/arcticicestudio/nord-docs
* License: MIT
*/

import React from "react";
import PropTypes from "prop-types";
import Helmet from "react-helmet";

/**
* Provides the linked data schema using the JSON-LD specification.
*
* @author Arctic Ice Studio <[email protected]>
* @author Sven Greb <[email protected]>
* @since 0.4.0
* @see https://json-ld.org
* @see https://schema.org
*/
const JsonLd = ({ author, canonicalUrl, defaultTitle, description, imageUrl, keywords, title, url }) => {
const baseSchema = [
{
"@context": "http://schema.org",
"@type": "Website",
url,
canonicalUrl,
name: title,
alternateName: defaultTitle,
description,
author: {
"@type": "Person",
name: author.name,
url: author.url
},
publisher: {
"@type": "Organization",
name: author.name,
url: author.url,
logo: imageUrl
},
creator: {
"@type": "Person",
name: author.name,
url: author.url
},
image: {
"@type": "ImageObject",
url: imageUrl
},
keywords
}
];

return (
<Helmet>
<script type="application/ld+json">{JSON.stringify(baseSchema)}</script>
</Helmet>
);
};

JsonLd.propTypes = {
author: PropTypes.shape({
name: PropTypes.string,
url: PropTypes.string
}).isRequired,
canonicalUrl: PropTypes.string.isRequired,
defaultTitle: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
imageUrl: PropTypes.string.isRequired,
keywords: PropTypes.arrayOf(PropTypes.string).isRequired,
title: PropTypes.string.isRequired,
url: PropTypes.string.isRequired
};

export default React.memo(JsonLd);
162 changes: 162 additions & 0 deletions src/components/atoms/core/SiteMetadata/SiteMetadata.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* Copyright (C) 2018-present Arctic Ice Studio <[email protected]>
* Copyright (C) 2018-present Sven Greb <[email protected]>
*
* Project: Nord Docs
* Repository: https://github.com/arcticicestudio/nord-docs
* License: MIT
*/

import React, { Fragment } from "react";
import PropTypes from "prop-types";
import { Helmet } from "react-helmet";
import { graphql, StaticQuery } from "gatsby";

import metadataBanner from "assets/images/metadata-banner.png";

import JsonLd from "./JsonLd";

const PureSiteMetadata = ({
data: {
site: {
siteMetadata: {
keywords: keywordsNordDocs,
nord: {
author,
description,
keywords: keywordsNord,
links: {
social: { twitter }
},
title
},
siteUrl
}
}
},
pathName
}) => (
<Fragment>
<Helmet defaultTitle={title} titleTemplate={`${title} | %s`}>
<html lang="en" prefix="og: http://ogp.me/ns#" />
<meta content={description} name="description" />
<meta content={author.name} name="author" />
<meta content={Array.from(new Set([...keywordsNord, ...keywordsNordDocs]))} name="keywords" />
<meta content={author.name} name="creator" />
<meta content={author.name} name="publisher" />

<meta content={`${siteUrl}${pathName}`} property="og:url" />
<meta content="website" property="og:type" />
<meta content="en" property="og:locale" />
<meta content={title} property="og:title" />
<meta content={title} property="og:site_name" />
<meta content={`${siteUrl}${metadataBanner}`} property="og:image" />
<meta content="image/png" property="og:image:type" />
<meta content="1200" property="og:image:width" />
<meta content="630" property="og:image:height" />
<meta content={description} property="og:description" />

<meta content="summary_large_image" name="twitter:card" />
<meta content={`@${twitter.id}`} name="twitter:site" />
<meta content={title} name="twitter:title" />
<meta content={description} name="twitter:description" />
<meta content={`${siteUrl}${metadataBanner}`} name="twitter:image" />
<meta content="A banner consisting of Nord's logo and a caption" name="twitter:image:alt" />
<meta content={`@${twitter.id}`} name="twitter:creator" />
</Helmet>
<JsonLd
author={author}
canonicalUrl={siteUrl}
defaultTitle={title}
description={description}
imageUrl={`${siteUrl}${metadataBanner}`}
keywords={Array.from(new Set([...keywordsNord, ...keywordsNordDocs]))}
title={title}
url={`${siteUrl}${pathName}`}
/>
</Fragment>
);

/**
* Provides metadata tags that'll be injected into the `<head>` for SEO & social media purposes including
* "Twitter Card", "Open Graph Protocol" and "JSON-LD" specification elements.
*
* @author Arctic Ice Studio <[email protected]>
* @author Sven Greb <[email protected]>
* @since 0.4.0
* @see https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/summary-card-with-large-image
* @see https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/markup
* @see https://developers.facebook.com/docs/sharing/opengraph/object-properties
* @see http://ogp.me
* @see https://developers.facebook.com/docs/sharing/best-practices
* @see https://json-ld.org
* @see https://schema.org
* @see https://rdfa.info
* @see https://en.wikipedia.org/wiki/RDFa
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta
*/
const SiteMetadata = ({ pathName, ...passProp }) => (
<StaticQuery
query={graphql`
{
site {
siteMetadata {
keywords
nord {
author {
name
url
}
description
keywords
links {
social {
twitter {
id
}
}
}
title
}
siteUrl
}
}
}
`}
/* eslint-disable-next-line react/jsx-no-bind */
render={data => <PureSiteMetadata data={data} pathName={pathName} {...passProp} />}
/>
);

PureSiteMetadata.propTypes = {
data: PropTypes.shape({
site: PropTypes.shape({
siteMetadata: PropTypes.shape({
keywords: PropTypes.arrayOf(PropTypes.string),
nord: PropTypes.shape({
author: PropTypes.shape({
name: PropTypes.string,
url: PropTypes.string
}),
description: PropTypes.string,
keywords: PropTypes.arrayOf(PropTypes.string),
links: PropTypes.shape({
social: PropTypes.shape({
twitter: PropTypes.shape({
id: PropTypes.string
})
})
}),
title: PropTypes.string
}),
siteUrl: PropTypes.string
})
})
}).isRequired,
pathName: PropTypes.string.isRequired
};

SiteMetadata.propTypes = { pathName: PropTypes.string.isRequired };

export { PureSiteMetadata };
export default SiteMetadata;
14 changes: 14 additions & 0 deletions src/components/atoms/core/SiteMetadata/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright (C) 2018-present Arctic Ice Studio <[email protected]>
* Copyright (C) 2018-present Sven Greb <[email protected]>
*
* Project: Nord Docs
* Repository: https://github.com/arcticicestudio/nord-docs
* License: MIT
*/

import JsonLd from "./JsonLd";
import SiteMetadata, { PureSiteMetadata } from "./SiteMetadata";

export { JsonLd, PureSiteMetadata };
export default SiteMetadata;
7 changes: 5 additions & 2 deletions src/components/layouts/core/BaseLayout/BaseLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import PropTypes from "prop-types";
import Header from "organisms/core/Header";
import Page from "containers/core/Page";
import Root from "containers/core/Root";
import SiteMetadata from "atoms/core/SiteMetadata";

/**
* The base page layout providing the main container that wraps the content.
Expand All @@ -21,17 +22,19 @@ import Root from "containers/core/Root";
* @author Sven Greb <[email protected]>
* @since 0.3.0
*/
const BaseLayout = ({ children }) => (
const BaseLayout = ({ children, pathName }) => (
<Root>
<Fragment>
<SiteMetadata pathName={pathName} />
<Header />
<Page>{children}</Page>
</Fragment>
</Root>
);

BaseLayout.propTypes = {
children: PropTypes.node.isRequired
children: PropTypes.node.isRequired,
pathName: PropTypes.string.isRequired
};

export default BaseLayout;
36 changes: 36 additions & 0 deletions test/__mocks__/gatsby.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (C) 2018-present Arctic Ice Studio <[email protected]>
* Copyright (C) 2018-present Sven Greb <[email protected]>
*
* Project: Nord Docs
* Repository: https://github.com/arcticicestudio/nord-docs
* License: MIT
*/

/**
* @file A mock for the `gatsby` module which makes it a lot easier to test components that use the `Link` component or
* any GraphQL feature.
* @author Arctic Ice Studio <[email protected]>
* @author Sven Greb <[email protected]>
* @see https://www.gatsbyjs.org/docs/unit-testing/#mocking-gatsby
* @see https://jestjs.io/docs/en/manual-mocks
* @since 0.4.0
*/

const React = require("react");

const gatsby = jest.requireActual("gatsby");

module.exports = {
...gatsby,
graphql: jest.fn(),
Link: jest.fn().mockImplementation(
({ to, ...rest }) =>
React.createElement("a", {
...rest,
href: to
})
/* eslint-disable-next-line function-paren-newline */
),
StaticQuery: jest.fn()
};
2 changes: 1 addition & 1 deletion test/__utils__/renderWithTheme.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ import Root from "containers/core/Root";
* @author Sven Greb <[email protected]>
* @since 0.3.0
*/
const renderWithTheme = components => render(<Root>{components}</Root>);
const renderWithTheme = (components, options = {}) => render(<Root>{components}</Root>, options);

export default renderWithTheme;
Loading

0 comments on commit 42bc8d0

Please sign in to comment.