# HK Logistics — website

Truck transportation and logistics website for **HK Logistics**, Mumbai. Built with Node.js, Express 5 and EJS (server-rendered), plus a small amount of progressive-enhancement JavaScript. The site works fully without JavaScript.

> **Before launch:** fill in the business details still marked `null` in [`src/config/site.js`](src/config/site.js): legal name and GSTIN (phone, WhatsApp, email, address and office hours are confirmed). Until then, development mode shows yellow **"to be confirmed"** placeholders and production mode hides those elements. See [docs/QA-REPORT.md](docs/QA-REPORT.md) for the full list of open items.

## Quick start (local)

```bash
npm install
cp .env.example .env        # then edit .env (see "Configuration")
npm run dev                 # http://localhost:3000, restarts on file changes
```

For local development without an SMTP account, set `MAIL_TRANSPORT=json` in `.env`. Emails are then built but not sent. This setting is ignored when `NODE_ENV=production`.

## Scripts

| Command | What it does |
|---|---|
| `npm start` | Start the server (`server.js`) |
| `npm run dev` | Start with auto-restart (`node --watch`) |
| `npm test` | 33 automated tests: pages, SEO, security/VAPT, forms, real SMTP delivery, rate limiting, production mode |
| `npm run build` | Minify CSS/JS into `*.min.css` / `*.min.js` (served automatically in production) |
| `npm run build:images` | Regenerate responsive AVIF/WebP/JPEG images, logo variants, favicons and the OG image from `assets-src/` |
| `npm run build:seo` | Write static `robots.txt` and `sitemap.xml` to the project root for `APP_URL` |
| `npm run qa:browser` | Browser QA in installed Chrome, Edge and Firefox at 8 viewport sizes (site must be running) |
| `node scripts/qa-journey.js` | Customer-journey QA (quote form, keyboard and mobile menu) in a real browser |
| `npm run audit` | `npm audit` of production dependencies |

## Configuration (`.env`)

All secrets come from environment variables. Nothing secret is in the code. See [`.env.example`](.env.example) for every option.

