Для проверки работы Ваших проектов на наших VDS предлагаем Вам льготный период, оформите заказ на 3 дня.
Мини-приложение на Express, которое:
Работает на shared-хостинге: слушает порт из process.env.PORT, легко настраивается через панель ISPmanager.
contact-form-app/ |- package.json |- app.js |- .env # для локального запуска (на хостинге переменные в панели) |- public/ | |- style.css |- views/ |- index.html # форма |- thankyou.html # страница "Спасибо" |- error.html # страница ошибки (опционально)
{ "name": "contact-form-app", "version": "1.0.0", "description": "Contact form on Express + Nodemailer for shared hosting (ISPmanager).", "main": "app.js", "scripts": { "start": "node app.js", "start:prod": "NODE_ENV=production node app.js" }, "engines": { "node": ">=18.x" }, "dependencies": { "dotenv": "^16.4.5", "express": "^4.19.2", "express-rate-limit": "^7.4.0", "express-validator": "^7.2.0", "helmet": "^7.1.0", "morgan": "^1.10.0", "nodemailer": "^6.9.14" } }
// app.js require('dotenv').config(); // .env для локалки; в проде переменные задаст панель const express = require('express'); const path = require('path'); const helmet = require('helmet'); const morgan = require('morgan'); const rateLimit = require('express-rate-limit'); const { body, validationResult } = require('express-validator'); const nodemailer = require('nodemailer'); const app = express(); const PORT = process.env.PORT || 3000; // Безопасность, логи, парсинг форм app.use(helmet()); app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev')); app.use(express.urlencoded({ extended: true })); // формы application/x-www-form-urlencoded app.use(express.json()); // если будете слать JSON app.use(express.static(path.join(__dirname, 'public'))); // Лимит запросов на /contact (anti-spam) const contactLimiter = rateLimit({ windowMs: 60 * 1000, // 1 минута max: 5, // не более 5 запросов/минуту с одного IP standardHeaders: true, legacyHeaders: false, }); // Health-check app.get('/health', (req, res) => res.json({ status: 'ok', uptime: process.uptime() })); // Главная (форма) app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'views', 'index.html')); }); // Спасибо app.get('/thankyou', (req, res) => { res.sendFile(path.join(__dirname, 'views', 'thankyou.html')); }); // Ошибка (опционально) app.get('/error', (req, res) => { res.sendFile(path.join(__dirname, 'views', 'error.html')); }); // Настраиваем Nodemailer SMTP-транспорт // Значения возьмём из окружения, чтобы не хранить пароли в коде. function createTransport() { const { SMTP_HOST, SMTP_PORT, SMTP_SECURE, // "true"/"false" SMTP_USER, SMTP_PASS } = process.env; return nodemailer.createTransport({ host: SMTP_HOST, port: Number(SMTP_PORT || 587), secure: String(SMTP_SECURE || 'false') === 'true', auth: SMTP_USER && SMTP_PASS ? { user: SMTP_USER, pass: SMTP_PASS } : undefined }); } // POST /contact — обработчик формы app.post( '/contact', contactLimiter, // антибот "honeypot" — скрытое поле, реальный пользователь его не заполнит (req, res, next) => { if (req.body.website) { return res.status(200).redirect('/thankyou'); // тихо игнорируем ботов } next(); }, // Валидация body('name').trim().isLength({ min: 2, max: 100 }).withMessage('Enter your name'), body('email').isEmail().normalizeEmail().withMessage('Enter a valid email'), body('message').trim().isLength({ min: 5, max: 5000 }).withMessage('Message is too short'), async (req, res) => { const errors = validationResult(req); const { name, email, message, phone = '' } = req.body; if (!errors.isEmpty()) { // Вариант 1: редирект на /error // return res.status(400).redirect('/error'); // Вариант 2: показать простую HTML-ошибку const msg = errors.array().map(e => e.msg).join(', '); return res.status(400).send(`Form error${msg}Back`); } try { const transporter = createTransport(); const mailFrom = process.env.MAIL_FROM || process.env.SMTP_USER || 'no-reply@example.com'; const mailTo = process.env.MAIL_TO || 'admin@example.com'; await transporter.sendMail({ from: `"Website Contact" <${mailFrom}>`, to: mailTo, subject: `New contact form message from ${name}`, replyTo: email, text: [ `Name: ${name}`, `Email: ${email}`, phone ? `Phone: ${phone}` : '', '', 'Message:', message ].join('\n'), html: ` New contact form submission Name: ${escapeHtml(name)} Email: ${escapeHtml(email)} ${phone ? `Phone: ${escapeHtml(phone)}` : ''} Message: ${escapeHtml(message).replace(/\n/g, '')} ` }); return res.redirect('/thankyou'); } catch (err) { console.error('Mail send error:', err); return res.status(500).redirect('/error'); } } ); // 404 app.use((req, res) => { res.status(404).send('404 Not Found'); }); // 500 app.use((err, req, res, next) => { console.error('Unhandled error:', err); res.status(500).send('500 Internal Server Error'); }); app.listen(PORT, () => { console.log(`Contact form app is running on port ${PORT}`); }); // Простой экранировщик для html function escapeHtml(str = '') { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); }
${msg}
Back
Name: ${escapeHtml(name)}
Email: ${escapeHtml(email)}
Phone: ${escapeHtml(phone)}
Message:
${escapeHtml(message).replace(/\n/g, '')}
<!doctype html> <html lang="ru"> <head> <meta charset="utf-8" /> <title>Связаться с нами</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link href="/style.css" rel="stylesheet" /> </head> <body> <main class="card"> <h1>Свяжитесь с нами</h1> <p>Заполните форму, и мы ответим в ближайшее время.</p> <form method="post" action="/contact" novalidate> <!-- Honeypot (скрытое поле для ботов) --> <div class="hidden"> <label>Website <input type="text" name="website" autocomplete="off"></label> </div> <label>Имя <input type="text" name="name" required minlength="2" maxlength="100" placeholder="Ваше имя" /> </label> <label>Email <input type="email" name="email" required placeholder="name@example.com" /> </label> <label>Телефон (необязательно) <input type="tel" name="phone" placeholder="+380 ..." /> </label> <label>Сообщение <textarea name="message" required minlength="5" maxlength="5000" rows="6" placeholder="Коротко опишите вопрос"></textarea> </label> <button type="submit">Отправить</button> </form> </main> </body> </html>
<!doctype html< <html lang="ru"< <head< <meta charset="utf-8" /< <title<Спасибо!</title< <meta name="viewport" content="width=device-width, initial-scale=1" /< <link href="/style.css" rel="stylesheet" /< </head< <body< <main class="card"< <h1<Спасибо!</h1< <p<Ваше сообщение успешно отправлено. Мы свяжемся с вами в ближайшее время.</p< <p<<a href="/"<Вернуться на главную</a<</p< </main< </body< </html<
<!doctype html> <html lang="ru"> <head> <meta charset="utf-8" /> <title>Ошибка отправки</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link href="/style.css" rel="stylesheet" /> </head> <body> <main class="card"> <h1>Не удалось отправить сообщение</h1> <p>Пожалуйста, попробуйте ещё раз позже или свяжитесь с нами по телефону.</p> <p><a href="/">Вернуться к форме</a></p> </main> </body> </html>
:root { --bg: #0f172a; --card: #111827; --text: #e2e8f0; --muted: #94a3b8; --primary: #22c55e; --primary-hover: #16a34a; } * { box-sizing: border-box; } html, body { margin: 0; font: 16px/1.6 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; background: var(--bg); color: var(--text); } main.card { max-width: 720px; margin: 6vh auto; background: var(--card); border-radius: 16px; padding: 24px; box-shadow: 0 10px 30px rgba(0,0,0,.25); } h1 { margin-top: 0; } p { color: var(--muted); } form { display: grid; gap: 14px; margin-top: 16px; } label { display: grid; gap: 6px; font-weight: 600; } input, textarea { width: 100%; padding: 10px 12px; border: 1px solid #1f2937; border-radius: 10px; background: #0b1220; color: var(--text); } button { padding: 12px 16px; border: 0; border-radius: 10px; background: var(--primary); color: #052e16; font-weight: 700; cursor: pointer; } button:hover { background: var(--primary-hover); } .hidden { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
PORT=3000 NODE_ENV=development # SMTP (пример) SMTP_HOST=smtp.example.com SMTP_PORT=587 SMTP_SECURE=false SMTP_USER=no-reply@example.com SMTP_PASS=super-secret-password # От кого и кому отправлять MAIL_FROM=no-reply@example.com MAIL_TO=admin@example.com
Важно: на продакшене (ISPmanager) задайте эти переменные в панели, а не храните в файлах.