# Editable content and forms

> Let anyone in the workspace edit a website’s words and pictures in Studio, and collect form entries there.

Canonical: https://benchy.world/docs/websites/content-and-forms
Last updated: 2026-10-09

Your website declares which words and pictures are editable and which forms it takes. Prices and opening hours then change without a deploy, order requests arrive in Studio, and the files stay as you built them.

## Declare content and forms [#declare]

Put a `benchy.site.json` file at the top of your output folder, next to `index.html`; Benchy reads it on each deploy. Content is grouped in **collections**: one set of fields, such as the shop’s details, or a list of items with the same fields, such as prints.

benchy.site.json

```
{
  "content": {
    "prints": {
      "label": "Prints",
      "list": true,
      "fields": {
        "title": { "type": "text", "label": "Name", "required": true },
        "price": { "type": "number", "label": "Price" },
        "photo": { "type": "image", "label": "Photo" },
        "inStock": { "type": "boolean", "label": "In stock" }
      }
    },
    "shop": {
      "label": "Shop details",
      "fields": {
        "hours": { "type": "text", "label": "Opening hours" },
        "about": { "type": "longtext", "label": "About us" }
      }
    }
  },
  "forms": {
    "order": {
      "label": "Order requests",
      "fields": {
        "name": { "type": "text", "required": true },
        "email": { "type": "email", "required": true },
        "print": { "type": "select", "options": ["Benchy", "Planter", "Lamp"] },
        "message": { "type": "longtext" }
      },
      "redirect": "/thanks/"
    }
  }
}
```

- Names of collections, forms and fields use letters, digits, `-` and `_`, and are case-sensitive. `id` is reserved, because every list item carries its own `id`. Each `label` is what Studio shows.
- A new field starts empty. A removed field is hidden and its values kept, so rolling back to a version that had it brings them back.
- A deploy without the file declares no content or forms, so `/_benchy/content/` answers 404 until a deploy that has it; nothing is deleted.
- A deploy with an invalid file is refused, naming the setting, such as `content.prints.fields.photo.type`. Only a JSON syntax error gives a line number.

## Field types [#field-types]

| Type | Holds | Used in |
| --- | --- | --- |
| `text` | One line, up to 500 characters | Content and forms |
| `longtext` | Plain paragraphs, up to 10,000 characters | Content and forms |
| `number` | A number | Content and forms |
| `boolean` | Yes or no; a checkbox in a form | Content and forms |
| `date` | A calendar date, as 2026-10-03 | Content and forms |
| `email` | An email address | Content and forms |
| `url` | A web address | Content and forms |
| `select` | One of the listed options | Content and forms |
| `image` | A picture with its alt text, up to 25 MiB | Content only |

## Edit content in Studio [#edit-content]

On the website’s page, **Content** shows one form per collection. Anyone with Can edit changes the words, adds, reorders and removes items, and uploads pictures, with no deploy. After **Save**, visitors get the change within seconds.

- Content isn’t part of a version: rolling back keeps today’s content.
- Content text counts against the website’s 16 MiB content limit; uploaded pictures count toward the workspace’s storage.
- Each save is in the website’s history, with who made it.

## Use content in your pages [#use-content]

Each collection is served on your website at `/_benchy/content/<collection>.json`. A list is `{ "$revision": n, "items": [...] }` in Studio’s order, each item with its own `id`; a single collection is `{ "$revision": n, ...fields }`. A picture is an object with a `url` on your website, `width` and `height` in pixels, and `alt`. An empty field is absent rather than null. `$revision` rises on every change.

js

```
const { items } = await (await fetch("/_benchy/content/prints.json")).json();
// [{ "id": "k3m9zq1x", "title": "3DBenchy", "price": 12, "inStock": true },
//  { "id": "p7w2bb0d", "title": "Amber ring",
//    "photo": { "url": "/_benchy/content/files/….png", "width": 1200, "height": 900, "alt": "…" } }]

const list = document.querySelector("#prints");
for (const print of items) {
  const item = document.createElement("li");
  if (print.photo) {
    const { url, width, height, alt } = print.photo;
    item.append(Object.assign(document.createElement("img"), { src: url, width, height, alt }));
  }
  item.append(Object.assign(document.createElement("h3"), { textContent: print.title }));
  list.append(item);
}
```

