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

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