feat: Containerize services with Docker Compose (PostgreSQL, Strapi CMS, Express Backend, Nginx Frontend) on custom ports >9000
This commit is contained in:
+14
-1
@@ -14,6 +14,8 @@ import multer from 'multer';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, 'uploads');
|
||||
const STRAPI_UPLOADS_DIR = path.join(__dirname, '..', 'strapi-cms', 'public', 'uploads');
|
||||
const PUBLIC_UPLOADS_DIR = path.join(__dirname, '..', 'public', 'uploads');
|
||||
// Public URL prefix on Apache (e.g. /citpl_website). Leave empty if site is at domain root.
|
||||
const SITE_BASE = (process.env.SITE_BASE ?? '').replace(/\/$/, '');
|
||||
const distDir = path.join(__dirname, '..', 'dist');
|
||||
@@ -42,6 +44,8 @@ function mountApp(base) {
|
||||
const prefix = base || '';
|
||||
|
||||
app.use(`${prefix}/uploads`, express.static(UPLOAD_DIR));
|
||||
app.use(`${prefix}/uploads`, express.static(STRAPI_UPLOADS_DIR));
|
||||
app.use(`${prefix}/uploads`, express.static(PUBLIC_UPLOADS_DIR));
|
||||
|
||||
app.get(`${prefix}/api/health`, (_req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
@@ -74,8 +78,14 @@ if (SITE_BASE) {
|
||||
// Fallback: serve public/assets directly at /assets/ and /citpl_website/assets/
|
||||
// This ensures cert/logo images load regardless of which base the build used
|
||||
app.use('/assets', express.static(publicAssetsDir));
|
||||
app.use('/uploads', express.static(UPLOAD_DIR));
|
||||
app.use('/uploads', express.static(STRAPI_UPLOADS_DIR));
|
||||
app.use('/uploads', express.static(PUBLIC_UPLOADS_DIR));
|
||||
if (SITE_BASE) {
|
||||
app.use(`${SITE_BASE}/assets`, express.static(publicAssetsDir));
|
||||
app.use(`${SITE_BASE}/uploads`, express.static(UPLOAD_DIR));
|
||||
app.use(`${SITE_BASE}/uploads`, express.static(STRAPI_UPLOADS_DIR));
|
||||
app.use(`${SITE_BASE}/uploads`, express.static(PUBLIC_UPLOADS_DIR));
|
||||
}
|
||||
|
||||
// Return proper 404 for missing static assets (prevents index.html served as JS/CSS)
|
||||
@@ -95,7 +105,7 @@ app.use((err, _req, res, _next) => {
|
||||
res.status(500).json({ error: err.message || 'Internal server error', code: 'SERVER_ERROR' });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`CITPL Node server on http://localhost:${PORT}`);
|
||||
console.log(` API: /api/* and ${SITE_BASE || ''}/api/*`);
|
||||
console.log(` Auth: ADMIN_USERNAME from .env (${process.env.ADMIN_USERNAME || 'admin'})`);
|
||||
@@ -103,3 +113,6 @@ app.listen(PORT, () => {
|
||||
console.log(` Static: ${distDir}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Keep process active in background subshells
|
||||
setInterval(() => {}, 1000 * 3600);
|
||||
|
||||
+42
-14
@@ -1,8 +1,10 @@
|
||||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import { saveLead, getLeads } from '../db.js';
|
||||
import { sendContactEmail } from '../services/mailService.js';
|
||||
import { sendContactEmail, sendCareersEmail, sendStrategyEmail } from '../services/mailService.js';
|
||||
|
||||
const router = express.Router();
|
||||
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } });
|
||||
|
||||
async function verifyTurnstile(token, remoteip) {
|
||||
const secretKey = process.env.TURNSTILE_SECRET_KEY || '1x000000000000000000000000000000AA';
|
||||
@@ -25,10 +27,10 @@ async function verifyTurnstile(token, remoteip) {
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/contact - Handle Contact Us Form submission via SMTP
|
||||
router.post('/', async (req, res) => {
|
||||
// POST /api/contact - Handle Contact, Careers, and Strategy Call form submissions via SMTP
|
||||
router.post('/', upload.single('resume'), async (req, res) => {
|
||||
try {
|
||||
const { name, email, phone, mobile, subject, message, interests, type, turnstileToken } = req.body;
|
||||
const { name, email, phone, mobile, subject, message, interests, type, designation, dateTime, resumeName, turnstileToken } = req.body;
|
||||
|
||||
const contactName = name?.trim();
|
||||
const contactEmail = email?.trim();
|
||||
@@ -55,17 +57,43 @@ router.post('/', async (req, res) => {
|
||||
phone: contactPhone,
|
||||
subject: contactSubject,
|
||||
message: contactMessage,
|
||||
designation,
|
||||
dateTime,
|
||||
resumeName,
|
||||
type: type || 'contact'
|
||||
});
|
||||
|
||||
// Attempt sending email via SMTP service
|
||||
const mailResult = await sendContactEmail({
|
||||
name: contactName,
|
||||
email: contactEmail,
|
||||
phone: contactPhone,
|
||||
subject: contactSubject,
|
||||
message: contactMessage
|
||||
});
|
||||
// Attempt sending email via SMTP service based on form type
|
||||
let mailResult = { success: false };
|
||||
|
||||
if (type === 'careers') {
|
||||
mailResult = await sendCareersEmail({
|
||||
name: contactName,
|
||||
email: contactEmail,
|
||||
phone: contactPhone,
|
||||
resumeName: req.file ? req.file.originalname : (resumeName || 'Resume Uploaded'),
|
||||
message: contactMessage,
|
||||
file: req.file
|
||||
});
|
||||
} else if (type === 'strategy') {
|
||||
mailResult = await sendStrategyEmail({
|
||||
name: contactName,
|
||||
email: contactEmail,
|
||||
phone: contactPhone,
|
||||
designation: designation || 'N/A',
|
||||
dateTime: dateTime || 'N/A',
|
||||
interests: interests || 'General Strategy Consultation',
|
||||
message: contactMessage
|
||||
});
|
||||
} else {
|
||||
mailResult = await sendContactEmail({
|
||||
name: contactName,
|
||||
email: contactEmail,
|
||||
phone: contactPhone,
|
||||
subject: contactSubject,
|
||||
message: contactMessage
|
||||
});
|
||||
}
|
||||
|
||||
if (!mailResult.success) {
|
||||
// If SMTP is not yet configured or fails, return 200 with lead saved & warning
|
||||
@@ -74,7 +102,7 @@ router.post('/', async (req, res) => {
|
||||
saved: true,
|
||||
leadId: lead.id,
|
||||
smtpError: mailResult.error,
|
||||
message: 'Your message was saved successfully. Note: SMTP email dispatch requires SMTP credentials in .env.'
|
||||
message: 'Your request was saved successfully. Note: SMTP email dispatch requires SMTP credentials in .env.'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -83,7 +111,7 @@ router.post('/', async (req, res) => {
|
||||
saved: true,
|
||||
leadId: lead.id,
|
||||
messageId: mailResult.messageId,
|
||||
message: 'Message sent successfully via SMTP!'
|
||||
message: 'Request sent successfully via SMTP!'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Contact Route Error]', error);
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import { Router } from 'express';
|
||||
import { getContent, updateSection, updateAllContent } from '../db.js';
|
||||
import { requireAuth } from '../auth.js';
|
||||
import { getStrapiBlogs } from '../strapi.js';
|
||||
import seedContent from '../seed/default-content.json' with { type: 'json' };
|
||||
|
||||
const VALID_SECTIONS = Object.keys(seedContent);
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', (_req, res) => {
|
||||
router.get('/', async (_req, res) => {
|
||||
const { content, updatedAt } = getContent();
|
||||
const strapiItems = await getStrapiBlogs();
|
||||
if (strapiItems && content.insights) {
|
||||
content.insights.items = strapiItems;
|
||||
}
|
||||
res.json({ ...content, _updatedAt: updatedAt });
|
||||
});
|
||||
|
||||
router.get('/blogs', async (_req, res) => {
|
||||
const { content } = getContent();
|
||||
const strapiItems = await getStrapiBlogs();
|
||||
res.json(strapiItems || content.insights?.items || []);
|
||||
});
|
||||
|
||||
router.put('/', requireAuth, (req, res) => {
|
||||
const { _updatedAt, ...content } = req.body;
|
||||
const updatedAt = updateAllContent(content);
|
||||
|
||||
@@ -5,11 +5,31 @@
|
||||
"copyright": "Copyrights reserved to CAVIN INFOTECH"
|
||||
},
|
||||
"navigation": [
|
||||
{ "label": "Home", "href": "#home", "type": "hash" },
|
||||
{ "label": "About Us", "href": "#aboutus", "type": "hash" },
|
||||
{ "label": "Products", "href": "#products", "type": "hash" },
|
||||
{ "label": "Careers", "href": "#careers", "type": "modal" },
|
||||
{ "label": "Contact Us", "href": "#contactus", "type": "hash" }
|
||||
{
|
||||
"label": "Home",
|
||||
"href": "#home",
|
||||
"type": "hash"
|
||||
},
|
||||
{
|
||||
"label": "About Us",
|
||||
"href": "#aboutus",
|
||||
"type": "hash"
|
||||
},
|
||||
{
|
||||
"label": "Products",
|
||||
"href": "#products",
|
||||
"type": "hash"
|
||||
},
|
||||
{
|
||||
"label": "Careers",
|
||||
"href": "#careers",
|
||||
"type": "modal"
|
||||
},
|
||||
{
|
||||
"label": "Contact Us",
|
||||
"href": "#contactus",
|
||||
"type": "hash"
|
||||
}
|
||||
],
|
||||
"hero": {
|
||||
"video": "/assets/citpl-hero.mp4",
|
||||
@@ -21,10 +41,22 @@
|
||||
"secondaryCta": "Book a Strategy Call"
|
||||
},
|
||||
"metrics": [
|
||||
{ "value": "100+", "label": "Clients" },
|
||||
{ "value": "5+", "label": "Years of Global Operations" },
|
||||
{ "value": "4", "label": "Group Companies" },
|
||||
{ "value": "2", "label": "Global Offices" }
|
||||
{
|
||||
"value": "100+",
|
||||
"label": "Clients"
|
||||
},
|
||||
{
|
||||
"value": "5+",
|
||||
"label": "Years of Global Operations"
|
||||
},
|
||||
{
|
||||
"value": "4",
|
||||
"label": "Group Companies"
|
||||
},
|
||||
{
|
||||
"value": "2",
|
||||
"label": "Global Offices"
|
||||
}
|
||||
],
|
||||
"about": {
|
||||
"heading": "Trusted Technology Partner",
|
||||
@@ -37,26 +69,71 @@
|
||||
"subheading": "from various industries",
|
||||
"body": "Professionals Trust Our Solutions And Complete Their Journeys.",
|
||||
"logos": [
|
||||
{ "name": "CavinKare", "image": "/assets/Frame 27.svg" },
|
||||
{ "name": "Valeo", "image": "/assets/Frame 28.svg" },
|
||||
{ "name": "Polyhose", "image": "/assets/Frame 30.svg" },
|
||||
{ "name": "ACA Global", "image": "/assets/Frame 31.svg" },
|
||||
{ "name": "Dawn Pictures", "image": "/assets/Frame 32.svg" },
|
||||
{ "name": "MRF", "image": "/assets/Frame 33.svg" },
|
||||
{ "name": "Chola MS", "image": "/assets/Frame 34.svg" },
|
||||
{ "name": "Hero", "image": "/assets/Frame 35.svg" },
|
||||
{ "name": "BorgWarner", "image": "/assets/Frame 36.svg" },
|
||||
{ "name": "KONE", "image": "/assets/kone.png" }
|
||||
{
|
||||
"name": "CavinKare",
|
||||
"image": "/assets/Frame 27.svg"
|
||||
},
|
||||
{
|
||||
"name": "Valeo",
|
||||
"image": "/assets/Frame 28.svg"
|
||||
},
|
||||
{
|
||||
"name": "Polyhose",
|
||||
"image": "/assets/Frame 30.svg"
|
||||
},
|
||||
{
|
||||
"name": "ACA Global",
|
||||
"image": "/assets/Frame 31.svg"
|
||||
},
|
||||
{
|
||||
"name": "Dawn Pictures",
|
||||
"image": "/assets/Frame 32.svg"
|
||||
},
|
||||
{
|
||||
"name": "MRF",
|
||||
"image": "/assets/Frame 33.svg"
|
||||
},
|
||||
{
|
||||
"name": "Chola MS",
|
||||
"image": "/assets/Frame 34.svg"
|
||||
},
|
||||
{
|
||||
"name": "Hero",
|
||||
"image": "/assets/Frame 35.svg"
|
||||
},
|
||||
{
|
||||
"name": "BorgWarner",
|
||||
"image": "/assets/Frame 36.svg"
|
||||
},
|
||||
{
|
||||
"name": "KONE",
|
||||
"image": "/assets/kone.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
"certifications": {
|
||||
"heading": "Certified to build what enterprises trust",
|
||||
"items": [
|
||||
{ "image": "/assets/Certifications Logo. 01.svg", "alt": "ISO 27001:2022 Certification" },
|
||||
{ "image": "/assets/Certifications Logo. 02.svg", "alt": "ISO 9001:2015 Certification" },
|
||||
{ "image": "/assets/Certifications Logo. 03.svg", "alt": "SOC 2 Type II Certification" },
|
||||
{ "image": "/assets/Certifications Logo. 04.svg", "alt": "Nasscom Member Logo" },
|
||||
{ "image": "/assets/Certifications Logo. 05.svg", "alt": "Great Place to Work Certified 2026 INDIA" }
|
||||
{
|
||||
"image": "/assets/Certifications Logo. 01.svg",
|
||||
"alt": "ISO 27001:2022 Certification"
|
||||
},
|
||||
{
|
||||
"image": "/assets/Certifications Logo. 02.svg",
|
||||
"alt": "ISO 9001:2015 Certification"
|
||||
},
|
||||
{
|
||||
"image": "/assets/Certifications Logo. 03.svg",
|
||||
"alt": "SOC 2 Type II Certification"
|
||||
},
|
||||
{
|
||||
"image": "/assets/Certifications Logo. 04.svg",
|
||||
"alt": "Nasscom Member Logo"
|
||||
},
|
||||
{
|
||||
"image": "/assets/Certifications Logo. 05.svg",
|
||||
"alt": "Great Place to Work Certified 2026 INDIA"
|
||||
}
|
||||
]
|
||||
},
|
||||
"services": {
|
||||
@@ -168,9 +245,18 @@
|
||||
}
|
||||
],
|
||||
"images": [
|
||||
{ "src": "/assets/whyus_arab_meeting.jpg", "alt": "Corporate collaboration" },
|
||||
{ "src": "/assets/whyus_vr_man.jpg", "alt": "VR technology" },
|
||||
{ "src": "/assets/whyus_teamwork_user.jpg", "alt": "Teamwork technology" }
|
||||
{
|
||||
"src": "/assets/whyus_arab_meeting.jpg",
|
||||
"alt": "Corporate collaboration"
|
||||
},
|
||||
{
|
||||
"src": "/assets/whyus_vr_man.jpg",
|
||||
"alt": "VR technology"
|
||||
},
|
||||
{
|
||||
"src": "/assets/whyus_teamwork_user.jpg",
|
||||
"alt": "Teamwork technology"
|
||||
}
|
||||
]
|
||||
},
|
||||
"gallery": {
|
||||
@@ -178,12 +264,54 @@
|
||||
"title": "Real Transformations. Proven Business Outcomes.",
|
||||
"subtitle": "From transformation stories to team innovation, explore how we build future-ready experiences.",
|
||||
"items": [
|
||||
{ "id": 1, "title": "Relentless Brand Growth & Innovation", "category": "Case Study", "excerpt": "Discover how we engineered a scalable digital platform that boosted brand engagement by 150% and accelerated global market reach.", "image": "https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&w=800&q=80", "readTime": "5 Min Read" },
|
||||
{ "id": 2, "title": "How We Transformed Billing at TTK", "category": "Transformation", "excerpt": "Transitioning legacy paper billing to an automated IoT-driven workflow, reducing checkout delays and improving customer satisfaction.", "image": "https://images.unsplash.com/photo-1554224155-8d04cb21cd6c?auto=format&fit=crop&w=800&q=80", "readTime": "8 Min Read" },
|
||||
{ "id": 3, "title": "How We Transformed Retail Operations", "category": "Retail", "excerpt": "Unifying online inventory with physical retail networks, enabling smart warehouse tracking and seamless order fulfillment.", "image": "https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?auto=format&fit=crop&w=800&q=80", "readTime": "6 Min Read" },
|
||||
{ "id": 4, "title": "How We Transformed Enterprise Workflows", "category": "Workflow", "excerpt": "Re-engineering internal communication channels and operations with Gen AI assistants, saving thousands of operational hours.", "image": "https://images.unsplash.com/photo-1531538606174-0f90ff5dce83?auto=format&fit=crop&w=800&q=80", "readTime": "10 Min Read" },
|
||||
{ "id": 5, "title": "Digital Product Showcase", "category": "Showcase", "excerpt": "A curation of sleek web interfaces, user-centric mobile applications, and custom enterprise portals designed by our UI/UX experts.", "image": "https://images.unsplash.com/photo-1507238691740-187a5b1d37b8?auto=format&fit=crop&w=800&q=80", "readTime": "7 Min Read" },
|
||||
{ "id": 6, "title": "Innovation Lab", "category": "Innovation", "excerpt": "Step inside our workshop where we prototype Agentic AI, smart hardware sensors, and computer vision systems for next-generation industries.", "image": "https://images.unsplash.com/photo-1485827404703-89b55fcc595e?auto=format&fit=crop&w=800&q=80", "readTime": "12 Min Read" }
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Relentless Brand Growth & Innovation",
|
||||
"category": "Case Study",
|
||||
"excerpt": "Discover how we engineered a scalable digital platform that boosted brand engagement by 150% and accelerated global market reach.",
|
||||
"image": "https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&w=800&q=80",
|
||||
"readTime": "5 Min Read"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "How We Transformed Billing at TTK",
|
||||
"category": "Transformation",
|
||||
"excerpt": "Transitioning legacy paper billing to an automated IoT-driven workflow, reducing checkout delays and improving customer satisfaction.",
|
||||
"image": "https://images.unsplash.com/photo-1554224155-8d04cb21cd6c?auto=format&fit=crop&w=800&q=80",
|
||||
"readTime": "8 Min Read"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "How We Transformed Retail Operations",
|
||||
"category": "Retail",
|
||||
"excerpt": "Unifying online inventory with physical retail networks, enabling smart warehouse tracking and seamless order fulfillment.",
|
||||
"image": "https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?auto=format&fit=crop&w=800&q=80",
|
||||
"readTime": "6 Min Read"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "How We Transformed Enterprise Workflows",
|
||||
"category": "Workflow",
|
||||
"excerpt": "Re-engineering internal communication channels and operations with Gen AI assistants, saving thousands of operational hours.",
|
||||
"image": "https://images.unsplash.com/photo-1531538606174-0f90ff5dce83?auto=format&fit=crop&w=800&q=80",
|
||||
"readTime": "10 Min Read"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Digital Product Showcase",
|
||||
"category": "Showcase",
|
||||
"excerpt": "A curation of sleek web interfaces, user-centric mobile applications, and custom enterprise portals designed by our UI/UX experts.",
|
||||
"image": "https://images.unsplash.com/photo-1507238691740-187a5b1d37b8?auto=format&fit=crop&w=800&q=80",
|
||||
"readTime": "7 Min Read"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "Innovation Lab",
|
||||
"category": "Innovation",
|
||||
"excerpt": "Step inside our workshop where we prototype Agentic AI, smart hardware sensors, and computer vision systems for next-generation industries.",
|
||||
"image": "https://images.unsplash.com/photo-1485827404703-89b55fcc595e?auto=format&fit=crop&w=800&q=80",
|
||||
"readTime": "12 Min Read"
|
||||
}
|
||||
]
|
||||
},
|
||||
"products": {
|
||||
@@ -196,8 +324,14 @@
|
||||
"description": "Comprehensive enterprise AI for intelligent automation, predictive insights, Generative AI, Agentic AI, and conversational intelligence.",
|
||||
"image": "/assets/Nebula Img.png",
|
||||
"stats": [
|
||||
{ "value": "60 %", "label": "Faster Process Automation" },
|
||||
{ "value": "24/7", "label": "AI Driven Assistance" }
|
||||
{
|
||||
"value": "60 %",
|
||||
"label": "Faster Process Automation"
|
||||
},
|
||||
{
|
||||
"value": "24/7",
|
||||
"label": "AI Driven Assistance"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -205,8 +339,14 @@
|
||||
"description": "AI-powered Industrial IoT platform with 16+ modules for OEE, energy management, traceability, AI vision, and smart manufacturing excellence.",
|
||||
"image": "/assets/Prodmax Img.png",
|
||||
"stats": [
|
||||
{ "value": "20%+", "label": "OEE Improvement" },
|
||||
{ "value": "15%", "label": "Reduction in Downtime" }
|
||||
{
|
||||
"value": "20%+",
|
||||
"label": "OEE Improvement"
|
||||
},
|
||||
{
|
||||
"value": "15%",
|
||||
"label": "Reduction in Downtime"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -214,7 +354,10 @@
|
||||
"description": "AI-driven HRMS for intelligent recruitment, workforce scheduling, engagement, and collaboration.",
|
||||
"image": "/assets/Budgie Img.png",
|
||||
"stats": [
|
||||
{ "value": "40 %", "label": "Faster HR Operations" }
|
||||
{
|
||||
"value": "40 %",
|
||||
"label": "Faster HR Operations"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -223,13 +366,81 @@
|
||||
"title": "Explore the Latest in Technology & Innovation",
|
||||
"subtitle": "Stay ahead with insights, stories, and updates on digital transformation, AI, automation, product design, and enterprise technology.",
|
||||
"items": [
|
||||
{ "category": "AI & Analytics", "title": "How AI is Reshaping Enterprise Decision Making", "date": "June 15, 2026", "image": "https://images.unsplash.com/photo-1620712943543-bcc4688e7485?auto=format&fit=crop&w=800&q=80", "excerpt": "Explore how deep learning models and agentic decision trees are replacing traditional business intelligence systems to drive proactive operations." },
|
||||
{ "category": "Digital Strategy", "title": "Why Digital Transformation Needs More Than Technology", "date": "June 02, 2026", "image": "https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&w=800&q=80", "excerpt": "Technology is only half the battle. True organizational agility requires architectural alignment, structural changes, and cultural evolution." },
|
||||
{ "category": "Product Design", "title": "Designing Enterprise Software Users Actually Love", "date": "May 18, 2026", "image": "https://images.unsplash.com/photo-1586717791821-3f44a563fa4c?auto=format&fit=crop&w=800&q=80", "excerpt": "Applying consumer-grade user experience principles to complex B2B systems reduces training costs and accelerates daily workflows." },
|
||||
{ "category": "Automation", "title": "The Future of Automation in Business Workflows", "date": "May 05, 2026", "image": "https://images.unsplash.com/photo-1485827404703-89b55fcc595e?auto=format&fit=crop&w=800&q=80", "excerpt": "From robotic process automation (RPA) to generative AI pipelines, learn how workflow automation is changing modern business scaling." },
|
||||
{ "category": "Cloud Architecture", "title": "Building Scalable Cloud-Native Infrastructure for Enterprises", "date": "April 22, 2026", "image": "https://images.unsplash.com/photo-1544197150-b99a580bb7a8?auto=format&fit=crop&w=800&q=80", "excerpt": "Microservices, Kubernetes, and serverless patterns are redefining how large-scale enterprise systems handle performance at unpredictable loads." },
|
||||
{ "category": "Cybersecurity", "title": "Zero Trust Security Models in the Age of Remote Work", "date": "April 08, 2026", "image": "https://images.unsplash.com/photo-1563986768494-4dee2763ff3f?auto=format&fit=crop&w=800&q=80", "excerpt": "As perimeter-based security crumbles, zero trust frameworks enforce identity verification at every layer — from endpoint to data center." },
|
||||
{ "category": "Data Engineering", "title": "Real-Time Data Pipelines That Power Smarter Decisions", "date": "March 25, 2026", "image": "https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=800&q=80", "excerpt": "Event-driven architectures and streaming platforms like Apache Kafka enable organizations to act on data the moment it is generated." }
|
||||
{
|
||||
"id": 60,
|
||||
"category": "AI & Data",
|
||||
"title": "Clean Content Form Test Blog",
|
||||
"excerpt": "Testing clean editing interface with hidden auto-fill fields.",
|
||||
"content": "# Clean Content Editing\n\nAuthors focus only on writing without form clutter.",
|
||||
"readTime": "4 min read",
|
||||
"date": "Jul 30, 2026",
|
||||
"image": "/uploads/arvindh_agentic_ai_3ea158c92b.jpg",
|
||||
"slug": "clean-content-form-test-blog",
|
||||
"author": "Dr. Arvindh Ramachandran",
|
||||
"authorDesignation": "Chief AI Architect & Head of Data Engineering",
|
||||
"editor": null,
|
||||
"editorDesignation": null,
|
||||
"tags": [
|
||||
"CleanUI",
|
||||
"AutoFill"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 62,
|
||||
"category": "Industrial IoT",
|
||||
"title": "Blog entry 123",
|
||||
"excerpt": "Blog Entry 123",
|
||||
"content": "A salon-exclusive waxing system inspired by Korean skincare. Infused with skin-loving actives, Dermaxix K-Line Glass Wax delivers a smooth, reflective finish for radiant, glass-like skin.\n\nSmooth Icon\nSMOOTH\nSilky-smooth finish\nevery time.\n\nRadiant Icon\nRADIANT\nReflective glow that\nlasts.\n\n",
|
||||
"readTime": "5 min read",
|
||||
"date": "Jul 30, 2026",
|
||||
"image": "/uploads/Quality_Control_Collage_33d26fa0cc.jpg",
|
||||
"slug": "blog-entry-123",
|
||||
"author": "Dr. Arvindh Ramachandran",
|
||||
"authorDesignation": "Chief AI Architect & Head of Data Engineering",
|
||||
"editor": null,
|
||||
"editorDesignation": null,
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"id": 63,
|
||||
"category": "AR/VR & Immersive Tech",
|
||||
"title": "Building Autonomous LLM Pipeline Workflows for Financial Services",
|
||||
"excerpt": "Designing fault-tolerant, audit-ready LLM orchestration layers for complex risk assessment and automated compliance.",
|
||||
"content": "# Building Autonomous LLM Pipelines\n\nFinancial services demand uncompromising accuracy, auditability, and speed when deploying LLM workflows...",
|
||||
"readTime": "5 min read",
|
||||
"date": "Jul 31, 2026",
|
||||
"image": "/assets/arvindh_llm_financial.jpg",
|
||||
"slug": "building-autonomous-llm-pipeline-workflows-for-financial-services",
|
||||
"author": "Dr. Arvindh Ramachandran",
|
||||
"authorDesignation": "Chief AI Architect & Head of Data Engineering",
|
||||
"editor": "Jainaressh BC",
|
||||
"editorDesignation": "Senior Editor - Enterprise Software & Cloud",
|
||||
"tags": [
|
||||
"LLM",
|
||||
"FinancialTech",
|
||||
"Compliance"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 64,
|
||||
"category": "Operation Technology",
|
||||
"title": "Zero-Trust Security Protocols for Connected SCADA & PLC Industrial Networks",
|
||||
"excerpt": "Implementing micro-segmentation and strict cryptographic handshake verification across industrial control systems.",
|
||||
"content": "# Zero-Trust Security for SCADA & PLCs\n\nLegacy PLCs were built without authentication. Zero-trust network segmentation safeguards operational continuity...",
|
||||
"readTime": "6 min read",
|
||||
"date": "Jul 31, 2026",
|
||||
"image": "/assets/whyus_arab_meeting.jpg",
|
||||
"slug": "zero-trust-security-protocols-for-connected-scada-plc-industrial-networks",
|
||||
"author": "Vikram Sen",
|
||||
"authorDesignation": "Senior Editor - Industrial Operations & OT Tech",
|
||||
"editor": "Jainaressh BC",
|
||||
"editorDesignation": "Senior Editor - Industrial Operations & OT Tech",
|
||||
"tags": [
|
||||
"ZeroTrust",
|
||||
"Cybersecurity",
|
||||
"PLC"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"footer": {
|
||||
@@ -241,25 +452,69 @@
|
||||
"secondaryButton": "Let's Solve Your Biggest Challenge →"
|
||||
},
|
||||
"quickLinks": [
|
||||
{ "label": "Home", "href": "#home" },
|
||||
{ "label": "About Us", "href": "#certifications" },
|
||||
{ "label": "Our Services", "href": "#services" }
|
||||
{
|
||||
"label": "Home",
|
||||
"href": "#home"
|
||||
},
|
||||
{
|
||||
"label": "About Us",
|
||||
"href": "#certifications"
|
||||
},
|
||||
{
|
||||
"label": "Our Services",
|
||||
"href": "#services"
|
||||
},
|
||||
{
|
||||
"label": "Contact Us",
|
||||
"href": "#contactus"
|
||||
}
|
||||
],
|
||||
"productLinks": [
|
||||
{ "label": "Nebula AI platform", "href": "#products" },
|
||||
{ "label": "Prodmax", "href": "#products" },
|
||||
{ "label": "Budgie", "href": "#products" }
|
||||
{
|
||||
"label": "Nebula AI Platform",
|
||||
"href": "#products"
|
||||
},
|
||||
{
|
||||
"label": "Prodmax",
|
||||
"href": "#products"
|
||||
},
|
||||
{
|
||||
"label": "Budgie",
|
||||
"href": "#products"
|
||||
}
|
||||
],
|
||||
"social": [
|
||||
{ "platform": "Facebook", "url": "https://www.facebook.com/cavininfotech1" },
|
||||
{ "platform": "X", "url": "https://x.com/cavin_infotech" },
|
||||
{ "platform": "Instagram", "url": "https://www.instagram.com/cavin_infotech" },
|
||||
{ "platform": "LinkedIn", "url": "https://www.linkedin.com/company/cavin-infotech/" },
|
||||
{ "platform": "YouTube", "url": "https://youtube.com/@cavin_infotech" }
|
||||
{
|
||||
"platform": "Facebook",
|
||||
"url": "https://www.facebook.com/cavininfotech1"
|
||||
},
|
||||
{
|
||||
"platform": "X",
|
||||
"url": "https://x.com/cavin_infotech"
|
||||
},
|
||||
{
|
||||
"platform": "Instagram",
|
||||
"url": "https://www.instagram.com/cavin_infotech"
|
||||
},
|
||||
{
|
||||
"platform": "LinkedIn",
|
||||
"url": "https://www.linkedin.com/company/cavin-infotech/"
|
||||
},
|
||||
{
|
||||
"platform": "YouTube",
|
||||
"url": "https://youtube.com/@cavin_infotech"
|
||||
}
|
||||
],
|
||||
"legal": [
|
||||
{ "label": "Privacy Policy", "href": "#" },
|
||||
{ "label": "Terms & Conditions", "href": "#" }
|
||||
{
|
||||
"label": "Privacy Policy",
|
||||
"href": "#"
|
||||
},
|
||||
{
|
||||
"label": "Terms & Conditions",
|
||||
"href": "#"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"updatedAt": "2026-07-30T14:33:08.281Z"
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export function createSmtpTransporter() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a contact form email via SMTP using nodemailer.
|
||||
* Sends a general contact form email via SMTP using nodemailer.
|
||||
*/
|
||||
export async function sendContactEmail({ name, email, phone, subject, message }) {
|
||||
const transporter = createSmtpTransporter();
|
||||
@@ -73,7 +73,7 @@ export async function sendContactEmail({ name, email, phone, subject, message })
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; padding-top: 20px; border-top: 1px solid #334155; color: #64748b; font-size: 12px;">
|
||||
Submitted on ${new Date().toLocaleString()} from Cavin Infotech Contact Us Page
|
||||
Submitted on ${new Date().toLocaleString()} from Cavin Infotech Website
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -93,7 +93,6 @@ Submitted on: ${new Date().toLocaleString()}
|
||||
`;
|
||||
|
||||
if (!transporter) {
|
||||
console.warn('[SMTP Service] SMTP credentials not set in environment (SMTP_HOST, SMTP_USER, SMTP_PASS).');
|
||||
return {
|
||||
success: false,
|
||||
error: 'SMTP credentials not configured on the server. Please set SMTP_HOST, SMTP_USER, and SMTP_PASS in .env file.'
|
||||
@@ -109,11 +108,219 @@ Submitted on: ${new Date().toLocaleString()}
|
||||
text: textContent,
|
||||
html: htmlContent
|
||||
});
|
||||
|
||||
console.log('[SMTP Service] Contact email sent via SMTP:', info.messageId);
|
||||
return { success: true, messageId: info.messageId };
|
||||
} catch (err) {
|
||||
console.error('[SMTP Service] Failed to send email via SMTP:', err);
|
||||
return { success: false, error: err.message || 'SMTP sending failed' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a Careers application form email via SMTP using nodemailer.
|
||||
*/
|
||||
export async function sendCareersEmail({ name, email, phone, resumeName, message, file }) {
|
||||
const transporter = createSmtpTransporter();
|
||||
|
||||
const toEmail = process.env.SMTP_TO || process.env.SMTP_USER || 'info@cavinfotech.com';
|
||||
const noReplyEmail = process.env.SMTP_FROM || `"Cavin Infotech Careers" <${process.env.SMTP_USER || 'info@cavinfotech.com'}>`;
|
||||
|
||||
const mailSubject = `[Careers Application] New Application from ${name}`;
|
||||
|
||||
// Internal notification HTML (sent to the team)
|
||||
const internalHtml = `
|
||||
<div style="font-family: 'Segoe UI', Helvetica, Arial, sans-serif; background-color: #020710; color: #f8fafc; padding: 40px 20px; max-width: 650px; margin: 0 auto; border-radius: 16px; border: 1px solid #1e293b;">
|
||||
<div style="text-align: center; padding-bottom: 24px; border-bottom: 1px solid #334155;">
|
||||
<h2 style="color: #38bdf8; font-size: 24px; margin: 0; font-weight: 700;">Cavin Infotech</h2>
|
||||
<p style="color: #94a3b8; font-size: 14px; margin-top: 6px;">New Careers / Job Application</p>
|
||||
</div>
|
||||
|
||||
<div style="padding: 24px 0;">
|
||||
<table style="width: 100%; border-collapse: collapse; color: #e2e8f0; font-size: 15px;">
|
||||
<tr>
|
||||
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8; width: 140px;">Applicant Name:</td>
|
||||
<td style="padding: 10px 0; color: #ffffff;">${name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8;">Email:</td>
|
||||
<td style="padding: 10px 0;"><a href="mailto:${email}" style="color: #38bdf8; text-decoration: none;">${email}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8;">Phone No:</td>
|
||||
<td style="padding: 10px 0; color: #ffffff;">${phone || 'Not provided'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8;">Resume File:</td>
|
||||
<td style="padding: 10px 0; color: #ffaa00; font-weight: 600;">${resumeName || 'Attached'}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
${message ? `
|
||||
<div style="margin-top: 24px; padding: 20px; background-color: #0b1528; border-left: 4px solid #38bdf8; border-radius: 8px;">
|
||||
<h4 style="margin: 0 0 10px 0; color: #94a3b8; font-size: 13px; text-transform: uppercase; letter-spacing: 0.05em;">Notes / Message:</h4>
|
||||
<p style="margin: 0; color: #f1f5f9; font-size: 15px; line-height: 1.6; white-space: pre-line;">${message}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; padding-top: 20px; border-top: 1px solid #334155; color: #64748b; font-size: 12px;">
|
||||
Submitted on ${new Date().toLocaleString()} from Cavin Infotech Careers Form
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Auto-reply HTML (sent to the applicant)
|
||||
const autoReplyHtml = `
|
||||
<div style="font-family: 'Segoe UI', Helvetica, Arial, sans-serif; background-color: #020710; color: #f8fafc; padding: 40px 20px; max-width: 650px; margin: 0 auto; border-radius: 16px; border: 1px solid #1e293b;">
|
||||
<div style="text-align: center; padding-bottom: 24px; border-bottom: 1px solid #334155;">
|
||||
<h2 style="color: #38bdf8; font-size: 24px; margin: 0; font-weight: 700;">Cavin Infotech</h2>
|
||||
<p style="color: #94a3b8; font-size: 14px; margin-top: 6px;">Application Received — Thank You!</p>
|
||||
</div>
|
||||
|
||||
<div style="padding: 28px 0;">
|
||||
<p style="font-size: 16px; color: #e2e8f0; line-height: 1.7; margin: 0 0 16px 0;">
|
||||
Dear <strong style="color: #ffffff;">${name}</strong>,
|
||||
</p>
|
||||
<p style="font-size: 15px; color: #cbd5e1; line-height: 1.7; margin: 0 0 16px 0;">
|
||||
Thank you for applying to <strong style="color: #38bdf8;">Cavin Infotech</strong>. We have successfully received your application and resume.
|
||||
</p>
|
||||
<p style="font-size: 15px; color: #cbd5e1; line-height: 1.7; margin: 0 0 24px 0;">
|
||||
Our team will review your profile and reach out to you if your qualifications match our current openings. We appreciate your interest in joining our team!
|
||||
</p>
|
||||
|
||||
<div style="background: linear-gradient(135deg, rgba(3, 135, 218, 0.15) 0%, rgba(2, 7, 16, 0.6) 100%); border: 1px solid rgba(56, 189, 248, 0.2); border-radius: 12px; padding: 20px 24px; margin-bottom: 24px;">
|
||||
<p style="margin: 0 0 8px 0; font-size: 13px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em;">Application Summary</p>
|
||||
<p style="margin: 4px 0; font-size: 14px; color: #e2e8f0;"><strong style="color: #38bdf8;">Name:</strong> ${name}</p>
|
||||
<p style="margin: 4px 0; font-size: 14px; color: #e2e8f0;"><strong style="color: #38bdf8;">Email:</strong> ${email}</p>
|
||||
<p style="margin: 4px 0; font-size: 14px; color: #e2e8f0;"><strong style="color: #38bdf8;">Phone:</strong> ${phone || 'Not provided'}</p>
|
||||
<p style="margin: 4px 0; font-size: 14px; color: #e2e8f0;"><strong style="color: #38bdf8;">Resume:</strong> ${resumeName || 'Submitted'}</p>
|
||||
</div>
|
||||
|
||||
<p style="font-size: 13px; color: #64748b; line-height: 1.6; margin: 0;">
|
||||
This is an automated confirmation email. Please do not reply to this message. For enquiries, contact us at
|
||||
<a href="mailto:info@cavinfotech.com" style="color: #38bdf8; text-decoration: none;">info@cavinfotech.com</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; padding-top: 20px; border-top: 1px solid #334155; color: #64748b; font-size: 12px;">
|
||||
© ${new Date().getFullYear()} Cavin Infotech. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!transporter) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'SMTP credentials not configured on the server. Please set SMTP_HOST, SMTP_USER, and SMTP_PASS in .env file.'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Send internal team notification (with resume attachment)
|
||||
const mailOptions = {
|
||||
from: noReplyEmail,
|
||||
to: toEmail,
|
||||
replyTo: `"${name}" <${email}>`,
|
||||
subject: mailSubject,
|
||||
text: `New Careers Application:\nName: ${name}\nEmail: ${email}\nPhone: ${phone}\nResume: ${resumeName}`,
|
||||
html: internalHtml
|
||||
};
|
||||
|
||||
if (file) {
|
||||
mailOptions.attachments = [
|
||||
{
|
||||
filename: file.originalname,
|
||||
content: file.buffer
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
|
||||
// 2. Send auto-reply confirmation to the applicant
|
||||
await transporter.sendMail({
|
||||
from: noReplyEmail,
|
||||
to: email,
|
||||
subject: `We've received your application — Cavin Infotech`,
|
||||
text: `Dear ${name},\n\nThank you for applying to Cavin Infotech. We have received your application and will review it shortly.\n\nThis is an automated confirmation. Please do not reply to this email.\n\n© ${new Date().getFullYear()} Cavin Infotech`,
|
||||
html: autoReplyHtml
|
||||
});
|
||||
|
||||
return { success: true, messageId: info.messageId };
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message || 'SMTP sending failed' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a Strategy Call booking email via SMTP using nodemailer.
|
||||
*/
|
||||
export async function sendStrategyEmail({ name, email, phone, designation, dateTime, interests, message }) {
|
||||
const transporter = createSmtpTransporter();
|
||||
|
||||
const toEmail = process.env.SMTP_TO || process.env.SMTP_USER || 'info@cavinfotech.com';
|
||||
const fromEmail = process.env.SMTP_FROM || `"Cavin Infotech Strategy Call" <${process.env.SMTP_USER || 'info@cavinfotech.com'}>`;
|
||||
|
||||
const mailSubject = `[Strategy Call Booking] New Session Request from ${name}`;
|
||||
|
||||
const htmlContent = `
|
||||
<div style="font-family: 'Segoe UI', Helvetica, Arial, sans-serif; background-color: #020710; color: #f8fafc; padding: 40px 20px; max-width: 650px; margin: 0 auto; border-radius: 16px; border: 1px solid #1e293b;">
|
||||
<div style="text-align: center; padding-bottom: 24px; border-bottom: 1px solid #334155;">
|
||||
<h2 style="color: #38bdf8; font-size: 24px; margin: 0; font-weight: 700;">Cavin Infotech</h2>
|
||||
<p style="color: #94a3b8; font-size: 14px; margin-top: 6px;">New Strategy Call Session Request</p>
|
||||
</div>
|
||||
|
||||
<div style="padding: 24px 0;">
|
||||
<table style="width: 100%; border-collapse: collapse; color: #e2e8f0; font-size: 15px;">
|
||||
<tr>
|
||||
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8; width: 160px;">Client Name:</td>
|
||||
<td style="padding: 10px 0; color: #ffffff;">${name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8;">Email:</td>
|
||||
<td style="padding: 10px 0;"><a href="mailto:${email}" style="color: #38bdf8; text-decoration: none;">${email}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8;">Mobile:</td>
|
||||
<td style="padding: 10px 0; color: #ffffff;">${phone || 'Not provided'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8;">Designation:</td>
|
||||
<td style="padding: 10px 0; color: #ffffff;">${designation || 'Not provided'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8;">Preferred Slot:</td>
|
||||
<td style="padding: 10px 0; color: #ffaa00; font-weight: 600;">${dateTime || 'Flexible'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8;">Areas of Interest:</td>
|
||||
<td style="padding: 10px 0; color: #ffffff;">${interests || 'General Consultation'}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; padding-top: 20px; border-top: 1px solid #334155; color: #64748b; font-size: 12px;">
|
||||
Submitted on ${new Date().toLocaleString()} from Cavin Infotech Book a Strategy Call Modal
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!transporter) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'SMTP credentials not configured on the server. Please set SMTP_HOST, SMTP_USER, and SMTP_PASS in .env file.'
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const info = await transporter.sendMail({
|
||||
from: fromEmail,
|
||||
to: toEmail,
|
||||
replyTo: `"${name}" <${email}>`,
|
||||
subject: mailSubject,
|
||||
text: `New Strategy Call Request:\nName: ${name}\nEmail: ${email}\nMobile: ${phone}\nDesignation: ${designation}\nPreferred Slot: ${dateTime}\nInterests: ${interests}`,
|
||||
html: htmlContent
|
||||
});
|
||||
return { success: true, messageId: info.messageId };
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message || 'SMTP sending failed' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const strapiDbPath = path.join(__dirname, '..', 'strapi-cms', '.tmp', 'data.db');
|
||||
const strapiUploadsDir = path.join(__dirname, '..', 'strapi-cms', 'public', 'uploads');
|
||||
const targetPublicUploads = path.join(__dirname, '..', 'public', 'uploads');
|
||||
const targetServerUploads = path.join(__dirname, '..', 'server', 'uploads');
|
||||
|
||||
function syncUploads() {
|
||||
try {
|
||||
if (!fs.existsSync(strapiUploadsDir)) return;
|
||||
if (!fs.existsSync(targetPublicUploads)) fs.mkdirSync(targetPublicUploads, { recursive: true });
|
||||
if (!fs.existsSync(targetServerUploads)) fs.mkdirSync(targetServerUploads, { recursive: true });
|
||||
|
||||
const files = fs.readdirSync(strapiUploadsDir);
|
||||
files.forEach((f) => {
|
||||
if (f === '.gitkeep') return;
|
||||
const srcFile = path.join(strapiUploadsDir, f);
|
||||
const destPublic = path.join(targetPublicUploads, f);
|
||||
const destServer = path.join(targetServerUploads, f);
|
||||
if (!fs.existsSync(destPublic)) fs.copyFileSync(srcFile, destPublic);
|
||||
if (!fs.existsSync(destServer)) fs.copyFileSync(srcFile, destServer);
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Note: Upload sync warning:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStrapiBlogs() {
|
||||
syncUploads();
|
||||
|
||||
// 1. Primary: Fetch live blogs directly from Strapi REST API (PostgreSQL / Live Strapi DB)
|
||||
try {
|
||||
const strapiBaseUrl = process.env.STRAPI_URL || 'http://127.0.0.1:1337';
|
||||
const response = await fetch(`${strapiBaseUrl}/api/blogs?populate=*`, {
|
||||
headers: { 'Accept': 'application/json' },
|
||||
signal: AbortSignal.timeout(3000)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const json = await response.json();
|
||||
const items = json.data || [];
|
||||
|
||||
if (Array.isArray(items) && items.length > 0) {
|
||||
const blogs = items.map((b) => {
|
||||
let coverUrl = b.coverImage?.url || b.ogImage?.url || null;
|
||||
if (!coverUrl) {
|
||||
const cat = (b.category || '').toLowerCase();
|
||||
if (b.title?.includes('LLM')) coverUrl = '/assets/arvindh_llm_financial.jpg';
|
||||
else if (b.title?.includes('Supply Chain')) coverUrl = '/assets/arvindh_supply_chain.jpg';
|
||||
else if (b.title?.includes('PRODMAX')) coverUrl = '/assets/prodmax_dashboard.png';
|
||||
else if (b.title?.includes('Incidents') || b.title?.includes('VR')) coverUrl = '/assets/whyus_vr_man.jpg';
|
||||
else if (b.title?.includes('SCADA') || b.title?.includes('IT/OT')) coverUrl = '/assets/whyus_arab_meeting.jpg';
|
||||
else if (b.title?.includes('Cleanroom')) coverUrl = 'https://images.unsplash.com/photo-1592478411213-6153e4ebc07d?auto=format&fit=crop&w=800&q=80';
|
||||
else if (b.title?.includes('Spatial') || b.title?.includes('Maintenance')) coverUrl = '/assets/whyus_hologram.jpg';
|
||||
else if (b.title?.includes('Energy')) coverUrl = '/assets/whyus_teamwork.png';
|
||||
else if (b.title?.includes('Telemetry') || b.title?.includes('Sensor')) coverUrl = 'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?auto=format&fit=crop&w=800&q=80';
|
||||
else if (b.title?.includes('Monoliths') || b.title?.includes('Micro-Frontends')) coverUrl = 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&w=800&q=80';
|
||||
else if (b.title?.includes('Zero-Trust')) coverUrl = 'https://images.unsplash.com/photo-1563986768494-4dee2763ff3f?auto=format&fit=crop&w=800&q=80';
|
||||
else if (b.title?.includes('Multi-Cloud')) coverUrl = 'https://images.unsplash.com/photo-1544197150-b99a580bb7a8?auto=format&fit=crop&w=800&q=80';
|
||||
else if (cat.includes('ai')) coverUrl = '/assets/arvindh_agentic_ai.jpg';
|
||||
else coverUrl = 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&w=800&q=80';
|
||||
}
|
||||
|
||||
const rawDate = b.publishDate || b.publishedAt || b.createdAt;
|
||||
const dateStr = rawDate
|
||||
? new Date(rawDate).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })
|
||||
: new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' });
|
||||
|
||||
return {
|
||||
id: b.id || b.documentId,
|
||||
documentId: b.documentId,
|
||||
category: b.category || 'Technology',
|
||||
title: b.title,
|
||||
excerpt: b.excerpt || '',
|
||||
content: b.content || '',
|
||||
readTime: b.readTime || '5 min read',
|
||||
date: dateStr,
|
||||
image: coverUrl,
|
||||
slug: b.slug,
|
||||
author: typeof b.author === 'string' ? b.author : (b.author?.firstname ? `${b.author.firstname} ${b.author.lastname || ''}`.trim() : (b.author?.username || 'Dr. Arvindh Ramachandran')),
|
||||
authorDesignation: b.authorDesignation || 'Chief AI Architect & Head of Data Engineering',
|
||||
editor: typeof b.editor === 'string' ? b.editor : (b.editor?.firstname ? `${b.editor.firstname} ${b.editor.lastname || ''}`.trim() : (b.editor?.username || null)),
|
||||
editorDesignation: b.editorDesignation || null,
|
||||
tags: b.tags || [],
|
||||
metaTitle: b.metaTitle || null,
|
||||
metaDescription: b.metaDescription || null,
|
||||
keywords: b.keywords || null,
|
||||
canonicalURL: b.canonicalURL || null
|
||||
};
|
||||
});
|
||||
|
||||
if (blogs.length > 0) return blogs;
|
||||
}
|
||||
}
|
||||
} catch (apiErr) {
|
||||
console.warn('Note: Live Strapi REST API fetch bypassed/failed, trying SQLite fallback:', apiErr.message);
|
||||
}
|
||||
|
||||
// 2. Fallback: SQLite `.tmp/data.db` if Strapi API is offline
|
||||
try {
|
||||
if (!fs.existsSync(strapiDbPath)) return null;
|
||||
const db = new DatabaseSync(strapiDbPath);
|
||||
const rows = db.prepare('SELECT * FROM blogs WHERE published_at IS NOT NULL ORDER BY id ASC').all();
|
||||
const mediaLinks = db.prepare("SELECT * FROM files_related_mph WHERE related_type = 'api::blog.blog'").all();
|
||||
const files = db.prepare('SELECT * FROM files').all();
|
||||
const adminUsers = db.prepare('SELECT id, firstname, lastname FROM admin_users').all();
|
||||
|
||||
const userMap = {};
|
||||
adminUsers.forEach((u) => {
|
||||
userMap[u.id] = `${u.firstname || ''} ${u.lastname || ''}`.trim();
|
||||
});
|
||||
|
||||
const seenSlugs = new Set();
|
||||
const blogs = [];
|
||||
|
||||
rows.forEach((b) => {
|
||||
if (!b.title || b.title.toLowerCase().startsWith('test 3')) return;
|
||||
if (seenSlugs.has(b.slug)) return;
|
||||
seenSlugs.add(b.slug);
|
||||
|
||||
const link = mediaLinks.find((l) => l.related_id === b.id);
|
||||
const file = link ? files.find((f) => f.id === link.file_id) : null;
|
||||
let image = file ? file.url : null;
|
||||
|
||||
if (!image) {
|
||||
const cat = (b.category || '').toLowerCase();
|
||||
if (b.title.includes('LLM')) image = '/assets/arvindh_llm_financial.jpg';
|
||||
else if (b.title.includes('Supply Chain')) image = '/assets/arvindh_supply_chain.jpg';
|
||||
else if (b.title.includes('PRODMAX')) image = '/assets/prodmax_dashboard.png';
|
||||
else if (b.title.includes('Incidents') || b.title.includes('VR')) image = '/assets/whyus_vr_man.jpg';
|
||||
else if (b.title.includes('SCADA') || b.title.includes('IT/OT')) image = '/assets/whyus_arab_meeting.jpg';
|
||||
else if (b.title.includes('Cleanroom')) image = 'https://images.unsplash.com/photo-1592478411213-6153e4ebc07d?auto=format&fit=crop&w=800&q=80';
|
||||
else if (b.title.includes('Spatial') || b.title.includes('Maintenance')) image = '/assets/whyus_hologram.jpg';
|
||||
else if (b.title.includes('Energy')) image = '/assets/whyus_teamwork.png';
|
||||
else if (b.title.includes('Telemetry') || b.title.includes('Sensor')) image = 'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?auto=format&fit=crop&w=800&q=80';
|
||||
else if (b.title.includes('Monoliths') || b.title.includes('Micro-Frontends')) image = 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&w=800&q=80';
|
||||
else if (b.title.includes('Zero-Trust')) image = 'https://images.unsplash.com/photo-1563986768494-4dee2763ff3f?auto=format&fit=crop&w=800&q=80';
|
||||
else if (b.title.includes('Multi-Cloud')) image = 'https://images.unsplash.com/photo-1544197150-b99a580bb7a8?auto=format&fit=crop&w=800&q=80';
|
||||
else if (cat.includes('ai')) image = '/assets/arvindh_agentic_ai.jpg';
|
||||
else image = 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&w=800&q=80';
|
||||
}
|
||||
|
||||
const dateStr = b.published_at
|
||||
? new Date(Number(b.published_at)).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })
|
||||
: new Date(Number(b.created_at || Date.now())).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' });
|
||||
|
||||
const author = userMap[b.created_by_id] || 'Dr. Arvindh Ramachandran';
|
||||
const authorRole = b.author_designation || 'Chief AI Architect & Head of Data Engineering';
|
||||
const hasEditor = Boolean(b.editor_designation || (b.updated_by_id && b.updated_by_id !== b.created_by_id));
|
||||
const editor = hasEditor ? (userMap[b.updated_by_id] && userMap[b.updated_by_id] !== author ? userMap[b.updated_by_id] : 'Cavin Editorial Board') : null;
|
||||
const editorRole = b.editor_designation || (hasEditor ? 'Senior Technical Reviewer' : null);
|
||||
|
||||
blogs.push({
|
||||
id: b.id,
|
||||
category: b.category || 'Technology',
|
||||
title: b.title,
|
||||
excerpt: b.excerpt || '',
|
||||
content: b.content || '',
|
||||
readTime: b.read_time || '5 min read',
|
||||
date: dateStr,
|
||||
image,
|
||||
slug: b.slug,
|
||||
author,
|
||||
authorDesignation: authorRole,
|
||||
editor,
|
||||
editorDesignation: editorRole,
|
||||
tags: b.tags ? JSON.parse(b.tags) : []
|
||||
});
|
||||
});
|
||||
|
||||
return blogs.length > 0 ? blogs : null;
|
||||
} catch (err) {
|
||||
console.warn('Note: Strapi SQLite DB sync check bypassed:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user