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
- Every field and error code: API reference.