Skip to content
All posts

1 min read

A contact form that posts to Telegram

The FlakeForge contact form sends each brief to a Telegram chat through a Server Action. Validation, spam checks, and escaping included.

  • next.js
  • telegram
  • forms

The contact form on this site delivers every brief to a Telegram chat. There is no database and no third-party form service: one Server Action and one call to the Bot API.

The Server Action

The form posts to a Server Action, which validates the fields with zod. It returns error codes, not sentences. The browser turns each code into a message in the reader's language, so the server code does not need translations at all.

const briefSchema = z.object({
  name: z.string().trim().min(1).max(120),
  email: z.email().max(200),
  telegram: z.string().trim().max(64).optional(),
  services: z.array(z.enum(SERVICE_IDS)),
  message: z.string().trim().min(1).max(4000),
})

Spam checks without a captcha

Two cheap checks deal with the obvious bots:

  • A hidden field that people never see. Bots tend to fill in every input, so a submission with that field set is dropped quietly.
  • A limit of five messages per IP address in ten minutes, kept in memory.

The in-memory counter resets when the server restarts and is not shared between replicas. For a single container that is fine. If the site ever runs on several replicas, the counter should move to Redis or a similar store.

Sending to Telegram

The Bot API's sendMessage accepts HTML. Everything the visitor typed is escaped before it goes into the message, so a name like <b> shows up as text instead of formatting:

const escapeTelegramHtml = (value: string) =>
  value.replace(/[&<>"]/g, char => HTML_ESCAPES[char] ?? char)

The bot token and chat ID come from environment variables. If they are missing, the form tells the visitor to write to our email address instead of failing without a word.