feat: fix Open Graph / Twitter cards on all dApp routes and ship community OG artwork #578
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#578
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Social previews for the dApp (
https://dex.cl8y.com) render on Telegram but not on X/Twitter. All public SPA routes share one staticindex.htmlshell, so this is a single origin + asset fix — not per-page React helmet work. Replace the current product-copy OG artwork with an upscaled, landscape Open Graph card derived from the community medallion concept already in the tree.Related (do not re-implement): #488 shipped the current
/og-image.png+index.htmlmeta. Design-system rule remains: canonical same-origin only — never user-controlled OG URLs.Current codebase
The dApp is a Vite SPA. Crawlers do not execute React. Every public path is rewritten to the same HTML shell, so one broken tag set fails every shareable URL.
frontend-dapp/index.htmlsetsog:title/og:description/og:type/og:imageandtwitter:card(summary_large_image) /twitter:image/twitter:title/twitter:description.og:imageandtwitter:imageare root-relative (/og-image.png). Telegram (and often Discord) resolve that against the page origin. X/Twitter Card crawler requires an absolutehttps://URL and silently drops the large image card when the path is relative.og:url,og:site_name,og:image:secure_url,og:image:width,og:image:height,og:image:type,og:image:alt, ortwitter:image:alt. Notwitter:site(this repo has no official X handle — do not invent one).frontend-dapp/public/og-image.pngis 1200×630 (~83 KB), left-weighted product copy from #488 (“Swap & limit orders on Terra Classic”).frontend-dapp/public/community-opengraph-concept.pngis 1254×1254 (~1.75 MB) square 3D medallion (laurel portrait, scales, chart,CL8Y — DEX —). Square is not a valid OG / Twitter large-card ratio; stretching it to 1200×630 will crush the medallion.docker/frontend/nginx.conftry_files $uri $uri/ /index.htmlso/,/trade,/pool,/charts/:pair,/trader/:address, etc. all return the sameindex.html. PNGs are immutable-cached (`location ~* .(pngfrontend-dapp/viteCsp.ts/frontend-dapp/vite.config.ts). Crawlers do not honor CSP; still keepimg-srcsame-origin for browsers.robots.txt. Fine for allow-all, but there is no explicit allow forTwitterbot/facebookexternalhit/TelegramBotif a future deny is added.react-helmet, no per-route meta, no query-param OG. Keep it that way.docs/design-system.mdandskills/AGENTS_FRONTEND_DESIGN_SYSTEM.mdstill describe the #488 product-copy OG.Shareable routes that must all present the same valid card (nginx →
index.html):/,/pool,/create,/tiers,/charts,/charts/:pairAddr,/portfolio,/trader,/trader/:address,/limits,/trade,/trade/:pairAddr,/protocol,/mint,/ust1,/wrap.Out of this repo:
terms.cl8y.com(Legal). Indexer JSON API is not a share surface.Why this is needed
og:image/twitter:imageis the documented failure mode.index.html+ the PNG once covers every public dApp URL. Do not add per-route OG, SSR, or user-supplied preview URLs.Constraints / guardrails
og:image/twitter:imagemust behttps://dex.cl8y.com/og-image.png(or the same path on a documented staging origin baked at build time). Never interpolatewindow.location, the HTTPHost/X-Forwarded-Hostheader, query params, hash, pair address, or wallet address into OG tags.react-helmet, prerender.io, or per-route titles for this issue. Crawlers never see client-updated meta.CL8Ywordmark). Optional 2×2400×1260is unnecessary if 1200×630 is sharp.og-image.png(or.jpgif compression needs it) < 1 MB preferred, hard max 5 MB (X). Keep PNG or JPEG — no SVG (scriptable), no WebP-only (X support is uneven)./og-image.pngfromfrontend-dapp/public/so Vite copies it todist/and nginx serves it asimage/pngwithout falling through toindex.html.twitter:site. Add it only if product supplies an official@handle.VITE_PUBLIC_ORIGIN(or similar) is used for local/staging, allowlisthttps://dex.cl8y.comand explicit staging hosts; rejecthttp:,javascript:,data:,//, and any non-https origin. Default production origin ishttps://dex.cl8y.com.og:image.X-Frame-Options, orReferrer-Policy“for crawlers.”Cache-Control: public, immutableis OK only if operators accept a crawler re-fetch after replace. Prefer replacingog-image.pngin place (same URL) plus an X card-cache reset after deploy. Query-string cache-bust (?v=) is discouraged — some crawlers dislike it.community-opengraph-concept.pngas the source. Production crawlers must fetch/og-image.png, not the 1.75 MB square.docs/design-system.md,skills/AGENTS_FRONTEND_DESIGN_SYSTEM.md,docs/qa/issue-488/README.mdso agents do not restore the typesetting card.Invariants:
og:imageandtwitter:imageare absolutehttps://URLs ondex.cl8y.com(or baked allowlisted staging origin), pointing at/og-image.png.twitter:cardremainssummary_large_image./og-image.pngis 1200×630 (±1 px), community-medallion composition (not a stretched square, not the #488 typesetting card).image/pngorimage/jpeg, no SVG.index.html); no JS-injected OG.Host, query, hash, or user input.og:image:width/og:image:heightmatch the file;og:image:alt/twitter:image:altdescribe the medallion (no wallet/pair data).Relevant files
frontend-dapp/index.htmlfrontend-dapp/public/og-image.pngfrontend-dapp/public/community-opengraph-concept.pngdocker/frontend/nginx.confdocker/frontend/Dockerfiledist/(must includeog-image.png)frontend-dapp/vite.config.tstransformIndexHtmlCSP inject — optional origin bakefrontend-dapp/viteCsp.tsimg-src; do not add random OG CDNsdocs/design-system.mdskills/AGENTS_FRONTEND_DESIGN_SYSTEM.mddocs/qa/issue-488/README.md.tmp-brand-concepts/07-opengraph-dark-1200x630.pngRecommended direction
Artwork. From
community-opengraph-concept.png, produce a 1200×630 PNG: extend canvas to 1.91:1 (pillarbox the hall / chart into the extra width, or recrop so the medallion sits slightly left-of-center with chart continuing right). Upscale/denoise so the portrait andCL8Ylettering stay sharp at card size. Write the result tofrontend-dapp/public/og-image.png. Keep the square concept as source; do not serve it asog:image.Absolute meta in
index.html(production):og:url=https://dex.cl8y.com/og:site_name=CL8Y DEXog:image+og:image:secure_url+twitter:image=https://dex.cl8y.com/og-image.pngog:image:width=1200,og:image:height=630,og:image:type=image/pngog:image:alt/twitter:image:alt= short description of the medallion (no user data)If local
vitemust not hardcode prod: bake viatransformIndexHtmlfrom allowlistedVITE_PUBLIC_ORIGINdefaulting tohttps://dex.cl8y.comin production builds. Local relative URLs are acceptable only whenmode !== 'production'.Optional
robots.txtallowingTwitterbot,facebookexternalhit,TelegramBot,Discordbot,Slackbot,LinkedInBot,WhatsApp— only if added; default allow-all is OK. NeverDisallow: /for those UAs.Tests (Vitest or a small Node script +
make verify-issue-<iid>): parseindex.html(and/or production-transformed HTML fixture) for absolutehttps://dex.cl8y.com/og-image.png; PNG dimensions 1200×630; file size < 5 MB; grep that production HTML does not leavecontent="/og-image.png"; grep noHost/window.locationOG construction.Post-deploy.
curl -sI https://dex.cl8y.com/og-image.png→200,image/png(or jpeg), bytes match. Re-fetch X card cache (composer preview or Card Validator if still available). Confirm Telegram, Discord, Slack.Acceptance criteria
index.html(vite build/ Coolify output) hasog:imageandtwitter:imageas absolutehttps://dex.cl8y.com/og-image.png(not/og-image.png).twitter:cardissummary_large_image; title/description still describe swaps/limits/Terra Classic.GET /og-image.pngon prod and localdist/is 1200×630, community-medallion composition (not stretched square, not #488 typesetting card), < 5 MB.curlof/,/trade,/pool,/charts,/limits,/ust1(and at least one/charts/:pairand/trader/:address) all include the same OG/Twitter tags in the HTML (not after JS).og:image:width/height/altpresent and match the file.Host, query, or user input. No SVG OG. No new third-party image host in CSP/og:image./og-image.png.https://dex.cl8y.comafter deploy + crawler refresh.Test plan — functional paths
index.htmlhttps://OG + Twitter image;summary_large_image; title/description unchanged in spiritog-image.pngbinaryvite builddist/index.html+dist/og-image.pngtry_files/og-image.png→ 200 image;/no-such-route→index.htmlwith same meta (not 404 JSON)/,/pool,/trade,/charts,/limits,/protocol,/portfolio,/ust1,/wrap,/create,/tiersvite(dev)https://dex.cl8y.comContent-Typeof imageimage/pngorimage/jpeg— nottext/htmlTest plan — attack, hack, and abuse
Host: evil.exampleon GET/https://dex.cl8y.com/..., neverhttps://evil.example/.../?og=https://evil/x.png,?image=,?url=/#og-image=.../trade/:pair,/trader/:addressog:title/og:image(phishing / address spoofing)javascript:/data:/http:image//dex.cl8y.com/og-image.pnghttps://og:imageog:imageis a static same-origin file path onlyX-Content-Type-Options: nosniffstaysimg-srcnot opened to*or random CDNs for this issueX-Frame-Options: DENYunchanged; cards fetch the image URL, not an iframe of the dAppDisallow: /or blockTwitterbot/facebookexternalhit/TelegramBotVary: Hoston HTML that would let a poisoned origin be cached as OG; HTMLCache-Control: no-cacheonindex.htmlstaysog-image.png; no hotlinked third-party OG URLHostfrom the request intoog:url/og:image. Use a baked allowlisted origin.Verification criteria
Close when all of the following are true:
make verify-issue-<iid>(or documented equivalent) passes F1–F3 and A1–A7 greps/tests in CI without LocalTerra.curl -sL https://dex.cl8y.com/ | grep og:imageshowshttps://dex.cl8y.com/og-image.png.curl -sI https://dex.cl8y.com/og-image.pngis200with image content-type; dimensions 1200×630.https://dex.cl8y.com(post-cache-bust), showing the community medallion.No chain, indexer, or wallet work required.
mentioned in merge request !1097
mentioned in commit
4068d8d48dMerged via !1097.
make verify-issue-578passed (OG allowlist, nginx PNG, Coolify origin bake). Remaining AC8 is post-deploy crawler QA after Coolify ships this main:curl -sL https://dex.cl8y.com/ | grep og:imageshows absolutehttps://dex.cl8y.com/og-image.pngcurl -sI https://dex.cl8y.com/og-image.png→ 200image/pngmentioned in issue #583
marked as related to #583
mentioned in issue #653
mentioned in issue #665
marked as related to #665
mentioned in issue #713