# Astro

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

Source: https://whiskerjs.com/guides/astro/ (last updated 2026-10-26)

> **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:

```sh
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`:

```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](https://whiskerjs.com/guides/verify-node/), [Go](https://whiskerjs.com/guides/verify-go/), [PHP](https://whiskerjs.com/guides/verify-php/), [Python](https://whiskerjs.com/guides/verify-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](https://whiskerjs.com/guides/verify-node/#a-complete-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](https://whiskerjs.com/guides/verify-node/#a-complete-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.

## Related pages

- [Plain JavaScript](https://whiskerjs.com/guides/javascript/): The same script without a build step.
- [Verify in Node](https://whiskerjs.com/guides/verify-node/): The server this page was tested against.
- [All integration guides](https://whiskerjs.com/guides/): The page-to-server contract.
- [Getting started](https://whiskerjs.com/docs/getting-started/): Build WhiskerJS from source.
