Skip to content

chore(dev): sync main → dev (bring SEO hotfixes + canonical fixes)#761

Merged
Flotapponnier merged 21 commits into
devfrom
chore/sync-main-into-dev
Jun 26, 2026
Merged

chore(dev): sync main → dev (bring SEO hotfixes + canonical fixes)#761
Flotapponnier merged 21 commits into
devfrom
chore/sync-main-into-dev

Conversation

@Flotapponnier

Copy link
Copy Markdown
Collaborator

Why

Main has accumulated several hotfixes that never made it back to dev (canonical chain URLs, permanent redirect, canonical→chains, search index filter, badge slug, etc.). Without a sync, the next `dev → main` PR would re-introduce some of these or conflict.

What's included

Merge is clean (no conflicts) because dev's PRs (#747 + #749) were already cherry-picked into main via #753, and git correctly detected the duplicated patches.

Test plan

  • CI green
  • After merge: `git log origin/dev ^origin/main` is empty modulo a couple staging-only commits

Flotapponnier and others added 21 commits June 26, 2026 14:45
…cing (follow-up A) (#741)

Completes PR #732 (sample-health system) by surfacing the computed
dataConfidence/sampleHealth/expectedN to users + machine-readable
consumers. Was dead-code on the data layer until this PR.

UI (ledger-table.tsx):
- Filter rows tagged 'insufficient' out of the sorted ranking.
- 'Low sample' pill on rows with dataConfidence='low' + tooltip
  showing the actual sample-health ratio ('45% of expected').

APIs:
- /api/citable: returns expectedN + dataConfidence per row. When
  bench-wide aggregate is 'insufficient', returns value=null +
  leader=null (refuses to crown a leader from undersized samples).
- /api/stat/[slug]: same treatment + drops 'insufficient' rows
  from the machine-readable ranking array. Per-row sampleHealth +
  dataConfidence exposed for downstream tooling.

Library (citation.ts):
- citationCandidates() drops 'insufficient' rows from the leader
  candidate set.
- fieldValue/leader/headlineSentence return null/'insufficient
  data' when bench-wide is insufficient.
- isInsufficient() helper preserved from main (used by hub cards +
  /api/citable hot path).

Effect: bench pages where YAML declares expected_n now visually
communicate sample confidence; agents/journalists hitting the
citable API can't accidentally quote a leader drawn from 3 data
points; the methodology page's 'n >= 1000' claim becomes honest.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…dCategory (D.2) (#743)

Final cherry-pick from dev-archive — adds 7 new SEO-indexable hub
pages (one per benchmark category) that target long-tail queries
like 'best crypto aggregator benchmarks 2026'.

D.2 was deferred from PR D because the BenchmarkCard ↔ MiniChart ↔
BenchmarkCardData type chain had cascading TS errors. Follow-up A
(citation.ts isInsufficient → InsufficientCheckInput structural
subset) silently fixed those, so this lands cleanly now.

Files:
- src/app/benchmarks/category/[cat]/page.tsx (new) — server route
  that renders BenchmarkGrid pre-filtered to one category + breadcrumb
  + ItemList JSON-LD. generateStaticParams builds the 7 hub routes
  from CATEGORIES. Each page is fully indexable (canonical, OG, etc.).
- src/components/benchmark-grid.tsx (updated) — adds two optional
  props (lockedCategory, allCategories). Backward compatible: when
  unset the grid behaves exactly as before (used on /benchmarks).
  When set (category hub routes), the grid pre-filters and shows the
  full category pill list so users can hop between hubs.

URLs that ship after deploy:
- /benchmarks/category/aggregators
- /benchmarks/category/bridges
- /benchmarks/category/blockchains
- /benchmarks/category/trading
- /benchmarks/category/wallets
- /benchmarks/category/rpcs
- /benchmarks/category/nft-apis

Each renders the canonical bench grid filtered to that category with
its own H1, meta description, and structured data. Sitemap pickup
automatic on next regen.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
… 043)

Adds 8 venues sourced via Mobula's CEFI funding-rate aggregator (perp_venue_funding_24h_bps, freshness via perp_venue_last_refresh_unix):
- Bitget, Coinbase, Deribit, Gate, Kraken, KuCoin, Lighter, MEXC

Same stddev-over-time semantics on a 24h window, BTC/ETH/SOL split per asset. Cohort jumps from 7 to 15 venues.

Logos: 5 PNGs sourced from CoinMarketCap exchange CDN (bitget, deribit, gate, kraken, mexc); 2 SVGs from simpleicons (coinbase, kucoin); 1 SVG from simpleicons for binance (was reusing /logos/bnb.png, the chain logo, not the exchange brand).

Bench copy + manifest entries updated.
Dev was force-pushed after PR #730 and the venue support in BenchmarkFilters (materialize/filters.ts) was lost in the spec/snapshot refactor PR #739. The worker still references f.venue and dims.venue, so typecheck failed. Re-add venue across:
- BenchmarkFilters union (load.ts)
- parseFilterSig allowlist
- Spec.dimensions schema (spec-schema.ts) so YAML venue dim parses
PR #743 created src/app/benchmarks/category/[cat]/page.tsx + benchmark-grid.tsx referencing @/lib/categories but the helper module was not cherry-picked to main, breaking typecheck.

Also re-apply binance: /logos/binance.svg (was reverted by auto-merge against pre-existing main entry pointing to /logos/bnb.png).
chore: cherry-pick funding-stability 15 venues + venue type re-add
hotfix(main): restore categories.ts + add venue dimension
The /api/badge/<bench>/<provider> SVG route and its /snippet helper
have been live for months but have no discovery surface. Visitors who
want to embed their rank on a README or doc had to read source.

