Skip to content

Building blocks

Contact

/contact lets visitors reach you without your address appearing in any page. It offers a form that mails you their message, a "Show email address" button that hands out the address after a Turnstile check, or both. You work here when you set the address for a deploy, choose what the page offers or change how it looks.

How it works

  1. NUXT_CONTACT_ADDRESS fills contactAddress, a private runtime-config key. Only the two routes below read it, at request time, so no build, page, payload or bundle carries it.
  2. modules/contact-routes.ts(Licensees only) registers one route per entry in contact.channels: POST /api/contact for form and POST /api/contact/address for reveal. A channel left out answers 404.
  3. Each route counts the attempt against the caller's IP, then checks that the address is set and that Turnstile passes. The form sends the message through deliverEmail with the sender as Reply-To, once, while the request lasts: it is never queued or stored. The reveal answers { address } with cache-control: no-store.
  4. The kit's support links (SupportLink, the footers and :contact-line in the legal pages) take their path from useContactPath(), which is null while contact.enabled is false, so they render nothing then.

Choose the channels

contact: {
  enabled: true,
  channels: ['form', 'reveal'],
},
channelsThe page shows
['form', 'reveal']The form, with "Prefer to write directly? Show email address" below
['form']The form alone
['reveal']"Show email address" as the page's only button

enabled: false drops the page and both routes from the build. The build refuses an empty channels, a channel it doesn't know, and form while email.enabled is false.

Set the address

Locally, put it in .env. On a deploy, set it on the Worker:

pnpm exec wrangler secret put NUXT_CONTACT_ADDRESS --name <worker>

Until it is set, both routes answer 503. The [env] line a deployed Worker logs at boot names everything the page still lacks: the address, the Turnstile keys and, with form, a configured email provider (Email).

Keep NUXT_EMAIL_FROM and NUXT_EMAIL_REPLY_TO on other addresses. Every mail the app sends carries both, and anyone can ask for a sign-in mail to their own inbox. The boot check warns when either one is the contact address.

Turnstile

Both routes call assertHuman (Auth) before they send or answer. Outside pnpm dev, the contact routes answer 503 while the Turnstile keys are unset, where the auth forms would skip the check. pnpm dev runs on Cloudflare's always-pass test keys.

Limits

WhatLimit
Attempts per IP address5 in 10 minutes on each route, then locked for 10 minutes. IPv6 by /64.
Messages, site-wide30 an hour, counted once Turnstile passes
Name1 to 100 characters, on one line
EmailUp to 254 characters
Message10 to 5,000 characters, with no control characters but tabs and newlines

Over a limit the route answers 429 with Retry-After (Rate limiting). A message every provider refuses answers 502 and is not retried; the form keeps it so the visitor can send it again. The numbers live in server/contact/limits.ts(Licensees only) and shared/schemas/contact.ts(Licensees only).

What you receive

A mail in plain text from NUXT_EMAIL_FROM with the subject "Contact from" and the sender's name, and the sender's address as Reply-To. Its first line names the signed-in account that sent it, marked when that account hasn't proven its address yet, or says it was sent signed out. The name and Reply-To are marked unverified, because anyone can type any address: confirm a request about an account with that account's address before you act on it.

Change the page

app/pages/contact.vue(Licensees only) is a short page around two components, so a page of your own can use them the same way:

  • <ContactForm @sent="..."> is the form. It fills in a signed-in account's name and address, and emits sent with the reply address once the message is accepted.
  • <ContactReveal> is the reveal. standalone draws it as a full-width button, for a page with no form.

Render each one only when its channel is in starter.contact.channels, since the route behind a channel that is off answers 404. The copy is under contact.* in the locale files. To link to the page from your own code, use <SupportLink> or useContactPath(), both of which follow contact.enabled.

A link with a lock opens the kit’s private repository, which every licensee can read.