Skip to content

SEO / AEO subsystem

defi-dex is a client-rendered Vite SPA with no SSR, so Helmet meta is invisible to non-JS crawlers — GPTBot, Google-Extended, PerplexityBot, ClaudeBot. Rather than migrate to SSR, a build-time prerender subsystem was added in June 2026.

It produces 114 static pages, a 120-URL sitemap, llms.txt, llms-full.txt and a per-page OG image.

How it works

Framework-free ESM is the whole trick

src/data/chainSeo.mjs is imported by both the React pages and the Node prerender script. One source of truth means the static HTML and the hydrated React render identical content — no cloaking risk, and no drift.

To add or edit a chain page, edit only that file.

Data modules

ModuleDrives
src/data/chainSeo.mjsPer-chain pages, SITE, PROOF_POINTS, FOOTER_COLUMNS, FOOTER_SOCIALS, ABOUT, JSON-LD builders
src/data/bridgeRouteSeo.mjs/bridge/{from}-to-{to} corridor pages
src/data/featureSeo.mjs/blazai, /rug-check, /wallet-checker, /buy-crypto-inr, /perpetuals
src/data/compareSeo.mjs/compare/blazpay-vs-{uniswap,1inch,jupiter}, /compare/best-dex-aggregators

Per-chain fields

Each chain carries slug, chainId, native, category, since, explorer, popularTokens, a hand-written narrative, extraKeywords and extraFaqs (including a "What is {Chain}?" definition FAQ).

Categories drive grouping on the hub. 24 chains are covered, from emerging (soneium, hemi, berachain, sei, sonic, unichain, apechain, abstract, worldchain, hyperevm, opbnb, ronin) through majors to solana and tron.

Page components

ComponentRoutes
pages/seo/ChainLandingPage.tsx/swap/:chain, /bridge/:chain (mode prop)
pages/seo/ChainsHub.tsx/chains
pages/seo/BridgeRoutePage.tsx/bridge/{from}-to-{to}and the dispatcher for /bridge/:chain
pages/seo/FeatureLandingPage.tsxThe five feature paths (slug passed as a prop)
pages/seo/ComparePage.tsxThe four comparison paths
pages/defiLandingPage/index.tsx/about

BridgeRoutePage checks whether the param contains -to-: if so it renders the corridor page, otherwise it delegates to ChainLandingPage in bridge mode.

The build chain

npm run build
  = vite build
  → node scripts/prerender-seo.mjs      # 114 pages + sitemap + llms.txt
  → node scripts/generate-og.mjs        # dist/og/*.png

prerender-seo.mjs composes dist/index.html as a shell and injects static HTML from the data modules. It also emits the same SiteFooter HTML into every page, so non-JS crawlers get the internal links too.

generate-og.mjs renders 1200×630 PNGs via @resvg/resvg-js with vendored Inter TTFs in scripts/assets/fonts. The key convention is ogImageKey(canonical)/swap/hemi/swap-hemi. The prerender's compose() rewrites og:image / twitter:image (and their alts); React pages pass ogImage={ogImageFor(meta.canonical)}.

Page inventory

GroupCount
Chain pages48 (24 chains × swap/bridge)
Bridge corridors55 (4 sources × 10 emerging destinations + 15 major pairs)
Feature pages5
Comparison pages4
Hub + about2
Total114, sitemap 120 URLs

Landing-page CTAs link with ?chain={slug}:

  • /defi/swap?chain=hemi sets fromToken to that chain's native asset.
  • /defi/bridge?chain=sei sets toToken.

Logic lives in src/utils/chainPreselect.tsresolveAppChain maps a slug to the backend chain by chainId or name, and buildNativeToken synthesises the token record. defaultChainId is passed to TokenModal, which syncs it via effect.

Solana and TRON resolve by name, because the app uses internal ids (102, 728126428).

Schema / JSON-LD

Two hard-won dedupe rules, both fixing Google's "Duplicate field FAQPage" warning:

Never put FAQPage in index.html's global JSON-LD

The shell is the base of every prerendered page and every SPA route, so a global FAQPage duplicates the page-level one everywhere.

