We offer you a grace period for 3 days to use VDS to check your projects.
A small Express app that:
Shared-hosting ready: uses process.env.PORT and is simple to set up in ISPmanager.
contact-form-app/ |- package.json |- app.js |- .env # for local run (on hosting, set env vars via the panel) |- public/ | |- style.css |- views/ |- index.html # form |- thankyou.html # "Thank you" page |- error.html # error page (optional)
{ "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 for local development; in production the panel sets the environment variables 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; // Security, logging, and form data parsing app.use(helmet()); app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev')); app.use(express.urlencoded({ extended: true })); // application/x-www-form-urlencoded forms app.use(express.json()); // if sending JSON app.use(express.static(path.join(__dirname, 'public'))); // Rate limiting for /contact (anti-spam) const contactLimiter = rateLimit({ windowMs: 60 * 1000, // 1 minute max: 5, // up to 5 requests per minute from one IP standardHeaders: true, legacyHeaders: false, }); // Health-check app.get('/health', (req, res) => res.json({ status: 'ok', uptime: process.uptime() })); // Home page (form) app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'views', 'index.html')); }); // Thank you app.get('/thankyou', (req, res) => { res.sendFile(path.join(__dirname, 'views', 'thankyou.html')); }); // Error (optional) app.get('/error', (req, res) => { res.sendFile(path.join(__dirname, 'views', 'error.html')); }); // Configure Nodemailer SMTP transport // Read values from environment variables so credentials aren’t hard-coded. 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 — form submission handler app.post( '/contact', contactLimiter, // Anti-bot "honeypot": hidden field; real users won’t fill it (req, res, next) => { if (req.body.website) { return res.status(200).redirect('/thankyou'); // silently ignore bots } next(); }, // Validation 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()) { // Option 1: redirect to /error // return res.status(400).redirect('/error'); // Option 2: show a simple HTML error 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}`); }); // Simple HTML escaping helper 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="en"> <head> <meta charset="utf-8" /> <title>Contact</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link href="/style.css" rel="stylesheet" /> </head> <body> <main class="card"> <h1>Contact us</h1> <p>Please fill out the form and we’ll reply soon.</p> <form method="post" action="/contact" novalidate> <!-- Honeypot (hidden field for bots) --> <div class="hidden"> <label>Website <input type="text" name="website" autocomplete="off"></label> </div> <label>Name <input type="text" name="name" required minlength="2" maxlength="100" placeholder="Your name" /> </label> <label>Email <input type="email" name="email" required placeholder="name@example.com" /> </label> <label>Phone (optional) <input type="tel" name="phone" placeholder="+380 ..." /> </label> <label>Message <textarea name="message" required minlength="5" maxlength="5000" rows="6" placeholder="Briefly describe your issue"></textarea> </label> <button type="submit">Submit</button> </form> </main> </body> </html>
<!doctype html< <html lang="en"< <head< <meta charset="utf-8" /< <title<Thank you!</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<Your message has been sent. We’ll contact you soon.</p< <p<<a href="/"<Back to homepage</a<</p< </main< </body< </html<
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <title>Failed to send</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link href="/style.css" rel="stylesheet" /> </head> <body> <main class="card"> <h1>Your message could not be sent</h1> <p>Please try again later or contact us by phone.</p> <p><a href="/">Back to the form</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 (example) SMTP_HOST=smtp.example.com SMTP_PORT=587 SMTP_SECURE=false SMTP_USER=no-reply@example.com SMTP_PASS=super-secret-password # From/To MAIL_FROM=no-reply@example.com MAIL_TO=admin@example.com
Note: In production (ISPmanager), set these env vars in the panel, not in files.