React Helmet and SEO: why meta tags don't show in link previews
React Helmet sets meta tags after JavaScript runs, so Google sees them late and link-preview bots never do. Why, and how to put the tags in the HTML.
LLaunchScaler·Published ·8 min read
React Helmet updates the <head> after your JavaScript runs, so in a client-rendered React app the tags it sets exist only in the browser's DOM, not in the HTML your server sends. Google sees them once it renders the page; link-preview bots and most AI crawlers read the HTML as served, which is your index.html with whatever defaults are hard-coded in it, so every shared link shows the same title and image.
The fix is to get per-route tags into the HTML response: sensible defaults in index.html, then prerendered or server-rendered HTML for every public route.
How does React Helmet set meta tags?
You render <Helmet> with <title>, <meta> and <link> children inside a component, and on the client Helmet collects every instance, removes duplicates and writes the result into the document head by manipulating the DOM. That happens after React has loaded and rendered, which is the root of every Helmet SEO problem.
Two packages carry the name, and they are not interchangeable:
Package
Status
Server rendering
react-helmet
Latest release 6.1.0, published June 2020
Uses react-side-effect, which react-helmet-async's README says is not thread-safe
Questions, answered
What people ask about this
01
Is React Helmet good for SEO?
Only partly. In a client-rendered app, Helmet writes tags into the head after JavaScript runs, so Google picks them up when it renders the page, but link-preview bots and most AI crawlers read the HTML as served and never see them. It works fully only when the page is rendered on the server.
Keeps state per request through <HelmetProvider context={...}>, so concurrent server renders don't mix tags
On the server, react-helmet-async fills a context object you pass to HelmetProvider, and you print helmet.title.toString() and the other tags into your HTML template. That server step is what makes Helmet work for crawlers. A client-only app never runs it.
Why don't React Helmet tags show up in link previews?
Because link-preview bots read the HTML response and a single-page app sends the same index.html for every URL. The tags Helmet adds later exist only in a browser that ran your JavaScript. The bot sees only the defaults in index.html.
What the platforms document:
Meta's crawler (user agent facebookexternalhit/1.1) says Open Graph properties must appear within the first 1 MB of the page or they are cut off.
Slack's link expander reads oEmbed, Twitter Card and Open Graph tags, and fetches as little of the page as it can using HTTP Range requests.
Lovable's docs, describing the same problem for its users, say platforms such as LinkedIn, Slack, Facebook, X and WhatsApp usually read Open Graph metadata directly from the HTML response and often do not execute client-side JavaScript.
AI crawlers behave the same way. Vercel's crawler analysis found that OpenAI's crawlers, ClaudeBot and PerplexityBot fetch JavaScript files but do not execute them, so a title or description set by Helmet does not reach them either.
The OG image not showing guide covers the other reasons a preview breaks, such as relative image URLs and images that are too large.
Does Google see React Helmet tags?
Yes, eventually. Google renders JavaScript and indexes the rendered HTML, and its JavaScript SEO guide says you can use JavaScript to set or change the meta description and the <title>. The catch is timing and a few rules that Helmet makes easy to break.
Rendering is deferred. A page may wait in Google's rendering queue "for a few seconds," or longer, before its Helmet tags exist for Google at all.
Canonical tags. Google says not to use JavaScript to change the canonical to anything other than the value in the original HTML. If index.html carries a site-wide canonical pointing at /, and Helmet changes it per route, you are sending exactly that conflict.
noindex. If the original HTML carries noindex, Google may skip rendering, so removing it with Helmet may not work as expected.
Meta descriptions. Even when Google reads your Helmet description, Google says it primarily uses page content for snippets and uses the meta description when it describes the page better; the meta description not showing guide explains when and why.
Does React 19 replace React Helmet?
For many apps, yes. React 19 lets any component render <title>, <meta> and <link>, and React hoists them into the document head. The React team says this works with client-only apps, streaming server rendering and Server Components, and that libraries like Helmet can build on it rather than be replaced by it.
function PricingPage() {
return (
<>
<title>Pricing | Acme</title>
<meta name="description" content="Plans from free to team, billed monthly or yearly." />
<link rel="canonical" href="https://acme.com/pricing" />
<h1>Pricing</h1>
</>
)
}
Two rules from React's docs: render only one <title> at a time, since several at once leave browsers and search engines with undefined behaviour, and pass the title a single string, such as one template literal, rather than text mixed with a JSX expression.
React 19 changes where the tags come from, not when a client-only app renders them. In a Vite single-page app with no server rendering, these tags still appear only after JavaScript runs, so link-preview bots and AI crawlers still miss them. react-helmet-async's README makes the same point from the other side: from version 3.0.0 it detects React 19 and lets React do the hoisting, and it suggests a new React 19 project may not need the package at all unless it relies on features like titleTemplate, htmlAttributes or server context serialization.
How do you fix React Helmet meta tags for crawlers?
Put the right tags in the HTML response for every public route. Keep Helmet or React 19 metadata for the browser tab if you like, but make sure crawlers never depend on it. Work through these in order; the first is a five-minute fix, the last is the complete one.
Set real defaults in index.html. Give the file your site name as <title>, a real <meta name="description">, and og:title, og:description, og:image (an absolute https:// URL) and twitter:card. Every shared link then at least shows your brand instead of a blank card.
Remove a site-wide canonical from index.html unless every route really is that URL. A canonical pointing at the homepage on every route tells Google all your pages are duplicates of /.
Prerender your public routes. web.dev defines prerendering as running a client-side app at build time to capture its initial state as static HTML. A prerender step writes one HTML file per route (/pricing/index.html, /blog/post/index.html), each with its own tags from Helmet or React 19, and your host serves those files.
Or inject tags at the server or edge. A small handler that reads the route, looks up its title, description and image, and writes them into index.html before sending it gives bots correct tags without changing the app.
Or move the marketing pages to a server-rendering framework. web.dev encourages server or static rendering over a full rehydration approach, and a framework that renders on the server writes the metadata for each route into the HTML response it sends.
The client-side rendering SEO guide compares those options in detail. If your React app came from Lovable, check which stack it is on first: the Lovable SEO guide explains how newer Lovable projects render on the server and how older ones are prerendered for verified crawlers.
Should you prerender or server-render a React app?
Prerender when your public pages change only when you deploy; server-render when their content changes between deploys or depends on data you can't bake in at build time. Both put per-route tags in the HTML response, which is all link-preview bots and AI crawlers need.
Your public pages
Choose
Why
A fixed set: home, pricing, features, about
Prerender at build
Nothing to run per request; any static host serves the files
A blog or docs you publish by deploying
Prerender at build
Each new post is a new HTML file on the next build
Listings, profiles or products from a database
Server rendering
New records need tags the build has never seen
Pages behind a sign-in
Neither
They are not meant to be crawled or shared publicly
Prerendering has one trap worth knowing: it captures the page as it looks at build time. If a route shows data that is fetched after load, the prerendered HTML contains the loading state, not the data. Fetch that data during the build, or move the route to server rendering.
Server rendering has its own: a browser-only library (anything that touches window or document on import) can crash the server render. Load those in the browser only, after hydration.
How do you check which tags crawlers receive?
Request the page without running JavaScript and read the head. curl -s https://yoursite.com/pricing | grep -iE "<title>|og:title|og:image|name=\"description\"|rel=\"canonical\"" prints exactly what a non-rendering bot gets. If the output shows your homepage defaults on the pricing URL, Helmet's per-route tags are not reaching crawlers.
Repeat it for two or three different routes; in a client-only app they will all print the same lines. After deploying a fix, re-share a link in a private channel or use the platform's own preview debugger, since platforms cache previews.
To check a live page's served tags alongside the rest of its search setup, run the free scan on LaunchScaler. It needs only your URL and no account, and its free checks read the title, meta description, canonical, robots directives and structured data from the HTML as served, and compare the raw page with a real browser render to flag content that only appears after JavaScript runs, among 156 checks across 6 of its 7 categories.
Social and chat platforms read Open Graph tags from the HTML response, and a single-page app serves the same index.html for every route. The preview shows whatever title and image are hard-coded there, not the tags Helmet sets later in the browser.
03
What is the difference between react-helmet and react-helmet-async?
react-helmet-async is a fork that keeps Helmet's state per request, which makes it safe for server rendering, and it needs a HelmetProvider. The original react-helmet package's latest release, 6.1.0, dates from June 2020.
04
Do I still need React Helmet with React 19?
Often not. React 19 renders title, meta and link tags from any component and hoists them to the head, including during streaming server rendering. react-helmet-async's own README says a new React 19 project may not need the package unless it uses features such as titleTemplate.
05
Does Google read meta tags added by JavaScript?
Yes, after rendering. Google says you can use JavaScript to set the title and meta description, but a canonical changed by JavaScript should match the original HTML, and a noindex in the original HTML may stop rendering altogether.