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.
Work against a single, already-selected game.
game_definition_id yet, call list_my_games and have the author pick one. Only ever touch that one game.get_game to read the game's name, description, mood, and voice, and list_files to see what already exists under /src — a theme, rules pages, the game code, any existing marketing page.mood (visual direction) and voice (the tone of the copy) so the page reads like the game, not generic ad copy.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.
/src/theme.html (the visual style reference) and /theme.json (the structured theme document with $-tokens). Read v2-design-theme-instructions and v2-game-themes for how these are produced.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.Before writing the .bwss file, agree with the author on what the page says and shows. Don't guess — converge on it together.
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 only index.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 catalogue 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:
border/background/font shorthands).rgb()/rgba(), named colors, or a $-token ($colors.surface.canvas). No url(), var(), calc().<number>(px|%|em|rem) string, or a $-token. No vh/vw/calc().$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.PlayButton and RulesButtonThese 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",
"padding": "12px 28px",
"borderRadius": 8,
"backgroundColor": "$colors.accent.default",
"color": "$semanticTokens.fg.inverted",
"fontWeight": "bold",
"fontSize": 18
},
"children": ["Play now"]
}
ThemeToggleIf 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:
defaultColorMode, that mode is baked into the page. The OS theme is ignored.defaultColorMode is unset, a no-JS visitor sees their prefers-color-scheme (light OS → light, dark OS → dark). On load, the script also respects a localStorage choice 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": { "padding": "8px 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.
AnchorIf 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.
{
"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": {
"padding": "12px 28px",
"borderRadius": 8,
"backgroundColor": "$colors.accent.default",
"color": "$semanticTokens.fg.inverted",
"fontWeight": "bold"
},
"children": ["Play now"]
},
{
"type": "RulesButton",
"style": { "padding": "8px 20px", "color": "$colors.accent.default", "fontWeight": "bold" },
"children": ["How to play →"]
}
]
}
Marketing pages compile at commit time, not on save. After writing the .bwss file:
commit tool — a developer message (e.g. "Add marketing landing page") and a player-facing changelogMessage (e.g. "Added a landing page.")./src/marketing/*.bwss source is validated and rendered to a hidden /marketing/<slug>.html artifact. The compile uses the committed theme and game images, so the page tracks the current look. The presence of /marketing/index.html sets has_marketing and makes /game/<slug> serve the page..bwss is 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..bwss and 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.
game_definition_id); existing files reviewed.Box / Image / Svg / Anchor / PlayButton / RulesButton / ThemeToggle; text as string children.PlayButton; a RulesButton only if the game has rules pages.Image.src keys exist on the game; cross-page links use bare slugs; #fragment links have a matching id; styles use theme tokens where possible./game/<slug> serves the page.