tutorial // Oct 22, 2021

How to Handle SEO Metadata in Next.js

How to create a custom React component for rendering SEO metadata tags and what tags to include to improve ranking and performance across the web.

How to Handle SEO Metadata in Next.js

Getting Started

For this tutorial, we're going to use the CheatCode Next.js Boilerplate as a starting point for our work. To start, let's clone a copy from Github:

Terminal

git clone https://github.com/cheatcode/nextjs-boilerplate

Next, cd into the project and install its dependencies:

Terminal

cd nextjs-boilerplate && npm install

Finally, go ahead and start up the development server:

Terminal

npm run dev

With that, we're ready to get started.

Creating an SEO component

For this tutorial, we're going to create a React component that will help us to render our SEO data within Next.js. This will make it easy for us to add SEO metadata to any page, only having to change the props we pass to the component.

/components/SEO/index.js

import React from "react";
import PropTypes from "prop-types";
import Head from "next/head";
import settings from "../../settings";

const SEO = (props) => {
  const { title, description, image } = props;
  return (
    <Head>
      <title>{title} | App</title>
      <meta name="description" content={description} />
      <meta itemprop="name" content={title} />
      <meta itemprop="description" content={description} />
      <meta itemprop="image" content={image} />
    </Head>
  );
};

SEO.defaultProps = {
  title: settings && settings.meta && settings.meta.title,
  description: settings && settings.meta && settings.meta.description,
  image:
    settings &&
    settings.meta &&
    settings.meta.social &&
    settings.meta.social.graphic,
};

SEO.propTypes = {
  title: PropTypes.string,
  description: PropTypes.string,
  image: PropTypes.string,
};

export default SEO;

Focusing near the top of the file, for our React component, we're going to use the function component pattern as we don't need access to any lifecycle methods or state for this component.

At the top of the file, we import another React component from the next/head package, <Head />. This is a component that's built-in to Next.js and helps us to define the data that will be displayed (automatically by Next.js) in the <head></head> tags of the HTML for the page where this component is rendered.

For the return value of our component—<SEO />—we render this <Head /> component as an open and close tag. This signifes to React that the content between the open and close tag are children of that component. Though we can't see it, React has a standard prop children that is assigned the markup passed between an open and close tag like we see above. Internally, the <Head /> component reads this children prop and uses it to populate the <head></head> tag in the rendered HTML for the page (what gets sent back to Google and other search engines).

Inbetween those tags, we pass a standard HTML <title /> tag along with a series of <meta /> tags. Even though we're in a React component, this markup represents plain HTML. If we were to copy these tags and paste them into the <head></head> of a plain .html file, the browser would render them without issue.

Here, because we're inside of React—or, more properly, JSX, the markup language React uses—we can pass dynamic values (here, known as a React expression) to the attributes of these tags using curly braces. In the code above, just inside the function body for our component, we're using JavaScript object destructuring to "pluck off" the title, description, and image we anticipate being passed to our <SEO /> component.

Assuming these are defined, we set these in our metadata markup using React expressions, rendering the title in the <title></title> tag and the others as the content attribute of their respective <meta /> tags. It's important to note here: because we're trying to cover all of our bases for the sake of SEO, we're going to see data being passed multiple times to different tags. This is intentional. This is because different search engines will parse our data in different ways. By doing this, we're ensuring maximum compatibility for our content.

Down toward the bottom of our file, we'll notice that we're taking advantage of React's .defaultProps and .propTypes attributes for our component. The latter, .propTypes, is intended to help us validate the contents of the props passed to us. This is for us as developers and doesn't impact our users. Here, using the PropTypes object we've imported up top, we set the expectation of our three props title, description, and image all containing a string value (designated in our code as PropTypes.string).

Just above this, we also define some defaultProps for our <SEO /> component. This is important. Notice that here, we're accessing a value settings which we assume is an object imported from elsewhere in our project. In the boilerplate that we cloned at the start of the tutorial, a convention exists for loading a file of arbitrary settings based on the current environment or value of process.env.NODE_ENV. By default, this value is development and so we expect the boilerplate to have loaded the contents of the /settings/settings-development.js file for us.

