A Node.js/Express web application for Specialised Steering (Pty) Ltd, a hydraulic component repair and sourcing business based in Germiston, Gauteng, South Africa. The application showcases hydraulic repair services, component sourcing, and service exchange programs for mining, agricultural, and automotive industries.
- 🏠 Dynamic Homepage - Professional hydraulic engineering services showcase with integrated contact form
- 🔧 Our Work Page - Detailed information about service exchange and OEM repair services, with links to photo gallery and social media (Instagram, LinkedIn)
- 📧 Contact Form - Customer inquiry form with spam protection and email notifications
- 🔍 Parts Enquiry - Specialised form for hydraulic component sourcing with image uploads
- 📸 Photo Gallery - Display of completed repair jobs powered by Google Photos Picker API and Cloudinary
- 🖼️ Image Optimisation - Automatic WebP conversion and quality optimisation via Cloudinary
- 📬 Email Notifications - Automatic email alerts for all form submissions with full details (✅ Production ready)
- 🧪 A/B Testing System - Cookie-based variant assignment with server-side and GA4 tracking
- 🔐 Admin Panel - Gallery management, A/B test reporting, and password reset functionality
- 🛡️ Security - reCAPTCHA v2, rate limiting, honeypot fields, spam detection, CSP headers, and comprehensive security logging
- 🚀 Performance - Response compression, static file caching, and optimised image delivery
- 🔗 Social Media Integration - Links to Instagram and LinkedIn for recent work updates
- Runtime: Node.js v20.19.0 (required)
- Framework: Express.js 4.18.3
- Template Engine: EJS 3.1.9
- Database: Airtable (form submissions, security logs)
- Data Storage: JSON files (gallery metadata, password reset tokens)
- Image Hosting: Cloudinary
- Email: Nodemailer (supports Gmail, Microsoft 365, SendGrid, any SMTP)
- Authentication: Google OAuth 2.0 (Google Photos Picker API), Basic Auth (admin panel)
- Security: reCAPTCHA v2, Content Security Policy, Rate Limiting, Spam Detection
- Analytics: Google Analytics 4 (GA4) with custom A/B testing events
- Node.js v20.19.0 (required - see Node.js Version Management)
- npm or yarn
- Airtable account and API key
- Cloudinary account
- Google reCAPTCHA v2 site key and secret
- (Optional) Google Cloud Platform credentials for Photos Picker API (for gallery feature)
- Clone the repository
git clone <repository-url>
cd specialised- Install dependencies
npm install(On servers where npm is not in PATH, use cPanel Node.js Setup → Run NPM Install, or run npm install in an environment where Node is available.)
- Configure environment variables
Create a .env file in the root directory with the following variables:
# Server Configuration
PORT=3300
# Airtable Configuration
AT_ENDPOINT=https://api.airtable.com
AT_API_KEY=your_airtable_api_key
BASE=your_airtable_base_id
# Cloudinary Configuration
CLOUD_NAME=your_cloudinary_cloud_name
API_KEY=your_cloudinary_api_key
API_SECRET=your_cloudinary_api_secret
# reCAPTCHA Configuration (v2 for forms)
reCAPTCHA_v2_SECRET_KEY=your_recaptcha_v2_secret_key
reCAPTCHA_v2_SITE_KEY=your_recaptcha_v2_site_key
# Email Configuration (for form notifications)
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_SECURE=false
EMAIL_USER=your-email@gmail.com
EMAIL_PASSWORD=your-app-specific-password
NOTIFICATION_EMAIL=email@example.comNote: Email configuration supports both modern (EMAIL_*) and legacy (SMTP_*) variable names for backward compatibility.
Additional environment variables for Google Photos Picker API and admin features:
# Google Photos Picker API OAuth Configuration
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_REDIRECT_URI=https://www.specialisedsteering.com/oauth2callback
# Admin Authentication (for gallery management and A/B reports)
ADMIN_USERNAME=admin
ADMIN_PASSWORD=your_secure_password
ADMIN_EMAIL=admin@example.com # For password reset emails
# Gallery Configuration
GALLERY_REPLACE_MODE=false # Set to true to replace all items, false to append
CLOUDINARY_FOLDER=gallery/google-photos # Optional: Cloudinary folder for gallery images
# Health Check Configuration (Optional)
HEALTHCHECK_EMAIL=email@example.com # For system health notifications
# Environment
NODE_ENV=production # Set to "production" for production deployments (affects cookie security)
# Google Analytics: optional comma-separated IPs to exclude (company/home); no gtag loaded for these
# ANALYTICS_EXCLUDE_IPS=1.2.3.4,5.6.7.8Alternatively, you can copy .env.example to .env and fill in your values.
4.Google Photos Picker API Setup (Optional)
If you want to enable the Google Photos gallery integration:
- Create a project in Google Cloud Platform
- Enable the Google Photos Picker API (not the Library API)
- Create OAuth 2.0 credentials (Desktop app type)
- Download credentials as
credentials.jsonand place in root directory - Run the OAuth setup:
node setup-google-picker-auth.js - Configure environment variables (see Environment Variables section)
- Access admin UI at
/admin/gallery(protected by basic auth)
Note: The Photos Picker API uses a different OAuth scope than the deprecated Library API:
- Scope:
https://www.googleapis.com/auth/photospicker.mediaitems.readonly
This project requires Node.js v20.19.0 (the server is configured for this version).
The project includes a prestart script that automatically validates the Node.js version before starting the server.
For nvm users (recommended):
-
Install Node.js v20.19.0:
nvm install 20.19.0 nvm use 20.19.0
-
The
.nvmrcfile will automatically use the correct version when youcdinto the project (if auto-switch is enabled) -
Verify version:
node --version # Should output: v20.19.0 -
Start the server:
npm start
If you see an error about Node.js version mismatch:
❌ Error: Node.js v20.19.0 required, but found vX.X.X
Run: nvm use 20Run nvm use 20 or nvm use 20.19.0 to switch to the correct version.
For other version managers:
- fnm:
fnm use(reads.nvmrc) - asdf:
asdf install nodejs 20.19.0 && asdf local nodejs 20.19.0 - n:
n 20.19.0
This project is developed using Cursor IDE. Agent rules are defined in .cursor/rules/ (.mdc files) and provide consistent guidance for AI-assisted editing—for example, checking the git reflog before merging into a parent branch, keeping client communications in the git-ignored MD/ folder, and updating the completed list in TODO.md for invoicing and reporting. These rules apply when working in Cursor and help keep contributions aligned with project conventions.
If you need to upgrade Node.js for local development:
-
Update version files:
- Update
.nvmrcwith the new version (e.g.,21.0.0) - Update
.node-versionwith the major version (e.g.,21) - Update
package.jsonprestartscript to check for the new version
- Update
-
Install the new version:
nvm install 21.0.0 nvm use 21.0.0
-
Rebuild native dependencies:
npm install # Or reinstall all dependencies: rm -rf node_modules package-lock.json npm install -
Test the application:
npm start node scripts/test-admin-routes.js
-
Commit the changes:
git add .nvmrc .node-version package.json package-lock.json git commit -m "chore: upgrade Node.js to v21.0.0"
-
Backup the current setup:
- Document current Node.js version:
node --version - Backup database files and configuration
- Document current Node.js version:
-
On the server, install the new Node.js version:
For servers using nvm:
ssh user@server cd /path/to/application nvm install 21.0.0 nvm use 21.0.0For servers using system Node.js (via package manager):
- Update system Node.js using your server's package manager
- Or install nvm on the server for better version management
-
Reinstall dependencies (if needed):
cd /path/to/application # Or if you want a clean rebuild: rm -rf node_modules npm ci # Uses package-lock.json for reproducible builds
-
Verify the installation:
node --version # Should match the new version -
Restart the application:
- For Passenger:
touch tmp/restart.txt - For PM2:
pm2 restart app - For systemd:
systemctl restart your-service - Or restart your process manager
- For Passenger:
-
Monitor for errors:
- Check application logs
- Verify database operations work correctly
- Test critical functionality
-
Rollback plan (if needed):
# Switch back to previous version nvm use 20.19.0 npm install # Restart application
The server is configured for Node.js v20.19.0. The prestart script validates the version before starting to ensure consistency between development and production environments.
For more details, see NODE_VERSION.md.
Start the development server:
npm startThe application will be available at http://localhost:3300 (or your configured PORT).
Note: The server will automatically check that you're using Node.js v20.19.0 before starting. If you get a version error, see Node.js Version Management below.
For production deployment, ensure:
- Node.js v20.19.0 is installed on the server (see Node.js Version Management)
- All environment variables are properly configured
- Static assets are served with proper caching headers
- Trust proxy is enabled if behind a reverse proxy
- SSL/TLS is configured at the web server level
- Dependencies are installed:
npm installornpm ci
specialised/
├── app.js # Main application entry point
├── googleapi.js # Google OAuth utilities
├── package.json # Dependencies and metadata
├── middleware/
│ └── error-handler.js # Error handling middleware
├── routes/
│ ├── default.js # Main routes (home, contact, enquiry)
│ └── dynamic.js # Dynamic pages (about, gallery, sitemap)
├── utils/
│ ├── airtable.js # Airtable configuration
│ ├── cloudinary.js # Cloudinary configuration
│ ├── google-photos.js # Google Photos API utilities
│ └── multer.js # File upload configuration
├── views/ # EJS templates
│ ├── *.ejs # Page templates
│ └── includes/ # Reusable components
├── public/ # Static assets
│ ├── css/ # Stylesheets
│ ├── js/ # Client-side JavaScript
│ ├── images/ # Image assets
│ ├── fonts/ # Web fonts
│ └── uploads/ # Temporary file uploads
└── images/ # Source imagesThe application uses two main Airtable tables:
Stores form submissions from contact and enquiry forms.
name,email,company,phone,message- Basic contact infostatus(Single Select) - Processing statusform(Single Select) - Form type (contact/enquiry)ip(Text) - Submitter IP addressimageUploads(Attachment) - Uploaded imagesbrand,type,partNo,partDesc,serialNo- Part information (enquiry form)street,town,postal,region,country- Address (enquiry form)
Automatic Email Notifications: When a form is submitted, an email notification is automatically sent to the configured email address with all submission details.
Stores security events for monitoring and analysis.
timestamp(Date/Time) - When the event occurredeventType(Single Select) - Type of security event (reCAPTCHA failure, spam attempt, CSP violation, rate limit)ip(Text) - IP address of the requestuserAgent(Long Text) - Browser user agentformType(Single Select) - Form type if applicable (contact/enquiry)details(Long Text) - Additional event detailsreferrer(Text) - HTTP referrer
Images are automatically optimised and served via Cloudinary CDN with:
- WebP format conversion
- Quality optimisation (
q_auto:good) - Organised folder structure:
Specialised/public/uploads/{customerName}/ - Gallery images:
gallery/google-photos/ - Streaming uploads from URLs (for Google Photos integration)
- Google Analytics 4 (G-V4W8VP4GL8) - Website traffic analytics and A/B testing event tracking
- reCAPTCHA v2 - Server-side spam protection on forms
- Google Photos Picker API - Photo selection for gallery (replaces deprecated Library API)
- Google OAuth 2.0 - Authentication for Google Photos Picker API
- Scope:
https://www.googleapis.com/auth/photospicker.mediaitems.readonly - Token stored in
token.jsonwith automatic refresh
- Scope:
- SMTP Support - Works with Gmail, Microsoft 365, SendGrid, or any SMTP server
- Email Types:
- Contact form notifications
- Parts enquiry notifications
- Password reset emails
- System health check notifications
- Features:
- HTML and plain text formats
- Timezone-aware timestamps (Africa/Johannesburg)
- Non-blocking async operation (form submissions succeed even if email fails)
- TLS configuration support
- Custom TLS servername for certificate mismatches
- Configuration: See environment variables section below
The application includes a comprehensive A/B testing system for optimising meta descriptions and content:
- Cookie-based Variant Assignment - 90-day persistence for consistent user experience
- Server-Side Logging - File-based logging to
logs/ab-tests.logfor detailed analysis - GA4 Event Tracking - Client-side tracking of exposures and conversions
- Reporting Tools - CLI tool for generating statistical reports
- Multiple Test Support - Configurable test registry with traffic split control
- Near-me Meta Description Test (
near_me_meta)- Routes:
/,/our-work,/about,/contact - Variants: A (control), B (near-me optimised)
- Traffic Split: 50/50
- Start Date: January 26, 2026
- Routes:
View A/B Test Report:
# All tests, last 7 days
node scripts/ab-test-report.js
# Specific test, last 30 days
node scripts/ab-test-report.js near_me_meta 30Access A/B Report API:
- Navigate to
/admin/ab-report(requires basic auth) - Returns JSON with exposure and conversion stats
For detailed documentation, see:
The admin panel provides management tools for gallery and testing:
- Access Admin UI: Navigate to
/admin/gallery(requires basic auth) - Update Gallery:
- Click "Update Gallery from Google Photos"
- Select photos in the Google Photos Picker (search for album name)
- Choose replace mode (replace all) or append mode (add to existing)
- Photos are automatically uploaded to Cloudinary and stored in JSON file (
data/gallery.json)
Note: The Google Photos Picker API has a 30-second timeout. You must complete photo selection within this time limit (this is a Google API limitation, not an application limitation).
- View exposure and conversion statistics
- JSON API endpoint for integration with dashboards
- Configurable time range (default: last 30 days)
- Token-based password reset system
- Email notifications with reset links
- 1-hour token expiration
- Automatic server restart after password change (Passenger)
Use the provided script to set admin credentials:
node scripts/set-admin-credentials.jsOr set ADMIN_USERNAME and ADMIN_PASSWORD environment variables manually.
Password Reset:
- Navigate to
/auth/forgot-passwordto request a password reset - Email will be sent with a reset link valid for 1 hour
- No basic auth required for password reset flow
- Content Security Policy - Report-only mode with violation reporting to Airtable
- reCAPTCHA v2 - Server-side verification for all form submissions
- Rate Limiting - 5 form submissions per 15 minutes per IP
- Honeypot Fields - Hidden form fields to catch bots
- Time-to-Submit Validation - Minimum 3 seconds before form can be submitted
- Spam Detection - Keyword filtering, suspicious email domain detection
- Security Logging - All security events logged to Airtable (reCAPTCHA failures, spam attempts, CSP violations)
- File Upload Validation - Restricted to image files only (10MB max) with MIME type validation
- Request Size Limits - 10MB maximum payload
- WordPress Parameter Blocking - Returns 410 Gone for WordPress/Elementor artifacts
- Trust Proxy - Accurate IP detection behind reverse proxies
- Cookie Security - httpOnly, secure (production), sameSite: lax
- Admin Security - Basic Auth for admin routes, token-based password reset
The project tracks known security vulnerabilities and their remediation status. See audit-report-comparison.md for:
- Historical vulnerability fixes (28 vulnerabilities resolved)
- Current Snyk scan results (5 new issues identified)
- Risk assessments and impact analysis
- Upgrade recommendations
- npm audit vs Snyk comparison
npm audit: ✅ 0 vulnerabilities
Snyk scan:
Last Scan: 26 February 2026
This is a private client project. For any issues or enhancement requests, please contact the maintainers.
- Maintainers: Sue Holder, Design Develop Host
- Version: 1.0.1
- License: ISC
- A/B Testing System - Complete infrastructure with server-side and GA4 tracking (January 2026)
- Security Logging - Comprehensive security event logging to Airtable (January 2026)
- Password Reset System - Token-based admin password reset with email notifications (January 2026)
- Google Photos Picker API - Migration from deprecated Library API to Photos Picker API (December 2025)
- Email Notifications - Multi-provider SMTP support with HTML/text formats (December 2025)
- Rate Limiting - Form submission rate limiting per IP (December 2025)
- Spam Detection - Enhanced spam detection with keyword and email domain filtering (December 2025)
- Implement admin dashboard for managing Airtable records
- Enhance IP blacklist with database storage
- Multi-armed bandit for A/B testing (automatic traffic adjustment to winning variant)
- Real-time A/B testing dashboard with live stats
- Enforce CSP (currently report-only mode)
- Implement comprehensive application logging system
- Add user segmentation for A/B tests (mobile vs. desktop, new vs. returning)
ISC License - Copyright (c) Design Develop Host
For more detailed technical information, see PROJECT_OVERVIEW.md.