Dynamically Generating Meta Tags in Next.js

Search Engine Optimization in a Server Side Rendering Progress Web Application

3-min read

Feature

You’re in a subpage on your website and you need to set SEO-related tags based on the information you receive from the back-end. For instance, if a user clicks on an events page, the title tag should expectedly display the eventName, and hopefully relevant information like the artistName and location, formatted for Facebook and Twitter (or any other relevant Open Graph protocol).

Problem Statement

You’ve tried hardcoding the meta tags, like such:

<title>Donda - Kanye West in Pittsburgh '21</title>

but instantly some issues pop up, namely: 1

  1. The data isn’t getting propagated, and/ or is missing.

  2. Duplicated tags (since you have base meta tags for the root page).

Fix

  1. Customized <Head/> component removed from _document.js, and instead imported into _app.js

  2. Each tag has a key prop which handles deduplication.

  3. calling getServerSideProps inside the relevant page.

Explanation:

  1. _document.js doesn’t support the title tag and I’d preferably like to keep all my tags in one single source of truth. 2 The easiest way to do this would be to create a wrapper component which renders all the base meta tags.

  2. Each tag also a key property which will handle potential duplication. I used an enum to handle the various meta tags, so I wouldn’t mistakenly misspell any attributes. I’ve added them below for posterity:

enum MetaTagKeys {
  TITLE = 'title',
  META_TITLE = 'meta_title',
  DESCRIPTION = 'description',
  OG_TYPE = 'og:type',
  OG_TITLE = 'og:title',
  OG_URL = 'og:url',
  OG_DESC = 'og:description',
  OG_IMG = 'og:image',
  TWITTER_CARD = 'twitter:card',
  TWITTER_URL = 'twitter:url',
  TWITTER_TITLE = 'twitter:title',
  TWITTER_DESC = 'twitter:description',
  TWITTER_IMG = 'twitter:image',
}

An example of a meta tag in <DocumentHead /> would look something like this:

<meta name={MetaTagKeys.DESCRIPTION} key={MetaTagKeys.DESCRIPTION} content="Kanye West performs Donda live at the PPG Paints Arena in Pittsburgh on 10/27/2021."/>

This wrapper also contains the various scripts (dangerouslySetInnerHTML), and link icons which inevitably end up in your head tag. I prefer to keep _app.js as clean as possible.

  1. To get Facebook, Twitter, and any other social network to scrape your page and prettily display your meta tags and content, one has to pre-render the page. In Next.js this can be done via getServerSideProps. 3

The final code in all its glory.

import Head from 'next/head'
import { GetServerSideProps, GetServerSidePropsContext } from 'next';

interface MetaTag {
  property: string,
  key?: string,
  content: string,
}

export const getServerSideProps: GetServerSideProps = async ({req}: GetServerSidePropsContext) => {
  const {url = ''} = req;
  const urlSlug = url.split('event/')[1];
  const eInfo = await User.getUserInfo(urlSlug);

  const metaTagTitle = `${eInfo.eventName} -  ${eInfo.artistName} in ${eInfo.eventState}`;

  const metaTagDescription = `${eInfo.artistName} performs live at the ${eInfo.eventLocation} in ${eInfo.eventState} on ${eInfo.eventDate}`;

  const metaTagsList: MetaTag[] = [
    // <meta property="title ... /> has the same property as the <title name="title>...</title>, but still needs a unique key.
    // we handle this logic while mapping over the metaTagsList prop.
    {property: MetaTagKeys.TITLE, key: MetaTagKeys.META_TITLE, content: metaTagTitle},
    {property: MetaTagKeys.DESCRIPTION, content: metaTagDescription},
    // <!-- Open Graph / Facebook -->
    {property: MetaTagKeys.OG_URL, content: url},
    {property: MetaTagKeys.OG_TITLE, content: metaTagTitle},
    {property: MetaTagKeys.OG_DESC, content: metaTagDescription},
    {property: MetaTagKeys.OG_IMG, content: "https://jpt-ugc.s3.ap-southeast-1.amazonaws.com/metaTag.png"},
    // <!-- Twitter -->
    {property: MetaTagKeys.TWITTER_CARD, content: 'summary_large_image'},
    {property: MetaTagKeys.TWITTER_URL, content: url},
    {property: MetaTagKeys.TWITTER_TITLE, content: metaTagTitle},
    {property: MetaTagKeys.TWITTER_DESC, content: metaTagDescription},
    {property: MetaTagKeys.TWITTER_IMG, content: "https://jpt-ugc.s3.ap-southeast-1.amazonaws.com/metaTag.png"}
  ];
  return {
    props: {
      metaTagsList
    }
  }
}

const Home = ({metaTagsList}: HomeProps): JSX.Element => {
// one might have a useEffect() hook here to get additional information or to set the title.

  const metaTagTitle = eInfo && eInfo.displayName !== null ?
    `${eInfo.eventName} -  ${eInfo.artistName} in ${eInfo.eventState}` : null;

return (
          <Head>
            <title
              key={MetaTagKeys.TITLE}>
                {metaTagTitle}
              </title>
            {metaTagsList
             && metaTagsList.map((entry: MetaTag) => (
               <meta
                 property={entry.property}
                 key={entry.key ? entry.key : entry.property}
                 content={entry.content}
               />
            ))}
       )
}

Another StackOverflow solution for reference.

1

(in order of severity).

2

Adding <title> in pages/_document.js will lead to unexpected results with next/head since _document.js is only rendered on the initial pre-render.

3

If you export an async function called getServerSideProps from a page, Next.js will pre-render this page on each request using the data returned by getServerSideProps.