updates
This commit is contained in:
@@ -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
|
// Generate metadata for each job page
|
||||||
export async function generateMetadata({ params }: JobPageProps): Promise<Metadata> {
|
export async function generateMetadata({ params }: JobPageProps): Promise<Metadata> {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
|
|
||||||
try {
|
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(
|
const response = await fetch(
|
||||||
`${API_CONFIG.BASE_URL}/api/career/jobs/${slug}/`,
|
`${apiUrl}/api/career/jobs/${slug}`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: getApiHeaders(),
|
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;
|
const { slug } = await params;
|
||||||
|
|
||||||
try {
|
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(
|
const response = await fetch(
|
||||||
`${API_CONFIG.BASE_URL}/api/career/jobs/${slug}/`,
|
`${apiUrl}/api/career/jobs/${slug}`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: getApiHeaders(),
|
headers: getApiHeaders(),
|
||||||
next: { revalidate: 3600 }, // Revalidate every hour
|
next: { revalidate: 60 }, // Revalidate every minute
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import ServicesInitAnimations from "@/components/pages/services/ServicesInitAnim
|
|||||||
import { Service } from "@/lib/api/serviceService";
|
import { Service } from "@/lib/api/serviceService";
|
||||||
import { generateServiceMetadata } from "@/lib/seo/metadata";
|
import { generateServiceMetadata } from "@/lib/seo/metadata";
|
||||||
import { ServiceSchema, BreadcrumbSchema } from "@/components/shared/seo/StructuredData";
|
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 {
|
interface ServicePageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -24,13 +24,13 @@ interface ServicePageProps {
|
|||||||
// This pre-generates known pages, but new pages can still be generated on-demand
|
// This pre-generates known pages, but new pages can still be generated on-demand
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
try {
|
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(
|
const response = await fetch(
|
||||||
`${apiUrl}/api/services/`,
|
`${API_CONFIG.BASE_URL}/api/services/`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: getApiHeaders(),
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
next: { revalidate: 60 }, // Revalidate every minute for faster image updates
|
next: { revalidate: 60 }, // Revalidate every minute for faster image updates
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -57,13 +57,13 @@ export async function generateMetadata({ params }: ServicePageProps) {
|
|||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
|
|
||||||
try {
|
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(
|
const response = await fetch(
|
||||||
`${apiUrl}/api/services/${slug}/`,
|
`${API_CONFIG.BASE_URL}/api/services/${slug}/`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: getApiHeaders(),
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
next: { revalidate: 60 }, // Revalidate every minute for faster image updates
|
next: { revalidate: 60 }, // Revalidate every minute for faster image updates
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -87,13 +87,13 @@ const ServicePage = async ({ params }: ServicePageProps) => {
|
|||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
|
|
||||||
try {
|
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(
|
const response = await fetch(
|
||||||
`${apiUrl}/api/services/${slug}/`,
|
`${API_CONFIG.BASE_URL}/api/services/${slug}/`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: getApiHeaders(),
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
next: { revalidate: 60 }, // Revalidate every minute for faster image updates
|
next: { revalidate: 60 }, // Revalidate every minute for faster image updates
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import Image from "next/legacy/image";
|
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 = () => {
|
const Thrive = () => {
|
||||||
return (
|
return (
|
||||||
@@ -20,7 +16,7 @@ const Thrive = () => {
|
|||||||
<div className="row vertical-column-gap-lg mt-60">
|
<div className="row vertical-column-gap-lg mt-60">
|
||||||
<div className="col-12 col-md-6 fade-top">
|
<div className="col-12 col-md-6 fade-top">
|
||||||
<div className="thumb">
|
<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>
|
||||||
<div className="content mt-40">
|
<div className="content mt-40">
|
||||||
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
|
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
|
||||||
@@ -35,7 +31,7 @@ const Thrive = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-md-6 fade-top">
|
<div className="col-12 col-md-6 fade-top">
|
||||||
<div className="thumb">
|
<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>
|
||||||
<div className="content mt-40">
|
<div className="content mt-40">
|
||||||
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
|
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
|
||||||
@@ -50,7 +46,7 @@ const Thrive = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-md-6 fade-top">
|
<div className="col-12 col-md-6 fade-top">
|
||||||
<div className="thumb">
|
<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>
|
||||||
<div className="content mt-40">
|
<div className="content mt-40">
|
||||||
<h4 className="mt-8 title-anim fw-7 text-white mb-16">Support</h4>
|
<h4 className="mt-8 title-anim fw-7 text-white mb-16">Support</h4>
|
||||||
@@ -63,7 +59,7 @@ const Thrive = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-md-6 fade-top">
|
<div className="col-12 col-md-6 fade-top">
|
||||||
<div className="thumb">
|
<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>
|
||||||
<div className="content mt-40">
|
<div className="content mt-40">
|
||||||
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
|
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import Image from "next/legacy/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCaseStudies } from "@/lib/hooks/useCaseStudy";
|
import { useCaseStudies } from "@/lib/hooks/useCaseStudy";
|
||||||
import { getImageUrl } from "@/lib/imageUtils";
|
import { getImageUrl } from "@/lib/imageUtils";
|
||||||
import one from "@/public/images/case/one.png";
|
|
||||||
|
|
||||||
const CaseItems = () => {
|
const CaseItems = () => {
|
||||||
const { caseStudies, loading: casesLoading } = useCaseStudies();
|
const { caseStudies, loading: casesLoading } = useCaseStudies();
|
||||||
@@ -56,7 +55,7 @@ const CaseItems = () => {
|
|||||||
<div className="thumb mb-24">
|
<div className="thumb mb-24">
|
||||||
<Link href={`/case-study/${caseStudy.slug}`} className="w-100">
|
<Link href={`/case-study/${caseStudy.slug}`} className="w-100">
|
||||||
<Image
|
<Image
|
||||||
src={caseStudy.thumbnail ? getImageUrl(caseStudy.thumbnail) : one}
|
src={caseStudy.thumbnail ? getImageUrl(caseStudy.thumbnail) : "/images/case/one.png"}
|
||||||
className="w-100 mh-300"
|
className="w-100 mh-300"
|
||||||
alt={caseStudy.title}
|
alt={caseStudy.title}
|
||||||
width={600}
|
width={600}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
|
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
|
||||||
import { getImageUrl } from "@/lib/imageUtils";
|
import { getImageUrl } from "@/lib/imageUtils";
|
||||||
import poster from "@/public/images/case/poster.png";
|
|
||||||
import project from "@/public/images/case/project.png";
|
|
||||||
|
|
||||||
interface CaseSingleProps {
|
interface CaseSingleProps {
|
||||||
slug: string;
|
slug: string;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
|
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
|
||||||
import { getImageUrl } from "@/lib/imageUtils";
|
import { getImageUrl } from "@/lib/imageUtils";
|
||||||
import one from "@/public/images/case/one.png";
|
|
||||||
|
|
||||||
interface RelatedCaseProps {
|
interface RelatedCaseProps {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -34,7 +33,7 @@ const RelatedCase = ({ slug }: RelatedCaseProps) => {
|
|||||||
<Link href={`/case-study/${relatedCase.slug}`} className="case-link">
|
<Link href={`/case-study/${relatedCase.slug}`} className="case-link">
|
||||||
<div className="case-image-wrapper">
|
<div className="case-image-wrapper">
|
||||||
<Image
|
<Image
|
||||||
src={relatedCase.thumbnail ? getImageUrl(relatedCase.thumbnail) : one}
|
src={relatedCase.thumbnail ? getImageUrl(relatedCase.thumbnail) : "/images/case/one.png"}
|
||||||
className="case-image"
|
className="case-image"
|
||||||
alt={relatedCase.title}
|
alt={relatedCase.title}
|
||||||
width={400}
|
width={400}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import Image from "next/legacy/image";
|
import Image from "next/legacy/image";
|
||||||
import thumb from "@/public/images/contact-thumb.png";
|
|
||||||
import { contactApiService, ContactFormData } from "@/lib/api/contactService";
|
import { contactApiService, ContactFormData } from "@/lib/api/contactService";
|
||||||
|
|
||||||
const ContactSection = () => {
|
const ContactSection = () => {
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import Link from "next/link";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useServices } from "@/lib/hooks/useServices";
|
import { useServices } from "@/lib/hooks/useServices";
|
||||||
import { serviceUtils } from "@/lib/api/serviceService";
|
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
|
// Default images array for fallback - use string paths
|
||||||
const defaultImages = [one, two, three, four, five];
|
const defaultImages = [
|
||||||
|
"/images/overview/one.png",
|
||||||
|
"/images/overview/two.png",
|
||||||
|
"/images/overview/three.png",
|
||||||
|
"/images/overview/four.png",
|
||||||
|
"/images/overview/five.png"
|
||||||
|
];
|
||||||
|
|
||||||
const Overview = () => {
|
const Overview = () => {
|
||||||
// Memoize the parameters to prevent infinite re-renders
|
// Memoize the parameters to prevent infinite re-renders
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import Image from "next/legacy/image";
|
import Image from "next/legacy/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import thumb from "@/public/images/leading.jpg";
|
|
||||||
|
|
||||||
const ServiceIntro = () => {
|
const ServiceIntro = () => {
|
||||||
return (
|
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' }}>
|
<div className="tp-service__thumb" style={{ maxWidth: '400px', border: 'none', padding: 0, margin: 0, overflow: 'hidden', borderRadius: '8px' }}>
|
||||||
<Link href="services">
|
<Link href="services">
|
||||||
<Image
|
<Image
|
||||||
src={thumb}
|
src="/images/leading.jpg"
|
||||||
alt="Enterprise Software Solutions"
|
alt="Enterprise Software Solutions"
|
||||||
width={400}
|
width={400}
|
||||||
height={500}
|
height={500}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { useEffect } from "react";
|
|||||||
import Image from "next/legacy/image";
|
import Image from "next/legacy/image";
|
||||||
import gsap from "gsap";
|
import gsap from "gsap";
|
||||||
import ScrollTrigger from "gsap/dist/ScrollTrigger";
|
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 { Service } from "@/lib/api/serviceService";
|
||||||
import { serviceUtils } from "@/lib/api/serviceService";
|
import { serviceUtils } from "@/lib/api/serviceService";
|
||||||
|
|
||||||
@@ -55,7 +53,7 @@ const Transform = ({ service }: TransformProps) => {
|
|||||||
<div className="transform__thumb">
|
<div className="transform__thumb">
|
||||||
<div className="enterprise-image-wrapper">
|
<div className="enterprise-image-wrapper">
|
||||||
<Image
|
<Image
|
||||||
src={serviceUtils.getServiceImageUrl(service) || thumb}
|
src={serviceUtils.getServiceImageUrl(service) || "/images/transform-thumb.png"}
|
||||||
className="enterprise-service-image"
|
className="enterprise-service-image"
|
||||||
alt={service.title}
|
alt={service.title}
|
||||||
width={600}
|
width={600}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { usePathname } from "next/navigation";
|
|||||||
import AnimateHeight from "react-animate-height";
|
import AnimateHeight from "react-animate-height";
|
||||||
import Image from "next/legacy/image";
|
import Image from "next/legacy/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import logoLight from "@/public/images/logo-light.png";
|
|
||||||
|
|
||||||
interface OffcanvasMenuProps {
|
interface OffcanvasMenuProps {
|
||||||
isOffcanvasOpen: boolean;
|
isOffcanvasOpen: boolean;
|
||||||
@@ -67,7 +66,7 @@ const OffcanvasMenu = ({
|
|||||||
<div className="offcanvas-menu__header nav-fade">
|
<div className="offcanvas-menu__header nav-fade">
|
||||||
<div className="logo">
|
<div className="logo">
|
||||||
<Link href="/" className="logo-img">
|
<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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -45,7 +45,52 @@ export const API_BASE_URL = isServer
|
|||||||
|
|
||||||
// Internal API key for server-side requests (must match backend INTERNAL_API_KEY)
|
// 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)
|
// 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
|
// Helper to get headers for API requests
|
||||||
// Adds API key header when calling internal backend directly
|
// 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),
|
// 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')) {
|
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;
|
return headers;
|
||||||
|
|||||||
@@ -10,6 +10,40 @@ export const FALLBACK_IMAGES = {
|
|||||||
DEFAULT: '/images/logo.png'
|
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 {
|
export function getValidImageUrl(imageUrl?: string, fallback?: string): string {
|
||||||
if (!imageUrl || imageUrl.trim() === '') {
|
if (!imageUrl || imageUrl.trim() === '') {
|
||||||
return fallback || FALLBACK_IMAGES.DEFAULT;
|
return fallback || FALLBACK_IMAGES.DEFAULT;
|
||||||
@@ -20,22 +54,28 @@ export function getValidImageUrl(imageUrl?: string, fallback?: string): string {
|
|||||||
return imageUrl;
|
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/')) {
|
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/')) {
|
if (imageUrl.startsWith('/images/')) {
|
||||||
return imageUrl;
|
return imageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it starts with /, check if it's a media file
|
// If it starts with /, check if it's a media file
|
||||||
if (imageUrl.startsWith('/')) {
|
if (imageUrl.startsWith('/')) {
|
||||||
// If it contains /media/, prepend API base URL
|
// If it contains /media/, prepend base URL
|
||||||
if (imageUrl.includes('/media/')) {
|
if (imageUrl.includes('/media/')) {
|
||||||
return `${API_BASE_URL}${imageUrl}`;
|
return `${baseUrl}${imageUrl}`;
|
||||||
}
|
}
|
||||||
|
// Other absolute paths (like /static/) are served directly
|
||||||
return imageUrl;
|
return imageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ const nextConfig = {
|
|||||||
// Enable standalone output for optimized production deployment
|
// Enable standalone output for optimized production deployment
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
images: {
|
images: {
|
||||||
// Enable image optimization in standalone mode
|
// Disable image optimization - nginx serves images directly
|
||||||
unoptimized: false,
|
// This prevents 400 errors from Next.js trying to optimize relative URLs
|
||||||
|
// Images are already optimized and served efficiently by nginx
|
||||||
|
unoptimized: true,
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
protocol: 'http',
|
protocol: 'http',
|
||||||
|
|||||||
@@ -14,8 +14,7 @@
|
|||||||
min-height: calc(var(--vh, 1vh) * 100); // Dynamic viewport height for mobile browsers
|
min-height: calc(var(--vh, 1vh) * 100); // Dynamic viewport height for mobile browsers
|
||||||
min-height: -webkit-fill-available; // iOS viewport fix
|
min-height: -webkit-fill-available; // iOS viewport fix
|
||||||
background: #0a0a0a;
|
background: #0a0a0a;
|
||||||
overflow-x: hidden; // Prevent horizontal scroll
|
overflow: hidden; // Prevent all scrolling in banner
|
||||||
overflow-y: auto; // Allow vertical scroll if content is too long
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start; // Align content to top
|
justify-content: flex-start; // Align content to top
|
||||||
|
|||||||
@@ -200,9 +200,9 @@ server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Frontend public images - must come before root location
|
# Frontend public images - use prefix location (must come before root location)
|
||||||
location ~ ^/images/(.*)$ {
|
location /images/ {
|
||||||
alias /var/www/GNX-WEB/frontEnd/public/images/$1;
|
alias /var/www/GNX-WEB/frontEnd/public/images/;
|
||||||
expires 30d;
|
expires 30d;
|
||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
access_log off;
|
access_log off;
|
||||||
@@ -216,6 +216,9 @@ server {
|
|||||||
image/webp webp;
|
image/webp webp;
|
||||||
}
|
}
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
# Try files, then fallback
|
||||||
|
try_files $uri =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Root location - Frontend (Next.js) - MUST be last
|
# Root location - Frontend (Next.js) - MUST be last
|
||||||
|
|||||||
Reference in New Issue
Block a user