Appearance
Deploying the frontend
Netlify, two sites from one repository, auto-building from the optimized branch.
The two sites
| Site | Build command | Publish | Config |
|---|---|---|---|
defi.blazpay.com | npm run build | dist/ | netlify.toml |
ai.blazpay.com | npm run build:ai | dist-ai/ | netlify.ai.toml |
Shipping
bash
cd defi-dex
git add -A && git commit -m "…"
git push origin optimized # Netlify auto-builds both sitesmain 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-jsnpm run build:ai
= vite build --mode ai --outDir dist-ai
→ node scripts/rewrite-ai-shell.mjs # MANDATORYNODE_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 OGThe 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:
| Variable | defi | ai |
|---|---|---|
VITE_SITE | defi | ai |
VITE_API_URL | https://api.blazpay.com/api | same |
VITE_GA_MEASUREMENT_ID | GA4 id | GA4 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 = trueNon-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:
- Redirects are evaluated top to bottom and must stay above the SPA catch-all.
force = trueis 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 type | Canonical |
|---|---|
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.
| Item | Value |
|---|---|
| Key | fbaf800f3098a10a73e20691138e2590 |
| Key file | public/{key}.txt — must match KEY in scripts/indexnow-submit.mjs |
| Manual | npm 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
- The
MARKETING_ROUTEregex insrc/index.tsx - The route table in
src/MarketingApp.tsx 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 optimizedThe 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:
| Pin | Prevents |
|---|---|
preload — vite/preload-helper, modulepreload-polyfill | Colocation 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-async | Rollup 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.