/settings/settings-development.js

const settings = {
  graphql: {
    uri: "http://localhost:5001/api/graphql",
  },
  meta: {
    rootUrl: "http://localhost:5000",
    title: "App",
    description: "The app description goes here.",
    social: {
      graphic:
        "https://cheatcode-assets.s3.amazonaws.com/default-social-graphic.png",
      twitter: "@cheatcodetuts",
    },
  },
  routes: {
    authenticated: {
      pathAfterFailure: "/login",
    },
    public: {
      pathAfterFailure: "/documents",
    },
  },
};

export default settings;

Of note, we'll see that in these settings, a meta object is set to a series of key/value pairs. This data is set as the default SEO metadata for our entire site (or, said another way, the fallback data we'll rely on if we don't pass any values for the props of our <SEO /> component).

/components/SEO/index.js

import React from "react";
import PropTypes from "prop-types";
import Head from "next/head";
import settings from "../../settings";

const SEO = (props) => {
  const { title, description, image } = props;
  return (
    <Head>
      <title>{title} | App</title>
      <meta name="description" content={description} />
      <meta itemprop="name" content={title} />
      <meta itemprop="description" content={description} />
      <meta itemprop="image" content={image} />
    </Head>
  );
};

SEO.defaultProps = {
  title: settings && settings.meta && settings.meta.title,
  description: settings && settings.meta && settings.meta.description,
  image:
    settings &&
    settings.meta &&
    settings.meta.social &&
    settings.meta.social.graphic,
};

SEO.propTypes = {
  title: PropTypes.string,
  description: PropTypes.string,
  image: PropTypes.string,
};

export default SEO;

Back in our component, we can see that we're pulling in that settings file and, in our .defaultProps object, passing the contents of the meta object in that file. Again, this ensures that if we do not pass these props, we'll have some data passed as opposed to an empty string or "undefined" value.

Adding metadata tags for social media

While the code we looked at above will certainly get us started for our SEO needs, in modern web terms, that would be like bringing a knife to a gun fight. Because the web has proliferated into different social networks and ever-complex search engine algorithms, it helps our case for the sake of ranking to add more specific data.

In particular, we want to add support for social data from two big sites: Twitter and Facebook. Fortunately, though we'll need to support more tags, the structure of those tags is similar enough that we can automate most of their output.

/components/SEO/index.js

import React from "react";
import PropTypes from "prop-types";
import Head from "next/head";
import settings from "../../settings";

const socialTags = ({
  openGraphType,
  url,
  title,
  description,
  image,
  createdAt,
  updatedAt,
}) => {
  const metaTags = [
    { name: "twitter:card", content: "summary_large_image" },
    {
      name: "twitter:site",
      content:
        settings &&
        settings.meta &&
        settings.meta.social &&
        settings.meta.social.twitter,
    },
    { name: "twitter:title", content: title },
    { name: "twitter:description", content: description },
    {
      name: "twitter:creator",
      content:
        settings &&
        settings.meta &&
        settings.meta.social &&
        settings.meta.social.twitter,
    },
    { name: "twitter:image:src", content: image },
    { name: "twitter:card", content: "summary_large_image" },
    { name: "og:title", content: title },
    { name: "og:type", content: openGraphType },
    { name: "og:url", content: url },
    { name: "og:image", content: image },
    { name: "og:description", content: description },
    {
      name: "og:site_name",
      content: settings && settings.meta && settings.meta.title,
    },
    {
      name: "og:published_time",
      content: createdAt || new Date().toISOString(),
    },
    {
      name: "og:modified_time",
      content: updatedAt || new Date().toISOString(),
    },
  ];

  return metaTags;
};

