Convert to template with dynamic configuration
Add comprehensive environment-based configuration system: - Created config.js module to centralize all environment variables - Updated server.js to use config module for all settings - Added /api/config endpoint to expose company info to frontend - Created init.js to dynamically inject config into HTML pages - Updated .env.example with comprehensive configuration options - Added data attributes to index.html for dynamic content - Updated Dockerfile to include config.js This allows users to customize: - Company name, tagline, and description - Branding colors (primary, accent, dark) - Contact information (email, phone, address, hours) - Social media links - About page content - Services offered 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
110
.env.example
110
.env.example
@@ -1,16 +1,110 @@
|
||||
# ===================================
|
||||
# AI Recruitment Site - Configuration
|
||||
# ===================================
|
||||
# Copy this file to .env and customize for your company
|
||||
|
||||
# ===================================
|
||||
# Company Information
|
||||
# ===================================
|
||||
COMPANY_NAME=Your Recruitment Firm
|
||||
COMPANY_TAGLINE=Finding the Perfect Match for Your Career
|
||||
COMPANY_DESCRIPTION=We specialize in connecting talented professionals with exceptional opportunities across various industries.
|
||||
|
||||
# ===================================
|
||||
# Company Branding (Colors)
|
||||
# ===================================
|
||||
# Primary brand color (e.g., #2563EB for blue)
|
||||
PRIMARY_COLOR=#2563EB
|
||||
# Accent/success color (e.g., #059669 for green)
|
||||
ACCENT_COLOR=#059669
|
||||
# Dark/secondary color (e.g., #1E293B)
|
||||
DARK_COLOR=#1E293B
|
||||
|
||||
# ===================================
|
||||
# Contact Information
|
||||
# ===================================
|
||||
CONTACT_EMAIL=info@yourcompany.com
|
||||
CONTACT_PHONE=+1 (555) 123-4567
|
||||
CONTACT_ADDRESS=123 Business St, Suite 100, City, State 12345
|
||||
|
||||
# Social Media Links (leave empty to hide)
|
||||
SOCIAL_LINKEDIN=https://linkedin.com/company/yourcompany
|
||||
SOCIAL_TWITTER=https://twitter.com/yourcompany
|
||||
SOCIAL_FACEBOOK=
|
||||
|
||||
# ===================================
|
||||
# Deployment Configuration
|
||||
# ===================================
|
||||
# Your custom subdomain (e.g., 'yourname' becomes yourname.recruitai.startanaicompany.com)
|
||||
SUBDOMAIN=yourname
|
||||
|
||||
# Your Gitea username and repository name
|
||||
GITEA_USERNAME=your-gitea-username
|
||||
GITEA_REPO_NAME=ai-recruit-site-template
|
||||
|
||||
# ===================================
|
||||
# Application Settings
|
||||
# ===================================
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
|
||||
# Session secret (will be auto-generated if empty)
|
||||
SESSION_SECRET=
|
||||
|
||||
# ===================================
|
||||
# Database Configuration
|
||||
# ===================================
|
||||
# PostgreSQL connection details
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
DB_NAME=recruitment
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=changeme123
|
||||
# Database password (will be auto-generated if empty)
|
||||
DB_PASSWORD=
|
||||
|
||||
# Application Configuration
|
||||
PORT=3000
|
||||
NODE_ENV=production
|
||||
# ===================================
|
||||
# API Tokens (for deployment script)
|
||||
# ===================================
|
||||
# These should be set as environment variables, NOT in this file
|
||||
# COOLIFY_API_TOKEN=your-coolify-api-token
|
||||
# GITEA_API_TOKEN=your-gitea-api-token
|
||||
|
||||
# Session Secret (CHANGE THIS IN PRODUCTION!)
|
||||
SESSION_SECRET=your-very-secret-session-key-change-this-in-production
|
||||
# ===================================
|
||||
# Feature Configuration
|
||||
# ===================================
|
||||
# Maximum CV file size in MB
|
||||
MAX_CV_SIZE_MB=5
|
||||
|
||||
# Optional: Application URL
|
||||
APP_URL=http://localhost:3000
|
||||
# Allowed CV file types (comma-separated)
|
||||
ALLOWED_CV_TYPES=application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||
|
||||
# ===================================
|
||||
# About Page Content
|
||||
# ===================================
|
||||
ABOUT_MISSION=Our mission is to bridge the gap between exceptional talent and outstanding opportunities.
|
||||
ABOUT_VISION=We envision a world where every professional finds their perfect career match.
|
||||
ABOUT_VALUES=Integrity, Excellence, Innovation, Partnership
|
||||
|
||||
# ===================================
|
||||
# Services Offered (comma-separated)
|
||||
# ===================================
|
||||
SERVICES_LIST=Executive Search,Contract Staffing,Permanent Placement,Career Consulting,Talent Assessment,Industry Expertise
|
||||
|
||||
# ===================================
|
||||
# Contact Page Settings
|
||||
# ===================================
|
||||
# Email address where contact form submissions are sent
|
||||
CONTACT_FORM_RECIPIENT=info@yourcompany.com
|
||||
|
||||
# Business hours
|
||||
BUSINESS_HOURS=Monday - Friday: 9:00 AM - 6:00 PM
|
||||
|
||||
# ===================================
|
||||
# Email Configuration (Optional)
|
||||
# ===================================
|
||||
# If you want to send email notifications
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM=noreply@yourcompany.com
|
||||
|
||||
@@ -10,6 +10,7 @@ RUN npm install --production
|
||||
|
||||
# Copy application code
|
||||
COPY server.js ./
|
||||
COPY config.js ./
|
||||
COPY migrations ./migrations
|
||||
|
||||
# Copy public directory explicitly
|
||||
|
||||
96
config.js
Normal file
96
config.js
Normal file
@@ -0,0 +1,96 @@
|
||||
// Configuration module for AI Recruitment Site Template
|
||||
// Centralizes all environment variable loading with sensible defaults
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
// Helper function to generate secure random strings
|
||||
function generateSecret(length = 64) {
|
||||
return crypto.randomBytes(length).toString('hex');
|
||||
}
|
||||
|
||||
// Company Information
|
||||
const company = {
|
||||
name: process.env.COMPANY_NAME || 'Your Recruitment Firm',
|
||||
tagline: process.env.COMPANY_TAGLINE || 'Finding the Perfect Match for Your Career',
|
||||
description: process.env.COMPANY_DESCRIPTION || 'We specialize in connecting talented professionals with exceptional opportunities across various industries.'
|
||||
};
|
||||
|
||||
// Branding Colors
|
||||
const branding = {
|
||||
primaryColor: process.env.PRIMARY_COLOR || '#2563EB',
|
||||
accentColor: process.env.ACCENT_COLOR || '#059669',
|
||||
darkColor: process.env.DARK_COLOR || '#1E293B'
|
||||
};
|
||||
|
||||
// Contact Information
|
||||
const contact = {
|
||||
email: process.env.CONTACT_EMAIL || 'info@yourcompany.com',
|
||||
phone: process.env.CONTACT_PHONE || '+1 (555) 123-4567',
|
||||
address: process.env.CONTACT_ADDRESS || '123 Business St, Suite 100, City, State 12345',
|
||||
businessHours: process.env.BUSINESS_HOURS || 'Monday - Friday: 9:00 AM - 6:00 PM'
|
||||
};
|
||||
|
||||
// Social Media Links
|
||||
const social = {
|
||||
linkedin: process.env.SOCIAL_LINKEDIN || '',
|
||||
twitter: process.env.SOCIAL_TWITTER || '',
|
||||
facebook: process.env.SOCIAL_FACEBOOK || ''
|
||||
};
|
||||
|
||||
// Application Settings
|
||||
const app = {
|
||||
nodeEnv: process.env.NODE_ENV || 'development',
|
||||
port: parseInt(process.env.PORT || '3000', 10),
|
||||
sessionSecret: process.env.SESSION_SECRET || generateSecret()
|
||||
};
|
||||
|
||||
// Database Configuration
|
||||
const database = {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5432', 10),
|
||||
name: process.env.DB_NAME || 'recruitment',
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'changeme123'
|
||||
};
|
||||
|
||||
// Feature Configuration
|
||||
const features = {
|
||||
maxCvSizeMB: parseInt(process.env.MAX_CV_SIZE_MB || '5', 10),
|
||||
allowedCvTypes: (process.env.ALLOWED_CV_TYPES || 'application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document').split(',')
|
||||
};
|
||||
|
||||
// About Page Content
|
||||
const about = {
|
||||
mission: process.env.ABOUT_MISSION || 'Our mission is to bridge the gap between exceptional talent and outstanding opportunities.',
|
||||
vision: process.env.ABOUT_VISION || 'We envision a world where every professional finds their perfect career match.',
|
||||
values: process.env.ABOUT_VALUES || 'Integrity, Excellence, Innovation, Partnership'
|
||||
};
|
||||
|
||||
// Services
|
||||
const services = {
|
||||
list: (process.env.SERVICES_LIST || 'Executive Search,Contract Staffing,Permanent Placement,Career Consulting,Talent Assessment,Industry Expertise').split(',')
|
||||
};
|
||||
|
||||
// Email Configuration
|
||||
const email = {
|
||||
smtpHost: process.env.SMTP_HOST || '',
|
||||
smtpPort: parseInt(process.env.SMTP_PORT || '587', 10),
|
||||
smtpUser: process.env.SMTP_USER || '',
|
||||
smtpPassword: process.env.SMTP_PASSWORD || '',
|
||||
smtpFrom: process.env.SMTP_FROM || 'noreply@yourcompany.com',
|
||||
contactFormRecipient: process.env.CONTACT_FORM_RECIPIENT || contact.email
|
||||
};
|
||||
|
||||
// Export configuration object
|
||||
module.exports = {
|
||||
company,
|
||||
branding,
|
||||
contact,
|
||||
social,
|
||||
app,
|
||||
database,
|
||||
features,
|
||||
about,
|
||||
services,
|
||||
email
|
||||
};
|
||||
@@ -5,11 +5,12 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ryans Recruit Firm - Find Your Dream Job</title>
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
<script src="/js/init.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="container">
|
||||
<div class="logo">Ryans Recruit Firm</div>
|
||||
<div class="logo" data-company-name>Ryans Recruit Firm</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="/" class="active">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
@@ -23,8 +24,8 @@
|
||||
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h1>Your Career Success Is Our Mission</h1>
|
||||
<p>Connecting talented professionals with leading companies worldwide</p>
|
||||
<h1 data-company-tagline>Your Career Success Is Our Mission</h1>
|
||||
<p data-company-description>Connecting talented professionals with leading companies worldwide</p>
|
||||
<div style="display: flex; gap: 1rem; justify-content: center; margin-top: 2rem;">
|
||||
<a href="/jobs" class="btn btn-primary btn-lg">Browse Jobs</a>
|
||||
<a href="/contact" class="btn btn-secondary btn-lg">Get in Touch</a>
|
||||
@@ -35,7 +36,7 @@
|
||||
<section>
|
||||
<div class="container">
|
||||
<div class="section-title">
|
||||
<h2>Why Choose Ryans Recruit Firm?</h2>
|
||||
<h2>Why Choose <span data-company-name>Ryans Recruit Firm</span>?</h2>
|
||||
<p>We're committed to finding the perfect match for both candidates and employers</p>
|
||||
</div>
|
||||
|
||||
@@ -198,8 +199,8 @@
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<h3>Ryans Recruit Firm</h3>
|
||||
<p style="color: rgba(255, 255, 255, 0.8);">
|
||||
<h3 data-company-name>Ryans Recruit Firm</h3>
|
||||
<p style="color: rgba(255, 255, 255, 0.8);" data-company-description>
|
||||
Your trusted partner in career advancement and talent acquisition.
|
||||
</p>
|
||||
</div>
|
||||
@@ -227,9 +228,9 @@
|
||||
<div class="footer-section">
|
||||
<h3>Contact</h3>
|
||||
<ul>
|
||||
<li>Email: info@ryansrecruit.com</li>
|
||||
<li>Phone: +1 (555) 123-4567</li>
|
||||
<li>Hours: Mon-Fri 9AM-6PM EST</li>
|
||||
<li>Email: <a href="mailto:info@ryansrecruit.com" data-contact-email>info@ryansrecruit.com</a></li>
|
||||
<li>Phone: <a href="tel:+15551234567" data-contact-phone>+1 (555) 123-4567</a></li>
|
||||
<li>Hours: <span data-business-hours>Mon-Fri 9AM-6PM EST</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
135
public/js/init.js
Normal file
135
public/js/init.js
Normal file
@@ -0,0 +1,135 @@
|
||||
// Initialize company configuration
|
||||
// This file loads company configuration from the API and updates the page
|
||||
|
||||
let siteConfig = {};
|
||||
|
||||
async function loadSiteConfig() {
|
||||
try {
|
||||
const response = await fetch('/api/config');
|
||||
siteConfig = await response.json();
|
||||
|
||||
// Update company information
|
||||
updateCompanyInfo();
|
||||
|
||||
// Update branding (colors)
|
||||
updateBranding();
|
||||
|
||||
// Update contact information
|
||||
updateContactInfo();
|
||||
|
||||
// Update social media links
|
||||
updateSocialLinks();
|
||||
|
||||
console.log('Site configuration loaded successfully');
|
||||
} catch (err) {
|
||||
console.error('Failed to load site configuration:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function updateCompanyInfo() {
|
||||
// Update company name
|
||||
const companyNameElements = document.querySelectorAll('[data-company-name]');
|
||||
companyNameElements.forEach(el => {
|
||||
el.textContent = siteConfig.company.name;
|
||||
});
|
||||
|
||||
// Update company tagline
|
||||
const taglineElements = document.querySelectorAll('[data-company-tagline]');
|
||||
taglineElements.forEach(el => {
|
||||
el.textContent = siteConfig.company.tagline;
|
||||
});
|
||||
|
||||
// Update company description
|
||||
const descElements = document.querySelectorAll('[data-company-description]');
|
||||
descElements.forEach(el => {
|
||||
el.textContent = siteConfig.company.description;
|
||||
});
|
||||
|
||||
// Update page title
|
||||
const titleElement = document.querySelector('title');
|
||||
if (titleElement && titleElement.textContent.includes('Ryans Recruit')) {
|
||||
titleElement.textContent = titleElement.textContent.replace(/Ryans Recruit Firm|Ryans Recruit/g, siteConfig.company.name);
|
||||
}
|
||||
}
|
||||
|
||||
function updateBranding() {
|
||||
// Set CSS custom properties for branding colors
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--primary-color', siteConfig.branding.primaryColor);
|
||||
root.style.setProperty('--accent-color', siteConfig.branding.accentColor);
|
||||
root.style.setProperty('--secondary-color', siteConfig.branding.darkColor);
|
||||
}
|
||||
|
||||
function updateContactInfo() {
|
||||
// Update email
|
||||
const emailElements = document.querySelectorAll('[data-contact-email]');
|
||||
emailElements.forEach(el => {
|
||||
if (el.tagName === 'A') {
|
||||
el.href = `mailto:${siteConfig.contact.email}`;
|
||||
}
|
||||
el.textContent = siteConfig.contact.email;
|
||||
});
|
||||
|
||||
// Update phone
|
||||
const phoneElements = document.querySelectorAll('[data-contact-phone]');
|
||||
phoneElements.forEach(el => {
|
||||
if (el.tagName === 'A') {
|
||||
el.href = `tel:${siteConfig.contact.phone}`;
|
||||
}
|
||||
el.textContent = siteConfig.contact.phone;
|
||||
});
|
||||
|
||||
// Update address
|
||||
const addressElements = document.querySelectorAll('[data-contact-address]');
|
||||
addressElements.forEach(el => {
|
||||
el.textContent = siteConfig.contact.address;
|
||||
});
|
||||
|
||||
// Update business hours
|
||||
const hoursElements = document.querySelectorAll('[data-business-hours]');
|
||||
hoursElements.forEach(el => {
|
||||
el.textContent = siteConfig.contact.businessHours;
|
||||
});
|
||||
}
|
||||
|
||||
function updateSocialLinks() {
|
||||
// Update LinkedIn
|
||||
const linkedinElements = document.querySelectorAll('[data-social-linkedin]');
|
||||
linkedinElements.forEach(el => {
|
||||
if (siteConfig.social.linkedin) {
|
||||
el.href = siteConfig.social.linkedin;
|
||||
el.style.display = '';
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Update Twitter
|
||||
const twitterElements = document.querySelectorAll('[data-social-twitter]');
|
||||
twitterElements.forEach(el => {
|
||||
if (siteConfig.social.twitter) {
|
||||
el.href = siteConfig.social.twitter;
|
||||
el.style.display = '';
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Update Facebook
|
||||
const facebookElements = document.querySelectorAll('[data-social-facebook]');
|
||||
facebookElements.forEach(el => {
|
||||
if (siteConfig.social.facebook) {
|
||||
el.href = siteConfig.social.facebook;
|
||||
el.style.display = '';
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load configuration when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', loadSiteConfig);
|
||||
} else {
|
||||
loadSiteConfig();
|
||||
}
|
||||
42
server.js
42
server.js
@@ -6,17 +6,18 @@ const multer = require('multer');
|
||||
const path = require('path');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const fs = require('fs');
|
||||
const config = require('./config');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const PORT = config.app.port;
|
||||
|
||||
// PostgreSQL connection
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST || 'postgres',
|
||||
port: process.env.DB_PORT || 5432,
|
||||
database: process.env.DB_NAME || 'recruitment',
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'postgres',
|
||||
host: config.database.host,
|
||||
port: config.database.port,
|
||||
database: config.database.name,
|
||||
user: config.database.user,
|
||||
password: config.database.password,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
@@ -64,11 +65,11 @@ app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(cookieParser());
|
||||
app.use(session({
|
||||
secret: process.env.SESSION_SECRET || 'recruitment-site-secret-key-change-in-production',
|
||||
secret: config.app.sessionSecret,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: false, // Set to true if using HTTPS
|
||||
secure: config.app.nodeEnv === 'production', // Automatically enable for production
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
@@ -106,19 +107,16 @@ app.use((req, res, next) => {
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: {
|
||||
fileSize: 5 * 1024 * 1024, // 5MB limit
|
||||
fileSize: config.features.maxCvSizeMB * 1024 * 1024,
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
const allowedTypes = /pdf|doc|docx/;
|
||||
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
|
||||
const mimetype = allowedTypes.test(file.mimetype) ||
|
||||
file.mimetype === 'application/msword' ||
|
||||
file.mimetype === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||
const allowedTypes = config.features.allowedCvTypes;
|
||||
const isAllowed = allowedTypes.includes(file.mimetype);
|
||||
|
||||
if (mimetype && extname) {
|
||||
if (isAllowed) {
|
||||
return cb(null, true);
|
||||
}
|
||||
cb(new Error('Only PDF, DOC, and DOCX files are allowed'));
|
||||
cb(new Error('File type not allowed. Only PDF, DOC, and DOCX files are accepted.'));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -135,6 +133,18 @@ const requireAuth = (req, res, next) => {
|
||||
// PUBLIC API ENDPOINTS
|
||||
// ============================================
|
||||
|
||||
// Get company configuration for frontend
|
||||
app.get('/api/config', (req, res) => {
|
||||
res.json({
|
||||
company: config.company,
|
||||
branding: config.branding,
|
||||
contact: config.contact,
|
||||
social: config.social,
|
||||
about: config.about,
|
||||
services: config.services
|
||||
});
|
||||
});
|
||||
|
||||
// Get all active job postings
|
||||
app.get('/api/jobs', async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user