If you build React applications and care about SEO, you have probably hit a frustrating wall: search engines need structured data in your HTML, but React renders everything client-side by default. Adding React schema markup the right way means understanding JSON-LD, picking the correct injection method, and handling server-side rendering when it matters.
In this hands-on guide, we walk through exactly how to inject valid Schema.org JSON-LD into a React app using react-helmet, with copy-paste examples for articles, products, and breadcrumbs. We also cover the SSR pitfalls that most tutorials skip.
Why Schema Markup Matters for React Apps
Schema markup is structured data that helps Google, Bing, and other search engines understand the content on your page. When implemented correctly, it can unlock rich results: star ratings, product prices, FAQ accordions, breadcrumb trails, and more.
For React developers specifically, the challenge is twofold:
- Client-side rendering means crawlers may not see your JSON-LD on first load
- Dynamic content (product pages, blog articles) needs schema generated per route
JSON-LD is the format Google officially recommends. It lives inside a <script type="application/ld+json"> tag in the document head, which makes it perfect for tools like react-helmet.

Choosing Your Approach: react-helmet vs react-schemaorg vs Next.js
Before writing code, pick the right tool for your stack.
| Approach | Best For | SSR Support | Type Safety |
|---|---|---|---|
| react-helmet-async | CRA, Vite, custom SSR | Yes | No |
| react-schemaorg | Teams needing strict typing | Yes (via Helmet) | Yes (TypeScript) |
| Next.js Metadata API | Next.js 13+ App Router | Built-in | Partial |
| Raw script tag in index.html | Static, global schema only | N/A | No |
For this guide, we focus on react-helmet-async because it works across most React setups and handles SSR cleanly.
Setting Up react-helmet-async
Install the package:
npm install react-helmet-async
Wrap your app with the provider in index.js or main.tsx:
import { HelmetProvider } from 'react-helmet-async';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<HelmetProvider>
<App />
</HelmetProvider>
);
Example 1: Article Schema for Blog Posts
Create a reusable component that accepts post data and injects the correct JSON-LD:
import { Helmet } from 'react-helmet-async';
function ArticleSchema({ post }) {
const schema = {
"@context": "https://schema.org",
"@type": "Article",
"headline": post.title,
"description": post.excerpt,
"image": post.featuredImage,
"datePublished": post.publishedAt,
"dateModified": post.updatedAt,
"author": {
"@type": "Person",
"name": post.author.name,
"url": post.author.profileUrl
},
"publisher": {
"@type": "Organization",
"name": "Coding4",
"logo": {
"@type": "ImageObject",
"url": "https://coding4.net/logo.png"
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": post.url
}
};
return (
<Helmet>
<script type="application/ld+json">
{JSON.stringify(schema)}
</script>
</Helmet>
);
}
export default ArticleSchema;
Drop it into any blog post page: <ArticleSchema post={currentPost} />.
Validating Your Output
Always test with Google’s Rich Results Test and the Schema.org Validator. Common mistakes to watch for:
- Dates must be in ISO 8601 format
- Image URLs must be absolute, not relative
- The
authorfield requires a Person or Organization with a name

Example 2: Product Schema for E-commerce
Product schema unlocks price, availability, and review stars directly in search results.
function ProductSchema({ product }) {
const schema = {
"@context": "https://schema.org",
"@type": "Product",
"name": product.name,
"image": product.images,
"description": product.description,
"sku": product.sku,
"brand": {
"@type": "Brand",
"name": product.brand
},
"offers": {
"@type": "Offer",
"url": product.url,
"priceCurrency": product.currency,
"price": product.price,
"availability": product.inStock
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",
"itemCondition": "https://schema.org/NewCondition"
},
"aggregateRating": product.reviewCount > 0 ? {
"@type": "AggregateRating",
"ratingValue": product.averageRating,
"reviewCount": product.reviewCount
} : undefined
};
return (
<Helmet>
<script type="application/ld+json">
{JSON.stringify(schema)}
</script>
</Helmet>
);
}
Notice the conditional aggregateRating: Google penalizes pages that declare ratings without actual reviews, so only include the field when data exists.
Example 3: BreadcrumbList Schema
Breadcrumbs are simple but high-value. They replace your URL in search results with a clean navigation path.
function BreadcrumbSchema({ items }) {
const schema = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": items.map((item, index) => ({
"@type": "ListItem",
"position": index + 1,
"name": item.name,
"item": item.url
}))
};
return (
<Helmet>
<script type="application/ld+json">
{JSON.stringify(schema)}
</script>
</Helmet>
);
}
// Usage
const breadcrumbs = [
{ name: "Home", url: "https://coding4.net/" },
{ name: "Blog", url: "https://coding4.net/blog/" },
{ name: "React Schema Markup", url: "https://coding4.net/blog/react-schema-markup/" }
];
Handling Multiple Schemas on One Page
It is perfectly valid to include several JSON-LD blocks on the same page. For example, an article page can have Article, BreadcrumbList, and Organization schemas at once. You have two options:
- Multiple Helmet components, each with its own script tag. React-helmet-async merges them correctly.
- A single graph object using
@graphto bundle entities together.
The graph approach looks like this:
const schema = {
"@context": "https://schema.org",
"@graph": [
{ "@type": "Article", ... },
{ "@type": "BreadcrumbList", ... },
{ "@type": "Organization", ... }
]
};

