# Publishing a Mirage game to itch.io
This document lists, in order, the steps that turn a game in its own
repository into a page that plays in the browser.
## What the game needs
- The wasm32 target and the packaging subcommand:
```sh
rustup target add wasm32-unknown-unknown
cargo install cargo-mirage
```
`cargo-mirage` runs `wasm-bindgen` from inside itself, so there is no
`wasm-bindgen-cli` to install and keep in step. It is released beside the
engine against one exact `wasm-bindgen` version. A game whose lock
resolves another version stops the run, which names both versions, and
names the `cargo install` line for the release built against the game's
version if there is one.
- A `[[bin]]` target, or a `src/main.rs`, whose `main` calls `run`. The
browser build starts at that `main` and nowhere else. A second `[[bin]]`,
such as a desktop tool beside the game, does not count when its
`required-features` names a feature a build with no flag has off. Two
bins a build with no flag builds both need `--bin <name>` to pick one. In
a workspace holding more than one package, `-p <package>` names the game.
## The build
From the game's repository root:
```sh
cargo mirage web
```
That builds the target for `wasm32-unknown-unknown` in release, runs
`wasm-bindgen` over it into `dist/game.js` and `dist/game_bg.wasm`, writes
`dist/index.html`, copies the asset folders, checks itch's own limits, and
writes `dist/<target>-web.zip` with the page at the zip's root, which is what
itch serves. The folder is emptied first, so nothing an earlier run left is
uploaded.
The page is the engine's own: it fills itself with the canvas the engine
creates, states the error that stopped a start where one did, and gives
`GPUCanvasContext` and `GPUAdapter` the instance check that Firefox
configurations running WebGPU without those interface globals need. Its
`<title>` is the game's:
```toml
[package.metadata.mirage]
title = "Deep Dive"
```
Where the manifest states no title, the built target's name is the title.
## The assets
```toml
[package.metadata.mirage]
assets = ["assets", "levels"]
```
A game's asset paths are relative to the workspace root: on the desktop that
is the folder the game is run from, and in the upload it is the page's own
folder. `cargo mirage web` builds `dist` at the workspace root and copies each
folder there under the same path, so a workspace game that names
`game/assets/sky.png` in `Config::with_assets` lists `game/assets` above and
reads `game/assets/sky.png` on both sides. For a game that is its own
workspace the two roots are the same folder. Where the manifest names no
folders, an `assets` folder at the workspace root is the one taken.
The paths in `Config::with_assets` are the game's to keep matching these
folders. A folder named with a leading `/`, one that climbs out of the
workspace, and one whose case differs from the disk's own are each refused by
name: itch's server reads a path by its exact case and refuses one that starts
at the root.
## The itch project settings
The names below are itch's own, from its HTML5 documentation.
- **Kind Of Game**: pick **HTML Game**, on a new project page or an existing
one.
- Upload the zip, and mark it as the file that is played in the browser,
with the checkbox on its own upload row. itch's documentation states only
that the **Embed options** section appears on the project's edit page
after an appropriate file is uploaded to be embedded; the checkbox's own
wording is from memory.
- **Embed options**: **Embed in page** takes the viewport dimensions, and
runs the game on the page at that size. Set them to the size the game
passes to `Config::with_size`, so the browser frame matches the desktop
window; the browser build itself ignores `with_size` and fills the page, so
the viewport sets the size the game draws at. **Click to launch in
fullscreen** takes no dimensions, and the game fills the viewer's screen.
**Fullscreen Button** adds a button over the **Embed in page** view that
does what **Click to launch in fullscreen** does.
- **Click to Play** is on by default, and the player's click is what loads
the game. Leave it on for a game with sound: itch's page, read on
2026-09-12, states that audio may be muted on some browsers when a project
starts on its own.
- The build uses no threads and no `SharedArrayBuffer`, so it needs no COOP
or COEP header. itch offers no setting for either.
- The game needs a browser with WebGPU. Chromium-based browsers run it as
shipped. Firefox on Linux ships WebGPU disabled, and a player turns it on
with `dom.webgpu.enabled` in `about:config`. State that on the game's
page: a player without WebGPU reads the engine's message over the canvas
and nothing else.
The limits itch states for an HTML game's zip, read on 2026-09-12, are 1,000
files once it is opened, 500MB opened in total, 200MB for any one file, and
240 characters for any one path. `cargo mirage web` checks all four before it
writes the zip and names the file that is past one.
## Testing before the upload
```sh
cargo mirage serve # 8000 by default; `cargo mirage serve 8123` names another port
```
That packages the game and serves `dist` on `127.0.0.1`, printing the URL to
open. Serve the folder rather than opening `index.html` as a file: the module
import and the wasm read both need HTTP, and the server states
`game_bg.wasm` as `application/wasm`, which is what the page starts it from.
A first upload confirms the five things only the page on itch shows:
- the assets load from itch's own paths,
- the viewport frames the canvas as the game expects,
- a `Cursor::Held` pointer hold takes its lock inside itch's frame,
- sound starts after the player's click,
- what the game `Saves` is kept between visits.