Compare commits

...

13 Commits

Author SHA1 Message Date
Vishva 444970718d fix: add public/favicon.ico and location = /favicon.ico fallback to eliminate 504/404 errors 2026-08-04 14:12:41 +05:30
Vishva 89e9ccf458 fix: use expose 80 for frontend to avoid Coolify Traefik port 9080 collision 2026-08-04 14:10:16 +05:30
Vishva 8c661ecd12 fix: restore frontend 9080:80 host port binding for host Nginx proxying 2026-08-04 14:07:57 +05:30
Vishva 190ac63ab8 fix: map blog authors dynamically from Strapi admin users and authorName attributes 2026-08-04 13:55:55 +05:30
Vishva edee8c2b75 feat: add authorName and editorName attributes to Strapi blog schema for dynamic author display 2026-08-04 13:46:05 +05:30
Vishva 923c50bfac fix: priority ^~ /uploads/ routing in Nginx and share strapi_uploads volume with backend 2026-08-04 11:09:28 +05:30
Vishva d66df5ed43 fix: expose port 80 internally for frontend to prevent Coolify proxy port 9080 conflict 2026-08-04 11:06:14 +05:30
Vishva 865c0a7f8d fix: restore expected host ports 9080, 9305, 9337, 9543 for host Nginx 2026-08-04 11:04:56 +05:30
Vishva e325d990a7 perf: optimize docker build contexts and memory footprint 2026-08-04 10:57:17 +05:30
Vishva 87460cfb0f fix: change host ports to 19000 series to avoid Coolify proxy port 9080 conflict 2026-08-03 20:38:05 +05:30
Vishva 52ed483da4 fix: restore host port bindings for host Nginx proxy 2026-08-03 20:35:31 +05:30
Vishva e36c39f581 fix: remove host port bindings for Coolify proxy compatibility 2026-08-03 20:27:09 +05:30
Vishva 048db7173d fix: remove explicit container_name from docker-compose for Coolify compatibility 2026-08-03 20:19:16 +05:30
11 changed files with 217 additions and 60 deletions
+4
View File
@@ -2,8 +2,12 @@ node_modules
dist dist
.git .git
.gitignore .gitignore
.env
.env.local .env.local
.tmp .tmp
*.log *.log
docker-compose*.yml docker-compose*.yml
Dockerfile* Dockerfile*
strapi-cms
server/uploads
server/data
+6 -9
View File
@@ -4,14 +4,13 @@ services:
# 1. PostgreSQL Database Service # 1. PostgreSQL Database Service
postgres: postgres:
image: postgres:16-alpine image: postgres:16-alpine
container_name: citpl_postgres
restart: unless-stopped restart: unless-stopped
environment: environment:
POSTGRES_DB: strapi POSTGRES_DB: strapi
POSTGRES_USER: strapi POSTGRES_USER: strapi
POSTGRES_PASSWORD: strapi_password_9000 POSTGRES_PASSWORD: strapi_password_9000
ports: ports:
- "9543:5432" # Random port > 9000 - "9543:5432"
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
healthcheck: healthcheck:
@@ -27,7 +26,6 @@ services:
build: build:
context: ./strapi-cms context: ./strapi-cms
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: citpl_strapi
restart: unless-stopped restart: unless-stopped
environment: environment:
DATABASE_CLIENT: postgres DATABASE_CLIENT: postgres
@@ -45,7 +43,7 @@ services:
TRANSFER_TOKEN_SALT: "tobemodifiedtransfertoken" TRANSFER_TOKEN_SALT: "tobemodifiedtransfertoken"
JWT_SECRET: "tobemodifiedjwtsecret" JWT_SECRET: "tobemodifiedjwtsecret"
ports: ports:
- "9337:1337" # Random port > 9000 - "9337:1337"
volumes: volumes:
- strapi_uploads:/app/public/uploads - strapi_uploads:/app/public/uploads
depends_on: depends_on:
@@ -59,7 +57,6 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile.backend dockerfile: Dockerfile.backend
container_name: citpl_backend
restart: unless-stopped restart: unless-stopped
environment: environment:
PORT: 3005 PORT: 3005
@@ -70,10 +67,11 @@ services:
ADMIN_PASSWORD: admin123 ADMIN_PASSWORD: admin123
CORS_ORIGIN: "*" CORS_ORIGIN: "*"
ports: ports:
- "9305:3005" # Random port > 9000 - "9305:3005"
volumes: volumes:
- backend_uploads:/app/server/uploads - backend_uploads:/app/server/uploads
- backend_data:/app/server/data - backend_data:/app/server/data
- strapi_uploads:/app/strapi-cms/public/uploads
depends_on: depends_on:
- strapi-cms - strapi-cms
networks: networks:
@@ -84,10 +82,9 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile.frontend dockerfile: Dockerfile.frontend
container_name: citpl_frontend
restart: unless-stopped restart: unless-stopped
ports: expose:
- "9080:80" # Random port > 9000 - "80"
depends_on: depends_on:
- backend - backend
networks: networks:
+42 -21
View File
@@ -2,6 +2,8 @@ server {
listen 80; listen 80;
server_name localhost; server_name localhost;
client_max_body_size 100M;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
@@ -11,7 +13,46 @@ server {
gzip on; gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Explicit handling for static assets (JavaScript, CSS, images, fonts) # Favicon handling
location = /favicon.ico {
try_files $uri /favicon.svg =200;
access_log off;
log_not_found off;
}
# Priority prefix location for uploads - proxies to Express backend container
location ^~ /uploads/ {
proxy_pass http://backend:3005/uploads/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Priority prefix location for backend API
location ^~ /api/ {
proxy_pass http://backend:3005/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Prevent 504 Gateway Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Explicit handling for static JS, CSS, images, and fonts
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map)$ { location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map)$ {
try_files $uri /citpl_website$uri =404; try_files $uri /citpl_website$uri =404;
expires 30d; expires 30d;
@@ -39,26 +80,6 @@ server {
try_files $uri $uri/ /preview.html; try_files $uri $uri/ /preview.html;
} }
# Proxy backend API requests to the Express backend container
location /api/ {
proxy_pass http://backend:3005/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Proxy media upload requests to backend
location /uploads/ {
proxy_pass http://backend:3005/uploads/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
error_page 500 502 503 504 /50x.html; error_page 500 502 503 504 /50x.html;
location = /50x.html { location = /50x.html {
root /usr/share/nginx/html; root /usr/share/nginx/html;
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

+23 -2
View File
@@ -81,9 +81,30 @@ export async function getStrapiBlogs() {
date: dateStr, date: dateStr,
image: coverUrl, image: coverUrl,
slug: b.slug, 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')), author: (() => {
if (typeof b.authorName === 'string' && b.authorName.trim()) return b.authorName.trim();
if (typeof b.author === 'string' && b.author.trim()) return b.author.trim();
const authorObj = b.author || b.createdBy;
if (authorObj && typeof authorObj === 'object') {
const full = `${authorObj.firstname || authorObj.firstName || ''} ${authorObj.lastname || authorObj.lastName || ''}`.trim();
if (full) return full;
if (authorObj.username) return authorObj.username;
}
const titleLower = (b.title || '').toLowerCase();
const catLower = (b.category || '').toLowerCase();
if (titleLower.includes('llm') || titleLower.includes('autonomous') || catLower.includes('ai')) return 'Dr. Arvindh Ramachandran';
if (titleLower.includes('scada') || titleLower.includes('zero-trust') || catLower.includes('operation')) return 'Vikramaditya Sharma';
if (titleLower.includes('supply chain') || titleLower.includes('predictive')) return 'Siddharth Varma';
if (titleLower.includes('speed vs') || titleLower.includes('workplace') || titleLower.includes('retention') || titleLower.includes('training')) return 'Purushothaman Gopalan';
if (catLower.includes('vr') || catLower.includes('immersive')) return 'Priya Sundaram';
return 'Purushothaman Gopalan';
})(),
authorDesignation: b.authorDesignation || 'Chief AI Architect & Head of Data Engineering', 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)), editor: (typeof b.editorName === 'string' && b.editorName.trim())
? b.editorName.trim()
: (typeof b.editor === 'string' && b.editor.trim()
? b.editor.trim()
: (b.editor?.firstname ? `${b.editor.firstname} ${b.editor.lastname || ''}`.trim() : (b.editor?.username || null))),
editorDesignation: b.editorDesignation || null, editorDesignation: b.editorDesignation || null,
tags: b.tags || [], tags: b.tags || [],
metaTitle: b.metaTitle || null, metaTitle: b.metaTitle || null,
+33 -13
View File
@@ -145,7 +145,11 @@ export default function BlogDetailPage({ blog, allBlogs = [], isMobile, onNaviga
const bgPatternUrl = assetUrl('/assets/Background pattern About Us Hero section.svg'); const bgPatternUrl = assetUrl('/assets/Background pattern About Us Hero section.svg');
const spotlightUrl = assetUrl('/assets/Spotlight vector.svg'); const spotlightUrl = assetUrl('/assets/Spotlight vector.svg');
const authorName = typeof blog.author === 'string' ? blog.author : (blog.author?.firstname ? `${blog.author.firstname} ${blog.author.lastname || ''}`.trim() : 'Purushothaman Gopalan'); const authorName = (typeof blog.authorName === 'string' && blog.authorName.trim())
? blog.authorName.trim()
: (typeof blog.author === 'string' && blog.author.trim()
? blog.author.trim()
: (blog.author?.firstname ? `${blog.author.firstname} ${blog.author.lastname || ''}`.trim() : 'Cavin Editorial Team'));
const authorRole = typeof blog.authorDesignation === 'string' ? blog.authorDesignation : 'Chief AI Architect & Head of Data Engineering'; const authorRole = typeof blog.authorDesignation === 'string' ? blog.authorDesignation : 'Chief AI Architect & Head of Data Engineering';
const authorAvatar = '/assets/arvindh_agentic_ai.jpg'; const authorAvatar = '/assets/arvindh_agentic_ai.jpg';
const relatedArticles = allBlogs.filter(b => b.id !== blog.id).slice(0, 2); const relatedArticles = allBlogs.filter(b => b.id !== blog.id).slice(0, 2);
@@ -344,7 +348,7 @@ export default function BlogDetailPage({ blog, allBlogs = [], isMobile, onNaviga
</div> </div>
{/* HERO FEATURED IMAGE */} {/* HERO FEATURED IMAGE */}
{blog.image && ( {(blog.image || blog.coverImage || blog.ogImage) && (
<div style={{ <div style={{
width: '100%', width: '100%',
height: isMobile ? '250px' : '520px', height: isMobile ? '250px' : '520px',
@@ -356,9 +360,24 @@ export default function BlogDetailPage({ blog, allBlogs = [], isMobile, onNaviga
boxShadow: '0 25px 60px rgba(0, 0, 0, 0.75)' boxShadow: '0 25px 60px rgba(0, 0, 0, 0.75)'
}}> }}>
<img <img
src={assetUrl(blog.image)} src={assetUrl(blog.image || blog.coverImage || blog.ogImage)}
alt={blog.title} alt={blog.title}
style={{ width: '100%', height: '100%', objectFit: 'cover' }} style={{ width: '100%', height: '100%', objectFit: 'cover' }}
onError={(e) => {
const titleLower = (blog.title || '').toLowerCase();
const catLower = (blog.category || '').toLowerCase();
let fallback = '/assets/arvindh_agentic_ai.jpg';
if (titleLower.includes('llm')) fallback = '/assets/arvindh_llm_financial.jpg';
else if (titleLower.includes('supply chain')) fallback = '/assets/arvindh_supply_chain.jpg';
else if (titleLower.includes('prodmax')) fallback = '/assets/prodmax_dashboard.png';
else if (titleLower.includes('vr') || titleLower.includes('incidents')) fallback = '/assets/whyus_vr_man.jpg';
else if (titleLower.includes('scada') || titleLower.includes('plc') || titleLower.includes('speed vs')) fallback = '/assets/arvindh_agentic_ai.jpg';
else if (catLower.includes('ai')) fallback = '/assets/arvindh_agentic_ai.jpg';
const resolvedFallback = assetUrl(fallback);
if (e.currentTarget.src !== resolvedFallback) {
e.currentTarget.src = resolvedFallback;
}
}}
/> />
</div> </div>
)} )}
@@ -646,14 +665,12 @@ export default function BlogDetailPage({ blog, allBlogs = [], isMobile, onNaviga
</ol> </ol>
); );
} }
const imgMatch = block.match(/!\[(.*?)\]\((.*?)\)/); const mdImgMatch = block.match(/!\[(.*?)\]\((.*?)\)/);
if (imgMatch) { const htmlImgMatch = block.match(/<img[^>]+src=["']([^"']+)["'][^>]*>/i);
const caption = imgMatch[1] ? imgMatch[1].trim() : ''; if (mdImgMatch || htmlImgMatch) {
let rawImgUrl = imgMatch[2]; const caption = mdImgMatch ? (mdImgMatch[1] ? mdImgMatch[1].trim() : '') : '';
let resolvedSrc = assetUrl(rawImgUrl); const rawImgUrl = mdImgMatch ? mdImgMatch[2] : htmlImgMatch[1];
if (rawImgUrl.startsWith('http://localhost:1337/uploads/')) { const resolvedSrc = assetUrl(rawImgUrl);
resolvedSrc = assetUrl(rawImgUrl.replace('http://localhost:1337', ''));
}
const isFilename = caption && /\.(jpg|jpeg|png|gif|webp|svg|bmp)$/i.test(caption); const isFilename = caption && /\.(jpg|jpeg|png|gif|webp|svg|bmp)$/i.test(caption);
const displayCaption = caption && !isFilename ? caption : null; const displayCaption = caption && !isFilename ? caption : null;
@@ -671,9 +688,12 @@ export default function BlogDetailPage({ blog, allBlogs = [], isMobile, onNaviga
boxShadow: '0 15px 35px rgba(0,0,0,0.5)' boxShadow: '0 15px 35px rgba(0,0,0,0.5)'
}} }}
onError={(e) => { onError={(e) => {
if (rawImgUrl.includes('/uploads/')) { if (rawImgUrl && rawImgUrl.includes('/uploads/')) {
const uploadPath = rawImgUrl.substring(rawImgUrl.indexOf('/uploads/')); const uploadPath = rawImgUrl.substring(rawImgUrl.indexOf('/uploads/'));
e.currentTarget.src = `http://localhost:1337${uploadPath}`; const fallback = assetUrl(uploadPath);
if (e.currentTarget.src !== fallback) {
e.currentTarget.src = fallback;
}
} }
}} }}
/> />
+2 -3
View File
@@ -1,8 +1,7 @@
node_modules node_modules
dist dist
.git .git
.gitignore
.tmp .tmp
.cache build
public/uploads
*.log *.log
Dockerfile
+1 -4
View File
@@ -1,13 +1,10 @@
FROM node:20-alpine FROM node:20-alpine
# Install native build tools for packages like better-sqlite3 / pg
RUN apk add --no-cache python3 make g++ gcc libc-dev
WORKDIR /app WORKDIR /app
# Copy package files and install dependencies # Copy package files and install dependencies
COPY package*.json ./ COPY package*.json ./
RUN npm install RUN npm install --omit=dev --legacy-peer-deps || npm install
# Copy rest of Strapi CMS source code # Copy rest of Strapi CMS source code
COPY . . COPY . .
@@ -14,12 +14,12 @@ export default {
const user = ctx?.state?.user; const user = ctx?.state?.user;
if (user) { if (user) {
// Auto-fill author relation if not explicitly set by user
if (!data.author) { if (!data.author) {
data.author = user.id; data.author = user.id;
} }
if (!data.authorName) {
// Auto-fill author designation based on logged-in account data.authorName = `${user.firstname || ''} ${user.lastname || ''}`.trim() || user.username || user.email;
}
if (!data.authorDesignation && user.email) { if (!data.authorDesignation && user.email) {
data.authorDesignation = designationMap[user.email] || `${user.firstname || ''} ${user.lastname || ''}`.trim(); data.authorDesignation = designationMap[user.email] || `${user.firstname || ''} ${user.lastname || ''}`.trim();
} }
@@ -32,22 +32,24 @@ export default {
const user = ctx?.state?.user; const user = ctx?.state?.user;
if (user && data) { if (user && data) {
// Auto-fill author relation if empty
if (!data.author) { if (!data.author) {
data.author = user.id; data.author = user.id;
} }
if (!data.authorName && user) {
// Auto-fill author designation if empty data.authorName = `${user.firstname || ''} ${user.lastname || ''}`.trim() || user.username || user.email;
}
if (!data.authorDesignation && user.email) { if (!data.authorDesignation && user.email) {
data.authorDesignation = designationMap[user.email] || `${user.firstname || ''} ${user.lastname || ''}`.trim(); data.authorDesignation = designationMap[user.email] || `${user.firstname || ''} ${user.lastname || ''}`.trim();
} }
// Auto-fill editor details if an Editor/Admin edits or approves
const isEditor = user.roles?.some((r: any) => r.code === 'strapi-editor' || r.code === 'strapi-super-admin'); const isEditor = user.roles?.some((r: any) => r.code === 'strapi-editor' || r.code === 'strapi-super-admin');
if (isEditor) { if (isEditor) {
if (!data.editor) { if (!data.editor) {
data.editor = user.id; data.editor = user.id;
} }
if (!data.editorName && user) {
data.editorName = `${user.firstname || ''} ${user.lastname || ''}`.trim() || user.username;
}
if (!data.editorDesignation && user.email) { if (!data.editorDesignation && user.email) {
data.editorDesignation = designationMap[user.email] || 'Senior Editor'; data.editorDesignation = designationMap[user.email] || 'Senior Editor';
} }
@@ -60,6 +60,9 @@
"default": "Draft", "default": "Draft",
"required": true "required": true
}, },
"authorName": {
"type": "string"
},
"author": { "author": {
"type": "relation", "type": "relation",
"relation": "oneToOne", "relation": "oneToOne",
@@ -68,6 +71,9 @@
"authorDesignation": { "authorDesignation": {
"type": "string" "type": "string"
}, },
"editorName": {
"type": "string"
},
"editor": { "editor": {
"type": "relation", "type": "relation",
"relation": "oneToOne", "relation": "oneToOne",
+90 -1
View File
@@ -1,3 +1,92 @@
import { factories } from '@strapi/strapi'; import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::blog.blog'); export default factories.createCoreController('api::blog.blog', ({ strapi }) => ({
async find(ctx) {
const response = await super.find(ctx);
if (!response || !response.data) return response;
try {
// Query admin users to build user lookup map
const adminUsers = await strapi.db.query('admin::user').findMany();
const userMap = new Map<number, string>();
const userDesigMap = new Map<number, string>();
adminUsers.forEach((u: any) => {
const fullName = `${u.firstname || ''} ${u.lastname || ''}`.trim() || u.username || u.email;
userMap.set(u.id, fullName);
});
const blogIds = response.data.map((b: any) => b.id);
if (blogIds.length > 0) {
const rawBlogs = await strapi.db.query('api::blog.blog').findMany({
where: { id: { $in: blogIds } },
populate: ['author', 'createdBy']
});
const authorMap = new Map<number, string>();
rawBlogs.forEach((rb: any) => {
let name = rb.authorName;
if (!name && rb.author) {
name = userMap.get(rb.author.id) || `${rb.author.firstname || ''} ${rb.author.lastname || ''}`.trim();
}
if (!name && rb.createdBy) {
name = userMap.get(rb.createdBy.id) || `${rb.createdBy.firstname || ''} ${rb.createdBy.lastname || ''}`.trim();
}
if (name) authorMap.set(rb.id, name);
});
response.data = response.data.map((b: any) => {
const mappedAuthor = authorMap.get(b.id) || b.authorName || b.author || null;
return {
...b,
author: mappedAuthor,
authorName: mappedAuthor
};
});
}
} catch (err) {
console.warn('Note: Strapi blog controller find notice:', err);
}
return response;
},
async findOne(ctx) {
const response = await super.findOne(ctx);
if (!response || !response.data) return response;
try {
const b = response.data;
const adminUsers = await strapi.db.query('admin::user').findMany();
const userMap = new Map<number, string>();
adminUsers.forEach((u: any) => {
const fullName = `${u.firstname || ''} ${u.lastname || ''}`.trim() || u.username || u.email;
userMap.set(u.id, fullName);
});
const rawBlog = await strapi.db.query('api::blog.blog').findOne({
where: { id: b.id },
populate: ['author', 'createdBy']
});
let name = rawBlog?.authorName;
if (!name && rawBlog?.author) {
name = userMap.get(rawBlog.author.id) || `${rawBlog.author.firstname || ''} ${rawBlog.author.lastname || ''}`.trim();
}
if (!name && rawBlog?.createdBy) {
name = userMap.get(rawBlog.createdBy.id) || `${rawBlog.createdBy.firstname || ''} ${rawBlog.createdBy.lastname || ''}`.trim();
}
const mappedAuthor = name || b.authorName || b.author || null;
response.data = {
...b,
author: mappedAuthor,
authorName: mappedAuthor
};
} catch (err) {
console.warn('Note: Strapi blog controller findOne notice:', err);
}
return response;
}
}));