updates
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -70,7 +70,6 @@ const SmoothScroll = () => {
|
||||
gestureOrientation: 'vertical',
|
||||
smoothWheel: true,
|
||||
wheelMultiplier: 1,
|
||||
smoothTouch: false,
|
||||
touchMultiplier: 2,
|
||||
infinite: false,
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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*`,
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user