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:
Mikael Westöö
2026-01-23 23:26:57 +01:00
parent d6523bc4b1
commit ac21e428a5
6 changed files with 370 additions and 33 deletions

View File

@@ -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
View 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();
}