- It is your website’s own address, so there is nothing to configure.
- A collection the live version doesn’t declare answers 404, as does a picture the content no longer shows.
- A preview reads the live website’s content, shaped by the preview’s own `benchy.site.json`: a field it adds shows empty, and one it removes isn’t served.
- For pages that must work without JavaScript: pull the content, build, deploy.

## Content from the CLI and agents [#content-agents]

shell

```
benchy site content pull olive-prints   # writes content/prints.json and content/shop.json
benchy site content push olive-prints   # saves your edited files as the website's content
```

`pull` writes one file per collection, in the shape above; `push` saves them back. An agent can read and change content if its key or connection can make changes in the workspace; a Publish websites key only reads it.

## Add a form [#forms]

Declare the form in `benchy.site.json`, then post an ordinary HTML form to `/_benchy/forms/<name>` and include Benchy’s form script:

html

```
<form method="post" action="/_benchy/forms/order">
  <label>Name <input name="name" required></label>
  <label>Email <input name="email" type="email" required></label>
  <label>Message <textarea name="message"></textarea></label>
  <button>Send</button>
</form>
<script src="/_benchy/forms.js" defer></script>
```

- The script adds a spam check and a hidden trap field, sends the entry, and shows the answer beside the button. It sets no cookies and works on a custom domain too. A form posted without it is refused.
- After a good entry the visitor goes to the form’s `redirect` page, or back to the same page with `?sent=order`.

## A form on a canvas [#canvas-forms]

On a canvas, choose **Create**, then **Elements**, **Forms**, **Contact form**. It adds Name, Email and Message fields and a Send button, each an ordinary layer you can restyle.

- Select the form or a field and open **Website** in the top bar. A form has a **Name** (such as `contact`) and **Sent**, the line a visitor reads after sending. A field has a **Type**, **Name**, **Label** and **Required**. Any rectangle inside the form can become a field or the send button.
- A field’s text is its placeholder; its **Label** is read by screen readers and names its column in the entries.
- The form takes entries once the canvas is [published as a website](https://benchy.world/docs/websites/publish#canvas); Benchy declares its forms for you.
- On an Instant link and in Preview, the form shows but takes no entries, and says so.

## Read entries [#entries]

**Forms** on the website’s page lists each form’s entries, newest first, with the page each came from. The owner and editors see them; viewers don’t, because entries hold people’s details.

- **Export CSV** downloads every entry, or those between two dates.
- Delete an entry any time; entries are deleted automatically after 90 days.
- One export holds up to 10,000 entries, the newest; the CLI and agents say when there were more, and an earlier `--until` reads the rest.
- Entries count toward the workspace’s monthly quota. See [Plans](https://benchy.world/docs/websites/plans-and-limits#plans).

shell

```
benchy site forms list olive-prints                                # each form's name and this month's entries
benchy site forms export olive-prints order > orders.csv
benchy site forms export olive-prints order --since 2026-09-01 > september.csv
```

Tell your visitors what you collect and why, on the page with the form.

## Email notifications [#notifications]

Each member chooses **Email me new entries** on a form for themselves. The email is plain text with the form’s fields and the website’s address; replying answers the entry’s `email` field, when there is one. A website sends at most 20 an hour, then one hourly summary of the rest.

## What forms accept [#form-rules]

- Declared fields only, checked against their types and `required`; anything else is dropped.
- No passwords, payment details or government ID numbers: a deploy declaring a password field, or fields for a card number, expiry date, security code or government ID number, is refused.
- One visitor can send 10 entries a minute to a site. Entries that fail the spam check are dropped and don’t count.
- At the monthly quota, on any plan, forms answer “This form isn’t taking entries right now” until the next month.

## Limits [#limits]

| Limit | Value |
| --- | --- |
| Collections per website | 20 |
| Forms per website | 20 |
| Fields in a collection or a form | 40 |
| Options in a choice field | 50 |
| A label | 80 characters |
| Items in a list | 1,000 |
| One item | 16 KiB |
| Links in one item | 50 |
| One collection’s content | 4 MiB |
| All content on a website | 16 MiB, pictures not included |
| One entry | 32 KiB of text, no files |
