serve
Serve the generated static site locally, optionally building it first.
ritual serve [options]By default, serve serves a previously built dist/ directory (or the directory given by --out-dir). A directory with no index.html is refused with exit code 1 (see Exit Codes); --api instead builds the missing site itself. Pass --build to build and then serve in one step. Pass --api to also host a live, read-only data API beside the site; see Hosting with a live backend.
Options
Section titled “Options”| Option | Description | Default |
|---|---|---|
-p, --port <number> |
Port to serve on (1–65535; an invalid value exits with code 2) | 3000 |
--host <address> |
Host address to bind to. 0.0.0.0 binds all interfaces. |
0.0.0.0 |
--build |
Build the site before serving it | |
--api |
(Beta) Serve a live read-only data API alongside the site (see below) | |
--out-dir <path> |
Serve this directory instead of dist/ (with --build, build into it and serve it) |
dist |
--sell-mode |
(Beta) Offer sell mode for this run. Valid with --api even without --build; a plain serve rejects it. |
Live API mode (--api)
Section titled “Live API mode (--api)”With --api, the server hosts the static dist/ plus a live, unauthenticated, read-only API on the same port:
| Method | Path | Serves |
|---|---|---|
| GET | /index.json, /decks/…, /collections/…, /wanted/… |
Live list data, computed from the markdown files on request instead of the built copies. Edits made via the CLI or admin appear without rebuilding. |
| GET | /locales/{tag}.json |
One published locale dictionary from the built site, as a static file. |
| GET | /art/{path} |
One custom art image, read live from the configured art directory (see below). |
| GET | /api/autocomplete |
Card-name autocomplete over the card cache, with the same term matching as the admin editor (in tre finds “In the Trenches”). |
| GET | /api/card-printings, /api/card-price |
Printings and staleness-gated price lookups (the same endpoints the admin editor uses). Served from the cache; a card the cache lacks, or a stale price, is fetched from Scryfall and cached. |
| GET | /api/cards?ids=… |
Cache-only card lookup by Scryfall ID (max 200 per request). Restores a shared trade link without Scryfall. A failure carries the message triple. |
| POST | /api/card-prices |
Batch price refresh by card name (the site’s Update Prices button in hosted mode). |
| GET | /api/buylist/status |
Which buyers this server can quote and how fresh its cached buylist is. 404 unless sell mode is on or priceSources includes cardkingdom. |
| POST | /api/buylist/quotes |
Card Kingdom’s current offer for specific printings (max 500 per request). Cache-only; there is deliberately no public refresh route. 404 under the same condition as /api/buylist/status. |
The public site does not price lists through the buylist routes; buy prices come inside the live list payloads (below). The quotes route covers printings the lists do not carry: add-card search results, which the printing pickers price under the Card Kingdom store. Other clients (a script, a second front end) may use both routes too.
The web app detects the backend through the served index.json and switches behavior: list pages refetch live data on navigation, the editor’s add-card search uses the cache-backed term matching (the “results may differ” Scryfall note disappears), and price refreshes go through the server. A Live badge appears in the site header.
Details:
-
Sell mode quotes come inside the live payloads. Sell mode is off unless
site.sellModeis on or the run passed--sell-mode. When on, each served list payload carries that list’s Card Kingdom buy prices, computed from the cached feed exactly asbuild-sitedoes. A newly refreshed feed shows on the next request without a rebuild. WhenpriceSourcesincludescardkingdom, the payloads also carry Card Kingdom’s printing picks for name-only lines and a quote for every printing each list carries, at every finish, for the card modal’s other-printings grid and the printing pickers. -
The buylist feed refreshes only at startup. No request can trigger the ~70 MB download. Before the server binds, a cached feed more than a day old is redownloaded under the run’s
--refreshpolicy (no-bulk/neverskip it). A failed download keeps the older feed with a warning. Startup only ever updates a feed: a workspace that has never downloaded one is left alone, the buylist routes answer503with the remedy, and sell mode shows a “prices unavailable” notice until you download one withritual sell --refresh autoor the admin site. -
Read-only, no auth. Only the routes above exist. An unmatched
/api/*path answers a JSON 404 (never the SPA fallback), and none of the admin server’s write or auth routes are reachable. Public edits stay client-side (export/import change bundles), as on the static site. One local exception: like every list-writing command, startup runs the card-ID backfill, which writes any missing&Ncard IDs into the list files. Plainserve, without--buildor--api, never does. -
Builds when there is nothing to serve. Under
--apithe data is live, so an unbuilt directory is only a missing app shell. The command builds the site and then serves it. (Plainserverefuses, since there the build is the content.) An existing build is served as-is. Pass--buildto rebuild it, which is also the only way to pass the build any of its flags. -
Cache warming. Live payloads come from the card cache with no Scryfall fallback, so startup applies the same freshness gates as
build-siteover every card the served lists reference (entries, deck primers, and change history), under the same--refreshpolicy.--refreshis therefore valid with--apieven without--build. The gates, in order:- A bulk download when the cache has never been downloaded, is more than a week old, or is missing many of those cards.
- The offer to redownload day-old prices.
- The offer to download oracle/art tags when a sample of the site’s cards carries none. Skipped on an empty cache.
The per-card refetch of missing or stale cards never runs.
--refresh no-bulktherefore warms nothing here, exactly like--refresh never, which is what a cache server deployment wants. Each gate is best-effort: a declined prompt or a cold network leaves the cache as it was and the server still starts. When a build ran in the same command (--build, or--apibuilding a missing site), startup does not ask again. -
Cache backend. The server reads the local
cache/cache.jsonby default, or a cache server when--cache-server/RITUAL_CACHE_SERVERis set. A bulk refresh run by a separate CLI process is picked up automatically, since the server watches the cache file. Never expose the cache server itself to browsers. It has unauthenticated write routes; only theserve --apiprocess should talk to it. -
CORS. API and JSON routes answer with
Access-Control-Allow-Origin: *, so a statically deployed site (CDN/GitHub Pages) can point at a separately hosted instance viasite.apiBaseUrl. See Hosting with a live backend. -
Freshness. Live JSON is served with
Cache-Control: no-cache, content ETags, andLast-Modified, so unchanged payloads revalidate as 304s. Every response also carriesX-Content-Type-Options: nosniffandReferrer-Policy: strict-origin-when-cross-origin. -
Images. Live data always uses Scryfall image URLs.
--cache-imagesonly affects the statically built assets. -
Custom art. Nothing is copied in live mode.
/art/{path}reads the workspace’sartDiron every request, so a new image or an edited.art.jsonshows up without a rebuild (the.art.jsonmodification time is part of each list’s freshness stamp, so the list payload re-renders too). Only.avif,.gif,.jpeg,.jpg,.png, and.webpare served (not SVG). Any other extension, or a path leaving the art directory, is a JSON404. Plainservehas no such route; it serves whateverbuild-sitecopied intodist/art/. -
Card categories. A list’s
<list>.categories.jsonmodification time is also part of its freshness stamp, so a category edit re-renders the list payload without the.mdchanging. -
Interface language. The live
index.jsonreports the currentuiLocale, soritual config set uiLocaletakes effect on the next index request with no restart and no rebuild, likedefaultPriceSource,searchDebounceMs, anddefaultCategories. Which dictionaries exist is still a build-time decision (--locales). NoAccept-Languagenegotiation happens in this server, so a CDN-hosted site and a served one behave identically.
Build options (require --build)
Section titled “Build options (require --build)”With --build, serve accepts every build-site option. Passing one without --build is a usage error: exit code 2 and an error naming the flag(s). Four exceptions:
--refreshand--sell-mode: also valid with--api(cache warming and live sell mode). Exempt only under--api.--out-dir: names the directory to serve whether or not a build runs.--locale: a global flag every command accepts.
| Option | Description |
|---|---|
-v, --verbose |
Show the cards being fetched from Scryfall |
--cache-images |
Download deck card images into images/ under the output directory (dist/ by default, or --out-dir) and use them instead of Scryfall URLs |
--decks [names...] |
Decks or deck URLs to include (default: the site.includeDecks config selection) |
--collections [names...] |
Collections to include (default: the site.includeCollections config selection) |
--wanted-lists [names...] |
Wanted lists to include (default: the site.includeWantedLists config selection) |
--price-sources <list> |
Comma-separated price stores to offer (default: every enabled priceSources store; narrows that list, never adds to it). Refused under --api. |
--refresh <mode> |
Card cache refresh policy: ask (default), auto, no-bulk, or never. See build-site. |
--theme <name> |
Initial theme for first-time visitors: a built-in name or a custom name from --theme-file. Default: default. |
--theme-file <path...> |
Load custom theme JSON files. Each is added to the theme list under its declared name. |
--locale <tag> |
Interface language the built site opens in (default: the uiLocale config value). Ritual’s own text, not the card language. Also a global flag, so it is accepted without --build, where it only changes this command’s own output (see the note above). See Localized builds. |
--locales <tags...> |
Locale dictionaries to publish into locales/ for the in-app language switcher (default: en; all publishes every one this build has). |
--locale-file <path...> |
Load locale dictionary JSON files, each named for its tag (de-AT.json). Their locales become selectable alongside the built-in ones. |
--moxfield-user-agent <agent> |
User agent for fetching Moxfield deck URLs (see build-site). |
--out-dir <path> |
Build into this directory instead of dist/, and serve it. Valid without --build, where it names the built directory to serve. The Ritual directory itself or any ancestor is refused with exit code 2, with or without --build. See build-site. |
--sell-mode |
(Beta) Offer sell mode for this run even when site.sellMode is off: update the Card Kingdom buylist (~70 MB) and serve its buy prices. Also valid with --api and no --build: the live payloads then carry quotes and the buylist routes answer instead of 404ing. Enable-only. See build-site → Sell mode. |
Plain serve, with no --build and no --api, is a static file server. It never consults sell mode; the served directory shows whatever its build produced. --sell-mode there is a usage error (exit code 2) like every other build-only flag. Add --build to bake the quotes, or --api to serve them live.
Examples
Section titled “Examples”Serve a previously built site on the default port (3000):
ritual serveServe a preview directory built earlier, without rebuilding it:
ritual build-site --out-dir previewritual serve --out-dir previewServe on a custom port:
ritual serve --port 8080Build everything and serve at http://localhost:3000:
ritual serve --buildBuild only specific decks, then serve:
ritual serve --build --decks "Atraxa Superfriends" "Mono Red Aggro"Host the site with the live backend, building it first if it has not been built yet:
ritual serve --apiRebuild the static site and then host it:
ritual serve --build --apiHost with a shared cache server providing the card data:
ritual serve --api --refresh never --cache-server cache-host:4000Exit Codes
Section titled “Exit Codes”| Code | Meaning |
|---|---|
0 |
The server ran (it serves until stopped with Ctrl+C). |
1 |
The build failed (e.g. an unreadable --theme-file); there is no built site to serve (no index.html in the served directory, or under --api the build it ran failed); or the port could not be bound (usually another process holds it). The server is not started. |
2 |
Usage error: invalid --port, a build-only flag without --build, an invalid --price-sources/--theme value, --price-sources naming no enabled store, or --price-sources under --api. |
- Files are served from
dist/, or from--out-dirwhen given. The build and the server always use the same directory. Without--buildor--api, runbuild-sitefirst. A directory with noindex.htmlis refused with exit code 1 and a message naming both remedies. - With
--build, the site is built exactly asbuild-sitewould build it. If the build fails, the server does not start. - If the port is in use,
serveprintsFailed to start the server on <host>:<port>: <reason>and exits with code 1. Pick another port with--port. --hostdefaults to0.0.0.0(all interfaces), matchingadmin. The printed URL names the bound address: a wildcard or loopback bind printshttp://localhost:<port>(use the machine’s address from another device), and an explicit--host 192.168.1.5prints that address.- Press
Ctrl+Cto stop the server. - For an auto-restarting workflow that rebuilds when source or data files change, see Development → Dev Workflow.
bun run dev serveadds--buildautomatically and requires an explicit--refreshmode (auto,no-bulk, ornever).
Ritual is unofficial Fan Content permitted under the Fan Content Policy. Not approved or endorsed by Wizards of the Coast. Portions of the materials used are property of Wizards of the Coast. ©Wizards of the Coast LLC.
Card data, images and symbols come from Scryfall, which is not affiliated with this project. Ritual is free software under the AGPL-3.0. License and attribution.