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
NUXT_CONTACT_ADDRESSfillscontactAddress, a private runtime-config key. Only the two routes below read it, at request time, so no build, page, payload or bundle carries it.modules/contact-routes.ts(Licensees only) registers one route per entry incontact.channels:POST /api/contactforformandPOST /api/contact/addressforreveal. A channel left out answers 404.- 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
deliverEmailwith the sender as Reply-To, once, while the request lasts: it is never queued or stored. The reveal answers{ address }withcache-control: no-store. - The kit's support links (
SupportLink, the footers and:contact-linein the legal pages) take their path fromuseContactPath(), which isnullwhilecontact.enabledis false, so they render nothing then.
Choose the channels
contact: {
enabled: true,
channels: ['form', 'reveal'],
},
channels | The 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
| What | Limit |
|---|---|
| Attempts per IP address | 5 in 10 minutes on each route, then locked for 10 minutes. IPv6 by /64. |
| Messages, site-wide | 30 an hour, counted once Turnstile passes |
| Name | 1 to 100 characters, on one line |
| Up to 254 characters | |
| Message | 10 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 emitssentwith the reply address once the message is accepted.<ContactReveal>is the reveal.standalonedraws 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.