> ## Documentation Index
> Fetch the complete documentation index at: https://docs.decktalk.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Write a page by hand

> Write a page that follows the page contract without decktalk-runtime.js, for a page built with another framework.

Most pages should include `decktalk-runtime.js` and skip this guide. Use this guide only when a page
comes from another framework and cannot include the runtime. It lists what the recorder and
`decktalk screenshots` need, and it shows a small page that does all of it.

## Before you start

You need to write JavaScript. Read [The page contract](/concepts/page-contract) first.

## Meet the needs of the recorder

The recorder drives the page through its URL and a few globals. This table pairs each action of
the recorder with what your page does.

| What the recorder does                                                                            | What your page does                                                                                                                 |
| ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| The recorder opens `page.html?scene=N&cues=id@s,…&t0=signal`, with any `params` from the section. | The page reads `scene` and `cues`. It schedules cue `id` at `s` seconds after the clock starts.                                     |
| The recorder adds `&words=word@s,…` when the section's take has words.                            | The page can use the words to show text one word at a time, or it can ignore them.                                                  |
| The recorder adds `&prevwords=word@s,…` when the section just before has words.                   | A page that opens on the previous section's last frame can read them, or it can ignore them.                                        |
| The recorder awaits `document.fonts.ready`, then `window.__decktalk.ready` when it is a Promise.  | The page sets `window.__decktalk.ready` to a Promise that resolves when the page is ready.                                          |
| The recorder calls `window.DeckTalk.startClock()` one time, at narration t=0.                     | The page starts its clock in that call and nowhere else.                                                                            |
| The recorder reads `window.__decktalk.catalog` after the recording.                               | The page lists at least one scene in `catalog`. A missing or empty list is a page error.                                            |
| `preflight` freezes a slide at and before each cue.                                               | Each catalog entry carries `slides`, the slide ids in page order, and `cues`, each slide id mapped to its cue ids in preview order. |
| The recorder reads `window.__decktalk.warnings`, `frameGaps`, and `spokenLog`.                    | The page adds a string to `warnings` for anything it cannot do. `frameGaps` and `spokenLog` are optional.                           |
| The recorder uses a browser window of `[video] width` by `height`.                                | The page draws at 1920 by 1080, or it scales a 1920 by 1080 element to fit the window.                                              |

