On aaPanel, Beget, and similar panels the simplest path is a contact.php next to the static files. Vite copies everything from public/ into dist/, so the endpoint lands at dist/api/contact.php after build.
Minimum contract
- POST + Accept: application/json
- Fields: name, email, message
- Hidden honeypot — bots fill it, humans do not
- Response { ok: true } or { ok: false, error: "..." }
Mail address stays on the server
The recipient lives in PHP (or in a .env outside the web root). It must not appear in HTML or JS — scrapers will harvest it immediately.
Deploy
rsync dist/ to the web root with excludes for .user.ini, .htaccess, and .env. If you keep a local config beside api/ on the server, exclude it from --delete or keep secrets outside dist.