OG images for React SPAs

Last updated October 30, 2026 · Reviewed by Sumera Yaqoob

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-async only in CSR mode
  • Setting meta inside useEffect after fetch
  • Relying on “Google runs JavaScript” folklore for WhatsApp/Facebook/LinkedIn previews
  • One shared index.html title for every client route

Troubleshooting order

  1. curl or View Source — is og:image present?
  2. Is the image URL absolute and public?
  3. Did you prerender this path, or only the homepage?
  4. 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.

Related pages