Skip to content

Content bundles

A content bundle is a self-contained web app (HTML/CSS/JS/images/fonts — any static web content) that you upload to the device and the device serves and displays locally, with no dependency on network reachability. It's the recommended way to show your own content, as opposed to navigating to a remote URL.

Authoring a bundle

A bundle is a plain zip archive with these rules:

  • index.html MUST exist at the root of the archive — this is the entry point that gets served.
  • Reference other files (CSS, JS, images) with paths relative to the archive root, e.g. <script src="app.js">, <link rel="stylesheet" href="css/style.css">.
  • No absolute paths and no .. traversal in entry names — the device rejects archives that contain them.
  • No symlink entries — rejected too.
  • The archive must be a genuine zip (checked by content, not by file extension) and every entry must decompress cleanly (CRC-32 verified).
  • If you need HTTP range requests to work on a file (e.g. seeking in a large video), store that entry uncompressed ("Store" method) in the zip rather than "Deflate" — the device can only serve byte ranges out of uncompressed entries. Most zip tools compress by default; check your tool's flag for "store"/"no compression" per file, or zip the whole archive with compression level 0 if range support matters more than upload size.
  • Bundle names (the {name} in the URLs below) should be short filesystem/URL-safe identifiers (letters, digits, -, _) — they become part of the path the bundle is served at on the device.

The page channel script (display-client.js, providing window.mcd for message-passing with the host — see REST and WebSocket API) is injected automatically into every page the kiosk shows, bundle or remote URL alike. You don't need to include it yourself, though you may — it self-deduplicates.

A minimal bundle is just:

promo.zip
├── index.html
├── style.css
└── logo.png

Upload and activate a bundle

Upload (or replace) a bundle by name:

curl -X PUT http://10.99.0.1/api/v1/content/promo \
  --data-binary @promo.zip \
  -H 'Content-Type: application/zip'

This returns 202 Accepted with an operation resource while the device transfers and validates the archive (see Long-running operations for how to poll or subscribe to its completion). Uploading to an existing name replaces it atomically — the old bundle stays untouched and servable until the new one fully validates.

Once the upload operation completes successfully, display it:

curl -X POST http://10.99.0.1/api/v1/display/bundle \
  -H 'Content-Type: application/json' \
  -d '{"name": "promo"}'

Web UI: Content page → Upload (file picker) → once listed, Display.

List bundles and check free space

curl http://10.99.0.1/api/v1/content

Returns every stored bundle (name, size, sha256, created, whether it's currently active on screen) plus disk: {free_bytes, total_bytes} for the data partition — check this before a large upload. The device also rejects an upload outright if it won't leave at least 64 MB of headroom on the data partition.

Web UI: Content page — the bundle list and the free-space indicator.

Re-validate a stored bundle

curl -X POST http://10.99.0.1/api/v1/content/promo/check

Returns 200 {"ok": true|false, "message": "…"} — this re-runs the same integrity checks as upload time against the archive already on the device (useful after suspecting SD-card wear or just to sanity-check before a scheduled display change). It's synchronous and may take a few seconds on a large archive; a 404 content_not_found means no bundle by that name exists.

Delete a bundle

curl -X DELETE http://10.99.0.1/api/v1/content/promo

Fails with 409 bundle_in_use if the bundle is currently displayed — switch to another target first (POST /display/waiting or display a different bundle).

Web UI: Content page → bundle row → Delete.

Set the boot-time bundle

To have a bundle shown automatically at boot (instead of the waiting page), set it in configuration — see Configuration → startup target for the full boot-target resolution order:

curl -X PATCH http://10.99.0.1/api/v1/config \
  -H 'Content-Type: application/json' \
  -d '{"content": {"startup_bundle": "promo"}}'

This only takes effect at the next boot — it does not navigate the currently running display. If the named bundle is missing or fails validation at boot time, the device falls through to the next configured target (ultimately the waiting page) rather than getting stuck, and reports the failure via a display_state event.

How bundles are served

Bundles are stored on the device exactly as uploaded (the zip archive itself, plus a small metadata sidecar) and served directly out of the archive — they are never extracted to disk. This is transparent to you as an integrator except for two practical consequences worth knowing:

  • Range requests only work on entries stored uncompressed in the zip (see "Authoring a bundle" above); compressed entries are always served whole.
  • Bundles are served from a local origin on the device (http://127.0.0.1:8081/bundles/{name}/) — you never address that origin directly from the host; you only ever reference bundles by name through the API above.

See also