На aaPanel, Beget и похожих панелях проще всего положить contact.php рядом со статикой. Vite копирует всё из public/ в dist/, поэтому файл оказывается в dist/api/contact.php после сборки.

Минимальный контракт

  • POST + Accept: application/json
  • Поля name, email, message
  • Скрытый honeypot — боты его заполняют, люди нет
  • Ответ { ok: true } или { ok: false, error: "..." }

Почта только на сервере

Адрес получателя живёт в PHP (или в .env вне веб-корня). В HTML и JS его быть не должно — иначе его сразу заберут сканеры.

Деплой

rsync dist/ на веб-корень с исключениями для .user.ini, .htaccess, .env. Если на сервере правите локальный конфиг рядом с api/ — исключите его из --delete или держите секреты вне dist.