---
name: dhtml
description: Build, change and deploy apps and sites that run on dhtml.net. Use whenever the user asks for a web app, form, blog, shop, booking page, API or mail flow on dhtml or dhtml.net, when a project has a dhtml.config, a server/pages or server/api folder or .dhtml files, or when the user asks to deploy to dhtml.net with a deploy key. A dhtml site is plain HTML pages with a few <d:...> tags, SQL migrations and one dhtml.config, zipped and deployed through the dhtml.net deploy API.
---

# dhtml on dhtml.net

dhtml runs only on dhtml.net. A site is a folder of readable HTML pages with a few `<d:...>` tags, SQL migrations and one `dhtml.config`. Pages read the database with `<d:query>`, forms post to actions in `server/api`, and every `{{ value }}` is escaped for where it lands. There is nothing to install and nothing to run on your side: you write the files, zip them, and deploy to dhtml.net.

## The loop

1. **Write the project files** (below). Read the reference for any tag you use.
2. **Zip the contents of the project folder** so `dhtml.config` is at the top. Leave out `.env`, `server/data` and `bin`.
3. **Deploy** with the deploy API and a bearer key from the environment (`$DHTML_KEY`).
4. **Read the status.** On `failed` the `message` is the output of the check dhtml.net runs on every upload: it names the file and line and says how to fix it. Do what the hint says, zip, deploy again.
5. **Look at the live site** at `https://NAME.dhtml.net/` and tell the owner what was built.

A failed deploy never changes the live site. The check on dhtml.net compiles every page, action, job and mail, so a deploy is also your test run: deploy early and often, every few files, not once at the end. Details and the answers of the API: `references/deploying.md`.

## Rules that matter

1. **Never invent a tag, attribute, filter or setting.** If it is not in `references/syntax.md` or `references/config.md`, it does not exist, and the check on dhtml.net refuses it. Need something that is not listed? Say so and propose it; do not work around it.
2. **Values are escaped.** There is no raw output. `markdown` and `json` are the only filters that make HTML or script, and they go last. Never build HTML or SQL from strings.
3. **`<d:...>` tags never go inside an HTML tag or attribute.** For a "selected" option write two whole elements, one in each `<d:if>` branch.
4. **A `<d:if test>` takes no filters.** Test the value itself (`test="rows"`), or compare with `== != < > <= >=`, `not`, `and`, `or`. Strings in a test use double quotes, so write the attribute with single quotes.
5. **Secrets are never in the project.** A setting names them as `env:NAME` (`password = env:MAIL_PASSWORD`), in CAPITALS with `_`. The owner sets the values in the site's Secrets section on dhtml.net. Never write a secret into a page, a config line, a message, a commit or a zip.
6. **Do not copy a rule between files.** Put a query or a guard used twice in `server/steps/name.dhtml` and use `<d:use src="name" />`.
7. **Every action validates each field it reads** (`<d:validate field="x" ... />` first, only declared fields exist). `ratelimit` is required for anything that logs in, mails, calls a model or another service, or enqueues. dhtml.net adds the CSRF token to every `<form method="post" action="/api/x">` by itself: never write one.
8. **Format numbers and dates with the filters or in SQL**: `{{ cents | div 100 | fixed 2 }}`, `{{ d | date "02 Jan 2006" }}`.
9. **Tests first for a feature**: decide what each page and action must do and reject, write it, deploy, and confirm on the live site. Seed rows go in migrations.
10. **When unsure, ask.** A question costs a minute. A guess in a site people use costs more.

## Files

| Where | What |
|---|---|
| `server/pages/**/*.dhtml` | GET pages; the path is the URL; `blog/[slug].dhtml` is `/blog/:slug`, read as `{{ route.slug }}` |
| `server/api/*.dhtml` | actions: forms and JSON, `POST /api/name` |
| `server/layouts/` `partials/` `components/` `steps/` | shell with `<d:slot />`; fragments; pieces with inputs; reusable steps |
| `server/jobs/*.dhtml` | work later or by the clock |
| `server/mails/*.dhtml` | HTML mail templates |
| `server/lang/<code>.lang` | the words of each language (`key = text`) |
| `server/migrations/NNN_name.sql` | plain SQL, applied in order on deploy; seed rows go here |
| `public/` | static files, served as they are |
| `dhtml.config` | settings (INI) |

## Which reference to read

Load a reference only when the task needs it. They are generated from the specification, so they are exact.

| Task | Read |
|---|---|
| Any tag, attribute, filter or variable: is it real, and how is it written | `references/syntax.md` |
| Starting an app; the usual shape of a blog, shop, booking or admin | `references/patterns.md` |
| Pages, layouts, components, queries, lists, ifs, client state, live updates | `references/templates-pages.md` |
| Forms, actions, validation, uploads, jobs, accounts and sign-in, API keys, webhooks, AI calls | `references/templates-actions.md` |
| Calling another service (`<d:http>`), signing with `hmac`, following redirects | `references/calls-and-signing.md` |
| Sending mail: to the visitor, to the owner, to many rows, HTML mail | `references/mail.md` |
| More than one language: `[lang]`, `t`, language files, mail in the recipient's language | `references/languages.md` |
| Any `dhtml.config` setting | `references/config.md` |
| An error message you do not understand | `references/errors.md` |
| Deploying to dhtml.net with a deploy key | `references/deploying.md` |

## Deploying

```bash
zip -r project.zip . -x '.env' 'server/data/*' 'bin/*'          # dhtml.config must be at the top of the zip
curl -X POST --data-binary @project.zip -H "Content-Type: application/zip" \
     -H "Authorization: Bearer $DHTML_KEY" "https://dhtml.net/api/deploy?site=25"
curl -H "Authorization: Bearer $DHTML_KEY" "https://dhtml.net/api/deploy?site=25"   # status: queued, running, ok, failed
```

The key (`dhk_...`) comes from the environment, never from a file you write and never into the chat: the owner makes it at `https://dhtml.net/keys`. The site number is in the panel address (`https://dhtml.net/sites/25`). The site is live at `https://NAME.dhtml.net/`.

## Done means

The deploy reports `ok`, every page you wrote renders on the live site, every form validates and rejects bad input, no secret is in a file you wrote, and the owner has been told in plain words what was built and where it is.