const SEO = (props) => {
  const { url, title, description, image } = props;

  return (
    <Head>
      <title>{title} | App</title>
      <meta name="description" content={description} />
      <meta itemprop="name" content={title} />
      <meta itemprop="description" content={description} />
      <meta itemprop="image" content={image} />
      {socialTags(props).map(({ name, content }) => {
        return <meta key={name} name={name} content={content} />;
      })}
    </Head>
  );
};

SEO.defaultProps = {
  url: "/",
  openGraphType: "website",
  ...
};

SEO.propTypes = {
  url: PropTypes.string,
  openGraphType: PropTypes.string,
  ...
};

export default SEO;

Before we dig into the social tags, real quick, we want to call attention to the url and openGraphType fields we've added to our propTypes and defaultProps. These represent the url of the page we're currently on (e.g., if we're on a blog post like /posts/the-slug-of-the-post) and an openGraphType which will map to a type value from the Open Graph Protcol's object types definition.

The part we really care about here is up in our return value: the new .map() we're doing.

Here, we've introduced a function up top that returns an array of objects with each object containing the value for a name and content attribute on a <meta /> tag. Notice that the names change based on the specific social network but the structure does not. This is intentional.

While Twitter and Facebook (the og stands for "Open Graph" here which is a standard created by Facebook) have their own unique meta data names, they both use the same mechanism to share that data. In our code, we can take advantage of this and loop over an array of objects, for each one spitting out a <meta /> tag, passing the name and content for the current item we're looping over as attributes on the tag.

To perform that loop, we call the socialTags() function first, passing in the props for our component and then dynamically populate the array of objects that function returns with those prop values. In return, we get back an array of objects which we anticipate down in our return value.

There, we chain a call to .map() on our call to socialTags(props), and for each item in the returned array, render a <meta /> tag with the corresponding attributes for that object.

It's important to note: what you see are just some of the available meta tags for Twitter and Facebook. Depending on your own site, you may want to include fewer or more tags.

For Twitter, you can reference their Card markup documentation and for Facebook, reference the Open Graph protocol documentation.

With these in place, now, when our content is shared on Twitter or Facebook, we'll get a properly displayed "card" element that looks nice in people's timelines.

Adding Google JSON-LD metadata

Before we put our <SEO /> component to use, we want to add one more type of metadata: Google's JSON-LD (the "LD" stands for "Linking Data"). This is the data that Google uses for features like their info cards in search results.

/components/SEO/index.js

import React from "react";
import PropTypes from "prop-types";
import Head from "next/head";
import settings from "../../settings";

const socialTags = ({
  openGraphType,
  url,
  title,
  description,
  image,
  createdAt,
  updatedAt,
}) => { ... };

const SEO = (props) => {
  const { url, title, description, image, schemaType } = props;

  return (
    <Head>
      ...
      {socialTags(props).map(({ name, content }) => {
        return <meta key={name} name={name} content={content} />;
      })}
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{
          __html: JSON.stringify({
            "@context": "http://schema.org",
            "@type": schemaType,
            name: title,
            about: description,
            url: url,
          }),
        }}
      />
    </Head>
  );
};

SEO.defaultProps = {
  url: "/",
  openGraphType: "website",
  schemaType: "Article",
  ...
};

SEO.propTypes = {
  url: PropTypes.string,
  openGraphType: PropTypes.string,
  schemaType: PropTypes.string,
  ...
};

export default SEO;

Just beneath our social tags .map(), now, we've added a <script /> tag with a type attribute set to application/ld+json (the MIME type that Google looks for when checking for JSON-LD data). Because the data for JSON-LD is typically passed as an object between the script tags, we need to "React-ify" it so that we don't get any runtime errors.

To do it, we take advantage of React's dangerouslySetInnerHTML prop, passing it an object with an __html attribute set to the stringified version of our JSON-LD object. When this renders, React will dynamically set the object here as the inner HTML or contents of our <script /> tag for us (making no difference to Google and working all the same).

On the object, we make use of the JSON-LD structure, using the schema.org type definitions to describe our data.

