Forms
Add contact forms with built-in anti-spam and configurable dispatch.
Forms
The :::form block defines an inline form with honeypot, HMAC
timestamp, and rate limiting built in. Submissions are dispatched
to configured targets (email, API, Slack) via a CGI handler.
Front matter
Enable forms on a page with the form: key:
---
title: Contact
form: contact
---
The name must be alphanumeric with hyphens and underscores. Without
form: in front matter, :::form blocks are not rendered.
Field syntax
::: form
field_name | Label text | rules
submit | Button label
:::
Field rules
required- field must be filled in, addsrequiredattributeoptional- field is optional (default)email- renders astype="email"inputtel- telephone input; gets a default validation pattern (override withpattern:)date/time- date / time picker (type="date"/type="time")number- numeric input;min:N/max:Nset the value boundsurl- URL input (type="url")password- masked inputtextarea- renders as a textareaselect:opt1,opt2,opt3- renders as a dropdownpattern:REGEX- HTML5 validation pattern. Example:phone | Phone | tel pattern:[0-9+()-]{7,20}placeholder:TEXT- greyed-out hint text inside the fieldmax:N- maxlength for text inputs (default 1000); the max value fornumbermin:N- the min value fornumberfile- renders a file picker (<input type="file">) for binary uploads (images, PDFs, ...). Addmultipleto allow several files in one field, andaccept:LISTto hint the browser's picker (accept:image/*oraccept:.png,.pdf). The form automatically switches toenctype="multipart/form-data"when it contains a file field.
Rules are whitespace-separated. A value that needs spaces (a placeholder, or a
pattern with a literal space) is quoted: placeholder:"Your full name" or
pattern:"[0-9 +()-]{7,20}".
Validation is enforced in the browser (HTML5 attributes); the handler accepts the submitted fields.
Handler configuration
Create lazysite/forms/FORMNAME.conf:
targets:
- type: smtp
url: http://localhost/plugins/form-smtp.pl
Target types: smtp (email via helper), api with format: json
(webhook), api with format: slack (Slack notification).
File uploads
A form accepts binary uploads only when its .conf declares upload limits (so a
form never accepts files by accident). Add any of these keys to
lazysite/forms/FORMNAME.conf:
targets:
- handler: jsonl # a "file" target is required to STORE the files
upload_max_files: 3 # max files per submission (default 5)
upload_max_kb: 5120 # max size of EACH file, KiB (default 5120 = 5 MiB)
upload_accept: png, jpg, pdf # allowed extensions (default: any)
A submission that breaks a limit is rejected before any handler runs, with a specific message to the visitor ("File 'x.png' is too large...", "File type not allowed...", "Too many files...").
Uploaded files are stored by the file (jsonl) target, in a per-submission
subdirectory next to the FORMNAME.jsonl:
lazysite/forms/submissions/FORMNAME.jsonl
lazysite/forms/submissions/FORMNAME.files/<submission-id>/photo.png
The submission record names the files (it never stores the bytes inline):
{ "name": "Ada", "_files": ["photo.png"],
"_files_dir": "FORMNAME.files/20260629T101500-1a2b", ... }
Filenames are sanitised to a safe basename (any path component is stripped, so a
crafted ../../etc/passwd cannot escape the submission directory). A form with a
file field but no file target validates uploads but does not keep them - add a
file target to store them.
Emailing uploads. The SMTP (email) handler has an Attach uploaded files
option (attach_files, off by default). When on, the uploaded files are attached
to the notification email and listed (name + size) below the message. Leave it off
to keep emails small and just store the files; mind your mail server's attachment
size limits when enabling it.
Example
---
title: Contact
form: contact
---
::: form
name | Your name | required max:200
email | Email address | required email
message | Your message | required textarea max:5000
submit | Send message
:::
Multi-step (wizard) forms
Split a long form into steps with a --- step --- line. Add a title with
--- step: Title ---. The visitor moves through the steps with Back / Next
(each step is validated before advancing) and submits once at the end.
::: form
name | Your name | required max:200
email | Email address | required email
--- step: Your enquiry ---
subject | Subject | required max:200
message | Your message | required textarea max:5000
submit | Send
:::
Notes:
- Delivery is unchanged - the whole form still posts once, with the same security. The steps are a presentation over one submission.
- Without JavaScript every step simply shows as a section and the form still submits (progressive enhancement).
- Steps are linear (no conditional skipping or save-and-resume yet).
Notes
- Security is automatic: honeypot, HMAC timestamp (3s-2h window), rate limiting (5/hour/IP), header injection prevention
- Forms submit via fetch (AJAX) - no page reload
- HMAC secret auto-generated at
lazysite/forms/.secret - Forms guide - full setup and configuration
- SMTP configuration - email delivery
- Form helpers - custom dispatch targets