Skip to content

Modelling content

Forms

Declare a public form, keep spam out, email the right people, pass submissions to another system, and decide how long they are kept.

Tidewater’s “Book a demo” form is how sales hears about new customers, so it has to work, reach the right inbox, land in the CRM and keep bots out. This guide covers all of that.

After reading this guide, you will know:

  • How to declare a form and which fields a public form may use.
  • How to put a form on a page.
  • How spam protection works, and how to turn on a CAPTCHA.
  • How to email submissions, and how to send them to another system’s API.
  • How long submissions are kept, and how to change it.

1. Declaring a form

bin/rails nibble:generate:form demo
# site/schema/forms/demo.yml
title: Book a demo
fields:
  - handle: name
    field: { type: text, display: Your name, required: true }
  - handle: email
    field: { type: text, input_type: email, display: Work email, required: true }
  - handle: company
    field: { type: text, display: Company }
  - handle: team_size
    field:
      type: select
      display: Team size
      options: { solo: Just me, small: 2–10, large: More than 10 }
success:
  message: Thanks — we'll be in touch within a working day.

Fields are declared exactly as in a blueprint, but a public form may only use text, textarea, integer, toggle, select, radio, checkboxes, date and files. nibble:check refuses anything else.

success is either a message shown in place of the form, or a redirect to a thank-you page:

success:
  redirect: /demo/thanks

Tip

A files field takes uploads — a brief, a logo — with max_files, max_file_size (in MB) and extensions to limit them. Uploaded files are kept with the submission.

2. Putting it on a page

A view asks for the form in its query sidecar, and renders it:

# site/themes/tidewater/views/demo.yml
form:
  from: form:demo

The view receives the form’s definition — its fields, where to post, the honeypot, the CAPTCHA — and useNibbleForm does the posting and error handling. See Theme components for a complete form component.

The form posts to POST /forms/demo. Submissions are stored and listed under Forms in the control panel.

Submissions to the Book a demo form

3. Emailing submissions

notify:
  - to: sales@tidewater.example
    reply_to: email
  - to: founders@tidewater.example
    fields: [name, company, team_size]

Each entry sends one email per submission. reply_to names a field, so pressing Reply answers the person who filled the form in. fields limits what that email includes.

Important

The address mail is sent from is a site setting: Globals → Integrations. Set it, and the SMTP settings in configuration, before relying on email.

cp_notify: true also notifies control panel users who can see the form’s submissions.

4. Spam

Every form gets two protections without asking:

  • A honeypot — a hidden field people never fill in and bots usually do. Rename it with spam.honeypot, or turn it off with honeypot: false.
  • Rate limiting — five submissions a minute from one address by default.

A public demo form on a startup’s site will still attract determined bots. Turn on a CAPTCHA:

spam:
  captcha: true
  rate_limit:
    requests: 3
    per_minutes: 10

Then choose the provider — Cloudflare Turnstile or Google reCAPTCHA — and paste its keys under Globals → Integrations. The theme’s form component renders the widget from the form’s definition.

Warning

With captcha: true and no keys in Integrations, submissions cannot be verified and are refused. Add the keys before you deploy the change.

5. Sending submissions to another system

Tidewater’s sales team lives in its CRM, so each demo request should arrive there too. An api delivery posts the submission to another system’s API:

api:
  - url: https://api.crm.example/v1/leads
    method: post
    format: json
    body:
      name: "{field.name}"
      email: "{field.email}"
      company: "{field.company}"
      source: website-demo
      token: "{secret.crm_token}"

{field.x} is a submitted value, {secret.x} a secret, and {config.x} a value from outbound.config in configuration. Deliveries are async by default: the visitor sees the success message at once, and a failed delivery is retried and shown on the submission in the control panel. mode: sync makes the visitor wait for it instead.

Nibble only calls out to hosts you allow, and only uses secrets you name:

# config/nibble.yml
outbound:
  allowed_hosts: [api.crm.example]
  secrets: [crm_token]

and the secret itself goes in the environment as NIBBLE_SECRET_CRM_TOKEN, or in credentials under nibble.secrets.crm_token.

Caution

Never put a key in the form file itself. Schema is committed to git and readable by anyone with the repository; {secret.…} keeps the key out of it.

5.1 API connections

When several forms call the same API, describe it once in site/schema/apis/crm.yml:

base_url: https://api.crm.example/v1
headers:
  Authorization: "Bearer {secret.crm_token}"
timeout: 10

and deliver to it with use: crm instead of url:.

5.2 Handlers in Ruby

For anything a template cannot express, a form can name a handler registered in Ruby. See Extending Nibble.

6. How long submissions are kept

retention_days: 90

Submissions older than that are deleted on a schedule, so a contact form does not quietly accumulate people’s personal details for years. store: false keeps nothing at all — the email and API deliveries still happen.

Note

Ninety days suits a demo form: long enough for sales to follow up, short enough that the site does not become a second, forgotten CRM.

7. Every key

Key Notes
title, fields required
success { message } or { redirect }
notify a list of { to, reply_to, fields }
api a list of { url or use, method, format, body, mode }
spam { honeypot, rate_limit: { requests, per_minutes }, captcha }
store, cp_notify true / false
retention_days a positive number
handler the name of a registered Ruby handler