The magenta cover belongs to the recorder, so a page needs no cover of its own.
[How it works](/concepts/how-it-works#why-the-cuts-are-exact) explains what the cover does.

## Support decktalk screenshots

`decktalk screenshots` uses the page in three ways:

* It opens the page with no query and reads `window.__decktalk.catalog`. If the page has no catalog, `screenshots` warns and skips the page.
* It opens `?slide=ID` for each slide id in the catalog. With `--after`, it opens `?slide=ID&after=CUE`.
* In frame mode, `screenshots --section N --at S` opens the recorder's URL. It logs `window.__decktalk.fired` with each frame.

`decktalk preflight` also reads `catalog[].cues`, which maps each slide id to its cue ids in preview order. It opens
`?slide=ID&after=CUE` and `?slide=ID&before=CUE`. A scene whose entry carries no `cues` map gets every cue skipped as
`NO_CATALOG`, and a cue that the map does not name is skipped as `NO_SLIDE`, because a freeze stops only at a
listed cue. A page that answers neither parameter is not skipped: it shows every element both times, and each cue
reads `NO CHANGE`.

## Write the minimal page

<Warning>Start the page clock only inside `DeckTalk.startClock()`. A page that starts its clock at
`load` fires every cue early, and every reveal in the video comes before its word.</Warning>

The page below follows the whole contract in about forty lines. It has one scene with one slide, and
each cue shows the element with the matching `data-cue`. Its `ORDER` list is the one place the cue
ids are written, which is what lets freeze mode answer `&after=` and `&before=`.

```html deck/plain.html theme={null}
<!doctype html>
<meta charset="utf-8">
<style>
  html, body { margin: 0; background: #0b0f14; color: #f4f6f8; font: 44px/1.3 Inter, sans-serif; }
  #stage { position: absolute; left: 0; top: 0; width: 1920px; height: 1080px; padding: 100px 160px; box-sizing: border-box; }
  [data-cue] { opacity: 0; transition: opacity .35s; }
  [data-cue].on { opacity: 1; }
  .block { margin-top: 40px; width: 1200px; height: 220px; border-radius: 24px; background: #2c1fea; }
</style>
<div id="stage">
  <h1 data-cue="4.1a">A page by hand</h1>
  <div class="block" data-cue="4.1b"></div>
</div>
<script>
  const ORDER = ["4.1a", "4.1b"];
  const params = new URLSearchParams(location.search);
  const state = { mode: "index", scene: params.get("scene"), slide: "4.1", cues: [], fired: [], warnings: [], origin: null,
                  catalog: [{ scene: "4", name: "By hand", slides: ["4.1"], cues: { "4.1": ORDER } }],
                  now: () => state.origin === null ? -Infinity : (performance.now() - state.origin) / 1000 };
  window.__decktalk = state;
  window.__decktalk.ready = document.fonts.ready;

  const cues = (params.get("cues") || "").split(",").filter(Boolean).map((token) => {
    const at = token.lastIndexOf("@");
    return { id: token.slice(0, at), t: parseFloat(token.slice(at + 1)) };
  });
  state.cues = cues;
  for (const c of cues) if (!document.querySelector(`[data-cue="${c.id}"]`)) state.warnings.push(`unknown cue id ${c.id}`);

  function fire(id) {
    state.fired.push(id);
    document.querySelectorAll(`[data-cue="${id}"]`).forEach((el) => el.classList.add("on"));
  }
  function startClock() {
    if (state.origin !== null) return;
    state.origin = performance.now();
    if (cues.length) state.mode = "cue";
    for (const c of cues) setTimeout(() => fire(c.id), c.t * 1000);
  }
  window.DeckTalk = { startClock };

  if (params.has("slide")) {
    state.mode = "freeze";
    const after = params.get("after"), before = params.get("before");
    const last = after ? ORDER.indexOf(after) : before ? ORDER.indexOf(before) - 1 : ORDER.length - 1;
    ORDER.slice(0, last + 1).forEach(fire);
  } else if (params.get("t0") !== "signal") {
    startClock();   // a browser preview starts at once
  }
</script>
```

## Put the page in a project

These steps add the page as section 4 of the starter that `decktalk init` writes, so nothing already
there has to change.

<Steps>
  <Step title="Save the page">
    Save the page above as `deck/plain.html` in the project.
  </Step>

  <Step title="Add the section to decktalk.toml">
    ```toml decktalk.toml theme={null}
    [[section]]
    number = 4
    chapter = "By hand"
    page = "deck/plain.html"
    scene = 4
    ```

    The scene number, the section number and the cue id prefix agree, which is the rule every project follows.
  </Step>

  <Step title="Add the script and the cues">
    <CodeGroup>
      ```md script.md theme={null}
      ## 4. By hand

      This page follows the contract with no runtime. [beat] It shows one block on its own word.
      ```

      ```json cues.json theme={null}
      "4": { "cues": [
        { "cue": "4.1a", "on": "This page" },
        { "cue": "4.1b", "on": "one block" }
      ] }
      ```
    </CodeGroup>
  </Step>

  <Step title="Resolve the cues">
    ```console theme={null}
    decktalk narrate --no-voice
    decktalk align
    ```

    ```text theme={null}
    sec  speech   need  cues
     04     7.6      -  4.1a@0.5,4.1b@4.94
    ```

    `align` finds each cue id in the page as a quoted literal, so `data-cue="4.1a"` and the `ORDER` list both count.
  </Step>

  <Step title="Check the frozen states">
    ```console theme={null}
    decktalk screenshots --page deck/plain.html
    decktalk preflight --only 4
    ```

    ```text theme={null}
    check              slide       cue   chg %  result
    4:4.1a             4.1        0.50    1.12  changed
    4:4.1b             4.1        4.94   12.69  changed
    2 cue(s): 2 changed. Frozen frames in build/preflight
    ```

    A page that ignores `&after=` and `&before=` shows every element in freeze mode, so each pair of frames is
    identical and every cue reads `NO CHANGE`. That is the check this page's `ORDER` list exists for.
  </Step>

  <Step title="Record the section">
    ```console theme={null}
    decktalk record --only 4
    ```

    ```text theme={null}
    [rec ] section 04 (deck/plain.html?scene=4)  9.2s ...
           build/recordings/04.webm  (t=0 at 1.440s, ok)
    sections 1, recorded 1, kept 0
    ```

    The row reads `ok`, and `t0_method` in `build/recordings/04.json` starts with `cover`. A `NO COVER` there
    means the page started its own clock instead of waiting for `DeckTalk.startClock()`.
  </Step>
</Steps>

The page has no preview timing. A browser preview with `?scene=4` shows nothing until you add
`&cues=4.1a@1,4.1b@2` to the URL. Run `decktalk serve` and open the page from the URL it prints, because
the recorder opens every page over http and not from a `file://` URL.

## Compare with the runtime

The runtime gives a page more than this minimal page does:

* an index of scenes and slides
* preview timing from `hold` and `data-delay`
* reveal effects, count-ups, typewriting, and word sync
* KaTeX typesetting, with a wait for the library
* a camera push
* a warning for each mismatch between cues and slides
* a list of gaps between animation frames

[Runtime](/reference/runtime) lists all of it. A page by hand can add any of these features.

## Next

* **Learn how a page plays:** [The page contract](/concepts/page-contract)
* **Look up the runtime's fields:** [Runtime](/reference/runtime#window-__decktalk)
* **Write a page with the runtime:** [Your first deck](/guides/first-deck)
