updates
This commit is contained in:
@@ -1,110 +1,89 @@
|
|||||||
"use client";
|
import { Metadata } from 'next';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import Header from "@/components/shared/layout/header/Header";
|
import Header from "@/components/shared/layout/header/Header";
|
||||||
import JobSingle from "@/components/pages/career/JobSingle";
|
import JobSingle from "@/components/pages/career/JobSingle";
|
||||||
import Footer from "@/components/shared/layout/footer/Footer";
|
import Footer from "@/components/shared/layout/footer/Footer";
|
||||||
import CareerScrollProgressButton from "@/components/pages/career/CareerScrollProgressButton";
|
import CareerScrollProgressButton from "@/components/pages/career/CareerScrollProgressButton";
|
||||||
import CareerInitAnimations from "@/components/pages/career/CareerInitAnimations";
|
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 { generateCareerMetadata } from "@/lib/seo/metadata";
|
||||||
|
import { API_CONFIG, getApiHeaders } from "@/lib/config/api";
|
||||||
|
|
||||||
const JobPage = () => {
|
interface JobPageProps {
|
||||||
const params = useParams();
|
params: Promise<{
|
||||||
const slug = params?.slug as string;
|
slug: string;
|
||||||
const { job, loading, error } = useJob(slug);
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
// Update metadata dynamically for client component
|
// Generate metadata for each job page
|
||||||
useEffect(() => {
|
export async function generateMetadata({ params }: JobPageProps): Promise<Metadata> {
|
||||||
if (job) {
|
const { slug } = await params;
|
||||||
const metadata = generateCareerMetadata(job);
|
|
||||||
const title = typeof metadata.title === 'string' ? metadata.title : `Career - ${job.title} | GNX Soft`;
|
try {
|
||||||
document.title = title;
|
const response = await fetch(
|
||||||
|
`${API_CONFIG.BASE_URL}/api/career/jobs/${slug}/`,
|
||||||
// Update meta description
|
{
|
||||||
let metaDescription = document.querySelector('meta[name="description"]');
|
method: 'GET',
|
||||||
if (!metaDescription) {
|
headers: getApiHeaders(),
|
||||||
metaDescription = document.createElement('meta');
|
next: { revalidate: 3600 }, // Revalidate every hour
|
||||||
metaDescription.setAttribute('name', 'description');
|
|
||||||
document.head.appendChild(metaDescription);
|
|
||||||
}
|
}
|
||||||
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
|
if (!response.ok) {
|
||||||
let canonical = document.querySelector('link[rel="canonical"]');
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
if (!canonical) {
|
|
||||||
canonical = document.createElement('link');
|
|
||||||
canonical.setAttribute('rel', 'canonical');
|
|
||||||
document.head.appendChild(canonical);
|
|
||||||
}
|
|
||||||
canonical.setAttribute('href', `${window.location.origin}/career/${job.slug}`);
|
|
||||||
}
|
}
|
||||||
}, [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 (
|
return (
|
||||||
<div className="tp-app">
|
<div className="tp-app">
|
||||||
<Header />
|
<Header />
|
||||||
<main>
|
<main>
|
||||||
<section className="pt-120 pb-120">
|
<JobSingle job={job} />
|
||||||
<div className="container">
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-12 text-center">
|
|
||||||
<h2>Loading job details...</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
<CareerScrollProgressButton />
|
<CareerScrollProgressButton />
|
||||||
<CareerInitAnimations />
|
<CareerInitAnimations />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !job) {
|
|
||||||
return (
|
|
||||||
<div className="tp-app">
|
|
||||||
<Header />
|
|
||||||
<main>
|
|
||||||
<section className="pt-120 pb-120">
|
|
||||||
<div className="container">
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-12 text-center">
|
|
||||||
<h2 className="text-danger">Job Not Found</h2>
|
|
||||||
<p className="mt-24">
|
|
||||||
The job position you are looking for does not exist or is no longer available.
|
|
||||||
</p>
|
|
||||||
<Link href="/career" className="btn mt-40">
|
|
||||||
View All Positions
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<Footer />
|
|
||||||
<CareerScrollProgressButton />
|
|
||||||
<CareerInitAnimations />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="tp-app">
|
|
||||||
<Header />
|
|
||||||
<main>
|
|
||||||
<JobSingle job={job} />
|
|
||||||
</main>
|
|
||||||
<Footer />
|
|
||||||
<CareerScrollProgressButton />
|
|
||||||
<CareerInitAnimations />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default JobPage;
|
export default JobPage;
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import Transform from "@/components/pages/services/Transform";
|
|||||||
import Footer from "@/components/shared/layout/footer/Footer";
|
import Footer from "@/components/shared/layout/footer/Footer";
|
||||||
import ServicesScrollProgressButton from "@/components/pages/services/ServicesScrollProgressButton";
|
import ServicesScrollProgressButton from "@/components/pages/services/ServicesScrollProgressButton";
|
||||||
import ServicesInitAnimations from "@/components/pages/services/ServicesInitAnimations";
|
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 { generateServiceMetadata } from "@/lib/seo/metadata";
|
||||||
import { ServiceSchema, BreadcrumbSchema } from "@/components/shared/seo/StructuredData";
|
import { ServiceSchema, BreadcrumbSchema } from "@/components/shared/seo/StructuredData";
|
||||||
|
import { API_CONFIG } from "@/lib/config/api";
|
||||||
|
|
||||||
interface ServicePageProps {
|
interface ServicePageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -19,16 +20,30 @@ interface ServicePageProps {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force static generation - pages are pre-rendered at build time
|
// Generate static params for all services at build time (optional - for better performance)
|
||||||
export const dynamic = 'force-static';
|
// This pre-generates known pages, but new pages can still be generated on-demand
|
||||||
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() {
|
export async function generateStaticParams() {
|
||||||
try {
|
try {
|
||||||
const services = await serviceService.getServices();
|
const response = await fetch(
|
||||||
return services.results.map((service: Service) => ({
|
`${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,
|
slug: service.slug,
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -39,9 +54,25 @@ export async function generateStaticParams() {
|
|||||||
|
|
||||||
// Generate enhanced metadata for each service page
|
// Generate enhanced metadata for each service page
|
||||||
export async function generateMetadata({ params }: ServicePageProps) {
|
export async function generateMetadata({ params }: ServicePageProps) {
|
||||||
|
const { slug } = await params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { slug } = await params;
|
const response = await fetch(
|
||||||
const service = await serviceService.getServiceBySlug(slug);
|
`${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);
|
return generateServiceMetadata(service);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -53,23 +84,34 @@ export async function generateMetadata({ params }: ServicePageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ServicePage = async ({ params }: ServicePageProps) => {
|
const ServicePage = async ({ params }: ServicePageProps) => {
|
||||||
let service: Service;
|
const { slug } = await params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { slug } = await params;
|
const response = await fetch(
|
||||||
service = await serviceService.getServiceBySlug(slug);
|
`${API_CONFIG.BASE_URL}/api/services/${slug}/`,
|
||||||
} catch (error) {
|
{
|
||||||
notFound();
|
method: 'GET',
|
||||||
}
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
next: { revalidate: 60 }, // Revalidate every minute for faster image updates
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Breadcrumb data for structured data
|
if (!response.ok) {
|
||||||
const breadcrumbItems = [
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
{ name: 'Home', url: '/' },
|
}
|
||||||
{ name: 'Services', url: '/services' },
|
|
||||||
{ name: service.title, url: `/services/${service.slug}` },
|
|
||||||
];
|
|
||||||
|
|
||||||
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 (
|
||||||
<div className="enterprise-app">
|
<div className="enterprise-app">
|
||||||
{/* SEO Structured Data */}
|
{/* SEO Structured Data */}
|
||||||
<ServiceSchema service={service} />
|
<ServiceSchema service={service} />
|
||||||
@@ -88,7 +130,10 @@ const ServicePage = async ({ params }: ServicePageProps) => {
|
|||||||
<ServicesScrollProgressButton />
|
<ServicesScrollProgressButton />
|
||||||
<ServicesInitAnimations />
|
<ServicesInitAnimations />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ServicePage;
|
export default ServicePage;
|
||||||
|
|||||||
@@ -427,32 +427,48 @@ export const serviceUtils = {
|
|||||||
}).format(numPrice);
|
}).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 relative URLs for same-domain images (Next.js can optimize via rewrites)
|
||||||
// Use absolute URLs only for external images
|
// Use absolute URLs only for external images
|
||||||
|
// Adds updated_at timestamp as query parameter for cache-busting when images change
|
||||||
getServiceImageUrl: (service: Service): string => {
|
getServiceImageUrl: (service: Service): string => {
|
||||||
|
let imageUrl: string = '';
|
||||||
|
|
||||||
// If service has an uploaded image
|
// If service has an uploaded image
|
||||||
if (service.image && typeof service.image === 'string' && service.image.startsWith('/media/')) {
|
if (service.image && typeof service.image === 'string' && service.image.startsWith('/media/')) {
|
||||||
// Use relative URL - Next.js rewrite will handle fetching from backend during optimization
|
imageUrl = service.image;
|
||||||
return service.image;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If service has an image_url
|
// If service has an image_url
|
||||||
if (service.image_url) {
|
else if (service.image_url) {
|
||||||
if (service.image_url.startsWith('http')) {
|
if (service.image_url.startsWith('http')) {
|
||||||
// External URL - keep as absolute
|
// External URL - keep as absolute
|
||||||
return service.image_url;
|
imageUrl = service.image_url;
|
||||||
}
|
} else if (service.image_url.startsWith('/media/')) {
|
||||||
if (service.image_url.startsWith('/media/')) {
|
|
||||||
// Same domain media - use relative URL
|
// 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
|
} else {
|
||||||
return service.image_url;
|
// 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)
|
// Add cache-busting query parameter using updated_at timestamp
|
||||||
return '/images/service/default.png';
|
// 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
|
// Generate service slug from title
|
||||||
|
|||||||
Reference in New Issue
Block a user