How to add dynamic meta tags to React App for SEO
Easy way to set Open Graph tags in React SPA without SSR

The Problem
Seam is a social network, built as a Single Page Application (SPA) in React. I wanted to allow users to share any post on the platform, and have an image, title, and description using Open Graph meta tags so people could see a preview of the post before clicking on it. Plus, it would be better for Search Engine Optimization (SEO).
Notoriously, SEO for React SPAs is a very challenging problem, because there is only one index.html for the whole site. In fact, React’s failures in SEO is part of the the reason why Next.js and Server Side Rendering (SSR) was invented — nonetheless, I didn’t want to migrate my entire codebase just for SEO and social media share cards.
There are off-the-shelf solutions like Prerender or React Snap, which crawl your site first and pre-renders it into static HTML. From there, you can modify the head tags in that HTML for SEO & Social Media Optimization (SMO). However, that doesn’t work for the usecase of a social media site, because new posts are constantly getting created and snapshotting them would be really expensive to maintain.
Most articles recommend react-helmet
, but it doesn’t actually work without Server-side Rendering (SSR). Although react-helmet
does alter the meta tags in your HTML, when social media crawlers come to visit your site, they take the first version of the HTML and don’t wait around for the modified version. If social media share links are not picking up meta tags from React Helmet, here’s how to fix it.
Solution: Using Vercel Middleware to modify HTML
In comes Vercel Middleware to the rescue. We can modify the HTML response and rewrite HTML before sending it to the client if it is a social crawler. In essence, we are using Vercel to create custom HTML for bots. As a bonus, this stops crawlers from ever hitting your main site cache, reducing costs and throughput.
This tutorial assumes that you already have a React app up and running on Vercel, either using Create React App or otherwise.
Edge Middleware is code that executes before a request is processed on a site. Based on the request, you can modify the response. Because it runs before the cache, using Middleware is an effective way of providing personalization to statically generated content.
To start, create a new file middleware.js
in the root of your repo:
// Set pathname where middleware will be executed
export const config = {
matcher: ['/post/:postId*']
}
export default async function middleware(req) {
}
Next, you need to differentiate if it is a bot request or a user request. This way we can leave genuine user requests untampered and they’ll get the real version of the site.
const userAgent = req.headers.get('user-agent')
const socialMediaCrawlerUserAgents = /Twitterbot|facebookexternalhit|Facebot|LinkedInBot|Pinterest|Slackbot|vkShare|W3C_Validator/i;
const isSocialMediaCrawler = socialMediaCrawlerUserAgents.test(userAgent);
// return the actual page if it's a user request
if (!isSocialMediaCrawler) {
return;
}
Otherwise, we want to return a new HTML response with all the meta tags that social media posts are looking for, obviously customized to your own usecase:
// Return an HTML response
return new Response(`
<html>
<head>
<meta charset="UTF-8">
<title>${title}</title>
<meta name="title" content="${title}" />
<meta name="description" content="${description}" />
<meta name="author" content="${author}" />
<meta property="og:title" content="${title}" />
<meta property="og:description" content="${description}" />
<meta property="og:image" content="${seoImage}" />
<meta property="og:url" content="${req.url}" />
<meta property="og:type" content="article" />
<meta name="twitter:title" content="${title}" />
<meta name="twitter:description" content="${description}" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@seam_xyz" />
<meta name="twitter:image" content="${seoImage}" />
</head>
<body><img src=${seoImage} /></body>
</html>`, {
headers: { 'content-type': 'text/html' },
});
That’s it! Running vercel dev
will host the middleware before all your localhost requests too so you can test it before shipping to production.
Some other handy testing tools:
- The Facebook Sharing Debugger to validate that your Open Graph (OG) tags are working as expected
- Inspecting your Open Graph metadata from your Vercel builds
- User-Agent Based Rendering example from Vercel on other possibilies with middleware
If this helped you out, give a clap on Medium, and leave a comment if you’re still stuck.