From 4c8b71fe0d2e0e7810debacf2530b244b713f951 Mon Sep 17 00:00:00 2001 From: Iliyan Angelov Date: Tue, 25 Nov 2025 09:21:00 +0200 Subject: [PATCH] updates --- frontEnd/app/career/[slug]/page.tsx | 147 +++++++++++--------------- frontEnd/app/services/[slug]/page.tsx | 95 ++++++++++++----- frontEnd/lib/api/serviceService.ts | 42 +++++--- 3 files changed, 162 insertions(+), 122 deletions(-) diff --git a/frontEnd/app/career/[slug]/page.tsx b/frontEnd/app/career/[slug]/page.tsx index 81c072fe..f46f6e05 100644 --- a/frontEnd/app/career/[slug]/page.tsx +++ b/frontEnd/app/career/[slug]/page.tsx @@ -1,110 +1,89 @@ -"use client"; - -import { useParams } from "next/navigation"; -import { useEffect } from "react"; -import Link from "next/link"; +import { Metadata } from 'next'; +import { notFound } from 'next/navigation'; import Header from "@/components/shared/layout/header/Header"; import JobSingle from "@/components/pages/career/JobSingle"; import Footer from "@/components/shared/layout/footer/Footer"; import CareerScrollProgressButton from "@/components/pages/career/CareerScrollProgressButton"; import CareerInitAnimations from "@/components/pages/career/CareerInitAnimations"; -import { useJob } from "@/lib/hooks/useCareer"; +import { JobPosition } from "@/lib/api/careerService"; import { generateCareerMetadata } from "@/lib/seo/metadata"; +import { API_CONFIG, getApiHeaders } from "@/lib/config/api"; -const JobPage = () => { - const params = useParams(); - const slug = params?.slug as string; - const { job, loading, error } = useJob(slug); +interface JobPageProps { + params: Promise<{ + slug: string; + }>; +} - // Update metadata dynamically for client component - useEffect(() => { - if (job) { - const metadata = generateCareerMetadata(job); - const title = typeof metadata.title === 'string' ? metadata.title : `Career - ${job.title} | GNX Soft`; - document.title = title; - - // Update meta description - let metaDescription = document.querySelector('meta[name="description"]'); - if (!metaDescription) { - metaDescription = document.createElement('meta'); - metaDescription.setAttribute('name', 'description'); - document.head.appendChild(metaDescription); +// Generate metadata for each job page +export async function generateMetadata({ params }: JobPageProps): Promise { + const { slug } = await params; + + try { + const response = await fetch( + `${API_CONFIG.BASE_URL}/api/career/jobs/${slug}/`, + { + method: 'GET', + headers: getApiHeaders(), + next: { revalidate: 3600 }, // Revalidate every hour } - const description = typeof metadata.description === 'string' ? metadata.description : `Apply for ${job.title} at GNX Soft. ${job.location || 'Remote'} position.`; - metaDescription.setAttribute('content', description); + ); - // Update canonical URL - let canonical = document.querySelector('link[rel="canonical"]'); - if (!canonical) { - canonical = document.createElement('link'); - canonical.setAttribute('rel', 'canonical'); - document.head.appendChild(canonical); - } - canonical.setAttribute('href', `${window.location.origin}/career/${job.slug}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); } - }, [job]); - if (loading) { + const job = await response.json(); + + return generateCareerMetadata({ + title: job.title, + description: job.short_description || job.about_role, + slug: job.slug, + location: job.location, + department: job.department, + employment_type: job.employment_type, + }); + } catch (error) { + return { + title: 'Job Not Found | GNX Soft', + description: 'The requested job position could not be found.', + }; + } +} + +const JobPage = async ({ params }: JobPageProps) => { + const { slug } = await params; + + try { + const response = await fetch( + `${API_CONFIG.BASE_URL}/api/career/jobs/${slug}/`, + { + method: 'GET', + headers: getApiHeaders(), + next: { revalidate: 3600 }, // Revalidate every hour + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const job: JobPosition = await response.json(); + return (
-
-
-
-
-

Loading job details...

-
-
-
-
+
); + } catch (error) { + notFound(); } - - if (error || !job) { - return ( -
-
-
-
-
-
-
-

Job Not Found

-

- The job position you are looking for does not exist or is no longer available. -

- - View All Positions - -
-
-
-
-
-
- - -
- ); - } - - return ( -
-
-
- -
-
- - -
- ); }; export default JobPage; diff --git a/frontEnd/app/services/[slug]/page.tsx b/frontEnd/app/services/[slug]/page.tsx index df3168c8..5b5802db 100644 --- a/frontEnd/app/services/[slug]/page.tsx +++ b/frontEnd/app/services/[slug]/page.tsx @@ -9,9 +9,10 @@ import Transform from "@/components/pages/services/Transform"; import Footer from "@/components/shared/layout/footer/Footer"; import ServicesScrollProgressButton from "@/components/pages/services/ServicesScrollProgressButton"; import ServicesInitAnimations from "@/components/pages/services/ServicesInitAnimations"; -import { serviceService, Service } from "@/lib/api/serviceService"; +import { Service } from "@/lib/api/serviceService"; import { generateServiceMetadata } from "@/lib/seo/metadata"; import { ServiceSchema, BreadcrumbSchema } from "@/components/shared/seo/StructuredData"; +import { API_CONFIG } from "@/lib/config/api"; interface ServicePageProps { params: Promise<{ @@ -19,16 +20,30 @@ 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) +// Generate static params for all services 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 { - const services = await serviceService.getServices(); - return services.results.map((service: Service) => ({ + const response = await fetch( + `${API_CONFIG.BASE_URL}/api/services/`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + next: { revalidate: 60 }, // Revalidate every minute for faster image updates + } + ); + + if (!response.ok) { + console.error('Error fetching services for static params:', response.status); + return []; + } + + const data = await response.json(); + const services = data.results || data; + + return services.map((service: Service) => ({ slug: service.slug, })); } catch (error) { @@ -39,9 +54,25 @@ export async function generateStaticParams() { // Generate enhanced metadata for each service page export async function generateMetadata({ params }: ServicePageProps) { + const { slug } = await params; + try { - const { slug } = await params; - const service = await serviceService.getServiceBySlug(slug); + const response = await fetch( + `${API_CONFIG.BASE_URL}/api/services/${slug}/`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + next: { revalidate: 60 }, // Revalidate every minute for faster image updates + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const service = await response.json(); return generateServiceMetadata(service); } catch (error) { @@ -53,23 +84,34 @@ export async function generateMetadata({ params }: ServicePageProps) { } const ServicePage = async ({ params }: ServicePageProps) => { - let service: Service; + const { slug } = await params; try { - const { slug } = await params; - service = await serviceService.getServiceBySlug(slug); - } catch (error) { - notFound(); - } + const response = await fetch( + `${API_CONFIG.BASE_URL}/api/services/${slug}/`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + next: { revalidate: 60 }, // Revalidate every minute for faster image updates + } + ); - // Breadcrumb data for structured data - const breadcrumbItems = [ - { name: 'Home', url: '/' }, - { name: 'Services', url: '/services' }, - { name: service.title, url: `/services/${service.slug}` }, - ]; + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } - return ( + const service: Service = await response.json(); + + // Breadcrumb data for structured data + const breadcrumbItems = [ + { name: 'Home', url: '/' }, + { name: 'Services', url: '/services' }, + { name: service.title, url: `/services/${service.slug}` }, + ]; + + return (
{/* SEO Structured Data */} @@ -88,7 +130,10 @@ const ServicePage = async ({ params }: ServicePageProps) => {
- ); + ); + } catch (error) { + notFound(); + } }; export default ServicePage; diff --git a/frontEnd/lib/api/serviceService.ts b/frontEnd/lib/api/serviceService.ts index 5bf19f5e..36f97538 100644 --- a/frontEnd/lib/api/serviceService.ts +++ b/frontEnd/lib/api/serviceService.ts @@ -427,32 +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/')) { - // Use relative URL - Next.js rewrite will handle fetching from backend during optimization - return 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')) { // External URL - keep as absolute - return service.image_url; - } - if (service.image_url.startsWith('/media/')) { + imageUrl = service.image_url; + } else if (service.image_url.startsWith('/media/')) { // Same domain media - use relative URL - return service.image_url; + imageUrl = service.image_url; + } else { + // Other relative URLs + imageUrl = service.image_url; } - // Other relative URLs - return service.image_url; + } else { + // Fallback to default image (relative is fine for public images) + imageUrl = '/images/service/default.png'; } - // Fallback to default image (relative is fine for public images) - 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