Skip to main content

Contact Form

webhull includes a built-in contact form that handles validation, spam filtering, rate limiting, and SMTP dispatch — no third-party form service required.

How it works​

Visitor fills form → POST /api/contact
→ honeypot check (reject bots instantly)
→ rate limit check (3 req / 15 min / IP)
→ field validation (name, email, subject, message)
→ spam filter (keywords, link count, character patterns)
→ SMTP dispatch to recipients
→ auto-reply to sender (optional)
→ JSON response → JS shows success/error message

Enabling the form​

The contact form is disabled by default. Enable it by adding a contact: block and configuring SMTP:

contact:
enabled: true
recipientName: "Acme Support"
recipients:
- "hello@example.com"
subject:
en: "New message from {{.Name}}"
de: "Neue Nachricht von {{.Name}}"
maxLinks: 2 # messages with more links are rejected as spam
rateLimit:
requests: 3
window: 15m

mail:
host: "smtp.example.com"
port: 587
username: "${SMTP_USERNAME}"
password: "${SMTP_PASSWORD}"
from: "noreply@example.com"
fromName: "Acme Corp"
useTLS: true

The contact template automatically renders the form when this is configured. No template changes needed.

Form fields​

The form collects:

FieldValidation
NameRequired, non-empty
EmailRequired, valid format, non-disposable domain
SubjectRequired, non-empty
MessageRequired, non-empty
WebsiteHoneypot — must be empty; any value triggers instant rejection

Rate limiting​

Submissions are rate-limited per IP address. Defaults: 3 requests per 15-minute window. Configure via contact.rateLimit:

contact:
rateLimit:
requests: 5
window: 30m

Spam filter​

The spam filter rejects messages that match any of the following:

  • Known spam keywords (viagra, casino, crypto lottery, etc.)
  • Banned phrases (click here, free money, etc.)
  • More than maxLinks URLs in the message body
  • High uppercase character ratio (>40%)
  • Low word variety (repetitive text)
  • Repeated character sequences

Custom spam keywords can be added programmatically — see the forms package.

Auto-reply​

Send a confirmation email to the visitor after a successful submission:

mail:
templates:
en:
subject: "We received your message"
body: |
<p>Hi {{.Name}},</p>
<p>Thank you for getting in touch. We will reply within one business day.</p>
<p>Acme Corp</p>
de:
subject: "Ihre Nachricht ist angekommen"
bodyFile: "mail/auto-reply-de.html" # load from file instead of inline

bodyFile is resolved relative to the config file directory. The template variables available are {{.Name}}, {{.Email}}, {{.Subject}}, {{.Message}}.

Customising form labels​

Form labels default to built-in DE/EN strings. Override any field per language via ui.contactForm:

ui:
en:
contactForm:
heading: "Send us a message"
nameLabel: "Your name"
emailLabel: "Email address"
subjectLabel: "Subject"
messageLabel: "Message"
submitText: "Send"
successMsg: "Thank you! We will be in touch."
successRefMsg: "Your reference:"
errorMsg: "Something went wrong. Please try again."
requiredMsg: "Please fill in all required fields."
de:
contactForm:
heading: "Schreib uns"
submitText: "Absenden"

Only non-empty values override the built-in defaults — you do not need to set every field.

Using the contact template​

Add a page with template: contact in your content directory:

---
id: contact
template: contact
title: "Contact"
description: "Get in touch."
heroLine1: "Let's talk."
heroSubtitle: "No pitch decks required."
---

<p>Fill in the form and we will get back to you within one business day.</p>

The body content (above) appears above the form. Leave it empty if you want just the form with no intro.

SMTP with TLS​

For SMTP servers that require TLS on connection (port 465 rather than STARTTLS on 587):

mail:
host: "smtp.example.com"
port: 465
useTLS: true

For STARTTLS (port 587, the default), useTLS: false (or omit — default is false; the connection upgrades automatically).