# Thumbnails

The overview grid (press <kbd>o</kbd>) would be expensive if it mounted every slide live. Instead, liebstoeckel captures each slide once (at **build time**, or when you start presenting) and embeds the images, so the grid is just cheap `<img>`s.

## On by default

Both entry points capture thumbnails automatically and **skip gracefully (never failing)** when no Chromium is available or `LIEBSTOECKEL_NO_THUMBS` is set:

- `buildDeck()` from `@liebstoeckel/thumbnails/build` (what scaffolded decks' `build.ts` use).
- `liebstoeckel live` / `bun run live` captures into the deck before serving/uploading (pass `--no-thumbnails` to skip).

Both route through the same `withThumbnails(html, opts)` primitive, so the gate/capture/embed policy lives in one place. To get thumbnails when capture is being skipped, see [Chromium setup](https://docs.liebstoeckel.app/guides/chromium/).

## How capture works

1. The built single-file deck is loaded in a **headless Chromium** (via Playwright) in a special *capture mode*: one motionless slide at a time on the fixed 1280×720 canvas, plugins rendered as their offline **fallback**.
2. Each slide is screenshotted, then transcoded to **WebP** with `Bun.Image` (Bun's built-in codec; no native `sharp` dependency for capture).
3. The images go into a thumbnails manifest (`index → data-URI`) embedded in the HTML. The engine prefers these `<img>`s in the overview grid; absent, it falls back to a static live render.
**Container-friendly:** Capture launches the full Chromium with `--no-sandbox --disable-gpu --single-process --no-zygote` (the lighter `chrome-headless-shell` can crash in sandboxes). Default output is the native **1280×720** so the grid never upscales on hi-dpi screens (~16 to 18 KB/slide WebP).

## Standalone CLI

Generate or refresh thumbnails for an already-built deck:

```bash
liebstoeckel thumbs dist/my-talk.html --format webp --width 640 --quality 80 --scale 2
```

Or in code: `addThumbnailsToFile` is **loud** (throws if no Chromium), for an explicit capture; `withThumbnails` is the **gated, never-fatal** in-memory variant the build/live paths use:

```ts
import { addThumbnailsToFile, withThumbnails } from "@liebstoeckel/thumbnails";

await addThumbnailsToFile("./dist/my-talk.html", { width: 640, format: "webp" });

const { html, manifest, skipped } = await withThumbnails(deckHtml, { width: 640 });
```

| Option | Default | Notes |
| --- | --- | --- |
| `format` | `webp` | `webp` \| `jpeg` \| `png` (AVIF needs an OS encoder) |
| `width` × `scale` | 640 × 2 | native 1280×720; lower `width` to shrink a big deck |
| `quality` | 80 | encoder quality |
**Presenter previews are live:** The **presenter** view does *not* use these thumbnails: its current/next previews render live (only two slides, so it's cheap, crisp, and reflects the real reveal step + live plugin state).

[Plugins overview](https://docs.liebstoeckel.app/plugins/overview/)