That's it! To wrap up, let's take a look at putting our component to use.

Using our SEO component

To put our component to use, we're going to quickly wire up an example page in our boilerplate. To do it, we're going to create a mock "blog post" in a file called /pages/post/index.js:

/pages/post/index.js

import React from "react";
import PropTypes from "prop-types";
import SEO from "../../components/SEO";
import StyledPost from "./index.css";

const Post = (props) => (
  <StyledPost>
    <SEO
      url={`${props.url}/post`}
      openGraphType="website"
      schemaType="article"
      title="The Fate of Empires"
      description="The only thing we learn from history, it has been said, 'is that men never learn from history'..."
      image={`${props.url}/colosseum.jpeg`}
    />
    <header>
      <h1>The Fate of Empires</h1>
      <h5>Sir John Glubb</h5>
    </header>
    <div>
      <img src="/colosseum.jpeg" alt="Colosseum" />
      <p>
        As we pass through life, we learn by experience. We look back on our
        behaviour when we were young and think how foolish we were. In the same
        way our family, our community and our town endeavour to avoid the
        mistakes made by our predecessors.
      </p>
      <p>
        The experiences of the human race have been recorded, in more or less
        detail, for some four thousand years. If we attempt to study such a
        period of time in as many countries as possible, we seem to discover the
        same patterns constantly repeated under widely differing conditions of
        climate, culture and religion. Surely, we ask ourselves, if we studied
        calmly and impartially the history of human institutions and development
        over these four thousand years, should we not reach conclusions which
        would assist to solve our problems today? For everything that is
        occurring around us has happened again and again before.
      </p>
      <p>
        No such conception ever appears to have entered into the minds of our
        historians. In general, historical teaching in schools is limited to
        this small island. We endlessly mull over the Tudors and the Stewarts,
        the Battle of Crecy, and Guy Fawkes. Perhaps this narrowness is due to
        our examination system, which necessitates the careful definition of a
        syllabus which all children must observe.
      </p>
      <p>
        The only thing we learn from history,’ it has been said, ‘is that men
        never learn from history’, a sweeping generalisation perhaps, but one
        which the chaos in the world today goes far to confirm. What then can be
        the reason why, in a society which claims to probe every problem, the
        bases of history are still so completely unknown?{" "}
      </p>
    </div>
  </StyledPost>
);

Post.propTypes = {
  url: PropTypes.string.isRequired,
};

export const getServerSideProps = (context) => {
  return {
    props: {
      url: context?.req?.headers?.host,
    },
  };
};

export default Post;

The part we care about here most is the rendering of our <SEO /> component. Notice that we've imported this up at the top of our file and are rendering it just inside the <StyledPost /> component here (this is a special type of React component known as a styled component). So you have it, real quick, here's the source for that component (pay attention to the path):

/pages/post/index.css.js

import styled from "styled-components";

export default styled.div`
  max-width: 800px;
  margin: 0 auto;

  header {
    margin: 25px 0;
    padding: 0;
  }

  header h1 {
    font-size: 28px;
    font-weight: bold;
  }

  header h5 {
    color: #888888;
  }

  div img {
    max-width: 100%;
    display: block;
    margin: 0px 0px 25px;
  }

  div p {
    font-size: 18px;
    line-height: 28px;
  }

  @media screen and (min-width: 768px) {
    header {
      margin: 50px 0 50px;
      padding: 0;
    }

    div img {
      max-width: 100%;
      display: block;
      margin: 0px 0px 50px;
    }
  }
`;

Here, we're using the styled-components library included in the Next.js boilerplate we're using to help us dynamically create a React component that returns an HTML <div /> element with the CSS passed between the backticks here as the styles for that <div />. The what and the why aren't terribly important for this tutorial, so after you've added this file, let's jump back to our post page.

/pages/post/index.js

import React from "react";
import PropTypes from "prop-types";
import SEO from "../../components/SEO";
import StyledPost from "./index.css";