| Variable | Required | Notes |
|---|---|---|
| `NODE_ENV` | yes | `production` on the live server |
| `APP_URL` | yes | Public origin, e.g. `https://www.hklogistics.co.in`. Used for canonical URLs, sitemap and OG tags |
| `APP_SECRET` | yes (prod) | ≥ 32 random characters. Generate with `node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"` |
| `TRUST_PROXY` | yes | Number of reverse proxies in front of Node (Nginx = `1`). **Must be correct** for rate limiting to work |
| `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURE`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_FROM` | yes | Authenticated SMTP. Port 587 uses STARTTLS (enforced). Port 465 uses `SMTP_SECURE=true`. **Gmail:** `smtp.gmail.com`, port 465, an App Password (needs 2-Step Verification), and `SMTP_FROM` set to that Gmail address |
| `CONTACT_EMAIL` | yes | Inbox(es) that receive quote requests and enquiries. Comma-separate several addresses |
| `SEND_ACKNOWLEDGEMENT` | optional | Default `true`: the visitor gets an automatic thank-you email with their reference number. It is a fixed template and never repeats their message |
| `DB_*` | optional | MySQL. When set, every enquiry is also stored, so a lead survives an email outage |
| `ENFORCE_CANONICAL_HOST` | optional | Default `true` in production: 301-redirects other host names to `APP_URL` |

## Project structure

```text
server.js                 Starts/stops the HTTP server only
src/app.js                Express app wiring (middleware order, static files, routes)
src/config/               index.js (env), site.js (business details/NAP), mail.js (Nodemailer), database.js (MySQL)
src/routes/               webRoutes.js (pages + forms), apiRoutes.js (/api/health)
src/controllers/          pageController.js, enquiryController.js
src/services/             enquiryService (store + email), mailService, seoService (meta/sitemap/robots), schemaService (JSON-LD)
src/middleware/           security (Helmet/CSP), csrf, rateLimiters, redirects, locals, errorHandlers
src/validators/           enquiryValidator.js (express-validator rules)
src/data/                 services, content, pages (SEO metadata + 301 map), image credits/manifest
views/layouts|partials|components|pages   EJS templates
public/                   css, js, images, icons (SVG sprite), fonts (self-hosted)
assets-src/               Original images and logo (input for npm run build:images; not served)
scripts/                  Build and browser-QA scripts
tests/                    node:test suites
docs/                     SEO keyword map, QA report
```

## Deployment (Linux VPS with Nginx — recommended)

1. **Server:** Node.js 20 LTS or newer, Nginx, and a TLS certificate (e.g. Let's Encrypt via certbot).
2. **Code:**
   ```bash
   git clone <repo> /var/www/hk-logistics && cd /var/www/hk-logistics
   npm ci --omit=dev
   npm run build            # needs devDependencies: run it in CI or before `npm ci --omit=dev`, or commit the .min files
   cp .env.example .env && nano .env   # NODE_ENV=production, APP_URL, APP_SECRET, TRUST_PROXY=1, SMTP_*, CONTACT_EMAIL
   chmod 600 .env
   ```
3. **Process manager** (systemd example `/etc/systemd/system/hk-logistics.service`):
   ```ini
   [Unit]
   Description=HK Logistics website
   After=network.target

   [Service]
   WorkingDirectory=/var/www/hk-logistics
   ExecStart=/usr/bin/node server.js
   Restart=always
   User=www-data
   Environment=NODE_ENV=production
   NoNewPrivileges=true
   ProtectSystem=full

   [Install]
   WantedBy=multi-user.target
   ```
   `sudo systemctl enable --now hk-logistics`
4. **Nginx** (TLS termination, HTTP→HTTPS redirect, proxy to Node):
   ```nginx
   server { listen 80; server_name hklogistics.co.in www.hklogistics.co.in; return 301 https://www.hklogistics.co.in$request_uri; }
   server {
     listen 443 ssl http2; server_name hklogistics.co.in;
     ssl_certificate /etc/letsencrypt/live/hklogistics.co.in/fullchain.pem;
     ssl_certificate_key /etc/letsencrypt/live/hklogistics.co.in/privkey.pem;
     return 301 https://www.hklogistics.co.in$request_uri;
   }
   server {
     listen 443 ssl http2; server_name www.hklogistics.co.in;
     ssl_certificate /etc/letsencrypt/live/hklogistics.co.in/fullchain.pem;
     ssl_certificate_key /etc/letsencrypt/live/hklogistics.co.in/privkey.pem;
     server_tokens off;
     client_max_body_size 64k;
     location / {
       proxy_pass http://127.0.0.1:3000;
       proxy_set_header Host $host;
       proxy_set_header X-Forwarded-For $remote_addr;   # overwrite, don't append (prevents spoofing)
       proxy_set_header X-Forwarded-Proto $scheme;
     }
   }
   ```
   Replace the domain with the real one. Bind Node to localhost only, or firewall port 3000.
5. **Verify after deploy:**
   - `curl -I https://www.<domain>/` shows HSTS, CSP, X-Frame-Options and no `X-Powered-By`.
   - Submit a real quote request and confirm it arrives in `CONTACT_EMAIL`.
   - The server logs include `mail.ready` (SMTP verified at startup).
   - Submit `https://www.<domain>/sitemap.xml` in Google Search Console.

### Shared hosting (cPanel "Setup Node.js App")

Set the application root to this folder and the startup file to `server.js`, and add the `.env` variables in the panel. Set `TRUST_PROXY=1`. Run `npm run build` locally and upload the `.min` files.

### Optional MySQL

Create a database and a user with `CREATE`, `INSERT`, `UPDATE` and `SELECT` on it, then set the `DB_*` variables. The `enquiries` table is created automatically at startup. All queries are parameterised.

## Updating content

- **Business details / NAP:** `src/config/site.js` is the single source for the header, footer, contact page and schema.
- **Services:** `src/data/services.js` (one object per service; adding one creates its page, nav entry, sitemap entry and schema).
- **Page titles / descriptions / redirects:** `src/data/pages.js`
- **FAQs, process, industries, fleet:** `src/data/content.js`
- **Images:** add the original to `assets-src/images/`, run `npm run build:images`, and record its licence in `src/data/imageCredits.json`.

## Security summary

Helmet with a strict CSP (no inline scripts or styles; the only third-party origin is a Google Maps frame), HSTS in production, X-Frame-Options DENY, nosniff, Referrer-Policy and Permissions-Policy. Also: signed double-submit CSRF tokens with an Origin check, express-validator allow-listing, CR/LF rejection (email header injection), output escaping in EJS and email HTML, parameterised SQL, a honeypot plus minimum fill-time, per-IP rate limits, a 20 KB body limit, GET/HEAD/POST only, no uploads, static files restricted to `/public` (dotfiles denied, no directory listings), generic error pages and no stack traces. Details and test evidence are in [docs/QA-REPORT.md](docs/QA-REPORT.md).
