Card designer
The designer at /design is how a card gets made and bought. An artist arrives
with cover art and leaves with an order; everything between is derived from that
one image.
This page describes current code. If it disagrees with the repo, the repo wins.
The five steps
| Step | What the artist does | What the system does |
|---|---|---|
| 1 Artwork | Drops in cover art, or picks a stock colour | Samples the image in all five moods and offers them as real swatches |
| 2 Layout | Picks one of fourteen designs | Renders every thumbnail with their art and their colours |
| 3 Words | Types name, title and any optional lines | Auto-fits the title, checks length and contrast, shows their slug URL |
| 4 Music | Uploads tracks, or skips | Creates the workspace and release, writes the player theme, themes the preview |
| 5 Order | Picks quantity of Custom Slappcards | Exports both print faces, saves the design, hands off to Stripe |
The card is on screen the whole time, front and back, in either orientation. The
draft saves itself to cardDesigns after every change, keyed by an opaque token
in localStorage, so someone can design before they have an account and still
have the work when they come back.
flowchart TD
art[Cover art] --> sample[extractPalette OKLab]
sample --> colourway[Card colourway]
sample --> theme[releases.playerTheme]
colourway --> card[CardFace]
layout[Template] --> card
words[Content slots] --> card
slug[artists.slug] --> qr[QR - slappcard.com/slug]
qr --> card
card --> export[300dpi front and back to R2]
export --> batch[cardBatches print files]
theme --> listen[Fan listen page]
The engine
apps/web/src/lib/card/ is the v5.1 designer bundle, ported as authored.
| File | What it holds |
|---|---|
templates.ts | SPEC, COLORWAYS, SLOTS, the 14 templates, the 10 house backs, the decoration library |
qr.ts | Module shapes, finder eyes, plate and logo knockout, on top of uqr for the encoding |
colorway.ts | Bridge from a sampled ExtractedPalette to a paintable CardColorway |
design.ts | Stored config to renderable data, plus the production check |
print.ts | 300 dpi geometry and rasterisation |
artwork.ts | Upload ingest — a working copy and a small stored preview |
Everything is authored in millimetres and mapped through one --mm property, so
the thumbnail, the live preview and the print export come from a single set of
numbers. Only --zoom changes.
Format. 88.9 × 50.8 mm trim, 3.175 mm bleed and safe area, 3 mm corner radius. At 300 dpi that is 1050 × 600 px trim, 1125 × 675 with bleed. This is a US business card, not the CR80 the placeholder system used.
The templates emit HTML strings rather than JSX. That is deliberate: rewriting
28 authored layouts as components would forfeit the decode and legibility
verification they arrived with. Copy is escaped by the engine (esc, including
quotes) and image sources are scheme-checked in CardFace before they reach an
img tag.
Layouts
split Two-Track · bleed Blackout · band Top Billing · sleeve The Sleeve ·
type All Caps · frame The Classic · ticket The Stub · stack Flyer ·
deck Boombox · stamp Certified · spine J-Card · meter In the Red ·
tile Clean Cut · strip Reel to Reel
Each is separately authored in landscape and portrait — portrait is not the landscape layout squeezed into a column. Each declares which of the ten house backs it uses, so the two faces read as one object, and each carries its own example copy so the picker shows a finished card rather than a blank one.
The scan code
One code per artist, encoding https://slappcard.com/\{slug\}.
Every card in a pack carries it, so a reprint never invalidates cards already in
someone's hands, and the artist can put the same link on socials. The catch-all
route resolves the slug — legacy Wix slugs first, then live artist handles — into
that artist's shared card, and redirects to /c/\{code\} so entitlements, gating
and scan analytics all run through the one existing path.
A batch therefore mints one shared card row, not one per physical card;
cardBatches.quantity remains the number being printed and shipped. Shared
cards are excluded from the abuse ratio for the same reason legacy ones are:
that ratio assumes one card per recipient.
The asset lives at qr/artist/\{artistId\}.svg, generated by
qrActions.generateArtistQr.
Keep slugs short. The block is 20 mm and a module must stay at or above
0.5 mm. slappcard.com/riz is comfortable; a very long handle pushes the code
denser and costs acquisition distance. The designer refuses to check out if the
link will not scan.
Print
The designer exports both faces at 300 dpi with bleed and the code already
composited, uploads them to R2, and records them on the design as
frontArtR2Key / backArtR2Key. Because every card in the run is identical,
printPipelineActions.generateBatchPrintFiles no longer renders anything: it
confirms both objects exist and marks the batch ready_to_print.
Bleed is produced by scaling the face up by the bleed ratio and centre-cropping, which runs type about 7% larger than the authored millimetres. The alternative — a trim-sized card centred on a bleed canvas — leaves a border of face colour around every full-bleed layout.
Corners export square. The printer die-cuts the 3 mm radius.
A batch still waits on tracklist: cards that scan to an empty page are not
worth printing.
Data
cardDesigns.config is v2:
{
version: 2,
template: "split" | … , // 14
orientation: "landscape" | "portrait",
colorway:
| { source: "stock", id: "og" | "blk" | "red" | "pur" | "lim" | "pnk" }
| { source: "artwork", mood, accent?, resolved: { face, ink, accent, dark, swatches } },
qr: { url, ec, shape, eye, dark, light, logo },
content: { title, artist, subtitle, meta, badge, cta, url },
}
Sampled palettes cache their resolved colours so ops and the print pipeline never re-run the sampler on an image they may not be able to read.
Pre-v2 rows are not migrated; the stored validator is a union and
convex/lib/cardDesign.ts lifts them to v2 on read, mapping retired faces onto
the closest layout and rebuilding an artwork colourway from the palette v1 kept
in custom.artFace/artInk/artAccent.
Alongside the config: artworkPreview (a downscaled data URL, cheap enough to
store on the document) and artworkR2Key (the full-resolution original).
Checkout
cardDesignId travels from the designer into createCheckoutSession, onto the
Stripe session metadata, and onto the pending purchases row. When payment
lands, mintCardBatch binds the design to the batch it just created and moves
it to ordered, then the asset check runs. Nobody has to chase artwork after
the fact — which is what used to leave batches sitting in needs_assets.
Checkout still requires an account, but the design does not: the workspace is created from the name already typed in the wizard rather than sending the artist away to make one first.
Where else the engine renders
CardFace is the only card renderer in the app. Marketing pages use
MarketingCard, a container-fitting wrapper around it, so the card on the home
page is the card that prints. Print Studio (/studio) reviews submitted designs
and pulls the two faces down for the print house.