The SSR Problem (and How to Solve It)
Google’s crawler can now execute JavaScript, but relying on client-side rendering for structured data is risky. Indexation can be delayed by days, and other crawlers (Bing, social platforms) often skip JS entirely.
Here are your real options:
- Next.js App Router: Use the
generateMetadatafunction or inline a<script>tag in server components - Remix: Use the
metaexport or render directly in route components (they SSR by default) - Vite + react-helmet-async with SSR: Call
HelmetProviderwith a context object on the server and inject collected tags into your HTML template - Static prerendering: Tools like react-snap or vite-plugin-prerender bake schema into HTML at build time
Next.js App Router Pattern
If you are on Next.js 14 or 15, the cleanest pattern looks like this:
// app/blog/[slug]/page.tsx
export default async function Page({ params }) {
const post = await getPost(params.slug);
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
"headline": post.title,
// ...
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<article>{post.content}</article>
</>
);
}
This renders the JSON-LD server-side with zero extra dependencies.
Common Mistakes to Avoid
- Escaping quotes manually: Use
JSON.stringifyinstead of writing JSON inline as a string - Mismatched content: The data in your schema must match what users see on the page
- Stale schema after navigation: When using SPAs, ensure your schema component re-renders on route change
- Forgetting absolute URLs: Always use full
https://URLs for images, authors, and pages - Adding schema for content that does not exist: Do not declare a FAQ schema if the page has no visible FAQ
Testing Checklist
- Run the page through the Google Rich Results Test
- Validate with the Schema.org Validator for syntax errors
- Check the rendered HTML with
view-source:to confirm SSR worked - Monitor the Enhancements tab in Google Search Console after deployment
- Re-test after every major content or template change
FAQ
Can Google read JSON-LD added by React on the client side?
Yes, but with limits. Googlebot renders JavaScript, but indexation is delayed and not guaranteed. For mission-critical pages, server-side rendering or static prerendering is strongly recommended.
Should I use react-helmet or react-helmet-async?
Use react-helmet-async. The original react-helmet is no longer actively maintained and has known issues with concurrent React features.
Can I put multiple JSON-LD scripts on the same page?
Yes. Google explicitly supports multiple JSON-LD blocks per page. You can also combine them into a single @graph object if you prefer cleaner output.
Do I need react-schemaorg or is plain JSON-LD enough?
Plain JSON-LD is enough for most projects. The react-schemaorg library adds TypeScript types, which is useful for large teams or codebases that want strict schema validation at compile time.
How do I add FAQ schema in React?
Use the same pattern as the examples above with @type: "FAQPage". The mainEntity array should contain Question objects, each with an acceptedAnswer. Only add FAQ schema when the questions and answers are actually visible on the page.
Will schema markup directly improve my rankings?
Schema is not a direct ranking factor, but it improves click-through rates by enabling rich results. Higher CTR can indirectly boost rankings over time.
Wrapping Up
Adding structured data to a React app is no longer painful once you have the right pattern. Pick react-helmet-async for classic React or Vite projects, use server components in Next.js, and always validate your output. Start with Article, Product, and Breadcrumb schemas: they cover the majority of rich result opportunities and take less than an hour to implement.
Need help auditing your React app’s SEO or implementing schema at scale? The team at Coding4 builds production-grade React applications with SEO baked in from day one.

