vagejs form to email

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:

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:

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:

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