Fixing duplicate meta tags in a React SPA by building a library (feat. react-head-safe)
Hi, I'm Sungjun Um, a frontend developer.
In React-based SPAs, react-helmet and react-helmet-async have been used as a de facto standard for SEO optimization. I have also used these libraries steadily in personal projects and in services at work.
But while running a real service, I found a problem where the initial meta tags defined in index.html and the meta tags created dynamically on page navigation were duplicated. Below are the limits of the existing libraries and how I came to build react-head-safe myself to solve this.
1. Why SEO is still tricky in an SPA
On the first load, a React SPA receives empty HTML (with only the root div) from the server. Then JavaScript runs and renders the content and meta tags, and two problems come up here.
- Crawler limits: Some crawlers only collect the static HTML from before JS runs.
- No initial information: Until JS runs, the title and og tags are empty, so the information is missing when the page is shared on social media.
To prevent this, people usually also write basic meta tags in index.html ahead of time.
2. The duplication issue with react-helmet-async
If you keep default tags in index.html and try to manage them dynamically with react-helmet-async, this is what happens.
- Symptom: The title tag is replaced correctly, but for meta and og tags, new tags keep piling up after the existing ones.
- Cause: react-helmet-async only controls the tags it creates and manages itself (tags with a data attribute). That means it doesn't touch static tags that already exist in index.html.
Result: Two or more meta tags with the same property end up inside <head>, and crawlers can't tell which information to trust. (They usually give priority to the static tag found first.)
3. My choice for a fix: building it myself
I considered two options to solve this problem.
- Contribute to an existing open source project (PR)
- Define the problem clearly and build my own library
I took the second path to act on a goal I'd had for a while, "publishing open source that solves a real problem," and that is how I came to build react-head-safe.
4. react-head-safe: safe head management
react-head-safe focuses on managing meta tags predictably in an SPA.
Core design principles
- Conflict prevention: Even if there are initial tags defined in index.html, the moment the library runs, it finds tags with those attributes (name, property) and removes them.
- Single instance (Singleton): It ensures that only the meta tags for the current page exist in <head>.
- Predictability: On page navigation in CSR, it uses useLayoutEffect to give DOM updates a higher priority.
| Aspect | react-helmet-async | react-head-safe |
|---|---|---|
| Handling static tags | Ignored (duplicates occur) | Detected, then replaced/removed right away |
| Tag management | Controls only the tags it manages | Controls all tags with the same attribute |
| Setup complexity | Needs a Context Provider | Works right away without a Provider |
5. Quick usage
Usage is very intuitive. You use it declaratively inside the component that needs it, without setting up a Provider.
import { ReactHeadSafe } from 'react-head-safe';
function MyPage() {
return (
<>
<ReactHeadSafe
title="My Page Title"
description="This is the page description for SEO."
keywords="react,seo,meta tags"
ogTitle="My Page Title for Social Media"
ogDescription="This is the description for social media."
ogImage="https://example.com/image.jpg"
/>
<div>Your page content...</div>
</>
);
}6. Check out the project
react-head-safe is open source. If you are dealing with duplicate meta tags in an SPA, see the links below for detailed usage and try it in your project!
Feedback, bug reports, and stars help a lot in maintaining and improving the project.
- npm: react-head-safe
- GitHub: umsungjun/react-head-safe
Wrapping up
react-head-safe takes the most practical SEO approach: on the first visit, it shows the default information in index.html to handle crawlers, and after a page change, it safely replaces it with the new information.
It is still at an early stage, but it was meaningful work because I solved a pain point I ran into at work myself.
- #description
- #GitHub
- #keywords
- #meta
- #npm
- #open source
- #react-head-safe
- #react-helmet
- #react-helmet-async
- #title