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
The data isn’t getting propagated, and/ or is missing.
Duplicated tags (since you have base meta tags for the root page).
Fix
Customized
<Head/>
component removed from_document.js
, and instead imported into_app.js
Each tag has a
key
prop which handles deduplication.calling
getServerSideProps
inside the relevant page.
Explanation:
_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.
Each tag also a
key
property which will handle potential duplication. I used anenum
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.
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.
(in order of severity).
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.
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
.