Skip to content

DocsBuilding on Dawnscan

Share cards

The image and the text behind a shared project link.

A Dawnscan project link is the thing founders and readers actually post. This document is the contract for what Dawnscan puts behind that link: the social image, the composed post, and the rules both obey.

It is the sharing counterpart to Card V7 — same facts, same wording, different surface.

1. The routes

Route What it renders Used by
GET /og/project/<slug> that project's card, 1200×630 PNG og:image / twitter:image on /project/<slug>
GET /og/default the site card, 1200×630 PNG the root layout, so /, /explore, /ships, /pulse, the narratives and everything else inherit it

Both are explicit route handlers (apps/web/src/app/og/**/route.tsx) rather than Next's opengraph-image file convention. The convention produces a build-hashed path nobody can type; an address a founder can paste into a card validator, a test can request and an operator can curl is worth more than the few lines the convention saves. The two file-convention images that existed before (app/opengraph-image.tsx and app/(site)/project/[slug]/opengraph-image.tsx) were removed with this change: file-based metadata overrides the metadata object, so leaving them in place would have silently won over openGraph.images.

2. What the project image shows

Exactly the browse card's facts, in the browse card's wording:

Fact Source When absent
Logo projects.logo_url, fetched and resized to PNG (lib/og-logo.ts) the monogram from ProjectLogo, same initials, same slug-seeded colour
Name projects.name never absent
Ticker tokens.symbol of the primary token, $AOS omitted — a tokenless project shows no ticker line
Activity status projects.activity_status, with ActivityChip's label and lucide icon never absent; research_level = INDEXED renders "Activity not researched yet", the same wording the project page uses
Last ship projects.last_meaningful_ship_at, Last shipped 2d ago omitted
Market cap the profile's stored reading (market_snapshots, seven-day window, source precedence) with its provider — $24K market cap · via DEX Screener omitted — never $0, never a figure from anywhere else
Launch origin launchedViaFromvia Pons omitted, including when it resolves to Unknown or Independent: those are placeholders for the absence of a launch record, not origins
Dawnscan mark and dawnscan.fun constant never absent

Nothing else is on the image: no score, no ship count, no description, no chart, no holders, no price. An absent fact is absent — the image never fills a gap with a zero or a guess.

Design: Midnight ground (#0A111D), the dark theme's ink and status colours from globals.css, one status accent, no gradient, no glow. Names step down through four sizes by length and are clamped at 44 characters; tickers clamp at 16. og-card.test.ts covers the fitting, because satori has no ellipsis and an unclamped name draws straight over the status row.

An unknown slug returns the site card at 200, not a 404: a crawler following a stale link should still render something Dawnscan-shaped.

3. The logo

lib/og-logo.ts. satori decodes the bytes itself and does not read WebP, so the image cannot point at /api/logo; the same pipeline runs inline instead — the guarded fetcher from @hey/domain (SSRF check per redirect hop, size cap), one sharp resize to a 264px PNG, a bounded in-process cache (200 entries, 16MB, failures remembered for ten minutes).

Bounds that matter, because a crawler asks in bursts:

  • only hosts in LOGO_HOSTS are fetched at all;
  • the fetch times out at 2.5s, tighter than the proxy's 5s — a slow logo must not hold a crawler's connection;
  • a failure is never an error: the card draws the monogram, exactly as the browse card does when an <img> fails.

4. The copy rules

lib/share.ts composes every share sentence, and share.test.ts enforces the rules. One composer, so the image, the page metadata and the Share button cannot drift apart.

AgentOS ($AOS) — Shipping · last shipped 2d ago · via Pons
https://dawnscan.fun/project/agentos
  • identity, then what Dawnscan can say about the building, then where the token launched; every clause is dropped rather than filled when Dawnscan lacks it;
  • no market figure at all in the text — no market cap, no price, no percentage. A sentence about building must not read as a sentence about price (CLAUDE.md product rules 3, 5, 7);
  • no score, no ranking, no percentile;
  • banned outright, asserted by test: 100x, alpha, ape, bullish, buy, degen, gem, moon, pump, undervalued;
  • a project Dawnscan has only indexed says "Activity not researched yet" and makes no activity claim.

5. The share control

components/share-project.tsx, on the project page in the row with Website, GitHub, X and Sources — a secondary control, because the page keeps one primary CTA (UI/UX non-negotiable 10).

  • Share copies the headline plus the project URL to the clipboard and says "Copied" for 1.6s, with an aria-live announcement. Falls back to a hidden textarea and execCommand where the clipboard API is refused.
  • Open on X opens https://twitter.com/intent/tweet?text=…&url=… in a new tab. The headline and the link are separate parameters, so X does not print the link twice.
  • Both stop the click from propagating, so dropping the control inside a card body later cannot turn "Share" into "navigate". Both are their own tab stop.

6. Cache policy

lib/og-headers.ts:

cache-control: public, max-age=300, s-maxage=3600, stale-while-revalidate=86400
x-content-type-options: nosniff
content-security-policy: default-src 'none'; sandbox

Five minutes in a browser, an hour in a shared cache, a day of serving the stale copy while the new one renders. A project whose status changed shows its new image within the hour, and no crawler ever waits on satori twice. The route is also rate-limited (api.og, 300/minute): a satori render plus a logo fetch is the most expensive public GET in the app.

7. Regenerating and checking

The image is generated per request; there is nothing to rebuild. To look at one:

# against a local production build
curl -o /tmp/agentos.png http://127.0.0.1:3000/og/project/agentos
curl -o /tmp/default.png http://127.0.0.1:3000/og/default

The one build artefact is the inlined mark, lib/og-mark.ts. It is apps/web/public/dawn-mark-dark.png at its native 136×128, base64'd, because the renderer has no access to public/ and a fetch would put a network call in the render path. Regenerate it when the artwork changes:

node -e "const s=require('sharp');s('apps/web/public/dawn-mark-dark.png').png().toBuffer().then(b=>console.log(b.toString('base64')))"

Coverage: e2e/share.spec.ts (PNG signature and 1200×630 dimensions, the metadata tags, the clipboard round trip, 375px) and the two unit suites (share.test.ts, og-card.test.ts).

8. Not done

  • No copy-for-X block on /ships. The ship feed and its card belong to the "Shipped this week" work in flight at the same time; adding a control there would have meant editing the same files. It is a small addition once that lands: the composer already takes a ShareFacts, and a ship's card has the facts it needs.
  • No per-ship image. /og/project/<slug> states the project's latest position, not one ship event.