I spent nearly 3 hours debugging why my React app looked perfect in the browser but turned into a blank card when shared on Twitter.
No image. No title. No description.
Just a sad naked URL.
The frustrating part was that my meta tags were technically correct. The real issue was simpler: most social crawlers do not execute client-side React the same way browsers do.
In this article, you’ll learn how to fix Open Graph React SEO properly using server-rendered meta tags, generate dynamic OG images, and avoid the most common SEO mistakes in React apps without rebuilding your entire stack.
The React SEO Problem Nobody Notices Until Production
If you’ve built a React SPA using Vite, CRA, or React Router, your index.html probably looks something like this:
<!DOCTYPE html>
<html lang="en">
<head>
<title>My React App</title>
<meta name="description" content="Cool app" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Then somewhere inside React:
import { Helmet } from "react-helmet";
export default function BlogPost() {
return (
<>
<Helmet>
<title>React SEO Guide</title>
<meta
property="og:title"
content="React SEO Guide"
/>
<meta
property="og:description"
content="How to fix Open Graph tags in React"
/>
</Helmet>
<h1>Blog Post</h1>
</>
);
}
Looks correct, right?
But many crawlers fetch the initial HTML before React hydrates.
That means they only see:
<title>My React App</title>
…and completely miss your dynamic Open Graph tags.
Result:
- Broken social previews
- Wrong titles on Discord, Twitter, and LinkedIn
- Poor indexing
- Lower click-through rates
This is one of the most common Open Graph React SEO issues developers run into.
The Fastest Fix Is Injecting OG Tags Server Side
You do not need to migrate your app to Next.js.
You just need the server to return fully rendered meta tags.
Here’s a lightweight Express setup that works with existing React apps.
Step 1: Create an Express server
Install Express:
npm install express
Create server.js:
import express from "express";
import fs from "fs";
import path from "path";
const app = express();
app.use(express.static("dist"));
app.get("/post/:slug", (req, res) => {
const slug = req.params.slug;
const post = {
title: "Fix React Open Graph Tags",
description: "Dynamic OG tags for React apps",
image: "https://mysite.com/og-image.png"
};
let html = fs.readFileSync(
path.resolve("dist/index.html"),
"utf8"
);
html = html
.replace("__TITLE__", post.title)
.replace("__DESCRIPTION__", post.description)
.replace("__IMAGE__", post.image);
res.send(html);
});
app.listen(3000);
Step 2: Add placeholders to index.html
<head>
<title>__TITLE__</title>
<meta
property="og:title"
content="__TITLE__"
/>
<meta
property="og:description"
content="__DESCRIPTION__"
/>
<meta
property="og:image"
content="__IMAGE__"
/>
</head>
Now crawlers receive the correct tags immediately.
No hydration required.
Result
When someone shares:
https://mysite.com/post/react-seo-guide
Twitter, Discord, LinkedIn, and Facebook now render:
- Correct title
- Correct description
- Preview image
This is the core fix behind most Open Graph React SEO problems.
Dynamic OG Images Improve CTR More Than Most Developers Expect
Most developers stop after adding titles and descriptions.
But preview images dramatically improve click-through rates.
Static images work, but dynamic images are much better for blogs, changelogs, dashboards, and documentation pages.
Here’s a simple dynamic OG image endpoint using React and Satori.
Install dependencies:
npm install satori sharp
Create og.js:
import satori from "satori";
import sharp from "sharp";
export async function generateOgImage(title) {
const svg = await satori(
{
type: "div",
props: {
style: {
display: "flex",
width: "1200px",
height: "630px",
background: "#111",
color: "#fff",
alignItems: "center",
justifyContent: "center",
fontSize: "64px",
padding: "40px"
},
children: title
}
},
{
width: 1200,
height: 630
}
);
return sharp(Buffer.from(svg))
.png()
.toBuffer();
}
Add route:
app.get("/og/:title", async (req, res) => {
const image = await generateOgImage(
req.params.title
);
res.set("Content-Type", "image/png");
res.send(image);
});
Now your OG tag becomes:
<meta
property="og:image"
content="https://mysite.com/og/react-seo-guide"
/>
Result
Every shared article automatically gets a branded preview image.
That alone can significantly improve engagement on:
- Dev.to
- Twitter/X
- Discord
Crawlers also cache aggressively, so dynamic images help prevent stale previews from sticking around too long.
A Cleaner Approach Using @power-seo
After manually wiring this up across multiple projects, I got tired of repeating the same boilerplate.
Especially:
- Meta tag injection
- Route matching
- OG image generation
- Crawler compatibility
- SSR edge cases
Instead of manually replacing placeholders, you define SEO data per route.
Example:
import { createSEO } from "@power-seo/core";
const seo = createSEO({
routes: {
"/post/:slug": async ({ slug }) => {
const post = await getPost(slug);
return {
title: post.title,
description: post.description,
image: post.image
};
}
}
});
Then mount middleware:
app.use(seo.middleware());
That’s it.
The important thing is not the library itself. The important thing is the architecture:
- Generate tags server-side
- Return crawler-friendly HTML
- Keep metadata tied to routes
- Avoid client-only rendering for SEO
The package simply removes repetitive plumbing.
I wrote a deeper breakdown here:
How to Verify Your Open Graph Tags Actually Work
A lot of developers assume tags work because they appear in DevTools.
That’s not enough.
You need to test what crawlers actually see.
Twitter Card Validator
https://cards-dev.twitter.com/validator
LinkedIn Post Inspector
https://www.linkedin.com/post-inspector/
Facebook Sharing Debugger
https://developers.facebook.com/tools/debug/
Paste your URL and verify:
- Title
- Description
- Image
- Canonical URL
If something looks wrong, the usual causes are:
- Tags are client-rendered
- Crawlers cached old HTML
- Invalid image dimensions
- Missing absolute URLs
Common Open Graph React SEO Mistakes
1. Using React Helmet Alone
react-helmet is useful for browsers.
It is not enough for crawler compatibility unless paired with SSR or prerendering.
2. Forgetting Absolute URLs
This breaks frequently:
<meta property="og:image" content="/image.png" />
Use:
<meta
property="og:image"
content="https://mysite.com/image.png"
/>
3. Wrong Image Size
Recommended dimensions:
1200 × 630
Smaller images often render poorly or fail entirely.
4. Not Caching OG Images
Dynamic generation can become expensive quickly.
Cache aggressively:
res.set(
"Cache-Control",
"public, max-age=86400"
);
What I Learned
- React SPAs often fail social previews because crawlers do not fully execute client-side rendering
- Open Graph React SEO works best when meta tags are generated server-side
- Dynamic OG images improve click-through rates more than most developers expect
-
react-helmethelps browsers, but SSR or prerendering fixes crawler visibility - You do not need a full Next.js migration to solve this problem
- Libraries like
@[power-seo](https://www.npmjs.com/org/power-seo)are useful because they automate repetitive server-side SEO plumbing.
What’s your current React SEO setup?
Are you using SSR, prerendering, middleware injection, or just hoping crawlers execute your app correctly?
I’m curious what approaches people are using in production right now.
Top comments (0)