feat: fix Open Graph / Twitter cards on all dApp routes and ship community OG artwork #578

Closed
opened 2026-08-19 11:53:23 +00:00 by PlasticDigits · 9 comments
PlasticDigits commented 2026-08-19 11:53:23 +00:00 (Migrated from gitlab.com)

Summary

Social previews for the dApp (https://dex.cl8y.com) render on Telegram but not on X/Twitter. All public SPA routes share one static index.html shell, 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.html meta. 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.

Surface What happens today
Meta frontend-dapp/index.html sets og:title / og:description / og:type / og:image and twitter:card (summary_large_image) / twitter:image / twitter:title / twitter:description.
Image URLs og:image and twitter:image are root-relative (/og-image.png). Telegram (and often Discord) resolve that against the page origin. X/Twitter Card crawler requires an absolute https:// URL and silently drops the large image card when the path is relative.
Missing tags No og:url, og:site_name, og:image:secure_url, og:image:width, og:image:height, og:image:type, og:image:alt, or twitter:image:alt. No twitter:site (this repo has no official X handle — do not invent one).
Artwork Tracked frontend-dapp/public/og-image.png is 1200×630 (~83 KB), left-weighted product copy from #488 (“Swap & limit orders on Terra Classic”).
Community source Untracked frontend-dapp/public/community-opengraph-concept.png is 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.
Serving docker/frontend/nginx.conf try_files $uri $uri/ /index.html so /, /trade, /pool, /charts/:pair, /trader/:address, etc. all return the same index.html. PNGs are immutable-cached (`location ~* .(png
CSP Production CSP is injected at Vite build (frontend-dapp/viteCsp.ts / frontend-dapp/vite.config.ts). Crawlers do not honor CSP; still keep img-src same-origin for browsers.
robots No robots.txt. Fine for allow-all, but there is no explicit allow for Twitterbot / facebookexternalhit / TelegramBot if a future deny is added.
Runtime OG No react-helmet, no per-route meta, no query-param OG. Keep it that way.
Docs docs/design-system.md and skills/AGENTS_FRONTEND_DESIGN_SYSTEM.md still 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

  1. X/Twitter is a primary distribution channel. Telegram showing a preview while X shows a naked URL is a crawler-tag bug, not “Twitter being flaky.” Relative og:image / twitter:image is the documented failure mode.
  2. One shell, all sites. Fixing index.html + the PNG once covers every public dApp URL. Do not add per-route OG, SSR, or user-supplied preview URLs.
  3. Brand. Operators want the community medallion (not the #488 typesetting card) as the share image, at real OG dimensions (1200×630, ~1.91:1), upscaled/recomposed so the coin and chart read clearly in Telegram, X, Discord, Slack, iMessage, Facebook, and LinkedIn.
  4. Docs are stale. Design-system still treats the typesetting OG as current; this issue explicitly replaces that artwork with the community piece while keeping the same-origin URL rule.

Constraints / guardrails

  1. Same-origin, static URLs only. og:image / twitter:image must be https://dex.cl8y.com/og-image.png (or the same path on a documented staging origin baked at build time). Never interpolate window.location, the HTTP Host / X-Forwarded-Host header, query params, hash, pair address, or wallet address into OG tags.
  2. No dynamic OG / no helmet. Do not add react-helmet, prerender.io, or per-route titles for this issue. Crawlers never see client-updated meta.
  3. Do not stretch the square. Recompose the 1254×1254 concept onto 1200×630 (safe crop/canvas: medallion + chart readable; avoid cutting the portrait or CL8Y wordmark). Optional 2× 2400×1260 is unnecessary if 1200×630 is sharp.
  4. File budget. Final og-image.png (or .jpg if 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).
  5. MIME + path. Served as static /og-image.png from frontend-dapp/public/ so Vite copies it to dist/ and nginx serves it as image/png without falling through to index.html.
  6. Do not invent twitter:site. Add it only if product supplies an official @handle.
  7. No Host-header origin. If a VITE_PUBLIC_ORIGIN (or similar) is used for local/staging, allowlist https://dex.cl8y.com and explicit staging hosts; reject http:, javascript:, data:, //, and any non-https origin. Default production origin is https://dex.cl8y.com.
  8. Do not wire user-controlled OG URLs, open-graph “debuggers” that echo attacker URLs, or token-logo hosts into og:image.
  9. CSP / headers. Do not weaken production CSP, X-Frame-Options, or Referrer-Policy “for crawlers.”
  10. Cache. PNG Cache-Control: public, immutable is OK only if operators accept a crawler re-fetch after replace. Prefer replacing og-image.png in place (same URL) plus an X card-cache reset after deploy. Query-string cache-bust (?v=) is discouraged — some crawlers dislike it.
  11. Concept file. Commit community-opengraph-concept.png as the source. Production crawlers must fetch /og-image.png, not the 1.75 MB square.
  12. Copy. Keep existing title/description unless product asks otherwise. Image is brand art; meta text stays product (swaps / limits / Terra Classic).
  13. Docs + skill in the same MR: update docs/design-system.md, skills/AGENTS_FRONTEND_DESIGN_SYSTEM.md, docs/qa/issue-488/README.md so agents do not restore the typesetting card.

Invariants:

ID Meaning
OG-1 Production og:image and twitter:image are absolute https:// URLs on dex.cl8y.com (or baked allowlisted staging origin), pointing at /og-image.png.
OG-2 twitter:card remains summary_large_image.
OG-3 Shipped /og-image.png is 1200×630 (±1 px), community-medallion composition (not a stretched square, not the #488 typesetting card).
OG-4 Image file < 5 MB (target < 1 MB), image/png or image/jpeg, no SVG.
OG-5 All SPA routes return the same static meta (nginx → index.html); no JS-injected OG.
OG-6 OG URLs are never derived from Host, query, hash, or user input.
OG-7 og:image:width / og:image:height match the file; og:image:alt / twitter:image:alt describe the medallion (no wallet/pair data).
OG-8 Design-system docs match the community artwork + absolute-URL rule.

Relevant files

File Role
frontend-dapp/index.html OG + Twitter meta (relative URLs today)
frontend-dapp/public/og-image.png Current 1200×630 typesetting card — replace
frontend-dapp/public/community-opengraph-concept.png Community source (1254×1254) — recompose from this
docker/frontend/nginx.conf SPA fallback + static PNG cache
docker/frontend/Dockerfile Copies Vite dist/ (must include og-image.png)
frontend-dapp/vite.config.ts transformIndexHtml CSP inject — optional origin bake
frontend-dapp/viteCsp.ts Production img-src; do not add random OG CDNs
docs/design-system.md Brand asset table — update OG row
skills/AGENTS_FRONTEND_DESIGN_SYSTEM.md Agent OG rule
docs/qa/issue-488/README.md Stale “product OG” note
.tmp-brand-concepts/07-opengraph-dark-1200x630.png Old concept pack — do not ship; community medallion wins
  1. 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 and CL8Y lettering stay sharp at card size. Write the result to frontend-dapp/public/og-image.png. Keep the square concept as source; do not serve it as og:image.

  2. Absolute meta in index.html (production):

    • og:url = https://dex.cl8y.com/
    • og:site_name = CL8Y DEX
    • og:image + og:image:secure_url + twitter:image = https://dex.cl8y.com/og-image.png
    • og:image:width = 1200, og:image:height = 630, og:image:type = image/png
    • og:image:alt / twitter:image:alt = short description of the medallion (no user data)

    If local vite must not hardcode prod: bake via transformIndexHtml from allowlisted VITE_PUBLIC_ORIGIN defaulting to https://dex.cl8y.com in production builds. Local relative URLs are acceptable only when mode !== 'production'.

  3. Optional robots.txt allowing Twitterbot, facebookexternalhit, TelegramBot, Discordbot, Slackbot, LinkedInBot, WhatsApp — only if added; default allow-all is OK. Never Disallow: / for those UAs.

  4. Tests (Vitest or a small Node script + make verify-issue-<iid>): parse index.html (and/or production-transformed HTML fixture) for absolute https://dex.cl8y.com/og-image.png; PNG dimensions 1200×630; file size < 5 MB; grep that production HTML does not leave content="/og-image.png"; grep no Host / window.location OG construction.

  5. 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

  • AC1 — Production index.html (vite build / Coolify output) has og:image and twitter:image as absolute https://dex.cl8y.com/og-image.png (not /og-image.png).
  • AC2 — twitter:card is summary_large_image; title/description still describe swaps/limits/Terra Classic.
  • AC3 — GET /og-image.png on prod and local dist/ is 1200×630, community-medallion composition (not stretched square, not #488 typesetting card), < 5 MB.
  • AC4 — curl of /, /trade, /pool, /charts, /limits, /ust1 (and at least one /charts/:pair and /trader/:address) all include the same OG/Twitter tags in the HTML (not after JS).
  • AC5 — og:image:width / height / alt present and match the file.
  • AC6 — No OG URL built from Host, query, or user input. No SVG OG. No new third-party image host in CSP/og:image.
  • AC7 — Design-system + AGENTS skill + #488 QA note updated. Concept source committed; crawlers use /og-image.png.
  • AC8 — Manual: Telegram and X/Twitter (and Discord) show the large image card for https://dex.cl8y.com after deploy + crawler refresh.

Test plan — functional paths

ID Path Expect
F1 Parse production index.html Absolute https:// OG + Twitter image; summary_large_image; title/description unchanged in spirit
F2 og-image.png binary 1200×630, PNG/JPEG magic bytes, size < 5_000_000
F3 vite build dist/index.html + dist/og-image.png Asset copied; meta not left relative in production mode
F4 nginx try_files /og-image.png → 200 image; /no-such-route → index.html with same meta (not 404 JSON)
F5 Routes: /, /pool, /trade, /charts, /limits, /protocol, /portfolio, /ust1, /wrap, /create, /tiers Identical OG tags in raw HTML
F6 Local vite (dev) Must not require Twitter; relative OG OK in non-prod or absolute prod URL (either is fine)
F7 Telegram share of https://dex.cl8y.com Large preview, new medallion (not typesetting card)
F8 X/Twitter compose/preview of same URL Large image card (not title-only). If cached, reset card cache after deploy
F9 Discord + Slack unfurl Same image
F10 Facebook Sharing Debugger / LinkedIn Post Inspector (optional) 200 scrape, 1200×630
F11 Content-Type of image image/png or image/jpeg — not text/html
F12 Docs grep Design-system no longer claims #488 typesetting OG as current art

Test plan — attack, hack, and abuse

ID Vector Expect
A1 Host-header injection — Host: evil.example on GET / OG URLs still https://dex.cl8y.com/..., never https://evil.example/...
A2 Query OG override — /?og=https://evil/x.png, ?image=, ?url= Ignored; static meta only
A3 Hash / fragment — /#og-image=... Ignored (crawlers strip hash anyway)
A4 Pair/wallet in meta — /trade/:pair, /trader/:address Same generic tags; no address or pair interpolated into og:title / og:image (phishing / address spoofing)
A5 javascript: / data: / http: image Rejected by allowlist / not present in HTML
A6 Protocol-relative //dex.cl8y.com/og-image.png Do not ship; use https://
A7 Open redirect as og:image og:image is a static same-origin file path only
A8 SVG / HTML disguised as PNG File is real PNG/JPEG; nginx X-Content-Type-Options: nosniff stays
A9 Oversized payload Image < 5 MB so crawlers do not abort; no zip-bomb
A10 CSP weakening Production img-src not opened to * or random CDNs for this issue
A11 Clickjack / preview iframe X-Frame-Options: DENY unchanged; cards fetch the image URL, not an iframe of the dApp
A12 robots trap Do not Disallow: / or block Twitterbot / facebookexternalhit / TelegramBot
A13 Cache poisoning No Vary: Host on HTML that would let a poisoned origin be cached as OG; HTML Cache-Control: no-cache on index.html stays
A14 Supply-chain art Only the committed community concept → og-image.png; no hotlinked third-party OG URL
A15 Staging leak Staging builds must not copy Host from the request into og:url / og:image. Use a baked allowlisted origin.

Verification criteria

Close when all of the following are true:

  1. make verify-issue-<iid> (or documented equivalent) passes F1–F3 and A1–A7 greps/tests in CI without LocalTerra.
  2. Production curl -sL https://dex.cl8y.com/ | grep og:image shows https://dex.cl8y.com/og-image.png.
  3. curl -sI https://dex.cl8y.com/og-image.png is 200 with image content-type; dimensions 1200×630.
  4. Screenshot evidence on the MR: Telegram preview and X/Twitter large card for https://dex.cl8y.com (post-cache-bust), showing the community medallion.
  5. Docs/skills updated; #488 QA note no longer treats the typesetting card as the live OG.

No chain, indexer, or wallet work required.

## Summary Social previews for the dApp (`https://dex.cl8y.com`) render on **Telegram** but **not on X/Twitter**. All public SPA routes share one static `index.html` shell, 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](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/488) shipped the current `/og-image.png` + `index.html` meta. 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. | Surface | What happens today | |---------|-------------------| | **Meta** | [`frontend-dapp/index.html`](frontend-dapp/index.html) sets `og:title` / `og:description` / `og:type` / `og:image` and `twitter:card` (`summary_large_image`) / `twitter:image` / `twitter:title` / `twitter:description`. | | **Image URLs** | `og:image` and `twitter:image` are **root-relative** (`/og-image.png`). Telegram (and often Discord) resolve that against the page origin. **X/Twitter Card crawler requires an absolute `https://` URL** and silently drops the large image card when the path is relative. | | **Missing tags** | No `og:url`, `og:site_name`, `og:image:secure_url`, `og:image:width`, `og:image:height`, `og:image:type`, `og:image:alt`, or `twitter:image:alt`. No `twitter:site` (this repo has no official X handle — do not invent one). | | **Artwork** | Tracked [`frontend-dapp/public/og-image.png`](frontend-dapp/public/og-image.png) is **1200×630** (~83 KB), left-weighted product copy from #488 (“Swap & limit orders on Terra Classic”). | | **Community source** | Untracked [`frontend-dapp/public/community-opengraph-concept.png`](frontend-dapp/public/community-opengraph-concept.png) is **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. | | **Serving** | [`docker/frontend/nginx.conf`](docker/frontend/nginx.conf) `try_files $uri $uri/ /index.html` so `/`, `/trade`, `/pool`, `/charts/:pair`, `/trader/:address`, etc. all return the same `index.html`. PNGs are immutable-cached (`location ~* \.(png|…)$`). | | **CSP** | Production CSP is injected at Vite build ([`frontend-dapp/viteCsp.ts`](frontend-dapp/viteCsp.ts) / [`frontend-dapp/vite.config.ts`](frontend-dapp/vite.config.ts)). Crawlers do not honor CSP; still keep `img-src` same-origin for browsers. | | **robots** | No `robots.txt`. Fine for allow-all, but there is no explicit allow for `Twitterbot` / `facebookexternalhit` / `TelegramBot` if a future deny is added. | | **Runtime OG** | No `react-helmet`, no per-route meta, no query-param OG. **Keep it that way.** | | **Docs** | [`docs/design-system.md`](docs/design-system.md) and [`skills/AGENTS_FRONTEND_DESIGN_SYSTEM.md`](skills/AGENTS_FRONTEND_DESIGN_SYSTEM.md) still 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 1. **X/Twitter is a primary distribution channel.** Telegram showing a preview while X shows a naked URL is a crawler-tag bug, not “Twitter being flaky.” Relative `og:image` / `twitter:image` is the documented failure mode. 2. **One shell, all sites.** Fixing `index.html` + the PNG once covers every public dApp URL. Do not add per-route OG, SSR, or user-supplied preview URLs. 3. **Brand.** Operators want the community medallion (not the #488 typesetting card) as the share image, at **real OG dimensions** (1200×630, ~1.91:1), upscaled/recomposed so the coin and chart read clearly in Telegram, X, Discord, Slack, iMessage, Facebook, and LinkedIn. 4. **Docs are stale.** Design-system still treats the typesetting OG as current; this issue **explicitly replaces** that artwork with the community piece while keeping the same-origin URL rule. ## Constraints / guardrails 1. **Same-origin, static URLs only.** `og:image` / `twitter:image` must be `https://dex.cl8y.com/og-image.png` (or the same path on a documented staging origin baked at **build** time). Never interpolate `window.location`, the HTTP `Host` / `X-Forwarded-Host` header, query params, hash, pair address, or wallet address into OG tags. 2. **No dynamic OG / no helmet.** Do not add `react-helmet`, prerender.io, or per-route titles for this issue. Crawlers never see client-updated meta. 3. **Do not stretch the square.** Recompose the 1254×1254 concept onto **1200×630** (safe crop/canvas: medallion + chart readable; avoid cutting the portrait or `CL8Y` wordmark). Optional 2× `2400×1260` is unnecessary if 1200×630 is sharp. 4. **File budget.** Final `og-image.png` (or `.jpg` if compression needs it) **&lt; 1 MB** preferred, **hard max 5 MB** (X). Keep **PNG or JPEG** — **no SVG** (scriptable), no WebP-only (X support is uneven). 5. **MIME + path.** Served as static `/og-image.png` from `frontend-dapp/public/` so Vite copies it to `dist/` and nginx serves it as `image/png` without falling through to `index.html`. 6. **Do not invent `twitter:site`.** Add it only if product supplies an official `@handle`. 7. **No Host-header origin.** If a `VITE_PUBLIC_ORIGIN` (or similar) is used for local/staging, allowlist `https://dex.cl8y.com` and explicit staging hosts; reject `http:`, `javascript:`, `data:`, `//`, and any non-https origin. Default production origin is `https://dex.cl8y.com`. 8. **Do not** wire user-controlled OG URLs, open-graph “debuggers” that echo attacker URLs, or token-logo hosts into `og:image`. 9. **CSP / headers.** Do not weaken production CSP, `X-Frame-Options`, or `Referrer-Policy` “for crawlers.” 10. **Cache.** PNG `Cache-Control: public, immutable` is OK **only if** operators accept a crawler re-fetch after replace. Prefer replacing `og-image.png` in place (same URL) plus an X card-cache reset after deploy. Query-string cache-bust (`?v=`) is **discouraged** — some crawlers dislike it. 11. **Concept file.** Commit `community-opengraph-concept.png` as the **source**. Production crawlers must fetch **`/og-image.png`**, not the 1.75 MB square. 12. **Copy.** Keep existing title/description unless product asks otherwise. Image is brand art; meta text stays product (swaps / limits / Terra Classic). 13. **Docs + skill** in the same MR: update `docs/design-system.md`, `skills/AGENTS_FRONTEND_DESIGN_SYSTEM.md`, `docs/qa/issue-488/README.md` so agents do not restore the typesetting card. Invariants: | ID | Meaning | |----|---------| | **OG-1** | Production `og:image` and `twitter:image` are absolute `https://` URLs on `dex.cl8y.com` (or baked allowlisted staging origin), pointing at `/og-image.png`. | | **OG-2** | `twitter:card` remains `summary_large_image`. | | **OG-3** | Shipped `/og-image.png` is 1200×630 (±1 px), community-medallion composition (not a stretched square, not the #488 typesetting card). | | **OG-4** | Image file &lt; 5 MB (target &lt; 1 MB), `image/png` or `image/jpeg`, no SVG. | | **OG-5** | All SPA routes return the same static meta (nginx → `index.html`); no JS-injected OG. | | **OG-6** | OG URLs are never derived from `Host`, query, hash, or user input. | | **OG-7** | `og:image:width` / `og:image:height` match the file; `og:image:alt` / `twitter:image:alt` describe the medallion (no wallet/pair data). | | **OG-8** | Design-system docs match the community artwork + absolute-URL rule. | ## Relevant files | File | Role | |------|------| | [`frontend-dapp/index.html`](frontend-dapp/index.html) | OG + Twitter meta (relative URLs today) | | [`frontend-dapp/public/og-image.png`](frontend-dapp/public/og-image.png) | Current 1200×630 typesetting card — **replace** | | [`frontend-dapp/public/community-opengraph-concept.png`](frontend-dapp/public/community-opengraph-concept.png) | Community source (1254×1254) — **recompose from this** | | [`docker/frontend/nginx.conf`](docker/frontend/nginx.conf) | SPA fallback + static PNG cache | | [`docker/frontend/Dockerfile`](docker/frontend/Dockerfile) | Copies Vite `dist/` (must include `og-image.png`) | | [`frontend-dapp/vite.config.ts`](frontend-dapp/vite.config.ts) | `transformIndexHtml` CSP inject — optional origin bake | | [`frontend-dapp/viteCsp.ts`](frontend-dapp/viteCsp.ts) | Production `img-src`; do not add random OG CDNs | | [`docs/design-system.md`](docs/design-system.md) | Brand asset table — update OG row | | [`skills/AGENTS_FRONTEND_DESIGN_SYSTEM.md`](skills/AGENTS_FRONTEND_DESIGN_SYSTEM.md) | Agent OG rule | | [`docs/qa/issue-488/README.md`](docs/qa/issue-488/README.md) | Stale “product OG” note | | [`.tmp-brand-concepts/07-opengraph-dark-1200x630.png`](.tmp-brand-concepts/07-opengraph-dark-1200x630.png) | Old concept pack — **do not ship**; community medallion wins | ## Recommended direction 1. **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 and `CL8Y` lettering stay sharp at card size. Write the result to `frontend-dapp/public/og-image.png`. Keep the square concept as source; do not serve it as `og:image`. 2. **Absolute meta** in `index.html` (production): - `og:url` = `https://dex.cl8y.com/` - `og:site_name` = `CL8Y DEX` - `og:image` + `og:image:secure_url` + `twitter:image` = `https://dex.cl8y.com/og-image.png` - `og:image:width` = `1200`, `og:image:height` = `630`, `og:image:type` = `image/png` - `og:image:alt` / `twitter:image:alt` = short description of the medallion (no user data) If local `vite` must not hardcode prod: bake via `transformIndexHtml` from **allowlisted** `VITE_PUBLIC_ORIGIN` defaulting to `https://dex.cl8y.com` in production builds. Local relative URLs are acceptable **only** when `mode !== 'production'`. 3. **Optional `robots.txt`** allowing `Twitterbot`, `facebookexternalhit`, `TelegramBot`, `Discordbot`, `Slackbot`, `LinkedInBot`, `WhatsApp` — only if added; default allow-all is OK. Never `Disallow: /` for those UAs. 4. **Tests** (Vitest or a small Node script + `make verify-issue-<iid>`): parse `index.html` (and/or production-transformed HTML fixture) for absolute `https://dex.cl8y.com/og-image.png`; PNG dimensions 1200×630; file size &lt; 5 MB; grep that production HTML does not leave `content="/og-image.png"`; grep no `Host` / `window.location` OG construction. 5. **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 - [ ] **AC1** — Production `index.html` (`vite build` / Coolify output) has `og:image` and `twitter:image` as absolute `https://dex.cl8y.com/og-image.png` (not `/og-image.png`). - [ ] **AC2** — `twitter:card` is `summary_large_image`; title/description still describe swaps/limits/Terra Classic. - [ ] **AC3** — `GET /og-image.png` on prod and local `dist/` is **1200×630**, community-medallion composition (not stretched square, not #488 typesetting card), &lt; 5 MB. - [ ] **AC4** — `curl` of `/`, `/trade`, `/pool`, `/charts`, `/limits`, `/ust1` (and at least one `/charts/:pair` and `/trader/:address`) all include the same OG/Twitter tags in the **HTML** (not after JS). - [ ] **AC5** — `og:image:width` / `height` / `alt` present and match the file. - [ ] **AC6** — No OG URL built from `Host`, query, or user input. No SVG OG. No new third-party image host in CSP/`og:image`. - [ ] **AC7** — Design-system + AGENTS skill + #488 QA note updated. Concept source committed; crawlers use `/og-image.png`. - [ ] **AC8** — Manual: Telegram **and** X/Twitter (and Discord) show the large image card for `https://dex.cl8y.com` after deploy + crawler refresh. ## Test plan — functional paths | ID | Path | Expect | |----|------|--------| | **F1** | Parse production `index.html` | Absolute `https://` OG + Twitter image; `summary_large_image`; title/description unchanged in spirit | | **F2** | `og-image.png` binary | 1200×630, PNG/JPEG magic bytes, size &lt; 5_000_000 | | **F3** | `vite build` `dist/index.html` + `dist/og-image.png` | Asset copied; meta not left relative in production mode | | **F4** | nginx `try_files` | `/og-image.png` → 200 image; `/no-such-route` → `index.html` with same meta (not 404 JSON) | | **F5** | Routes: `/`, `/pool`, `/trade`, `/charts`, `/limits`, `/protocol`, `/portfolio`, `/ust1`, `/wrap`, `/create`, `/tiers` | Identical OG tags in raw HTML | | **F6** | Local `vite` (dev) | Must not require Twitter; relative OG OK in non-prod **or** absolute prod URL (either is fine) | | **F7** | Telegram share of `https://dex.cl8y.com` | Large preview, new medallion (not typesetting card) | | **F8** | X/Twitter compose/preview of same URL | Large image card (not title-only). If cached, reset card cache after deploy | | **F9** | Discord + Slack unfurl | Same image | | **F10** | Facebook Sharing Debugger / LinkedIn Post Inspector (optional) | 200 scrape, 1200×630 | | **F11** | `Content-Type` of image | `image/png` or `image/jpeg` — not `text/html` | | **F12** | Docs grep | Design-system no longer claims #488 typesetting OG as current art | ## Test plan — attack, hack, and abuse | ID | Vector | Expect | |----|--------|--------| | **A1** | **Host-header injection** — `Host: evil.example` on GET `/` | OG URLs still `https://dex.cl8y.com/...`, never `https://evil.example/...` | | **A2** | **Query OG override** — `/?og=https://evil/x.png`, `?image=`, `?url=` | Ignored; static meta only | | **A3** | **Hash / fragment** — `/#og-image=...` | Ignored (crawlers strip hash anyway) | | **A4** | **Pair/wallet in meta** — `/trade/:pair`, `/trader/:address` | Same generic tags; **no** address or pair interpolated into `og:title` / `og:image` (phishing / address spoofing) | | **A5** | **`javascript:` / `data:` / `http:` image** | Rejected by allowlist / not present in HTML | | **A6** | **Protocol-relative** `//dex.cl8y.com/og-image.png` | Do not ship; use `https://` | | **A7** | **Open redirect** as `og:image` | `og:image` is a static same-origin file path only | | **A8** | **SVG / HTML disguised as PNG** | File is real PNG/JPEG; nginx `X-Content-Type-Options: nosniff` stays | | **A9** | **Oversized payload** | Image &lt; 5 MB so crawlers do not abort; no zip-bomb | | **A10** | **CSP weakening** | Production `img-src` not opened to `*` or random CDNs for this issue | | **A11** | **Clickjack / preview iframe** | `X-Frame-Options: DENY` unchanged; cards fetch the **image URL**, not an iframe of the dApp | | **A12** | **robots trap** | Do not `Disallow: /` or block `Twitterbot` / `facebookexternalhit` / `TelegramBot` | | **A13** | **Cache poisoning** | No `Vary: Host` on HTML that would let a poisoned origin be cached as OG; HTML `Cache-Control: no-cache` on `index.html` stays | | **A14** | **Supply-chain art** | Only the committed community concept → `og-image.png`; no hotlinked third-party OG URL | | **A15** | **Staging leak** | Staging builds must not copy `Host` from the request into `og:url` / `og:image`. Use a baked allowlisted origin. | ## Verification criteria Close when all of the following are true: 1. `make verify-issue-<iid>` (or documented equivalent) passes F1–F3 and A1–A7 greps/tests in CI without LocalTerra. 2. Production `curl -sL https://dex.cl8y.com/ | grep og:image` shows `https://dex.cl8y.com/og-image.png`. 3. `curl -sI https://dex.cl8y.com/og-image.png` is `200` with image content-type; dimensions 1200×630. 4. Screenshot evidence on the MR: Telegram preview **and** X/Twitter large card for `https://dex.cl8y.com` (post-cache-bust), showing the community medallion. 5. Docs/skills updated; #488 QA note no longer treats the typesetting card as the live OG. No chain, indexer, or wallet work required.
PlasticDigits commented 2026-08-20 01:38:20 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1097

mentioned in merge request !1097
PlasticDigits commented 2026-08-20 03:16:10 +00:00 (Migrated from gitlab.com)

mentioned in commit 4068d8d48d

mentioned in commit 4068d8d48d2a84cbf9cc66f45be91f85cf19396b
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-20 03:16:11 +00:00
PlasticDigits commented 2026-08-20 03:54:14 +00:00 (Migrated from gitlab.com)

Merged via !1097.

make verify-issue-578 passed (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:image shows absolute https://dex.cl8y.com/og-image.png
  • curl -sI https://dex.cl8y.com/og-image.png → 200 image/png
  • Telegram + X/Twitter (reset card cache) + Discord large preview
Merged via !1097. `make verify-issue-578` passed (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:image` shows absolute `https://dex.cl8y.com/og-image.png` - `curl -sI https://dex.cl8y.com/og-image.png` → 200 `image/png` - Telegram + X/Twitter (reset card cache) + Discord large preview
PlasticDigits commented 2026-08-20 03:54:52 +00:00 (Migrated from gitlab.com)

mentioned in issue #583

mentioned in issue #583
PlasticDigits commented 2026-08-20 03:55:00 +00:00 (Migrated from gitlab.com)

marked as related to #583

marked as related to #583
PlasticDigits commented 2026-08-26 01:11:06 +00:00 (Migrated from gitlab.com)

mentioned in issue #653

mentioned in issue #653
PlasticDigits commented 2026-08-26 04:16:12 +00:00 (Migrated from gitlab.com)

mentioned in issue #665

mentioned in issue #665
PlasticDigits commented 2026-08-26 04:16:14 +00:00 (Migrated from gitlab.com)

marked as related to #665

marked as related to #665
PlasticDigits commented 2026-08-31 05:36:05 +00:00 (Migrated from gitlab.com)

mentioned in issue #713

mentioned in issue #713
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#578
No description provided.