artcraft-launcher-web-<version>.zip (from the GitHub release, or packaging/web/package.sh)
holds a static site in artcraft-launcher-web-<version>/:
| File | What it is |
|---|---|
index.html |
The page. It loads everything through relative URLs. |
artcraft-launcher-web-<hash>.js |
wasm-bindgen glue (generated, ES module) |
artcraft-launcher-web-<hash>_bg.wasm |
The app (egui, built with the wasm-release Cargo profile) |
_headers, .htaccess |
Sample header rules for Netlify/Cloudflare Pages and Apache |
There is no server-side code and no hand-written JavaScript. Upload the folder's contents anywhere that serves static files.
In the browser the launcher can't install anything: it shows the Craft App Suite catalog, the
latest release of each app and the right download for the visitor's platform, and links to the
files on GitHub. Release data comes straight from the GitHub REST API (api.github.com allows
cross-origin requests), so the page needs no backend. Unauthenticated visitors share GitHub's
limit of 60 API requests per hour per IP address.
All URLs in index.html are relative (public_url = "./" in apps/artcraft-launcher-web/Trunk.toml),
so the site works at a domain root (https://example.com/), under a prefix
(https://example.com/apps/launcher/) and from a CDN bucket. The asset names carry a content
hash, so they can be cached forever. Only index.html needs revalidation.
packaging/web/package.sh fails if the wasm grows past 24 MiB (Cloudflare Pages and Workers
reject any single file over 25 MiB).
- MIME type: serve
.wasmasapplication/wasm. Browsers refuse to stream-compile it under any other type. Serve.jsastext/javascript. Most hosts already do both. For nginx, check thatmime.typeshasapplication/wasm wasm;. - Compression: turn on gzip or Brotli for
.wasm,.jsand.html. - Caching:
Cache-Control: public, max-age=31536000, immutableon the hashed.wasmand.jsfiles, andno-cacheonindex.html. - Content Security Policy: if you send one, allow
connect-src https://api.github.com(andhttps://raw.githubusercontent.comfor the app icons) plus'wasm-unsafe-eval'inscript-src. - No special isolation headers: the app doesn't use
SharedArrayBuffer, so it doesn't needCross-Origin-Opener-PolicyorCross-Origin-Embedder-Policy.
nginx example:
location /launcher/ {
types { application/wasm wasm; text/javascript js; text/html html; }
gzip on;
gzip_types application/wasm text/javascript text/html;
location ~* \.(wasm|js)$ { add_header Cache-Control "public, max-age=31536000, immutable"; }
location ~* index\.html$ { add_header Cache-Control "no-cache"; }
}Local test: python3 -m http.server 8765 inside the folder, then open http://localhost:8765/.
<iframe
src="https://example.com/launcher/"
title="ArtCraft Launcher"
style="width: 100%; height: 640px; border: 0;"
allow="fullscreen"
allowfullscreen>
</iframe>- The app fills the iframe and follows its size, so size the iframe and not the app.
- Sandboxed iframes need at least
sandbox="allow-scripts allow-same-origin allow-popups";allow-popupslets download links open. - Don't send
X-Frame-Options: DENYor aframe-ancestorsCSP that excludes the embedding page.