Skip to content

Deploying the frontend

Netlify, two sites from one repository, auto-building from the optimized branch.

The two sites

SiteBuild commandPublishConfig
defi.blazpay.comnpm run builddist/netlify.toml
ai.blazpay.comnpm run build:aidist-ai/netlify.ai.toml

Shipping

bash
cd defi-dex
git add -A && git commit -m "…"
git push origin optimized          # Netlify auto-builds both sites

main is stale

Roughly 80 commits behind and effectively abandoned. optimized is the live branch. Pushing to main deploys nothing.

What the build does

npm run build
  = vite build
  → node scripts/prerender-seo.mjs      # 114 pages + sitemap + llms.txt
  → node scripts/generate-og.mjs        # dist/og/*.png via @resvg/resvg-js
npm run build:ai
  = vite build --mode ai --outDir dist-ai
  → node scripts/rewrite-ai-shell.mjs   # MANDATORY

NODE_OPTIONS = "--max-old-space-size=4096" is set in netlify.toml — the prerender plus OG generation needs it.

Faster local iteration

bash
npm run build:app     # vite build only, skips prerender and OG

The AI shell rewrite

rewrite-ai-shell.mjs is not optional

The shared index.html head is hardcoded to defi. Without the rewrite, every AI URL ships canonical=defi.blazpay.com — telling Google the entire domain is a duplicate — and unfurls as "Blazpay DEX" on social.

The script fails the build if any defi.blazpay.com string survives. If you see AI pages with a defi canonical, the wrong build command ran.

FullApp and MarketingApp are tree-shaken out of dist-ai because VITE_SITE is inlined and Rollup folds the branch — so there is no dead-chunk problem.

Environment variables

Set per site in Netlify:

Variabledefiai
VITE_SITEdefiai
VITE_API_URLhttps://api.blazpay.com/apisame
VITE_GA_MEASUREMENT_IDGA4 idGA4 id

Analytics is a no-op without VITE_GA_MEASUREMENT_ID

src/utils/analytics.ts short-circuits. That also means AI-referral tracking — the only measurement of AEO/GEO performance — silently does nothing if the variable is unset.

Only VITE_-prefixed variables reach the bundle. Never put a secret in one.

Netlify redirect rules

The SPA rewrite must stay non-forced

force = true breaks all 114 SEO pages

toml
[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200
  # NO force = true

Non-forced rewrites let Netlify serve existing prerendered files first. Forced ones shadow them, so every static page becomes the empty SPA shell.

The 301 map is staged commented out

netlify.toml contains the full BlazAI → ai.blazpay.com redirect map, deliberately commented:

toml
# COMMENTED OUT ON PURPOSE. Enabling these before ai.blazpay.com resolves
# would 301 live, publicly-posted share links into a dead domain.

Enable only at runbook step 5

AI_SITE_SETUP.md §5 is the cutover step. Enabling early 301s live share links — audit reports, certificates, embedded rug-check badges on third-party sites — into a domain that does not answer.

Two rules apply when you do enable them:

  1. Redirects are evaluated top to bottom and must stay above the SPA catch-all.
  2. force = true is required for these — without it Netlify serves the existing prerendered or SPA file first and the redirect never fires.

The map covers /defi/blazai, /defi/blazai/:id, /blazai, /rug-check, /wallet-checker, and the share artifacts /report/:id, /certificate/:id, /token/:address, /token/:address/embed, /wallet/:wallet.

Keep the share-artifact redirects forever

They are posted on X and Telegram, and the embed badge is iframed on third-party sites. Removing them later breaks links Blazpay does not control.

Trailing slashes

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

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

Sitemap <loc> entries must match.

Headers

X-Frame-Options: SAMEORIGIN on /* with no override

This blocks the rug-check embed badge from third-party iframes on defi — which defeats a deliberate growth mechanic. netlify.ai.toml fixes it for /token/*/embed via CSP frame-ancestors *, so the badge only works once artifacts live on ai.blazpay.com.

IndexNow

A local plugin, plugins/netlify-plugin-indexnow, runs onSuccess in the production context only and submits every sitemap URL to api.indexnow.org → Bing and Yandex → Copilot and ChatGPT search.

ItemValue
Keyfbaf800f3098a10a73e20691138e2590
Key filepublic/{key}.txtmust match KEY in scripts/indexnow-submit.mjs
Manualnpm run indexnow

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 already been rotated once; do not rotate and submit in the same deploy.

Google does not use IndexNow. Submit the sitemap in Search Console.

Pre-push checklist

bash
npm run build            # full production build must succeed
npm run check:routes     # route sanity
□ vite build passes (NOT tsc — ~3,300 pre-existing node_modules errors)
□ 114 prerendered pages present in dist/
□ dist/sitemap.xml and dist/llms.txt regenerated
□ dist/og/*.png generated
□ Exactly one FAQPage per prerendered page
□ For a new marketing route: all THREE lists updated
□ SITE.updated bumped if copy changed
□ npm run build:ai passes (the rewrite script did not fail)

Three lists for a new marketing route

  1. The MARKETING_ROUTE regex in src/index.tsx
  2. The route table in src/MarketingApp.tsx
  3. src/routes/index.tsx

Miss the regex and the page loads the 7 MB FullApp instead of the 234 KB marketing tree. Miss MarketingApp and it 404s inside the light tree.

swap-sdk bumps

bash
npm install github:blazpay/swap-sdk#vX.Y.Z --package-lock-only
# commit BOTH package.json and package-lock.json
git push origin optimized

The lock pins by resolved commit

npm install obeys it even after rm -rf node_modules/swap-sdk. Robinhood Chain support went live this way at swap-sdk v1.12.0 (commit 7bc1a8c, 2026-07-26) — and only after the lockfile was fixed.

Note that defi-dex and bz-backend pin independently and can drift. The frontend has lagged before (quote-display precision only, but still).

Bundle-size regressions

Two manualChunks pins in vite.config.ts are load-bearing:

PinPrevents
preloadvite/preload-helper, modulepreload-polyfillColocation with a big vendor chunk that the bootstrap entry then statically imports on every page. Symptom: every marketing page pays 595 KB
vendor-react — react, react-dom, scheduler, react-router, @remix-run, react-helmet-asyncRollup colocating react-dom inside vendor-charts, dragging ~1 MB of chart libraries onto marketing pages

If marketing pages get slow, check these first

Both were discovered empirically after the split appeared to work. The vendor-react split was verified TDZ-safe via headless Chrome on /defi/swap and /.

Rollback

Netlify keeps every deploy. Use the dashboard's "Publish deploy" on a previous build — instant, no rebuild.

For a code rollback, revert on optimized and push.

Verifying a deploy

bash
# prerendered content actually present?
curl -s https://defi.blazpay.com/swap/hemi/ | grep -o 'Hemi is'

# canonical correct on the AI site?
curl -s https://ai.blazpay.com/ | grep -o 'canonical[^>]*'

# OG image exists?
curl -sI https://defi.blazpay.com/og/swap-hemi.png | head -1

# sitemap regenerated?
curl -s https://defi.blazpay.com/sitemap.xml | grep -c '<loc>'

Headless Chrome on macOS clamps --window-size to ~500 px wide

So mobile screenshots look like overflow bugs when they are not. Verify at 500 px or above.

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