Building a marketing page
You are building the marketing page for a Boardweaver game: the public landing page a prospective player sees at /game/<slug> before they've played. Its job is to sell the game: set the mood, show it off, and get the visitor to start playing or read the rules. Each page is a file at /src/marketing/<slug>.bwss, a JSON document holding a BWSS tree. At commit time the platform compiles every .bwss source into a standalone HTML page; you never write HTML yourself.
This is reference-and-art work, not gameplay logic. A marketing page renders no live state and dispatches no game actions — it is static, themed presentation: a hero, prose, illustrations, and two special calls-to-action (a Play button and a Rules button) that the platform wires up for you.
It mirrors the rules pages pipeline (/src/rules/*.bwss) almost exactly — if you've built rules pages, this will feel familiar. The two differences are the source directory (/src/marketing/ vs /src/rules/) and the two extra interactive button nodes available only here.
Begin by locking the game
Work against a single, already-selected game.
- If you don't have a
game_definition_idyet, calllist_my_gamesand have the author pick one. Only ever touch that one game. - Call
get_gameto read the game'sname,description,estimatedPlayTime,complexity,mood, andvoice, andlist_filesto see what already exists under/src: a theme, rules pages, the game code, any existing marketing page. - A marketing page is the first impression. Lean on
mood(visual direction) andvoice(the tone of the copy) so the page reads like the game, not generic ad copy.
Step 1 — Develop the theme first
A marketing page is the most theme-dependent surface in the game — it's pure presentation, the visitor's first look at the world. If the game has no theme yet, develop one before writing the marketing page. Building it first means redoing it once the visual world exists.
- Check for
/src/theme.html(the visual style reference) and/theme.json(the structured theme document with$-tokens). Readv2-design-theme-instructionsandv2-game-themesfor how these are produced. - If neither exists, pause and develop the theme — offer to run the theme workflow (the
design-game/ theme step), or do it now. A theme gives you a palette, typography, and mood to design the page against, and/theme.json$-tokens you can reference directly in BWSS styles. - If the author explicitly wants to skip theming, you may proceed with restrained, neutral styling — but say so, and prefer theme tokens over hardcoded colors so the page picks up the theme later.
Step 2 — Align on the page with the author
Before writing the .bwss file, agree with the author on what the page says and shows. Don't guess — converge on it together.
- A marketing page is usually a single page,
index— the landing page served at the bare/game/<slug>route. You can add secondary pages (/src/marketing/<slug>.bwss) and link between them, but most games need onlyindex. - Propose a section outline for the page, top to bottom, e.g.
- Hero — the game's name/logo, a one-line hook, and the Play button.
- What it is — a short paragraph or two on the premise and the feel.
- How it plays — a few illustrated beats, with a Rules button for the detail.
- Footer CTA — a second Play button for visitors who scrolled.
- Confirm the hook, the copy's length and voice, which images appear, and where the two buttons go. Get explicit agreement, then build to it. Revise with the author rather than silently restructuring mid-build.
Step 3 — Author the page as BWSS
Each /src/marketing/<slug>.bwss file is one JSON value: a single root BWSS node (almost always a Box) with nested children. Write the file with write_file, passing the game_definition_id and the full JSON.
The marketing surface allows seven node types — the four from the rules surface plus the two special buttons and a theme-toggle button:
type |
Purpose |
|---|---|
"Box" |
Generic flex container. children are nested nodes or plain text strings. |
"Image" |
{ src: GameImageKey, alt?, style? } — a game image by key. Unknown keys render nothing. |
"Svg" |
{ viewBox: {width,height}, paths: [{d, fill?, ...}], style? } — inline vector art. Only <svg> + <path> are emitted. |
"Anchor" |
{ href, style?, children? } — a hyperlink to another marketing page or an in-page #fragment. Internal targets only. |
"PlayButton" |
A call-to-action that starts a match. No href — the platform builds it. children are the button's content/label. |
"RulesButton" |
A call-to-action that opens this game's rules. No href — the platform builds it. children are the button's content/label. |
"ThemeToggle" |
A button that flips the page between light and dark mode. No href, no author JS. children are the button's content/label. Only meaningful when the theme declares both background.light and background.dark; otherwise one side has nothing to render. |
Text is a plain string child inside a Box (or Anchor / button) — there is no Text node. Style text by setting typography fields (fontSize, fontWeight, color, textAlign, …) on the enclosing Box.
Image.src is a GameImageKey: a string key for media already uploaded to the game (the same keys pieces use). You don't invent keys; reference ones the game has (typos render nothing). This is separate from the fixed cover and logo slots (upload_marketing_image), which feed the game catalog and lobby, not the page body.
Styling is the standard BWSS allow-list — the same strict, structured style object rules pages use. Read v2-page-styling for the full property list and value grammar. The essentials:
- Strict allow-list: an unknown style property throws (no
border/background/fontshorthands). - Colors: hex,
rgb()/rgba(), named colors, or a$-token ($colors.surface.canvas). Nourl(),var(),calc(). - Lengths: a number (px), a
<number>(px|%|em|rem)string, or a$-token. Novh/vw/calc(). One value per property:"padding": "12px 28px"is rejected, so usepaddingX/paddingYor the per-side keys (paddingTop, ...). The same goes formargin. - Prefer theme tokens (
$colors.*,$semanticTokens.*,$spacing.*,$fonts.*) over hardcoded values so the page tracks the theme. Tokens resolve against/theme.json; with no theme,$-tokens resolve to nothing, so supply concrete fallbacks if you skip theming.
The two special buttons — PlayButton and RulesButton
These are the point of the page. Both take no href — the platform constructs the correct link from the game's slug, so an author can't point them anywhere else.
PlayButton→ starts a match. The compiled link goes to/play/<slug>, carrying the version channel of whatever commit is being served. A logged-in visitor lands straight in a new match; a logged-out one gets a lobby preview with a login box, then resumes into the match after signing in.RulesButton→ opens this game's rules. The page links to/game/<slug>/rules, and the served page also intercepts the click to open the rules in an in-page slide-over drawer (with JS off it's a normal link to the standalone rules page). Only meaningful if the game has rules pages — build those too, or drop the button.
Both wrap styled children exactly like a Box, so the visible button is yours to design — a styled Box, an Image, text, whatever fits the theme:
{
"type": "PlayButton",
"style": {
"display": "inline-flex",
"paddingY": "12px", "paddingX": "28px",
"borderRadius": 8,
"backgroundColor": "$colors.accent.default",
"color": "$semanticTokens.fg.inverted",
"fontWeight": "bold",
"fontSize": 18
},
"children": ["Play now"]
}
Light/dark mode and ThemeToggle
If the theme declares both background.light and background.dark variants, the compiled page renders both background stacks and the semantic-token color values for each mode, then hides the inactive side with CSS. Switching modes is a single attribute flip on .bw-page — instant, no reflow, no recompile.
What the platform picks on first paint:
- If the theme sets
defaultColorMode, that mode is baked into the page. The OS theme is ignored. - If
defaultColorModeis unset, a no-JS visitor sees theirprefers-color-scheme(light OS → light, dark OS → dark). On load, the script also respects alocalStoragechoice from the visitor's previous visit.
A ThemeToggle is the author's opt-in toggle button. The platform binds the click — author input never reaches a script. Wrap any styled children (text, an Image, an Svg) just like a Box:
{
"type": "ThemeToggle",
"style": { "paddingY": "8px", "paddingX": "14px", "borderRadius": 8, "borderWidth": 1, "borderStyle": "solid", "borderColor": "$semanticTokens.border.muted" },
"children": ["Toggle theme"]
}
Only drop a ThemeToggle on a page whose theme has both background variants — otherwise the inactive side has nothing to paint.
Linking between pages — Anchor
If you split the marketing page into more than one, Anchor.href is validated to internal targets only, same grammar as rules pages:
<slug>— a bare sibling marketing page slug (the filename without extension). Compiles to<slug>.html.<slug>#step-2— a slug plus a fragment.#overview— a same-page fragment.
Everything else is rejected: http(s):, javascript:, data:, protocol-relative //host, absolute /path, and ... You cannot link off-site. To make a #fragment link land somewhere, give the destination node an id (a letter, then letters / digits / _ / -); id is allowed on Box, Anchor, and the two buttons. To anchor to an Image or Svg, wrap it in a Box with an id.
An Anchor, a RulesButton and a PlayButton all render with no built-in link styling: no underline, and their text inherits the surrounding color rather than falling back to browser blue. Style them like any other node. For an underline, set textDecorationLine: "underline" (plus the optional textDecorationStyle / textDecorationColor / textDecorationThickness / textUnderlineOffset). Set it on the node holding the text: a decoration set on a card-shaped link draws through every line inside it, and a child cannot cancel it.
A minimal page
{
"type": "Box",
"style": {
"display": "flex",
"flexDirection": "column",
"gap": 24,
"padding": 32,
"alignItems": "center",
"backgroundColor": "$colors.surface.canvas",
"color": "$semanticTokens.fg.default"
},
"children": [
{
"type": "Box",
"style": { "fontSize": 40, "fontWeight": "bold", "fontFamily": "$fonts.display" },
"children": ["Harvest Moon Bay"]
},
{
"type": "Box",
"style": { "fontSize": 18, "textAlign": "center", "maxWidth": 520 },
"children": ["Trade tides and tend your shore in a 30-minute game of quiet rivalry."]
},
{
"type": "PlayButton",
"style": {
"paddingY": "12px", "paddingX": "28px",
"borderRadius": 8,
"backgroundColor": "$colors.accent.default",
"color": "$semanticTokens.fg.inverted",
"fontWeight": "bold"
},
"children": ["Play now"]
},
{
"type": "RulesButton",
"style": { "paddingY": "8px", "paddingX": "20px", "color": "$colors.accent.default", "fontWeight": "bold" },
"children": ["How to play →"]
}
]
}
Step 4: Fill in the catalog card
The public directory shows every game as a card: cover, name, description, player counts, and two tags the author sets, estimated play time and complexity. The card is what a player sees before the marketing page, so it is part of the same job. Read the fields with get_game; any that come back null or empty are missing from the card.
estimatedPlayTimeis one of the fixed bucketsLT15,M15,M30,M45,H1,H1_5,H2,H3,GT180(under 15 minutes, 15 minutes, 30 minutes, 45 minutes, 1 hour, 1.5 hours, 2 hours, 3 hours, over 3 hours). Pick the bucket closest to a typical full game.complexityis a whole number on BoardGameGeek's weight scale:1Light,2Medium Light,3Medium,4Medium Heavy,5Heavy. It rates how much rules weight a new player takes on, not how long the game runs.descriptionis the card's blurb, clamped to about three lines, so lead with the hook.howToPlayVideoUrlis optional: a YouTube link to a video that teaches the game. When it is set, the match lobby shows a How to play tab beside the description with the video embedded, so players can watch instead of reading the rules. Any YouTube video link works (watch page, youtu.be, Shorts). If it isnull, ask the author once whether they have one; never search for a video or invent a link.
If either tag is missing, ask the author for both in one short exchange, offering the options as "Label (n)" so they rate against the scale players see (e.g. "How heavy are the rules: Light (1), Medium Light (2), Medium (3), Medium Heavy (4), or Heavy (5)?"), then persist the answers with update_game. If the author would rather leave a tag unset, that is fine: the card simply omits the pill. These fields describe the game rather than a release, so the card reflects them as soon as the change is committed, whatever the game's publish state.
Step 5: Commit to compile
Marketing pages compile at commit time, not on save. After writing the .bwss file:
- Before committing, run
validate_code: it compiles every/src/rules/*.bwssand/src/marketing/*.bwsspage the way a commit does and lists each problem with the file and the path to the offending node, so you can fix them all in one pass. - Commit with the
committool — a developermessage(e.g. "Add marketing landing page") and a player-facingchangelogMessage(e.g. "Added a landing page."). - On commit, every
/src/marketing/*.bwsssource is validated and rendered to a hidden/marketing/<slug>.htmlartifact. The compile uses the committed theme and game images, so the page tracks the current look. The presence of/marketing/index.htmlsetshas_marketingand makes/game/<slug>serve the page. - If any
.bwssis invalid JSON, violates the marketing-surface schema, or references an unknown image key, the commit fails and rolls back — nothing partial is committed. Read the error (it names the file and the reason), fix the tree, and commit again. - Editing or deleting a
.bwssand re-committing regenerates the pages; stale pages for deleted sources disappear.
To preview the page before committing, use the draft preview action in the Studio marketing panel — it compiles the current /src/marketing/*.bwss working copy and shows it in a drawer.
Checklist
- One game locked (
game_definition_id); existing files reviewed. - Theme developed first (or skip explicitly acknowledged; tokens preferred regardless).
- Page sections and copy agreed with the author before authoring.
- Page is one JSON BWSS tree using only
Box/Image/Svg/Anchor/PlayButton/RulesButton/ThemeToggle; text as string children. - At least one
PlayButton; aRulesButtononly if the game has rules pages. -
Image.srckeys exist on the game; cross-page links use bare slugs;#fragmentlinks have a matchingid; styles use theme tokens where possible. - Catalog card filled in:
description,estimatedPlayTime, andcomplexityset with the author (or an unset tag explicitly accepted). - Committed; compile errors resolved;
/game/<slug>serves the page.