This commit is contained in:
Iliyan Angelov
2025-11-25 20:18:23 +02:00
parent 8823edc8b3
commit e639736187
16 changed files with 190 additions and 61 deletions

View File

@@ -15,17 +15,51 @@ interface JobPageProps {
}>;
}
// Generate static params for all job positions at build time (optional - for better performance)
// This pre-generates known pages, but new pages can still be generated on-demand
export async function generateStaticParams() {
try {
// Use internal API URL for server-side requests
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
const response = await fetch(
`${apiUrl}/api/career/jobs`,
{
method: 'GET',
headers: getApiHeaders(),
next: { revalidate: 60 }, // Revalidate every minute
}
);
if (!response.ok) {
console.error('Error fetching jobs for static params:', response.status);
return [];
}
const data = await response.json();
const jobs = data.results || data;
return jobs.map((job: JobPosition) => ({
slug: job.slug,
}));
} catch (error) {
console.error('Error generating static params for jobs:', error);
return [];
}
}
// Generate metadata for each job page
export async function generateMetadata({ params }: JobPageProps): Promise<Metadata> {
const { slug } = await params;
try {
// Use internal API URL for server-side requests
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
const response = await fetch(
`${API_CONFIG.BASE_URL}/api/career/jobs/${slug}/`,
`${apiUrl}/api/career/jobs/${slug}`,
{
method: 'GET',
headers: getApiHeaders(),
next: { revalidate: 3600 }, // Revalidate every hour
next: { revalidate: 60 }, // Revalidate every minute
}
);
@@ -55,12 +89,14 @@ const JobPage = async ({ params }: JobPageProps) => {
const { slug } = await params;
try {
// Use internal API URL for server-side requests
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
const response = await fetch(
`${API_CONFIG.BASE_URL}/api/career/jobs/${slug}/`,
`${apiUrl}/api/career/jobs/${slug}`,
{
method: 'GET',
headers: getApiHeaders(),
next: { revalidate: 3600 }, // Revalidate every hour
next: { revalidate: 60 }, // Revalidate every minute
}
);

View File

@@ -12,7 +12,7 @@ import ServicesInitAnimations from "@/components/pages/services/ServicesInitAnim
import { Service } from "@/lib/api/serviceService";
import { generateServiceMetadata } from "@/lib/seo/metadata";
import { ServiceSchema, BreadcrumbSchema } from "@/components/shared/seo/StructuredData";
import { API_CONFIG, getApiHeaders } from "@/lib/config/api";
import { API_CONFIG } from "@/lib/config/api";
interface ServicePageProps {
params: Promise<{
@@ -24,13 +24,13 @@ interface ServicePageProps {
// This pre-generates known pages, but new pages can still be generated on-demand
export async function generateStaticParams() {
try {
// Use internal API URL for server-side requests
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
const response = await fetch(
`${apiUrl}/api/services/`,
`${API_CONFIG.BASE_URL}/api/services/`,
{
method: 'GET',
headers: getApiHeaders(),
headers: {
'Content-Type': 'application/json',
},
next: { revalidate: 60 }, // Revalidate every minute for faster image updates
}
);
@@ -57,13 +57,13 @@ export async function generateMetadata({ params }: ServicePageProps) {
const { slug } = await params;
try {
// Use internal API URL for server-side requests
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
const response = await fetch(
`${apiUrl}/api/services/${slug}/`,
`${API_CONFIG.BASE_URL}/api/services/${slug}/`,
{
method: 'GET',
headers: getApiHeaders(),
headers: {
'Content-Type': 'application/json',
},
next: { revalidate: 60 }, // Revalidate every minute for faster image updates
}
);
@@ -87,13 +87,13 @@ const ServicePage = async ({ params }: ServicePageProps) => {
const { slug } = await params;
try {
// Use internal API URL for server-side requests
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
const response = await fetch(
`${apiUrl}/api/services/${slug}/`,
`${API_CONFIG.BASE_URL}/api/services/${slug}/`,
{
method: 'GET',
headers: getApiHeaders(),
headers: {
'Content-Type': 'application/json',
},
next: { revalidate: 60 }, // Revalidate every minute for faster image updates
}
);

View File

@@ -1,8 +1,4 @@
import Image from "next/legacy/image";
import time from "@/public/images/time.png";
import trans from "@/public/images/trans.png";
import support from "@/public/images/support.png";
import skill from "@/public/images/skill.png";
const Thrive = () => {
return (
@@ -20,7 +16,7 @@ const Thrive = () => {
<div className="row vertical-column-gap-lg mt-60">
<div className="col-12 col-md-6 fade-top">
<div className="thumb">
<Image src={time} alt="Image" width={80} height={80} />
<Image src="/images/time.png" alt="Image" width={80} height={80} />
</div>
<div className="content mt-40">
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
@@ -35,7 +31,7 @@ const Thrive = () => {
</div>
<div className="col-12 col-md-6 fade-top">
<div className="thumb">
<Image src={trans} alt="Image" width={80} height={80} />
<Image src="/images/trans.png" alt="Image" width={80} height={80} />
</div>
<div className="content mt-40">
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
@@ -50,7 +46,7 @@ const Thrive = () => {
</div>
<div className="col-12 col-md-6 fade-top">
<div className="thumb">
<Image src={support} alt="Image" width={80} height={80} />
<Image src="/images/support.png" alt="Image" width={80} height={80} />
</div>
<div className="content mt-40">
<h4 className="mt-8 title-anim fw-7 text-white mb-16">Support</h4>
@@ -63,7 +59,7 @@ const Thrive = () => {
</div>
<div className="col-12 col-md-6 fade-top">
<div className="thumb">
<Image src={skill} alt="Image" width={80} height={80} />
<Image src="/images/skill.png" alt="Image" width={80} height={80} />
</div>
<div className="content mt-40">
<h4 className="mt-8 title-anim fw-7 text-white mb-16">

View File

@@ -3,7 +3,6 @@ import Image from "next/legacy/image";
import Link from "next/link";
import { useCaseStudies } from "@/lib/hooks/useCaseStudy";
import { getImageUrl } from "@/lib/imageUtils";
import one from "@/public/images/case/one.png";
const CaseItems = () => {
const { caseStudies, loading: casesLoading } = useCaseStudies();
@@ -56,7 +55,7 @@ const CaseItems = () => {
<div className="thumb mb-24">
<Link href={`/case-study/${caseStudy.slug}`} className="w-100">
<Image
src={caseStudy.thumbnail ? getImageUrl(caseStudy.thumbnail) : one}
src={caseStudy.thumbnail ? getImageUrl(caseStudy.thumbnail) : "/images/case/one.png"}
className="w-100 mh-300"
alt={caseStudy.title}
width={600}

View File

@@ -5,8 +5,6 @@ import Link from "next/link";
import { useRouter } from "next/navigation";
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
import { getImageUrl } from "@/lib/imageUtils";
import poster from "@/public/images/case/poster.png";
import project from "@/public/images/case/project.png";
interface CaseSingleProps {
slug: string;

View File

@@ -3,7 +3,6 @@ import Image from "next/image";
import Link from "next/link";
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
import { getImageUrl } from "@/lib/imageUtils";
import one from "@/public/images/case/one.png";
interface RelatedCaseProps {
slug: string;
@@ -34,7 +33,7 @@ const RelatedCase = ({ slug }: RelatedCaseProps) => {
<Link href={`/case-study/${relatedCase.slug}`} className="case-link">
<div className="case-image-wrapper">
<Image
src={relatedCase.thumbnail ? getImageUrl(relatedCase.thumbnail) : one}
src={relatedCase.thumbnail ? getImageUrl(relatedCase.thumbnail) : "/images/case/one.png"}
className="case-image"
alt={relatedCase.title}
width={400}

View File

@@ -2,7 +2,6 @@
import { usePathname } from "next/navigation";
import { useState, useEffect, useRef } from "react";
import Image from "next/legacy/image";
import thumb from "@/public/images/contact-thumb.png";
import { contactApiService, ContactFormData } from "@/lib/api/contactService";
const ContactSection = () => {

View File

@@ -5,14 +5,15 @@ import Link from "next/link";
import { useMemo } from "react";
import { useServices } from "@/lib/hooks/useServices";
import { serviceUtils } from "@/lib/api/serviceService";
import one from "@/public/images/overview/one.png";
import two from "@/public/images/overview/two.png";
import three from "@/public/images/overview/three.png";
import four from "@/public/images/overview/four.png";
import five from "@/public/images/overview/five.png";
// Default images array for fallback
const defaultImages = [one, two, three, four, five];
// Default images array for fallback - use string paths
const defaultImages = [
"/images/overview/one.png",
"/images/overview/two.png",
"/images/overview/three.png",
"/images/overview/four.png",
"/images/overview/five.png"
];
const Overview = () => {
// Memoize the parameters to prevent infinite re-renders

View File

@@ -1,6 +1,5 @@
import Image from "next/legacy/image";
import Link from "next/link";
import thumb from "@/public/images/leading.jpg";
const ServiceIntro = () => {
return (
@@ -11,7 +10,7 @@ const ServiceIntro = () => {
<div className="tp-service__thumb" style={{ maxWidth: '400px', border: 'none', padding: 0, margin: 0, overflow: 'hidden', borderRadius: '8px' }}>
<Link href="services">
<Image
src={thumb}
src="/images/leading.jpg"
alt="Enterprise Software Solutions"
width={400}
height={500}

View File

@@ -3,8 +3,6 @@ import { useEffect } from "react";
import Image from "next/legacy/image";
import gsap from "gsap";
import ScrollTrigger from "gsap/dist/ScrollTrigger";
import thumb from "@/public/images/transform-thumb.png";
import teamThumb from "@/public/images/team-thumb.png";
import { Service } from "@/lib/api/serviceService";
import { serviceUtils } from "@/lib/api/serviceService";
@@ -55,7 +53,7 @@ const Transform = ({ service }: TransformProps) => {
<div className="transform__thumb">
<div className="enterprise-image-wrapper">
<Image
src={serviceUtils.getServiceImageUrl(service) || thumb}
src={serviceUtils.getServiceImageUrl(service) || "/images/transform-thumb.png"}
className="enterprise-service-image"
alt={service.title}
width={600}

View File

@@ -4,7 +4,6 @@ import { usePathname } from "next/navigation";
import AnimateHeight from "react-animate-height";
import Image from "next/legacy/image";
import Link from "next/link";
import logoLight from "@/public/images/logo-light.png";
interface OffcanvasMenuProps {
isOffcanvasOpen: boolean;
@@ -67,7 +66,7 @@ const OffcanvasMenu = ({
<div className="offcanvas-menu__header nav-fade">
<div className="logo">
<Link href="/" className="logo-img">
<Image src={logoLight} priority alt="Image" title="Logo" width={160} height={60} />
<Image src="/images/logo-light.png" priority alt="Image" title="Logo" width={160} height={60} />
</Link>
</div>
<button

View File

@@ -45,7 +45,52 @@ export const API_BASE_URL = isServer
// 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';
// 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
@@ -55,9 +100,25 @@ export const getApiHeaders = (): Record<string, string> => {
};
// If we're calling the internal backend directly (not through nginx),
// add the API key header
// add the API key header (lazy evaluation - only when actually needed)
if (isServer && API_BASE_URL.includes('127.0.0.1:1086')) {
headers['X-Internal-API-Key'] = INTERNAL_API_KEY;
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;

View File

@@ -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;
}

View File

@@ -3,8 +3,10 @@ const nextConfig = {
// Enable standalone output for optimized production deployment
output: 'standalone',
images: {
// Enable image optimization in standalone mode
unoptimized: false,
// Disable image optimization - nginx serves images directly
// This prevents 400 errors from Next.js trying to optimize relative URLs
// Images are already optimized and served efficiently by nginx
unoptimized: true,
remotePatterns: [
{
protocol: 'http',

View File

@@ -14,8 +14,7 @@
min-height: calc(var(--vh, 1vh) * 100); // Dynamic viewport height for mobile browsers
min-height: -webkit-fill-available; // iOS viewport fix
background: #0a0a0a;
overflow-x: hidden; // Prevent horizontal scroll
overflow-y: auto; // Allow vertical scroll if content is too long
overflow: hidden; // Prevent all scrolling in banner
display: flex;
flex-direction: column;
justify-content: flex-start; // Align content to top