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.
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.
::: form
field_name | Label text | rules
submit | Button label
:::
required - field must be filled in, adds required attributeoptional - field is optional (default)email - renders as type="email" inputtel - telephone input; gets a default validation pattern (override with pattern:)date / time - date / time picker (type="date" / type="time")number - numeric input; min:N / max:N set the value boundsurl - URL input (type="url")password - masked inputtextarea - renders as a textareaselect:opt1,opt2,opt3 - renders as a dropdownradio:opt1,opt2,opt3 - the same choice as a radio group, with every option
visible. Better than a dropdown on a page of short questions answered in
sequence, where a hidden list makes a mis-pick easy and silentchecklist:opt1,opt2 - checkboxes; the submission carries every ticked valuechecklist-qty:opt1,opt2 - checkboxes with a quantity box beside each. The
submission reads opt1=60; opt2=40, and an option ticked without a quantity
keeps its bare label. This is the shape of a "which ones, and how many"
answer, and it removes the free-text box people otherwise type it intoselect:"Smith, John","Jones". Quoting is
only needed for a label containing a comma - spaces and brackets have
always worked unquoted. Put any of these list rules LAST among a field's
rules, since each takes the rest of the linepattern: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 for numbermin:N - the min value for numberfile - renders a file picker (<input type="file">) for binary uploads
(images, PDFs, ...). Add multiple to allow several files in one field, and
accept:LIST to hint the browser's picker (accept:image/* or
accept:.png,.pdf). The form automatically switches to
enctype="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.
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).
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.
---
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
:::
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:
lazysite/forms/.secret