Skip to content

seo: purge thin compare pages + benchmark-first title on products & compare - #908

Merged
Flotapponnier merged 3 commits into
devfrom
fix/seo-foundations
Jul 5, 2026
Merged

seo: purge thin compare pages + benchmark-first title on products & compare#908
Flotapponnier merged 3 commits into
devfrom
fix/seo-foundations

Conversation

@Flotapponnier

Copy link
Copy Markdown
Collaborator

Summary

Bing WMT audit 2026-07-05 showed only 2 of 5093 sitemap URLs indexed. Root cause: 4938 ad-hoc /compare/* pages emitted at ≥1 shared bench threshold, producing near-duplicate templates that starved Bing's crawl budget and hurt domain trust.

Changes

1. Sitemap — hybrid threshold (src/app/sitemap.ts + new src/lib/compare/brand-whitelist.ts)

  • Both providers in BRAND_WHITELIST → emit at ≥ 1 shared bench
  • Otherwise → emit only at ≥ 3 shared benches
  • Drops from 4938 → ~226 ad-hoc + 21 curated ≈ 247 URLs
  • Preserves every commercial X vs Y pair users actually search for: helius-vs-mobula, alchemy-vs-moralis, chain-vs-chain, perp-vs-perp, PM-vs-PM, bridge-vs-bridge…

2. Curated pair fix (src/data/compare-pairs.ts)

  • Remove jupiter-vs-raydium — raydium has 0 bench appearances so the page 404s at render via hasSharedBenches(). Re-add when raydium is measured in any OCB bench.

3. Product page SEO (src/app/products/[slug]/page.tsx)

  • Title now leads with {Provider} Benchmark {year} — head-term match on <provider> benchmark queries (the query pattern OCB gets from LLM search).
  • Meta description ends with As of YYYY-MM-DD — LLM extractability signal (ChatGPT/Perplexity/Copilot cite dated content more).

4. Compare page SEO (src/app/compare/[slug]/page.tsx)

  • Title now leads with {a} vs {b} Benchmark {year}.
  • Meta description is unique per pair — bench count + provider names + date. Kills the identical duplicate signal that had Bing skipping compare pages.

Numbers

Simulated locally on current bench specs:

Threshold Total URLs Brand-vs-brand coverage
≥ 1 (current) 4959 ✅ but with 4712 garbage
≥ 2 uniform 165 ⚠️ 110 brand pairs lost
≥ 3 uniform 54 ❌ 191 brand pairs lost
Hybrid (this PR) 247 ✅ all preserved

Test plan

  • pnpm typecheck — clean
  • pnpm lint — clean
  • Manual on staging: /products/moralis → title contains 'Moralis Benchmark 2026'
  • Manual on staging: /compare/codex-vs-mobula → title 'Codex vs Mobula Benchmark 2026', meta unique
  • Manual on staging: /compare/jupiter-vs-raydium → 404 (removed)
  • Manual on staging: /compare/metascalp-vs-wunder → 404 (dropped by hybrid threshold)
  • Sitemap.xml on staging → ~250 /compare/* URLs (was 4865)

Follow-ups (separate PRs)

  • Per-URL <lastmod> from actual data timestamps (currently a single global timestamp)
  • Split sitemap into per-category files + index
  • IndexNow wire-up for Bing fast-indexation
  • Unique intro content per compare page via LLM at build-time (~$0.20 total)

Florent Tapponnier added 2 commits July 5, 2026 10:49
…ompare

Bing WMT audit 2026-07-05: only 2 of 5093 sitemap URLs indexed. Root
cause: 4938 ad-hoc /compare/ pages emitted at ≥1 shared bench threshold,
producing near-duplicate templates that starved crawl budget and hurt
domain trust.

Changes:
- sitemap: hybrid threshold. Brand-whitelist pairs emit at ≥1 shared,
  others at ≥3. Drops from 4938 → ~226 ad-hoc + 21 curated = ~247 URLs.
  Preserves every commercial X vs Y pair users actually search for
  (helius-vs-mobula, alchemy-vs-moralis, chain-vs-chain, perp-vs-perp).
- compare-pairs: remove jupiter-vs-raydium (raydium has 0 bench
  appearances so /compare/jupiter-vs-raydium 404s at render).
- products/[slug]: title now leads with 'Benchmark {year}' + head-term
  match on '<provider> benchmark' queries. Meta description ends with
  'As of YYYY-MM-DD' for LLM citation extractability (LLM-mediated
  discovery drives ~80% of Bing query traffic).
- compare/[slug]: title now leads with '{a} vs {b} Benchmark {year}'.
  Meta description is unique per pair (shared bench count + date)
  killing the identical duplicate signal that had Bing skipping pages.
Replaces the identical templated intro paragraph on both pages with
a data-driven prose summary derived from live measurements. Kills the
last piece of duplicate above-the-fold text so Bing sees genuinely
unique substantive content per URL.

/compare/[slug]:
  Before: 'Side by side OpenChainBench measurements. Identical layout,
          no editorial verdict...'  (same on 247 pages)
  After:  'Codex leads on 2 of 4 shared benchmarks, Mobula on 2. Codex
          wins on aggregator-head-lag (128ms vs 195ms), wallet-labels-
          coverage (42.8% vs 38.1%). Mobula wins on metadata-coverage
          (96.9% vs 89.1%), network-coverage (80 vs 42 chains).'

/products/[slug]:
  Before: 'Moralis performance benchmarks, live across 2 categories.
          Reproducible measurements, open methodology.'  (same on 104 pages)
  After:  'Moralis is measured across 2 live OpenChainBench benchmarks,
          with 1 #1 finish: NFT collection metadata (ranks #1, 96.9% p50),
          Wallet labels coverage (ranks #2 of 5, 42.4% p50).'

Falls back to a neutral sentence when the provider's p50 data is
missing (cold ISR, harness restart) so cold pages don't render a lie.
@Flotapponnier

Copy link
Copy Markdown
Collaborator Author

Follow-up commit 46be62e : data-driven prose above the fold on both /compare/[slug] and /products/[slug]. Replaces the identical templated intro paragraphs (which were the last piece of above-the-fold duplicate content) with unique per-page summaries derived from live Prom data.

Example /products/moralis before → after:

  • Before: 'Moralis performance benchmarks, live across 2 categories. Reproducible measurements, open methodology.' (identical on 104 product pages)
  • After: 'Moralis is measured across 2 live OpenChainBench benchmarks, with 1 drop journal framing #1 finish: NFT collection metadata (ranks drop journal framing #1, 96.9% p50), Wallet labels coverage (ranks publish harnesses #2 of 5, 42.4% p50).'

Example /compare/codex-vs-mobula:

  • Before: 'Side by side OpenChainBench measurements. Identical layout, no editorial verdict...' (identical on 247 compare pages)
  • After: 'Codex leads on 2 of 4 shared benchmarks, Mobula on 2. Codex wins on aggregator-head-lag (128ms vs 195ms), wallet-labels-coverage. Mobula wins on metadata-coverage, network-coverage.'

Zero editorial claim, zero LLM, zero $. All prose derived deterministically from the same Prom values the table renders.

Fallback to a neutral sentence when p50 <= 0 so cold ISR / harness restart doesn't render lies.

infrastructure/monitoring-ui and infrastructure/prom-admin are
independently deployed Next.js apps (own package.json + Railway
config, own build/lint/typecheck pipeline). Scanning them from the
main frontend's lint pass caused every unrelated PR to fail on their
pre-existing warnings that the sub-app maintainers can fix in their
own dedicated PRs.

worker/ is the materialize worker (own package.json + Railway config),
same reasoning.

Unblocks PR #908 (SEO purge) and any subsequent PR touching only
src/*.
@Flotapponnier
Flotapponnier merged commit 8327439 into dev Jul 5, 2026
1 check passed
Flotapponnier added a commit that referenced this pull request Jul 5, 2026
…ompare (#908) (#909)

* seo: purge thin compare pages + benchmark-first title on products & compare

Bing WMT audit 2026-07-05: only 2 of 5093 sitemap URLs indexed. Root
cause: 4938 ad-hoc /compare/ pages emitted at ≥1 shared bench threshold,
producing near-duplicate templates that starved crawl budget and hurt
domain trust.

Changes:
- sitemap: hybrid threshold. Brand-whitelist pairs emit at ≥1 shared,
  others at ≥3. Drops from 4938 → ~226 ad-hoc + 21 curated = ~247 URLs.
  Preserves every commercial X vs Y pair users actually search for
  (helius-vs-mobula, alchemy-vs-moralis, chain-vs-chain, perp-vs-perp).
- compare-pairs: remove jupiter-vs-raydium (raydium has 0 bench
  appearances so /compare/jupiter-vs-raydium 404s at render).
- products/[slug]: title now leads with 'Benchmark {year}' + head-term
  match on '<provider> benchmark' queries. Meta description ends with
  'As of YYYY-MM-DD' for LLM citation extractability (LLM-mediated
  discovery drives ~80% of Bing query traffic).
- compare/[slug]: title now leads with '{a} vs {b} Benchmark {year}'.
  Meta description is unique per pair (shared bench count + date)
  killing the identical duplicate signal that had Bing skipping pages.

* seo: prose summary above the fold on /compare + /products

Replaces the identical templated intro paragraph on both pages with
a data-driven prose summary derived from live measurements. Kills the
last piece of duplicate above-the-fold text so Bing sees genuinely
unique substantive content per URL.

/compare/[slug]:
  Before: 'Side by side OpenChainBench measurements. Identical layout,
          no editorial verdict...'  (same on 247 pages)
  After:  'Codex leads on 2 of 4 shared benchmarks, Mobula on 2. Codex
          wins on aggregator-head-lag (128ms vs 195ms), wallet-labels-
          coverage (42.8% vs 38.1%). Mobula wins on metadata-coverage
          (96.9% vs 89.1%), network-coverage (80 vs 42 chains).'

/products/[slug]:
  Before: 'Moralis performance benchmarks, live across 2 categories.
          Reproducible measurements, open methodology.'  (same on 104 pages)
  After:  'Moralis is measured across 2 live OpenChainBench benchmarks,
          with 1 #1 finish: NFT collection metadata (ranks #1, 96.9% p50),
          Wallet labels coverage (ranks #2 of 5, 42.4% p50).'

Falls back to a neutral sentence when the provider's p50 data is
missing (cold ISR, harness restart) so cold pages don't render a lie.

* chore(lint): exclude standalone sub-apps from main frontend lint

infrastructure/monitoring-ui and infrastructure/prom-admin are
independently deployed Next.js apps (own package.json + Railway
config, own build/lint/typecheck pipeline). Scanning them from the
main frontend's lint pass caused every unrelated PR to fail on their
pre-existing warnings that the sub-app maintainers can fix in their
own dedicated PRs.

worker/ is the materialize worker (own package.json + Railway config),
same reasoning.

Unblocks PR #908 (SEO purge) and any subsequent PR touching only
src/*.

---------

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Flotapponnier added a commit that referenced this pull request Jul 6, 2026
…+ rich SERP (#916)

Three P0 SEO wins from post-shipping audit (2026-07-05):

1. products/[slug]/page.tsx — H1 now emits '{Provider} Benchmark'
   instead of bare '{Provider}'. Matches the SEO title '{Provider}
   Benchmark 2026' shipped in #908. Semantic mismatch was killing CTR
   (only 1 click observed on 'sui benchmark' at pos 2). Small styled
   'Benchmark' suffix preserves visual hierarchy.

2. compare/[slug]/page.tsx — FAQPage JSON-LD. Bing + Google render rich
   FAQ dropdowns in SERP for pages emitting valid FAQPage. Every answer
   is derived from the shared benchmarks (winner + metric + link to
   methodology), so it re-uses data already in DOM — no editorial claim.

3. layout.tsx — RSS feed discovery + rich SERP directives:
   - <link rel='alternate' type='application/rss+xml' href='/rss.xml'>
     via Metadata.alternates.types so feed readers, Perplexity, Bing
     News, Claude auto-discover the existing /rss.xml route.
   - meta robots gains max-snippet:-1 + max-image-preview:large +
     max-video-preview:-1 so SERP stops truncating our data-rich prose
     and stops suppressing og:image on AI-scraped queries.

Skipped from the audit punch list (deferred to a separate PR):
- Cache-Control s-maxage / stale-while-revalidate on dynamic routes
- next/image migration for WebP/AVIF
- Preconnect hints for analytics.ahrefs.com

The 3 shipped here are the highest-signal / lowest-risk subset.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Flotapponnier added a commit that referenced this pull request Jul 6, 2026
…+ rich SERP (#916) (#917)

Three P0 SEO wins from post-shipping audit (2026-07-05):

1. products/[slug]/page.tsx — H1 now emits '{Provider} Benchmark'
   instead of bare '{Provider}'. Matches the SEO title '{Provider}
   Benchmark 2026' shipped in #908. Semantic mismatch was killing CTR
   (only 1 click observed on 'sui benchmark' at pos 2). Small styled
   'Benchmark' suffix preserves visual hierarchy.

2. compare/[slug]/page.tsx — FAQPage JSON-LD. Bing + Google render rich
   FAQ dropdowns in SERP for pages emitting valid FAQPage. Every answer
   is derived from the shared benchmarks (winner + metric + link to
   methodology), so it re-uses data already in DOM — no editorial claim.

3. layout.tsx — RSS feed discovery + rich SERP directives:
   - <link rel='alternate' type='application/rss+xml' href='/rss.xml'>
     via Metadata.alternates.types so feed readers, Perplexity, Bing
     News, Claude auto-discover the existing /rss.xml route.
   - meta robots gains max-snippet:-1 + max-image-preview:large +
     max-video-preview:-1 so SERP stops truncating our data-rich prose
     and stops suppressing og:image on AI-scraped queries.

Skipped from the audit punch list (deferred to a separate PR):
- Cache-Control s-maxage / stale-while-revalidate on dynamic routes
- next/image migration for WebP/AVIF
- Preconnect hints for analytics.ahrefs.com

The 3 shipped here are the highest-signal / lowest-risk subset.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
@Flotapponnier
Flotapponnier deleted the fix/seo-foundations branch July 17, 2026 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant