In Week 1 of this series, I found a duplicate og:title bug on valuefy.app and wrote: "Fix: strip the static og: tags out of index.html. About 15 minutes of work, touches one file."
That was twelve days ago.
I curled the homepage this morning. Two og:title tags. Still there.
I went to figure out why.
The setup
Here's what was running this week:
- Eight content automation commits in seven days: "SEO: improve [X] calculator content — add verified benchmarks, examples, internal links." NPV, ROI, Payback Period, CAPM, Dividend Yield, EPS, DCF, Cap Rate, Rental Yield — one calculator per day, every day.
- One site-wide editorial redesign (April 22): 1,437 insertions, 857 deletions, 23 files. New palette (oxblood
#8B2E2A, off-black, paper#F5F2EA), Fraunces serif headlines, masthead-style layout. The redesign cascades to 143+ calculator pages via CSS variable remap. - One targeted SEO tweak on the Purchase Order Generator: title rewritten to "Free Purchase Order Generator & PO Maker," H1 updated to include "PO" and "Maker" variants. Targeting four queries the page already appeared for at positions 10-19.
That's a lot of shipping. The og:title fix — which was supposed to be tonight's work twelve days ago — did not ship.
I expected this to be a story about procrastination. It wasn't.
What I expected vs. what I found
I expected to find that the duplicate og:title was a legacy accident — a leftover static tag I'd never cleaned up after wiring in React Helmet.
Instead, I found git log --oneline -- client/index.html:
79e0c67 Design: site-wide editorial redesign
3ef0538 SEO: add static Open Graph meta tags to index.html for crawler compatibility
1b90426 SEO: remove static meta description from index.html to fix duplicate tag
d7fdcf5 SEO: fix prerender meta description hydration
The static og:title tag was introduced on March 31, deliberately, in a commit that also wrote an explanation for why it was safe. The bug I found in Week 1 wasn't a legacy artifact. It was a design decision.
Finding #1: The commit that introduced it had a valid reason and a wrong assumption
Here's the full commit message from March 31:
SEO: add static Open Graph meta tags to index.html for crawler compatibility
SPA crawlers (RankInPublic, Facebook, Slack) don't execute JS so they
miss og:image from React SEOHelmet. Static fallbacks in index.html
ensure previews work everywhere.
The rationale is completely valid. When Facebook or LinkedIn crawl your React SPA, they don't execute JavaScript. They read index.html as-is. If your og:image only exists in the React Helmet component, social sharing is broken for a large class of crawlers.
The commit added:
<!-- Open Graph defaults (overridden by SEOHelmet per page) -->
<meta property="og:title" content="Valuefy - Free Business Calculators & Financial Tools" />
<meta property="og:description" content="150+ free business calculators..." />
<meta property="og:image" content="https://valuefy.app/og-default.jpg" />
The comment says "overridden by SEOHelmet per page." This is the assumption that turned out to be wrong.
Finding #2: React Helmet adds alongside. It does not remove.
Sit two lines above those new Open Graph tags in index.html. There's already a comment there:
<!-- Meta description intentionally omitted — React Helmet injects the correct
page-specific description at runtime and during prerendering. A static tag
here would create a duplicate that Google reads instead of the real one. -->
That comment is about <meta name="description">, which was correctly removed in 1b90426. The understanding was right: if you put a static meta description in index.html, the prerendering step captures it alongside the React Helmet version, and you get two description tags.
What the March 31 commit missed is that the same behavior applies to og:title and og:description. React Helmet does not replace static tags it didn't create. When the prerender runs, it appends the Helmet-managed tags — with data-rh="true" attributes — to whatever was already in index.html. The static tags stay.
I confirmed this by curling the prerendered homepage:
<meta property="og:title" content="Valuefy - Free Business Calculators & Financial Tools">
...
<meta property="og:title" content="Free Business Calculators & AI Generators | Valuefy" data-rh="true">
Two og:title tags. On every page. When you share a specific calculator on LinkedIn, most crawlers read the first og:title they encounter. That first one is the generic site-wide fallback, not the calculator's specific title.
There's also a stale number in the mix. The static og:description says "150+ free business calculators." Every other file in the codebase says "105+." The static tag is from March 31 and was never updated.
Finding #3: 1,437 lines of redesign — 1 line deleted in index.html
The April 22 redesign is the most significant commit this month. 23 files, 1,437 insertions, 857 deletions. The palette changed, the typography changed, the favicon changed, the nav structure changed (Blog renamed to Journal, direct hub links). The commit message describes it as cascading to 143+ calculator pages.
index.html was touched in that commit. One deletion:
- <link rel="alternate icon" href="/favicon.ico" />
The old favicon ICO. That's it.
I'm not criticizing the choice. The redesign was clearly scoped as a visual layer change, and the og:title is a meta tag problem with a different owner in the commit history. But it's an interesting illustration of how bugs survive complex refactors: when a refactor's scope is well-defined, edge cases at the boundary of that scope don't get pulled in.
Finding #4: The four-week scorecard
49 clicks in the last 28 days across 50 pages. That's up from the Week 1 baseline of 45 total in 90 days — roughly a 3.5× improvement in click rate.
| Period | Clicks |
|---|---|
| 90-day baseline (Week 1) | 45 |
| Last 28 days (Week 4) | 49 |
The directions are right but the absolute numbers are still small. The USA remains structurally broken: 8,221 impressions in 28 days, 3 clicks, CTR 0.036%, average position 56. India gets 892 impressions and 10 clicks at position 16. The US market is generating more than 9× the impressions of India but less than a third of the clicks — and all of that comes from deep-page positions where nobody clicks.
The daily automation is producing work. The positions haven't moved on the core head terms. The authority gap from Week 1 is still the binding constraint.
What I'm going to do about it
-
Fix the og: tags tonight. Remove
og:title,og:description, andog:urlfromindex.html. Keepog:image,og:image:width,og:image:height, andog:type— those are the tags that React Helmet doesn't reliably inject and that non-JS crawlers need. The title and description are page-specific; the image and type are site-wide defaults. Split them correctly. - Update the static og:description count. "150+" is wrong; it should match the "105+" used everywhere else in the codebase. One character change.
- Verify the Purchase Order Generator title change landed. GSC shows "po generator" at position 16.3 over 28 days. The April 21 commit targeted that query. I'll check position week-over-week in two more GSC pulls to see if the title change moved anything.
- Keep the daily automation running. The 3.5× improvement in weekly clicks is directional. I won't interrupt it.
The uncomfortable lesson
The comment in index.html explains the exact problem that the line right below it creates.
That's not unusual. Code comments often describe constraints that are immediately violated — by the person who wrote the comment. It's not laziness; it's the difference between understanding the abstract principle ("duplicates are bad") and applying it consistently to every concrete case that fits the pattern ("so og:title is a duplicate too").
The React Helmet behavior is genuinely non-obvious if you haven't run into it before. <title> and <meta name="description"> get replaced because React Helmet has special handling for those. Custom og: properties don't have the same handling. The distinction isn't documented prominently. You only know it if you've either read deeply into Helmet's code or encountered the resulting bug.
I'm writing it here so the next person building a React SPA with prerendering and React Helmet doesn't spend a week running the wrong commit log search.
The four-week summary: the content machine is running, the click curve is improving slowly, and there's one structural SEO bug left that's been there since before Week 1. That's next.
I'm running these experiments on valuefy.app and writing the findings as I go. If you're building programmatic SEO on a React SPA, dealing with prerendering quirks, or fighting the same "I know about the bug but haven't fixed it" wall — drop a comment or reach out.
I also run AImiten, where we build AI tooling for companies. This side project is where I stress-test ideas before they touch client work.
Top comments (0)