This commit is contained in:
Iliyan Angelov
2025-11-25 02:06:38 +02:00
parent 2f6dca736a
commit 82024016cd
37 changed files with 1800 additions and 1478 deletions

View File

@@ -1,26 +0,0 @@
node_modules
.next
.git
.gitignore
*.log
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
.vscode
.idea
*.swp
*.swo
*~
coverage
.nyc_output
dist
build
README.md
*.md

View File

@@ -1,50 +0,0 @@
# Next.js Frontend Dockerfile
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Copy package files
COPY package*.json ./
RUN npm ci
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Set environment variables for build
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
# Build Next.js
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy necessary files from builder
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 1087
ENV PORT=1087
ENV HOSTNAME="0.0.0.0"
# Use the standalone server
CMD ["node", "server.js"]

View File

@@ -19,6 +19,11 @@ interface ServicePageProps {
}>;
}
// Force static generation - pages are pre-rendered at build time
export const dynamic = 'force-static';
export const dynamicParams = false; // Return 404 for unknown slugs
export const revalidate = false; // Never revalidate - fully static
// Generate static params for all services (optional - for better performance)
export async function generateStaticParams() {
try {
@@ -27,6 +32,7 @@ export async function generateStaticParams() {
slug: service.slug,
}));
} catch (error) {
console.error('Error generating static params for services:', error);
return [];
}
}

View File

