Guides · WhiskerJS 0.1.0

Astro

An Astro page whose bundled script collects on submit; the page itself stays static HTML.

In plain words

An Astro page is HTML made on the server or at build time. The small script at the bottom is the only part that runs in the visitor’s browser: when the form is submitted it gets a one-time code from your server, lets WhiskerJS read the browser and posts the result with the form.

Install: build from source

WhiskerJS is not on npm yet; the npm package is coming. Build it and install the tarball:

cd whiskerjs
pnpm install && pnpm build && pnpm pack    # writes whiskerjs-0.1.0.tgz
cd ../my-astro-site
npm install ../whiskerjs/whiskerjs-0.1.0.tgz

The page

src/pages/index.astro:

---
// Rendered on the server or at build time: no WhiskerJS code runs here.
---
<html lang="en">
  <head><meta charset="utf-8" /><title>Sign in</title></head>
  <body>
    <form id="login">
      <label>Email <input name="email" type="email" required /></label>
      <label>Password <input name="password" type="password" required /></label>
      <button type="submit">Sign in</button>
      <output id="status"></output>
    </form>

    <script>
      // Astro bundles this script and runs it in the browser only.
      import { load } from 'whiskerjs';

      const agentReady = load(); // cheap: no collection work happens yet
      const form = document.getElementById('login') as HTMLFormElement;
      const status = document.getElementById('status') as HTMLOutputElement;

      form.addEventListener('submit', async (event) => {
        event.preventDefault();
        const data = new FormData(form);
        // A fresh nonce from your server, then collect. collect() never throws.
        const binding = await (await fetch('/api/whisker-nonce')).json();
        const device = await (await agentReady).collect({ timeoutMs: 3000, binding });
        const response = await fetch('/api/login', {
          method: 'POST',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify({ email: data.get('email'), password: data.get('password'), device }),
        });
        status.textContent = response.ok ? 'Signed in' : `Refused (${response.status})`;
      });
    </script>
  </body>
</html>

How it works

  • The frontmatter (between the --- lines) runs on the server or at build time. Nothing from WhiskerJS belongs there: there is no browser to read.
  • The <script> is processed by Astro: TypeScript is stripped, import { load } from 'whiskerjs' is resolved from node_modules, and the result is bundled into one module under /_astro/ that the browser loads. It runs once per page load, in the browser only.
  • The binding. /api/whisker-nonce returns { nonce, timestamp }, passed to collect({ binding }); your server checks result.binding: Node, Go, PHP, Python.
  • Errors. collect() never throws or rejects. Add a try block around the fetch calls if you want a message on network errors, as the other guides do.

Things to know

  • Static output needs a backend. With Astro’s default static build the endpoints live in your existing server. The tested setup served the built dist/ folder from the Node server.
  • Content Security Policy. The bundled script is a normal same-origin module file, and WhiskerJS makes no network request, so script-src 'self' is enough for it.
  • Secure context. https, or localhost with astro dev or astro preview.

Tested

Tested with WhiskerJS 0.1.0 (signalsVersion 5, idVersion 2) and Astro 7.4.1 on 2026-10-26: built with astro build, the dist/ folder served by the Node server and submitted in headless Chromium 140 (Playwright 1.55.0). The server accepted the binding and refused the same result sent a second time.

Questions and answers

Do I need a UI framework island?

No. A plain script tag in the .astro file is enough. Astro bundles it as a module and runs it in the browser; the rest of the page stays static HTML.

Where do the nonce and login endpoints go in a static Astro site?

A static build has no server of its own, so they live in your existing backend (any of the server guides). With an Astro server adapter you can also write them as Astro endpoints.

Can I use is:inline?

Not with import 'whiskerjs': an inline script is not bundled, so the bare import cannot be resolved. Either let Astro bundle the script, or serve the dist files and import them by URL as in the plain JavaScript guide.