const Post = (props) => (
  <StyledPost>
    <SEO
      url={`${props.url}/post`}
      openGraphType="website"
      schemaType="article"
      title="The Fate of Empires"
      description="The only thing we learn from history, it has been said, 'is that men never learn from history'..."
      image={`${props.url}/colosseum.jpeg`}
    />
    <header>
      <h1>The Fate of Empires</h1>
      <h5>Sir John Glubb</h5>
    </header>
    <div>
      <img src="/colosseum.jpeg" alt="Colosseum" />
      <p>
        As we pass through life, we learn by experience. We look back on our
        behaviour when we were young and think how foolish we were. In the same
        way our family, our community and our town endeavour to avoid the
        mistakes made by our predecessors.
      </p>
      <p>
        The experiences of the human race have been recorded, in more or less
        detail, for some four thousand years. If we attempt to study such a
        period of time in as many countries as possible, we seem to discover the
        same patterns constantly repeated under widely differing conditions of
        climate, culture and religion. Surely, we ask ourselves, if we studied
        calmly and impartially the history of human institutions and development
        over these four thousand years, should we not reach conclusions which
        would assist to solve our problems today? For everything that is
        occurring around us has happened again and again before.
      </p>
      <p>
        No such conception ever appears to have entered into the minds of our
        historians. In general, historical teaching in schools is limited to
        this small island. We endlessly mull over the Tudors and the Stewarts,
        the Battle of Crecy, and Guy Fawkes. Perhaps this narrowness is due to
        our examination system, which necessitates the careful definition of a
        syllabus which all children must observe.
      </p>
      <p>
        The only thing we learn from history,’ it has been said, ‘is that men
        never learn from history’, a sweeping generalisation perhaps, but one
        which the chaos in the world today goes far to confirm. What then can be
        the reason why, in a society which claims to probe every problem, the
        bases of history are still so completely unknown?{" "}
      </p>
    </div>
  </StyledPost>
);

Post.propTypes = {
  url: PropTypes.string.isRequired,
};

export const getServerSideProps = (context) => {
  return {
    props: {
      url: context?.req?.headers?.host,
    },
  };
};

export default Post;

Look at our rendering of the <SEO /> component, like we hinted at during its development, all we're doing is passing props with the data we want to map to our various meta tags inside the component. While we're hardcoding our example props here, technically, you could (and likely will) use a React expression to pass some variable value depending on where you render the component.

Before we call this done, real quick, we want to call attention to the usage of getServerSideProps near the bottom of our file. This is a function that Next.js uses to, like the name implies, get any props for our component in a server context before it server-side renders our component. This is important. Server-side rendering is the term used to describe the initial response sent back to an HTTP request. That response "renders" some HTML which the requester receives.

This is how search engines work. Sites like Google have a "crawler" which visits all of the public URLs on the internet. It looks for this initial response to get the HTML it uses to generate search results. This is precisely when we expect our <SEO /> component to be rendered and "picked up" by search engines.

Here, inside of getServerSideProps we want to get the base URL (current domain) for the app and pass it down to our component as a prop url. We want to do this so that when we render our <SEO /> component as part of the initial HTML response, the URL that we pass for the url prop on our component is correct. If we didn't do this, the initial response we send back to a search engine would have an "undefined" URL.

With that, we're ready for a test. Let's open up the http://localhost:5000/post page in our web browser and view the source of our page, checking to make sure our metadata is rendering as expected:

Great. Because we see our metadata rendered here, we can trust that this is what Google (or any other search engine will see) when their crawler requests our website.

Wrapping up

In this tutorial, we learned how to wire up a custom <SEO /> React component to help us dynamically render metadata tags based on the props we passed to that component. We learned about rendering the basic HTML <meta /> tags, as well as the tags necessary for social media sites like Twitter and Facebook. Finally, we learned how to add Google's JSON-LD <script /> to our component to add more context and improve our chances of ranking in search results.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode