A React single-page app can look perfect in Chrome and still produce blank link previews. The usual reason is architectural: social crawlers do not wait for your bundle to run. This guide explains that gap and the fixes that actually work—without pretending useEffect meta tags are enough.
For the general missing-image checklist, see og:image not showing. Framework-specific implementations: Next.js, Astro.
What goes wrong in a client-only SPA
Typical CRA/Vite index.html:
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
Your router then sets document.title and injects meta tags in the browser. Facebook’s facebookexternalhit, LinkedInBot, WhatsApp, and similar agents fetch the URL and parse that first document. They often never execute your JS. Result: no og:image, or a single sitewide default from the shell.
DevTools Elements after hydration is not proof. Always check View Source or curl -sL https://yoursite.com/page | findstr og:image.
Correct mental model
| Layer | User browser | Social crawler |
|---|---|---|
| Downloads HTML | Yes | Yes |
| Runs React | Yes | Usually no |
| Sees helmet/useEffect tags | Yes | No |
Needs absolute og:image in HTML |
Nice | Required |
Fix paths that work
1. Prerender shareable routes
Use a prerender step (for example Vite SSR/prerender plugins or a static-export pass) so each important URL writes an .html file that already contains:
<meta property="og:title" content="Post title" />
<meta property="og:description" content="One-sentence summary" />
<meta property="og:image" content="https://example.com/og/post.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
Host those files so the crawler’s GET returns them without needing the SPA router.
2. Move marketing or blog routes to SSR/SSG
Next.js and Astro emit HTML with metadata APIs or layouts. Keep the interactive app where it matters; put shareable content on routes that SSR/SSG. See Next.js OG images and Astro OG images.
3. Edge or host HTML injection
Some hosts let you attach meta tags per path at the CDN. This can work for a handful of campaign URLs. Prefer real prerender/SSR when you have many pages—injection configs drift.
4. Absolute images regardless of stack
Even with perfect HTML structure, /og.png without a host fails on many platforms. Use https://example.com/og.png. Build cards with the OG Image Generator, validate with the Social Image Checker, and confirm live tags with the Link Preview Checker and Meta Tag Generator.
What does not work (alone)
react-helmet/react-helmet-asynconly in CSR mode- Setting meta inside
useEffectafter fetch - Relying on “Google runs JavaScript” folklore for WhatsApp/Facebook/LinkedIn previews
- One shared
index.htmltitle for every client route
Troubleshooting order
curlor View Source — isog:imagepresent?- Is the image URL absolute and public?
- Did you prerender this path, or only the homepage?
- Platform cache after a real HTML fix (Facebook, LinkedIn, WhatsApp).
Common mistakes
| Mistake | Better approach |
|---|---|
| Testing only in Elements panel | View Source / curl |
Prerendering / but not /blog/:slug |
Prerender every shareable route |
| Relative image paths | Absolute https |
| Huge PNGs for WhatsApp | Compress; see WhatsApp guide |
Checklist
- Raw HTML contains absolute Open Graph tags for each shareable URL
- Images ~1200×630 and publicly fetchable
- Prerender or SSR/SSG chosen deliberately
- Link Preview Checker passes before launch posts
When the architecture is fixed but an image still fails everywhere, continue with og:image not showing.
Organizational patterns that succeed
Product teams often keep a CSR dashboard and a separate marketing site. That split is healthy for Open Graph: put blog posts, docs, and landing pages on Astro or Next, and leave the authenticated app as an SPA. Trying to force every deep link in a client router to be crawler-perfect is expensive.
If you must share in-app URLs, prerender a static HTML shell for those paths at build time, or serve a minimal HTML response from the edge that includes the correct meta tags and a redirect or soft navigate into the SPA for humans. Document which routes are “shareable” so support staff know which URLs are expected to unfurl.
Measure success with curl in CI against staging. A smoke test that asserts og:image appears in the raw HTML for critical paths catches regressions when someone moves meta back into a client layout. Browser-only snapshot tests will not protect you.
Field notes from real share failures
Most broken previews are boring: a relative URL, a duplicate plugin tag, a cache that nobody re-scraped, or a file that is fine in Chrome but too heavy for a messenger. Resist the urge to rewrite the entire brand system until those basics are proven in View Source and a private window.
Write down the exact URL string you tested, including trailing slash and query parameters. Support threads go sideways when one person scrapes example.com/page and another shares example.com/page/. Treat the share string as part of the specification.
When you finally change artwork, change the path. Overwriting og.png in place is the most common reason a “fixed” card never appears. Pair the new file with a short note in your changelog so the next person knows which version is live.
Verification habits worth repeating
After every deploy that touches templates or SEO plugins, spot-check three URLs: the homepage, one article or product, and one utility page. Look for a single absolute og:image and open that file logged out. This ten-minute habit catches more regressions than redesigning share cards.
Document the owner of social tags for your stack—theme, plugin, or layout component—in the repository README. When something breaks, people should know which lever to pull first. Ambiguous ownership is how duplicate tags accumulate.
Finally, separate “works in our browser” from “works for crawlers.” If you only validate inside an authenticated preview session, you will ship failures to Facebook, LinkedIn, Slack, Discord, WhatsApp, and X without noticing until a campaign is live.
Frequently asked questions
Short answers from this guide.
Why does my React SPA have no og:image on Facebook?
Create React App and many Vite SPAs ship a nearly empty index.html. Tags set in JavaScript after load are invisible to most social crawlers. Prerender or move to SSR/SSG.
Will react-helmet fix Open Graph?
Only if the final HTML that crawlers download already contains the tags. Helmet in a pure CSR app still runs too late for Facebook, LinkedIn, and WhatsApp.
What are practical fixes for a Vite or CRA app?
Prerender routes to static HTML, adopt Next.js or Astro for marketing pages, or inject route-specific meta at the edge/host. Keep absolute https image URLs.
Can I keep the SPA and only fix sharing?
Yes for a limited set of URLs: prerender those paths or serve a static HTML shell per shareable route while the app remains CSR for users.
How do I verify the fix?
Use View Source or curl the URL. If og:image is absent in the raw response, crawlers still fail—even if DevTools Elements looks fine after hydration.