@@ -28,7 +28,8 @@ const SupportCenterPage = () => {
url: "/support-center",
});
document.title = metadata.title || "Support Center | GNX Soft";
const titleString = typeof metadata.title === 'string' ? metadata.title : "Support Center | GNX Soft";
document.title = titleString;
let metaDescription = document.querySelector('meta[name="description"]');
if (!metaDescription) {

View File

@@ -12,6 +12,8 @@ const Process = ({ slug }: ProcessProps) => {
return null;
}
const processSteps = caseStudy.process_steps;
return (
<section className="case-study-process luxury-process pt-120 pb-120">
<div className="container">
@@ -28,7 +30,7 @@ const Process = ({ slug }: ProcessProps) => {
</div>
<div className="col-12 col-lg-7">
<div className="process-steps-list">
{caseStudy.process_steps.map((step, index) => (
{processSteps.map((step, index) => (
<div key={step.id} className="process-step-item">
<div className="step-number">
{String(step.step_number).padStart(2, '0')}
@@ -37,7 +39,7 @@ const Process = ({ slug }: ProcessProps) => {
<h4 className="step-title">{step.title}</h4>
<p className="step-description">{step.description}</p>
</div>
{index < caseStudy.process_steps.length - 1 && (
{index < processSteps.length - 1 && (
<div className="step-connector"></div>
)}
</div>

View File

@@ -27,7 +27,7 @@ const KnowledgeBase = () => {
const filtered = allArticles.filter(article =>
article.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
article.summary.toLowerCase().includes(searchTerm.toLowerCase()) ||
article.content.toLowerCase().includes(searchTerm.toLowerCase())
(article.content && article.content.toLowerCase().includes(searchTerm.toLowerCase()))
);
return {
displayArticles: filtered,

View File

@@ -70,7 +70,6 @@ const SmoothScroll = () => {
gestureOrientation: 'vertical',
smoothWheel: true,
wheelMultiplier: 1,
smoothTouch: false,
touchMultiplier: 2,
infinite: false,
});

View File

@@ -1,4 +1,4 @@
import { API_CONFIG } from '../config/api';
import { API_CONFIG, getApiHeaders } from '../config/api';
// Types for Service API
export interface ServiceFeature {
@@ -104,9 +104,7 @@ export const serviceService = {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
headers: getApiHeaders(),
});
if (!response.ok) {
@@ -134,9 +132,7 @@ export const serviceService = {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
headers: getApiHeaders(),
});
if (!response.ok) {
@@ -164,9 +160,7 @@ export const serviceService = {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
headers: getApiHeaders(),
});
if (!response.ok) {
@@ -194,9 +188,7 @@ export const serviceService = {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
headers: getApiHeaders(),
});
if (!response.ok) {
@@ -224,9 +216,7 @@ export const serviceService = {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
headers: getApiHeaders(),
});
if (!response.ok) {
@@ -254,9 +244,7 @@ export const serviceService = {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
headers: getApiHeaders(),
});
if (!response.ok) {
@@ -284,9 +272,7 @@ export const serviceService = {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
headers: getApiHeaders(),
});
if (!response.ok) {
@@ -442,21 +428,30 @@ export const serviceUtils = {
},
// Get service image URL
// Use relative URLs for same-domain images (Next.js can optimize via rewrites)
// Use absolute URLs only for external images
getServiceImageUrl: (service: Service): string => {
// If service has an uploaded image
if (service.image && typeof service.image === 'string' && service.image.startsWith('/media/')) {
return `${API_CONFIG.BASE_URL}${service.image}`;
// Use relative URL - Next.js rewrite will handle fetching from backend during optimization
return service.image;
}
// If service has an image_url
if (service.image_url) {
if (service.image_url.startsWith('http')) {
// External URL - keep as absolute
return service.image_url;
}
return `${API_CONFIG.BASE_URL}${service.image_url}`;
if (service.image_url.startsWith('/media/')) {
// Same domain media - use relative URL
return service.image_url;
}
// Other relative URLs
return service.image_url;
}
// Fallback to default image
// Fallback to default image (relative is fine for public images)
return '/images/service/default.png';
},

View File

@@ -6,17 +6,62 @@
* In Production: Uses Next.js rewrites/nginx proxy at /api (internal network only)
*/
// Production: Use relative URLs (nginx proxy)
// Development: Use full backend URL
// Docker: Use backend service name or port 1086
// Production: Use relative URLs (nginx proxy) for client-side
// For server-side (SSR), use internal backend URL or public domain
const isProduction = process.env.NODE_ENV === 'production';
const isDocker = process.env.DOCKER_ENV === 'true';
export const API_BASE_URL = isDocker
? (process.env.NEXT_PUBLIC_API_URL || 'http://backend:1086')
: isProduction
? '' // Use relative URLs in production (proxied by nginx)
: (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000');
// Detect if we're on the server (Node.js) or client (browser)
const isServer = typeof window === 'undefined';
// For server-side rendering, we need an absolute URL
// During build time, use internal backend URL directly (faster, no SSL issues)
// At runtime, use public domain (goes through nginx which adds API key header)
const getServerApiUrl = () => {
if (isProduction) {
// Check if we're in build context (no access to window, and NEXT_PHASE might be set)
// During build, use internal backend URL directly
// At runtime (SSR), use public domain through nginx
const isBuildTime = process.env.NEXT_PHASE === 'phase-production-build' ||
!process.env.NEXT_RUNTIME;
if (isBuildTime) {
// Build time: use internal backend URL directly
return process.env.INTERNAL_API_URL || 'http://127.0.0.1:1086';
} else {
// Runtime SSR: use public domain - nginx will proxy and add API key header
return process.env.NEXT_PUBLIC_SITE_URL || 'https://gnxsoft.com';
}
}
return process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
};
// For client-side, use relative URLs in production (proxied by nginx)
// For server-side, use absolute URLs
export const API_BASE_URL = isServer
? getServerApiUrl() // Server-side: absolute URL
: (isProduction
? '' // Client-side production: relative URLs (proxied by nginx)
: (process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086')); // Development: direct backend
// Internal API key for server-side requests (must match backend INTERNAL_API_KEY)
// This is only used when calling backend directly (build time or internal requests)
export const INTERNAL_API_KEY = process.env.INTERNAL_API_KEY || '9hZtPwyScigoBAl59Uvcz_9VztSRC6Zt_6L1B2xTM2M';
// Helper to get headers for API requests
// Adds API key header when calling internal backend directly
export const getApiHeaders = (): Record<string, string> => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
// If we're calling the internal backend directly (not through nginx),
// add the API key header
if (isServer && API_BASE_URL.includes('127.0.0.1:1086')) {
headers['X-Internal-API-Key'] = INTERNAL_API_KEY;
}
return headers;
};
export const API_CONFIG = {
// Django API Base URL

View File

@@ -1,8 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
// Enable standalone output for Docker
// Enable standalone output for optimized production deployment
output: 'standalone',
images: {
// Enable image optimization in standalone mode
unoptimized: false,
remotePatterns: [
{
protocol: 'http',
@@ -33,15 +35,60 @@ const nextConfig = {
hostname: 'images.unsplash.com',
pathname: '/**',
},
// Add your production domain when ready
// {
// protocol: 'https',
// hostname: 'your-api-domain.com',
// pathname: '/media/**',
// },
// Production domain configuration
{
protocol: 'https',
hostname: 'gnxsoft.com',
pathname: '/media/**',
},
{
protocol: 'https',
hostname: 'gnxsoft.com',
pathname: '/images/**',
},
{
protocol: 'https',
hostname: 'gnxsoft.com',
pathname: '/_next/static/**',
},
{
protocol: 'http',
hostname: 'gnxsoft.com',
pathname: '/media/**',
},
{
protocol: 'http',
hostname: 'gnxsoft.com',
pathname: '/images/**',
},
{
protocol: 'https',
hostname: 'www.gnxsoft.com',
pathname: '/media/**',
},
{
protocol: 'https',
hostname: 'www.gnxsoft.com',
pathname: '/images/**',
},
{
protocol: 'https',
hostname: 'www.gnxsoft.com',
pathname: '/_next/static/**',
},
{
protocol: 'http',
hostname: 'www.gnxsoft.com',
pathname: '/media/**',
},
{
protocol: 'http',
hostname: 'www.gnxsoft.com',
pathname: '/images/**',
},
],
// Legacy domains format for additional compatibility
domains: ['images.unsplash.com'],
domains: ['images.unsplash.com', 'gnxsoft.com', 'www.gnxsoft.com'],
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
@@ -99,7 +146,7 @@ const nextConfig = {
},
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: http://localhost:8000 http://localhost:8080; font-src 'self' data:; connect-src 'self' http://localhost:8000 https://www.google-analytics.com; frame-src 'self' https://www.google.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'"
value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https: http://localhost:8000 http://localhost:8080; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' http://localhost:8000 https://www.google-analytics.com; frame-src 'self' https://www.google.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'"
},
// Performance Headers
{
@@ -153,7 +200,6 @@ const nextConfig = {
// Rewrites for API proxy (Production: routes /api to backend through nginx)
async rewrites() {
// In development, proxy to Django backend
// In production, nginx handles this
if (process.env.NODE_ENV === 'development') {
return [
{
@@ -166,8 +212,14 @@ const nextConfig = {
},
]
}
// In production, these are handled by nginx reverse proxy
return []
// In production, add rewrite for media files so Next.js image optimization can access them
// This allows Next.js to fetch media images from the internal backend during optimization
return [
{
source: '/media/:path*',
destination: `${process.env.INTERNAL_API_URL || 'http://127.0.0.1:1086'}/media/:path*`,
},
]
},
}