/badges is the shields.io-style catalog: search, filter by category,
preview every (benchmark, provider) pair, copy a Markdown / HTML / URL
/ JSON snippet for any of them. Chain and region scope dropdowns are
exposed when the parent bench declares those dimensions.

Each embed is one more permanent backlink from the provider's README
or docs site, so this page should also move the needle on referring
domains.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Wraps /api/citable, /api/stat/<slug>, /api/series/<slug> as a typed
synchronous client with frozen dataclass models and a small exception
hierarchy (NotFound, RateLimit, APIUnavailable).

Ships:
- src layout under packages/python-client/, hatchling backend
- httpx-based OpenChainBench client, list/get/series methods
- 15 tests: 12 mocked, 3 live integration (skip via OCB_SKIP_INTEGRATION=1)
- .github/workflows/python-client-ci.yml for PR tests on 3.10 to 3.13
- .github/workflows/pypi-publish.yml triggered on python-v* tags,
  publishes via PyPI Trusted Publisher (OIDC, no token in repo)
- PUBLISHING.md with the one-time PyPI Trusted Publisher setup steps

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…ble in dark mode)

simpleicons SVGs are monochrome (renders black) so coinbase and kucoin logos vanished in dark mode. Replace with the colored 200x200 PNGs from the CoinMarketCap exchange CDN, same source as bitget/deribit/gate/kraken/mexc.
… slug + ton→gram scope) (#753)

* fix(seo): drop dead /alternatives links from perp/PM venue footers + dead compare pair

Two SEO regressions feeding 404s into Google's crawl:

1. /products/<slug> for perp/PM venues without an alternatives YAML
   (lighter, manifold, vertex, grvt, ostium, variational, pacifica,
   aevo, edgex, paradex, extended, myriad, limitless, aster) hardcoded
   a /alternatives/<slug> footer link in PmVenueSection and
   PerpVenueSection. Now gated on loadAlternativeSlugs() membership so
   only venues with an actual YAML emit the link.

2. /compare hub rendered jupiter-vs-raydium even though raydium has
   no bench appearances, so /compare/jupiter-vs-raydium 404s
   downstream. Pre-filter COMPARE_PAIRS against getProvider() so dead
   pairs disappear from the list and the JSON-LD ItemList until both
   sides resolve.

Plus a defensive guard in buildAlternativesReverseMap() so the
emitted alt.slug is always a known live YAML, even if the alt list
source ever changes shape.

* fix(seo): blacklist hex builder slugs and shared composite filter

Drop 15 anonymous 0x... HL builder slugs and 5 dead composite slugs
from /products listing, sitemap and individual /products/<slug> routes.
DEAD_COMPOSITE_SLUGS now lives in providers.ts as the single source of
truth; related-providers.ts imports it. Bumps providers cache key v3->v4.

* fix(seo): scope ton→gram redirect to benches with gram explainer

The catch-all /benchmarks/:slug/ton redirect fired on every bench,
including aggregator-head-lag/gas-estimation/rpc-capabilities/etc that
have no per_chain_explainer for gram. Result: 308 chain into 404, which
Ahrefs + Google flag as a soft-404 anti-pattern.

Now explicit per-bench: l1-finality, network-fees, wallet-labels-coverage.

---------

Co-authored-by: Florent Tapponnier <contact@mobula.io>
chore: cherry-pick colored coinbase + kucoin logos
…hans) (#755)

Parent /benchmarks/<slug> page rendered links to /benchmarks/<slug>/ETH
straight from the YAML dimension value, while the sitemap emitted the
lowercase canonical /eth. matchesChainSlug() is case-insensitive so
both URLs returned 200 — Ahrefs flagged the lowercase variants as
orphans (zero inlinks) and Google sees duplicate content.

Two fixes:
- PerChainPagesNav + SiblingChains now use canonicalChainSlug(c.value)
  so internal hrefs match the sitemap canonical
- [chain]/page.tsx 308s any non-canonical casing or legacy alias to
  the canonical lowercase slug, consolidating external links too

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…#756)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…anonical chain URLs (#757)

redirect() defaults to 307 which Google treats as temporary - the
source URL stays in the index competing with the target. 308 tells
Google to consolidate rank signal on the canonical lowercase URL.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…#758)

/products/eth-usd, /products/bnb-usd, /products/avalanche-official etc.
resolve to a canonical provider whose slug matches a chain (ethereum,
bnb, avalanche...). The previous canonical /products/<chain> then 308s
to /chains/<chain>, so Ahrefs + GSC flag 'canonical points to redirect'
and Google may split rank instead of consolidating on the final URL.

Now we check the chain registry and short-circuit the canonical (+ OG
url) straight to /chains/<slug> for chain-shaped providers. Non-chain
providers (helius, dRPC, mobula) keep /products/<slug>.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
PR #749 stopped /compare from rendering pairs where either provider
is unresolved (e.g. jupiter-vs-raydium), but the search index built
in buildSearchIndex iterated COMPARE_PAIRS directly. Ahrefs crawls
the embedded JSON and flagged /compare → /compare/jupiter-vs-raydium
as a dead internal link.

Same getProvider() probe as /compare/page.tsx, so the two surfaces
stay in lockstep.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…ct slug (#760)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
@Flotapponnier
Flotapponnier merged commit f2df4f8 into dev Jun 26, 2026
5 checks passed
@Flotapponnier
Flotapponnier deleted the chore/sync-main-into-dev branch July 17, 2026 14:41
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