updates
This commit is contained in:
@@ -30,14 +30,35 @@ export interface PolicyListItem {
|
||||
}
|
||||
|
||||
class PolicyServiceAPI {
|
||||
private baseUrl = `${API_BASE_URL}/api/policies`;
|
||||
private getBaseUrl(): string {
|
||||
// Safely get base URL, handling both server and client environments
|
||||
try {
|
||||
const base = API_BASE_URL || '';
|
||||
if (base) {
|
||||
return `${base}/api/policies`;
|
||||
}
|
||||
// Fallback for SSR or when API_BASE_URL is not available
|
||||
if (typeof window !== 'undefined') {
|
||||
// Client-side: use relative URL (proxied by nginx)
|
||||
return '/api/policies';
|
||||
}
|
||||
// Server-side: use environment variable or fallback
|
||||
return `${process.env.NEXT_PUBLIC_SITE_URL || 'https://gnxsoft.com'}/api/policies`;
|
||||
} catch (error) {
|
||||
// Ultimate fallback
|
||||
if (typeof window !== 'undefined') {
|
||||
return '/api/policies';
|
||||
}
|
||||
return 'https://gnxsoft.com/api/policies';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all policies
|
||||
*/
|
||||
async getPolicies(): Promise<PolicyListItem[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/`, {
|
||||
const response = await fetch(`${this.getBaseUrl()}/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -60,20 +81,36 @@ class PolicyServiceAPI {
|
||||
*/
|
||||
async getPolicyByType(type: 'privacy' | 'terms' | 'support'): Promise<Policy> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/${type}/`, {
|
||||
const baseUrl = this.getBaseUrl();
|
||||
const url = `${baseUrl}/${type}/`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
// Add cache control for client-side requests
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const errorText = await response.text().catch(() => 'Unknown error');
|
||||
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Validate response structure
|
||||
if (!data || typeof data !== 'object') {
|
||||
throw new Error('Invalid response format from API');
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
// Log error for debugging (only on client side)
|
||||
if (typeof window !== 'undefined') {
|
||||
console.error(`Error fetching policy type "${type}":`, error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -83,7 +120,7 @@ class PolicyServiceAPI {
|
||||
*/
|
||||
async getPolicyById(id: number): Promise<Policy> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/${id}/`, {
|
||||
const response = await fetch(`${this.getBaseUrl()}/${id}/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -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) {
|
||||
@@ -441,23 +427,48 @@ export const serviceUtils = {
|
||||
}).format(numPrice);
|
||||
},
|
||||
|
||||
// Get service image URL
|
||||
// Get service image URL with cache-busting
|
||||
// Use relative URLs for same-domain images (Next.js can optimize via rewrites)
|
||||
// Use absolute URLs only for external images
|
||||
// Adds updated_at timestamp as query parameter for cache-busting when images change
|
||||
getServiceImageUrl: (service: Service): string => {
|
||||
let imageUrl: 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}`;
|
||||
imageUrl = service.image;
|
||||
}
|
||||
|
||||
// If service has an image_url
|
||||
if (service.image_url) {
|
||||
else if (service.image_url) {
|
||||
if (service.image_url.startsWith('http')) {
|
||||
return service.image_url;
|
||||
// External URL - keep as absolute
|
||||
imageUrl = service.image_url;
|
||||
} else if (service.image_url.startsWith('/media/')) {
|
||||
// Same domain media - use relative URL
|
||||
imageUrl = service.image_url;
|
||||
} else {
|
||||
// Other relative URLs
|
||||
imageUrl = service.image_url;
|
||||
}
|
||||
return `${API_CONFIG.BASE_URL}${service.image_url}`;
|
||||
} else {
|
||||
// Fallback to default image (relative is fine for public images)
|
||||
imageUrl = '/images/service/default.png';
|
||||
}
|
||||
|
||||
// Fallback to default image
|
||||
return '/images/service/default.png';
|
||||
// Add cache-busting query parameter using updated_at timestamp
|
||||
// This ensures images refresh when service is updated
|
||||
if (service.updated_at && imageUrl && !imageUrl.includes('?')) {
|
||||
try {
|
||||
const timestamp = new Date(service.updated_at).getTime();
|
||||
const separator = imageUrl.includes('?') ? '&' : '?';
|
||||
imageUrl = `${imageUrl}${separator}v=${timestamp}`;
|
||||
} catch (error) {
|
||||
// If date parsing fails, just return the URL without cache-busting
|
||||
console.warn('Failed to parse updated_at for cache-busting:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
},
|
||||
|
||||
// Generate service slug from title
|
||||
|
||||
@@ -6,17 +6,123 @@
|
||||
* 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)
|
||||
// SECURITY: Never hardcode API keys in production - always use environment variables
|
||||
const getInternalApiKey = (): string => {
|
||||
const apiKey = process.env.INTERNAL_API_KEY;
|
||||
|
||||
// Check if we're in build phase (Next.js build context)
|
||||
// During build, NEXT_RUNTIME is typically not set
|
||||
// Also check for specific build phases
|
||||
const isBuildTime =
|
||||
!process.env.NEXT_RUNTIME || // Most reliable indicator - not set during build
|
||||
process.env.NEXT_PHASE === 'phase-production-build' ||
|
||||
process.env.NEXT_PHASE === 'phase-production-compile' ||
|
||||
process.env.NEXT_PHASE === 'phase-development-build';
|
||||
|
||||
if (!apiKey) {
|
||||
// During build time, be lenient - allow build to proceed
|
||||
// The key will be validated when actually used (in getApiHeaders)
|
||||
if (isBuildTime) {
|
||||
// Build time: allow fallback (will be validated when actually used)
|
||||
return 'build-time-fallback-key';
|
||||
}
|
||||
|
||||
// Runtime production: require the key (but only validate when actually used)
|
||||
if (isProduction) {
|
||||
// Don't throw here - validate lazily in getApiHeaders when actually needed
|
||||
// This allows the build to complete even if key is missing
|
||||
return 'runtime-requires-key';
|
||||
}
|
||||
|
||||
// Development fallback (only for local development)
|
||||
return 'dev-key-not-for-production';
|
||||
}
|
||||
|
||||
return apiKey;
|
||||
};
|
||||
|
||||
// Lazy getter - only evaluates when accessed
|
||||
let _internalApiKey: string | null = null;
|
||||
export const getInternalApiKeyLazy = (): string => {
|
||||
if (_internalApiKey === null) {
|
||||
_internalApiKey = getInternalApiKey();
|
||||
}
|
||||
return _internalApiKey;
|
||||
};
|
||||
|
||||
// For backward compatibility - evaluates at module load but uses lenient validation
|
||||
export const INTERNAL_API_KEY = getInternalApiKeyLazy();
|
||||
|
||||
// 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 (lazy evaluation - only when actually needed)
|
||||
if (isServer && API_BASE_URL.includes('127.0.0.1:1086')) {
|
||||
const apiKey = getInternalApiKeyLazy();
|
||||
|
||||
// Validate API key when actually used (not at module load time)
|
||||
if (apiKey === 'runtime-requires-key' && isProduction) {
|
||||
const actualKey = process.env.INTERNAL_API_KEY;
|
||||
if (!actualKey) {
|
||||
throw new Error(
|
||||
'INTERNAL_API_KEY environment variable is required in production runtime. ' +
|
||||
'Set it in your .env.production file or environment variables.'
|
||||
);
|
||||
}
|
||||
// Update cached value
|
||||
_internalApiKey = actualKey;
|
||||
headers['X-Internal-API-Key'] = actualKey;
|
||||
} else {
|
||||
headers['X-Internal-API-Key'] = apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const API_CONFIG = {
|
||||
// Django API Base URL
|
||||
|
||||
@@ -63,13 +63,23 @@ export const usePolicy = (type: 'privacy' | 'terms' | 'support' | null): UsePoli
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't fetch on server side
|
||||
if (typeof window === 'undefined') {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const result = await getPolicyByType(type);
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('An error occurred'));
|
||||
const errorMessage = err instanceof Error ? err.message : 'An error occurred while loading the policy';
|
||||
console.error('Policy fetch error:', err);
|
||||
setError(new Error(errorMessage));
|
||||
// Set data to null on error to prevent rendering issues
|
||||
setData(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,40 @@ export const FALLBACK_IMAGES = {
|
||||
DEFAULT: '/images/logo.png'
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the correct image URL for the current environment
|
||||
*
|
||||
* During build: Use internal backend URL so Next.js can fetch images
|
||||
* During runtime (client): Use relative URLs (nginx serves media files)
|
||||
* During runtime (server/SSR): Use relative URLs (nginx serves media files)
|
||||
* In development: Use API_BASE_URL (which points to backend)
|
||||
*/
|
||||
function getImageBaseUrl(): string {
|
||||
const isServer = typeof window === 'undefined';
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
// Check if we're in build phase (Next.js build context)
|
||||
const isBuildTime =
|
||||
!process.env.NEXT_RUNTIME || // Not set during build
|
||||
process.env.NEXT_PHASE === 'phase-production-build' ||
|
||||
process.env.NEXT_PHASE === 'phase-production-compile';
|
||||
|
||||
// During build time in production: use internal backend URL
|
||||
// This allows Next.js to fetch images during static generation
|
||||
if (isProduction && isBuildTime && isServer) {
|
||||
return process.env.INTERNAL_API_URL || 'http://127.0.0.1:1086';
|
||||
}
|
||||
|
||||
// Runtime (both client and server): use relative URLs
|
||||
// Nginx will serve /media/ files directly
|
||||
if (isProduction) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Development: use API_BASE_URL (which points to backend)
|
||||
return API_BASE_URL;
|
||||
}
|
||||
|
||||
export function getValidImageUrl(imageUrl?: string, fallback?: string): string {
|
||||
if (!imageUrl || imageUrl.trim() === '') {
|
||||
return fallback || FALLBACK_IMAGES.DEFAULT;
|
||||
@@ -20,22 +54,28 @@ export function getValidImageUrl(imageUrl?: string, fallback?: string): string {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// If it starts with /media/, it's a Django media file - prepend API base URL
|
||||
// Get the base URL for images (handles client/server differences)
|
||||
const baseUrl = getImageBaseUrl();
|
||||
|
||||
// If it starts with /media/, it's a Django media file
|
||||
if (imageUrl.startsWith('/media/')) {
|
||||
return `${API_BASE_URL}${imageUrl}`;
|
||||
// In production client-side, baseUrl is empty, so this becomes /media/... (correct)
|
||||
// In production server-side, baseUrl is the domain, so this becomes https://domain.com/media/... (correct)
|
||||
return `${baseUrl}${imageUrl}`;
|
||||
}
|
||||
|
||||
// If it starts with /images/, it's a local public file
|
||||
// If it starts with /images/, it's a local public file (always relative)
|
||||
if (imageUrl.startsWith('/images/')) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// If it starts with /, check if it's a media file
|
||||
if (imageUrl.startsWith('/')) {
|
||||
// If it contains /media/, prepend API base URL
|
||||
// If it contains /media/, prepend base URL
|
||||
if (imageUrl.includes('/media/')) {
|
||||
return `${API_BASE_URL}${imageUrl}`;
|
||||
return `${baseUrl}${imageUrl}`;
|
||||
}
|
||||
// Other absolute paths (like /static/) are served directly
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ export const SITE_CONFIG = {
|
||||
country: 'Bulgaria',
|
||||
},
|
||||
social: {
|
||||
linkedin: 'https://www.linkedin.com/company/gnxtech',
|
||||
github: 'https://github.com/gnxtech',
|
||||
linkedin: 'https://linkedin.com',
|
||||
github: 'https://github.com',
|
||||
},
|
||||
businessHours: 'Monday - Friday: 9:00 AM - 6:00 PM PST',
|
||||
foundedYear: 2020,
|
||||
@@ -90,6 +90,15 @@ export function generateMetadata({
|
||||
const pageUrl = url ? `${SITE_CONFIG.url}${url}` : SITE_CONFIG.url;
|
||||
const allKeywords = [...DEFAULT_SEO.keywords, ...keywords];
|
||||
|
||||
// Safely create metadataBase URL
|
||||
let metadataBase: URL;
|
||||
try {
|
||||
metadataBase = new URL(SITE_CONFIG.url);
|
||||
} catch (e) {
|
||||
// Fallback to a default URL if SITE_CONFIG.url is invalid
|
||||
metadataBase = new URL('https://gnxsoft.com');
|
||||
}
|
||||
|
||||
const metadata: Metadata = {
|
||||
title: pageTitle,
|
||||
description: pageDescription,
|
||||
@@ -112,7 +121,7 @@ export function generateMetadata({
|
||||
address: false,
|
||||
telephone: false,
|
||||
},
|
||||
metadataBase: new URL(SITE_CONFIG.url),
|
||||
metadataBase: metadataBase,
|
||||
alternates: {
|
||||
canonical: pageUrl,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user