Prerendered JSON-LD is marked and removed on hydration

Prerender-injected page JSON-LD carries data-seo-prerender="true". SEOHead removes those scripts on mount — but only when it has its own jsonLd — so a JS-rendering crawler sees exactly one copy.

pageJsonLd includes a WebPage node with dateModified from SITE.updated. index.html carries a WebSite node, full sameAs socials and a featureList. SEOHead gained jsonLd and noindex props.

Canonical and trailing slash

Netlify serves prerendered directories at {path}/ and 301s the slash-less form. Therefore:

Page typeCanonical form
Prerendered (/swap/hemi/, /chains/, /about/)Trailing slash
SPA-only (/defi/swap)Slash-less

The SPA rewrite must not be forced

netlify.toml's from = "/*" to = "/index.html" status = 200 must stay non-forced. Non-forced rewrites let Netlify serve the existing prerendered files first; force = true shadows them and silently breaks all 114 pages.

Crawler access

public/robots.txt explicitly allows AI crawlers. llms.txt and llms-full.txt are regenerated on every build.

IndexNow

ItemValue
Keyfbaf800f3098a10a73e20691138e2590
Key filepublic/{key}.txtmust match KEY in scripts/indexnow-submit.mjs
Automationplugins/netlify-plugin-indexnow, onSuccess, production context only
Manualnpm run indexnow
ReachesBing and Yandex → Copilot, ChatGPT search

Never submit before the key file is deployed

IndexNow caches failed key validations for hours or days and returns 403 until they expire. The key has been rotated once already; do not rotate and submit in the same deploy.

Google does not use IndexNow. Use a Search Console sitemap submission instead.

Analytics

src/utils/analytics.ts, called from src/index.tsx. A no-op unless VITE_GA_MEASUREMENT_ID is set as a Netlify env var.

It detects AI referrers (chatgpt, perplexity, gemini, copilot, claude, grok, meta-ai, …) and fires an ai_referral event once per session plus an ai_referrer user property. That is how AEO/GEO performance is measured at all.

Proof points

PROOF_POINTS in chainSeo.mjs holds the only numbers allowed in marketing copy:

StatValue
AI chat messages processed136,000+
AI chat sessions10,000+
AI intent accuracy99.9%+
Live liquidity providers21

These are real, rounded, all-time figures — and were kept honest deliberately

A request to inflate them 100× was refused. Do not fabricate. SITE.updated (currently 2026-06-10) is the freshness stamp — bump it when copy changes, because dateModified in the JSON-LD reads from it.

src/component/SiteFooter.tsx is mounted in MainLayout, so it renders on every app page. The link model is FOOTER_COLUMNS / FOOTER_SOCIALS in chainSeo.mjs, and the prerender script emits the same footer HTML into every static page so non-JS crawlers get the links.

chainSeo.mjs also exports AI_SITE_ORIGIN — hardcoded, because that module is imported by both the browser and Node — and the footer points AI tools at it, so all 114 prerendered pages cross-link to ai.blazpay.com.

Route checking

npm run check:routes is phase-aware. Share artifacts (/report/:id, /certificate/:id, /token/:address, /wallet/:wallet) must keep working on defi until ai.blazpay.com resolves.

Pass -- --cutover only at step 5 of the runbook in AI_SITE_SETUP.md.

Adding a page

Checklist: add an SEO landing page.

Known limits

  • No server-side OG rendering. Social bots see the global OG fallback on /report/:id, /certificate/:id and /wallet/:wallet. Googlebot is fine because SEOHead runs.
  • / and /defi/* are not prerendered. They serve the base index.html only. Prerendering the home page is a listed remaining idea.
  • The 301 map for the AI split is staged commented-out in netlify.toml. Enabling it early 301s live public share links into a dead domain.
  • X-Frame-Options: SAMEORIGIN on /* with no override blocks the rug-check embed badge from third-party iframes on defi. netlify.ai.toml fixes it for /token/*/embed.
  • Content freshness is manual. SITE.updated does not update itself.

Internal + developer documentation. Usage figures are point-in-time snapshots — re-verify before publishing them.