# File format

A built deck is one HTML5 file. Everything it needs to render is inside it: the JavaScript, the CSS, the fonts, and the slide assets. Nothing loads from disk or over the network when the page opens. This page describes how that file is laid out so you can inspect it, diff it, or read its embedded data from your own tooling.

For how to produce the file and where to put it, see [Building & deploying](https://docs.liebstoeckel.app/guides/building/). This page is the format itself.

## Document structure

The file is a complete HTML document. Open it and you'll find roughly this shape:

```html
<!doctype html>
<!-- Generated by liebstoeckel engine 0.3.4, cli 0.3.6 · https://liebstoeckel.app -->
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="generator" content="liebstoeckel engine 0.3.4, cli 0.3.6" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Your deck title</title>
    <style>/* compiled Tailwind + brand variables + code/atmosphere styles */</style>
  </head>
  <body data-brand="your-brand">
    <div id="root"></div>
    <script type="module">/* the whole client bundle */</script>

    <!-- inert metadata blocks, described below -->
  </body>
</html>
```

A few things are worth calling out:

- **One module script holds the whole app.** The engine, your slides, and any plugins' client code are bundled and minified into a single `<script type="module">`. The browser runs this; nothing else loads.
- **CSS lives in `<style>`.** The compiled Tailwind output, your brand variables, and the code and atmosphere styles are all inline. Highlighted code is pre-tokenized at build time, so there is no syntax highlighter running in the page.
- **Fonts and images are data URIs.** Font files become base64 `url()` values inside the CSS. Imported image assets are inlined the same way. Base64 makes the file larger than its parts, which is the cost of having no external requests.
- **The active brand is on `<body>`.** Brand tokens render as `[data-brand]` CSS, and `data-brand` on the body selects which one applies.

## Generator header

Every build stamps its own provenance into the head, so you can tell what produced a file without opening it in a player. There are two copies of the same information:

- A comment right after the doctype, for a human reading "view source".
- A standard `<meta name="generator">` in `<head>`, for tooling that wants to read the version programmatically.

```html
<!-- Generated by liebstoeckel engine 0.3.4, cli 0.3.6 · https://liebstoeckel.app -->
<meta name="generator" content="liebstoeckel engine 0.3.4, cli 0.3.6" />
```

The `engine` version is the runtime that renders the deck; the second tool is whatever drove the build (the `cli` here). A deck built through its own `build.ts` rather than the CLI records the engine version on its own.

The stamp carries versions only, never a build timestamp. That keeps the file byte-deterministic: the same deck built twice with the same toolchain produces the same bytes, which matters because the embedded source package is itself deterministic.

## Metadata blocks

Below the module script, a build can embed up to four inert blocks. Each is a `<script>` the browser never executes, keyed by a `data-liebstoeckel-*` attribute so tools can find it. They sit before the final `</body>`.

| Attribute | `type` | Contents | When present |
| --- | --- | --- | --- |
| `data-liebstoeckel-plugins` | `application/json` | Plugin manifest | Deck declares plugins |
| `data-liebstoeckel-licenses` | `text/plain` | Third-party license notices | Default; off with `--no-inline-licenses` |
| `data-liebstoeckel-source` | `application/octet-stream` | The deck's source, compressed | Default; off with `--no-inline-package` |
| `data-liebstoeckel-thumbnails` | `application/json` | Slide thumbnail manifest | A Chromium was available at build time |

The JSON blocks carry a `"v"` version field so the shape can change without breaking older readers.

### Plugin manifest

Present only when the deck uses plugins. It lists each plugin by name and version, and for plugins with a server it carries the server bundle as base64-encoded ESM:

```json
{
  "v": 1,
  "plugins": [
    { "name": "@liebstoeckel/plugin-poll", "version": "0.1.0", "hasServer": false },
    {
      "name": "@acme/quiz",
      "version": "1.0.0",
      "hasServer": true,
      "id": "quiz",
      "server": "<base64 ESM>",
      "audienceWrites": ["answer"]
    }
  ]
}
```

The browser never decodes the `server` field. A [live server](https://docs.liebstoeckel.app/guides/live/) reads the manifest, runs the server bundles, and uses `audienceWrites` to decide what the audience may write. Offline, none of this runs and each plugin renders its `fallback` instead.

### License notices

A plain-text block of third-party attribution, starting with a `THIRD-PARTY SOFTWARE NOTICES` header. It lists the MPL-2.0 engine, the liebstoeckel packages in the bundle, and every third-party library and font, with each one's license text. Minification strips license comments out of the code, so the build collects the attribution and puts it back here. It is computed from what this build actually bundled, so swapping a font or adding a library updates it.

Read it or gate on it with [`liebstoeckel licenses`](https://docs.liebstoeckel.app/reference/cli/#licenses). Disable the embed with `build --no-inline-licenses` (not recommended, since the notices are how you stay compliant with the bundled licenses).

### Source package

By default the build embeds the deck's own source so the `.html` can be turned back into an editable project. It is the deck packed with `bun pm pack`, recompressed, and base64-encoded. The `data-codec` attribute records the compression so the format can change later:

```html
<script type="application/octet-stream" data-liebstoeckel-source data-codec="zstd">
  <!-- base64 of a zstd-compressed tar of the deck -->
</script>
```

The pack respects your `package.json` `files` allowlist (or `.npmignore` / `.gitignore`), and never includes `node_modules`, `dist`, or `.git`. A build-time secret gate refuses to embed a `.env` or any file matching a credential signature, so this block should not leak secrets. For a smaller, opaque file with no recoverable source, build with `--no-inline-package`.

Turn the file back into a project with [`liebstoeckel eject`](https://docs.liebstoeckel.app/reference/cli/#eject):

```bash
liebstoeckel eject my-talk.html ./my-talk
```
**Caution:** Ejecting writes files from whatever HTML you hand it. `eject` validates archive paths and caps the decompressed size, but it never runs install or build scripts for you. Treat an untrusted deck like any other untrusted archive.

### Thumbnail manifest

If a headless Chromium was available at build time, the file carries one small WebP image per slide as a data URI, used to draw the overview grid without rendering every slide live:

```json
{
  "v": 1,
  "w": 640,
  "h": 360,
  "thumbs": { "0": "data:image/webp;base64,...", "1": "data:image/webp;base64,..." }
}
```

Thumbnails are optional. The build skips them, with a note rather than an error, when no Chromium is found or when you set `LIEBSTOECKEL_NO_THUMBS=1`. Without the manifest the overview falls back to rendering slides directly. See [Thumbnails](https://docs.liebstoeckel.app/guides/thumbnails/).

## Standalone and live from the same file

The file does not contain a relay URL or a session token, and it makes no network requests on its own. Opened from disk or a static host, it runs entirely offline: plugins show their `fallback`, and the presenter view works through the browser's `BroadcastChannel`.

The same file goes live without rebuilding. When you serve it with [`liebstoeckel live`](https://docs.liebstoeckel.app/guides/live/), the server injects a small bootstrap script ahead of the bundle. That script connects to a relay over a Yjs document, and from then on plugins instantiate their server bundles and sync state across the room. Because the live wiring is injected at serve time and not baked into the file, the exact same `.html` you email still opens standalone.

[Building & deploying](https://docs.liebstoeckel.app/guides/building/)
[CLI reference](https://docs.liebstoeckel.app/reference/cli/)