# vagejs form to email

> Add a FormDoc form to a vagejs site. Put the form in a layout, send it with fetch and allow the API in the site’s Content-Security-Policy.

vagejs keeps sites safe by default, which affects forms in two ways. Markdown pages are sanitized, so a `<form>` pasted into a `.md` page is removed. And on Cloudflare and Netlify targets the generated Content-Security-Policy only lets forms post to the site itself (`form-action 'self'`). The setup below works with both: it’s exactly how the contact form on this site is built.

## 1\. Put the form in a layout

Create `src/layouts/contact.ts`. Layouts are trusted code, so the form stays:

TS

```ts
import { html, raw, type Layout } from "vagejs";
import defaultLayout from "./default.ts";

const layout: Layout = (props) => {
  const form = html`<form class="contact-form" action="https://api.formdoc.dev/v1/submit" method="POST">
  <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
  <label>Name <input type="text" name="name" autocomplete="name" required /></label>
  <label>Email <input type="email" name="email" autocomplete="email" required /></label>
  <label>Message <textarea name="message" rows="6" required></textarea></label>
  <input type="checkbox" name="botcheck" tabindex="-1" autocomplete="off" aria-hidden="true" hidden />
  <button type="submit">Send</button>
  <p class="contact-status" role="status" aria-live="polite"></p>
</form>
<script src="/contact.js" defer></script>`;
  return defaultLayout({ ...props, content: String(html`${raw(props.content)}${form}`) });
};

export default layout;
```

Then set `layout: contact` in the frontmatter of your contact page.

## 2\. Send it with fetch

Because the policy blocks posting forms to another site, send the form from a small script instead. Save this as `public/contact.js`:

JavaScript

```js
const form = document.querySelector(".contact-form");
const status = document.querySelector(".contact-status");

form?.addEventListener("submit", async (event) => {
  event.preventDefault();
  status.textContent = "Sending…";
  try {
    const res = await fetch(form.action, {
      method: "POST",
      headers: { Accept: "application/json" },
      body: new FormData(form),
    });
    const result = await res.json();
    if (result.success) form.reset();
    status.textContent = result.success ? "Thanks, message sent." : result.error.message;
  } catch {
    status.textContent = "Your message couldn’t be sent. Please try again.";
  }
});
```

## 3\. Allow the API in the policy

In `vage.config.ts`, let the script reach FormDoc:

TS

```ts
security: { cspSources: { "connect-src": ["https://api.formdoc.dev"] } },
```

Then run `npm run check`. It reports `link-unapproved` for `api.formdoc.dev` until you approve it with `npx vage links approve api.formdoc.dev`.

## Next

-   Every field and error code: [API reference](https://www.formdoc.dev/docs/api-reference/).
