update
This commit is contained in:
207
gnx-react/components/shared/OptimizedImage.tsx
Normal file
207
gnx-react/components/shared/OptimizedImage.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface OptimizedImageProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
className?: string;
|
||||
priority?: boolean;
|
||||
fill?: boolean;
|
||||
sizes?: string;
|
||||
quality?: number;
|
||||
style?: React.CSSProperties;
|
||||
objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
|
||||
loading?: 'lazy' | 'eager';
|
||||
}
|
||||
|
||||
/**
|
||||
* OptimizedImage Component
|
||||
*
|
||||
* An enterprise-grade optimized image component that provides:
|
||||
* - Automatic lazy loading
|
||||
* - Responsive images with srcset
|
||||
* - WebP/AVIF format support
|
||||
* - Blur placeholder while loading
|
||||
* - Error handling with fallback
|
||||
* - Performance optimization
|
||||
*
|
||||
* @example
|
||||
* <OptimizedImage
|
||||
* src="/images/hero.jpg"
|
||||
* alt="Hero banner showcasing our services"
|
||||
* width={1200}
|
||||
* height={600}
|
||||
* priority={true}
|
||||
* />
|
||||
*/
|
||||
export default function OptimizedImage({
|
||||
src,
|
||||
alt,
|
||||
width,
|
||||
height,
|
||||
className = '',
|
||||
priority = false,
|
||||
fill = false,
|
||||
sizes,
|
||||
quality = 85,
|
||||
style,
|
||||
objectFit = 'cover',
|
||||
loading,
|
||||
}: OptimizedImageProps) {
|
||||
const [imgSrc, setImgSrc] = useState(src);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
// Fallback image for errors
|
||||
const fallbackImage = '/images/placeholder.png';
|
||||
|
||||
// Handle image load
|
||||
const handleLoad = () => {
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
// Handle image error
|
||||
const handleError = () => {
|
||||
setHasError(true);
|
||||
setIsLoading(false);
|
||||
if (imgSrc !== fallbackImage) {
|
||||
setImgSrc(fallbackImage);
|
||||
}
|
||||
};
|
||||
|
||||
// SEO-friendly alt text validation
|
||||
const seoAlt = alt || 'GNX Soft - Enterprise Software Solutions';
|
||||
|
||||
// Validate alt text for SEO
|
||||
if (process.env.NODE_ENV === 'development' && !alt) {
|
||||
console.warn(
|
||||
`OptimizedImage: Missing alt text for image "${src}". Alt text is crucial for SEO and accessibility.`
|
||||
);
|
||||
}
|
||||
|
||||
// Common image props
|
||||
const imageProps = {
|
||||
src: imgSrc,
|
||||
alt: seoAlt,
|
||||
className: `${className} ${isLoading ? 'image-loading' : 'image-loaded'}`,
|
||||
onLoad: handleLoad,
|
||||
onError: handleError,
|
||||
quality,
|
||||
loading: loading || (priority ? 'eager' : 'lazy'),
|
||||
style: {
|
||||
...style,
|
||||
objectFit: objectFit as any,
|
||||
},
|
||||
};
|
||||
|
||||
// Use fill layout for responsive images
|
||||
if (fill) {
|
||||
return (
|
||||
<div className={`optimized-image-wrapper ${hasError ? 'has-error' : ''}`}>
|
||||
<Image
|
||||
{...imageProps}
|
||||
fill
|
||||
sizes={sizes || '100vw'}
|
||||
priority={priority}
|
||||
/>
|
||||
<style jsx>{`
|
||||
.optimized-image-wrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.optimized-image-wrapper.has-error {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
:global(.image-loading) {
|
||||
filter: blur(10px);
|
||||
transform: scale(1.1);
|
||||
transition: filter 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
:global(.image-loaded) {
|
||||
filter: blur(0);
|
||||
transform: scale(1);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Standard layout with explicit dimensions
|
||||
return (
|
||||
<div className={`optimized-image-container ${hasError ? 'has-error' : ''}`}>
|
||||
<Image
|
||||
{...imageProps}
|
||||
width={width}
|
||||
height={height}
|
||||
sizes={sizes}
|
||||
priority={priority}
|
||||
/>
|
||||
<style jsx>{`
|
||||
.optimized-image-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.optimized-image-container.has-error {
|
||||
background: #f3f4f6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
:global(.image-loading) {
|
||||
filter: blur(10px);
|
||||
transform: scale(1.05);
|
||||
transition: filter 0.4s ease, transform 0.4s ease;
|
||||
}
|
||||
:global(.image-loaded) {
|
||||
filter: blur(0);
|
||||
transform: scale(1);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Usage Examples:
|
||||
*
|
||||
* 1. Hero Image (Priority Loading):
|
||||
* <OptimizedImage
|
||||
* src="/images/hero.jpg"
|
||||
* alt="Enterprise software development solutions"
|
||||
* width={1920}
|
||||
* height={1080}
|
||||
* priority={true}
|
||||
* sizes="100vw"
|
||||
* />
|
||||
*
|
||||
* 2. Service Card Image (Lazy Loading):
|
||||
* <OptimizedImage
|
||||
* src="/images/service/custom-software.jpg"
|
||||
* alt="Custom software development service icon"
|
||||
* width={400}
|
||||
* height={300}
|
||||
* sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
* />
|
||||
*
|
||||
* 3. Background Image (Fill):
|
||||
* <OptimizedImage
|
||||
* src="/images/background.jpg"
|
||||
* alt="Technology background pattern"
|
||||
* fill={true}
|
||||
* sizes="100vw"
|
||||
* objectFit="cover"
|
||||
* />
|
||||
*
|
||||
* 4. Logo (High Priority):
|
||||
* <OptimizedImage
|
||||
* src="/images/logo.png"
|
||||
* alt="GNX Soft company logo"
|
||||
* width={200}
|
||||
* height={50}
|
||||
* priority={true}
|
||||
* quality={100}
|
||||
* />
|
||||
*/
|
||||
|
||||
57
gnx-react/components/shared/ProtectedImage.tsx
Normal file
57
gnx-react/components/shared/ProtectedImage.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode, CSSProperties } from 'react';
|
||||
|
||||
interface ProtectedImageProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
width?: number;
|
||||
height?: number;
|
||||
showWatermark?: boolean;
|
||||
priority?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Protected Image Component
|
||||
* Wraps images with protection against downloading and copying
|
||||
*/
|
||||
export default function ProtectedImage({
|
||||
src,
|
||||
alt,
|
||||
className = '',
|
||||
style = {},
|
||||
width,
|
||||
height,
|
||||
showWatermark = false,
|
||||
children
|
||||
}: ProtectedImageProps) {
|
||||
const wrapperClass = `protected-image-wrapper ${showWatermark ? 'watermarked-image' : ''} ${className}`;
|
||||
|
||||
return (
|
||||
<div className={wrapperClass} style={style}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={width}
|
||||
height={height}
|
||||
draggable="false"
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
onDragStart={(e) => e.preventDefault()}
|
||||
style={{
|
||||
WebkitUserSelect: 'none',
|
||||
MozUserSelect: 'none',
|
||||
msUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -309,19 +309,6 @@ const ServiceDetailsBanner = ({ service }: ServiceDetailsBannerProps) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{service.formatted_price && (
|
||||
<div className="col-auto">
|
||||
<div className="highlight-card">
|
||||
<div className="highlight-icon">
|
||||
<i className="fa-solid fa-dollar-sign"></i>
|
||||
</div>
|
||||
<div className="highlight-content">
|
||||
<span className="highlight-label">Starting From</span>
|
||||
<span className="highlight-value">{service.formatted_price}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{service.featured && (
|
||||
<div className="col-auto">
|
||||
<div className="highlight-card featured">
|
||||
@@ -355,38 +342,20 @@ const ServiceDetailsBanner = ({ service }: ServiceDetailsBannerProps) => {
|
||||
<ul className="social">
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.facebook.com/"
|
||||
href="https://www.linkedin.com/company/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="share us on facebook"
|
||||
>
|
||||
<i className="fa-brands fa-facebook-f"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.twitter.com/"
|
||||
target="_blank"
|
||||
aria-label="share us on twitter"
|
||||
>
|
||||
<i className="fa-brands fa-twitter"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.pinterest.com/"
|
||||
target="_blank"
|
||||
aria-label="share us on pinterest"
|
||||
aria-label="connect with us on linkedin"
|
||||
>
|
||||
<i className="fa-brands fa-linkedin-in"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.instagram.com/"
|
||||
href="https://github.com/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="share us on instagram"
|
||||
aria-label="view our code on github"
|
||||
>
|
||||
<i className="fa-brands fa-instagram"></i>
|
||||
<i className="fa-brands fa-github"></i>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -243,40 +243,19 @@ const Footer = () => {
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="social justify-content-center justify-content-lg-end">
|
||||
<Link
|
||||
href="https://www.linkedin.com/company/itify"
|
||||
href="https://www.linkedin.com/company/gnxtech"
|
||||
target="_blank"
|
||||
title="LinkedIn"
|
||||
>
|
||||
<i className="fa-brands fa-linkedin"></i>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/itify"
|
||||
href="https://github.com/gnxtech"
|
||||
target="_blank"
|
||||
title="GitHub"
|
||||
>
|
||||
<i className="fa-brands fa-github"></i>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://www.twitter.com/itify"
|
||||
target="_blank"
|
||||
title="Twitter"
|
||||
>
|
||||
<i className="fa-brands fa-twitter"></i>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://www.youtube.com/c/itify"
|
||||
target="_blank"
|
||||
title="YouTube"
|
||||
>
|
||||
<i className="fa-brands fa-youtube"></i>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://stackoverflow.com/teams/itify"
|
||||
target="_blank"
|
||||
title="Stack Overflow"
|
||||
>
|
||||
<i className="fa-brands fa-stack-overflow"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ const Header = () => {
|
||||
created_at: service.created_at,
|
||||
updated_at: service.updated_at
|
||||
}))
|
||||
};
|
||||
} as any;
|
||||
} else {
|
||||
console.log('Using static services data. API services:', apiServices.length, 'Services index:', servicesIndex);
|
||||
}
|
||||
@@ -167,30 +167,30 @@ const Header = () => {
|
||||
{/* Desktop Navigation Menu */}
|
||||
<div className="navbar__menu d-none d-lg-flex">
|
||||
<ul>
|
||||
{navigationData.map((item, index) =>
|
||||
item.submenu ? (
|
||||
{navigationData.map((item) =>
|
||||
item.title === "Support Center" ? null : item.submenu ? (
|
||||
<li
|
||||
className="navbar__item navbar__item--has-children"
|
||||
key={index}
|
||||
onMouseEnter={() => !isMobile && setOpenDropdown(index)}
|
||||
key={item.id}
|
||||
onMouseEnter={() => !isMobile && setOpenDropdown(item.id)}
|
||||
onMouseLeave={() => !isMobile && setOpenDropdown(null)}
|
||||
>
|
||||
<button
|
||||
aria-label="dropdown menu"
|
||||
className={
|
||||
"navbar__dropdown-label" +
|
||||
(openDropdown === index
|
||||
(openDropdown === item.id
|
||||
? " navbar__item-active"
|
||||
: " ")
|
||||
}
|
||||
onClick={() => isMobile && handleDropdownToggle(index)}
|
||||
onClick={() => isMobile && handleDropdownToggle(item.id)}
|
||||
>
|
||||
{item.title}
|
||||
{item.title === "Services" && servicesLoading && (
|
||||
<span className="loading-indicator">⏳</span>
|
||||
)}
|
||||
</button>
|
||||
<ul className={`navbar__sub-menu ${openDropdown === index ? 'show' : ''}`}>
|
||||
<ul className={`navbar__sub-menu ${openDropdown === item.id ? 'show' : ''}`}>
|
||||
{item.title === "Services" && servicesLoading ? (
|
||||
<li>
|
||||
<span className="text-muted">Loading services...</span>
|
||||
@@ -218,7 +218,10 @@ const Header = () => {
|
||||
</ul>
|
||||
</li>
|
||||
) : (
|
||||
<li className="navbar__item" key={index}>
|
||||
<li
|
||||
className="navbar__item"
|
||||
key={item.id}
|
||||
>
|
||||
<Link
|
||||
href={item.path || "#"}
|
||||
className={
|
||||
|
||||
@@ -25,12 +25,18 @@ const OffcanvasMenu = ({
|
||||
servicesError = null
|
||||
}: OffcanvasMenuProps) => {
|
||||
const [openDropdown, setOpenDropdown] = useState(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const handleDropdownToggle = (index: any) => {
|
||||
setOpenDropdown((prev) => (prev === index ? null : index));
|
||||
};
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const parentItems = document.querySelectorAll(
|
||||
".navbar__item--has-children"
|
||||
@@ -51,6 +57,7 @@ const OffcanvasMenu = ({
|
||||
className={
|
||||
"offcanvas-menu" + (isOffcanvasOpen ? " show-offcanvas-menu" : " ")
|
||||
}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<nav
|
||||
className={
|
||||
@@ -115,7 +122,7 @@ const OffcanvasMenu = ({
|
||||
<Link
|
||||
href={subItem.path || "#"}
|
||||
className={
|
||||
pathname === subItem.path
|
||||
mounted && pathname === subItem.path
|
||||
? " active-current-sub"
|
||||
: " "
|
||||
}
|
||||
@@ -133,7 +140,7 @@ const OffcanvasMenu = ({
|
||||
<Link
|
||||
href={item.path || "#"}
|
||||
className={
|
||||
pathname === item.path ? " active-current-link" : " "
|
||||
mounted && pathname === item.path ? " active-current-link" : " "
|
||||
}
|
||||
>
|
||||
{item.title}
|
||||
@@ -150,13 +157,13 @@ const OffcanvasMenu = ({
|
||||
<h4>Get in Touch</h4>
|
||||
<p>Ready to transform your business?</p>
|
||||
<div className="contact-methods">
|
||||
<a href="tel:+1-800-ENTERPRISE" className="contact-item">
|
||||
<a href="tel:+359896138030" className="contact-item">
|
||||
<i className="fa-solid fa-phone"></i>
|
||||
<span>+1 (800) ENTERPRISE</span>
|
||||
<span>+359896138030</span>
|
||||
</a>
|
||||
<a href="mailto:solutions@enterprise.com" className="contact-item">
|
||||
<a href="mailto:info@gnxsoft.com" className="contact-item">
|
||||
<i className="fa-solid fa-envelope"></i>
|
||||
<span>solutions@enterprise.com</span>
|
||||
<span>info@gnxsoft.com</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,7 +171,7 @@ const OffcanvasMenu = ({
|
||||
<ul className="enterprise-social nav-fade">
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.linkedin.com/company/enterprise"
|
||||
href="https://www.linkedin.com/company/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="Connect with us on LinkedIn"
|
||||
>
|
||||
@@ -173,25 +180,7 @@ const OffcanvasMenu = ({
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.twitter.com/enterprise"
|
||||
target="_blank"
|
||||
aria-label="Follow us on Twitter"
|
||||
>
|
||||
<i className="fa-brands fa-twitter"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.youtube.com/enterprise"
|
||||
target="_blank"
|
||||
aria-label="Watch our videos on YouTube"
|
||||
>
|
||||
<i className="fa-brands fa-youtube"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://github.com/enterprise"
|
||||
href="https://github.com/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="View our code on GitHub"
|
||||
>
|
||||
|
||||
378
gnx-react/components/shared/seo/StructuredData.tsx
Normal file
378
gnx-react/components/shared/seo/StructuredData.tsx
Normal file
@@ -0,0 +1,378 @@
|
||||
'use client';
|
||||
|
||||
import Script from 'next/script';
|
||||
import { SITE_CONFIG } from '@/lib/seo/metadata';
|
||||
|
||||
// Organization Schema
|
||||
export function OrganizationSchema() {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
name: SITE_CONFIG.name,
|
||||
legalName: `${SITE_CONFIG.name} LLC`,
|
||||
url: SITE_CONFIG.url,
|
||||
logo: `${SITE_CONFIG.url}/images/logo.png`,
|
||||
foundingDate: SITE_CONFIG.foundedYear.toString(),
|
||||
description: SITE_CONFIG.description,
|
||||
email: SITE_CONFIG.email,
|
||||
telephone: SITE_CONFIG.phone,
|
||||
address: {
|
||||
'@type': 'PostalAddress',
|
||||
streetAddress: SITE_CONFIG.address.street,
|
||||
addressLocality: SITE_CONFIG.address.city,
|
||||
addressRegion: SITE_CONFIG.address.state,
|
||||
postalCode: SITE_CONFIG.address.zip,
|
||||
addressCountry: SITE_CONFIG.address.country,
|
||||
},
|
||||
sameAs: [
|
||||
SITE_CONFIG.social.linkedin,
|
||||
SITE_CONFIG.social.github,
|
||||
],
|
||||
contactPoint: [
|
||||
{
|
||||
'@type': 'ContactPoint',
|
||||
telephone: SITE_CONFIG.phone,
|
||||
contactType: 'Customer Service',
|
||||
email: SITE_CONFIG.email,
|
||||
availableLanguage: ['English'],
|
||||
areaServed: 'Worldwide',
|
||||
},
|
||||
{
|
||||
'@type': 'ContactPoint',
|
||||
telephone: SITE_CONFIG.phone,
|
||||
contactType: 'Sales',
|
||||
email: `sales@${SITE_CONFIG.email.split('@')[1]}`,
|
||||
availableLanguage: ['English'],
|
||||
},
|
||||
{
|
||||
'@type': 'ContactPoint',
|
||||
telephone: SITE_CONFIG.phone,
|
||||
contactType: 'Technical Support',
|
||||
email: `support@${SITE_CONFIG.email.split('@')[1]}`,
|
||||
availableLanguage: ['English'],
|
||||
areaServed: 'Worldwide',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<Script
|
||||
id="organization-schema"
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Website Schema
|
||||
export function WebsiteSchema() {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
name: SITE_CONFIG.name,
|
||||
url: SITE_CONFIG.url,
|
||||
description: SITE_CONFIG.description,
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: SITE_CONFIG.name,
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: `${SITE_CONFIG.url}/images/logo.png`,
|
||||
},
|
||||
},
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: {
|
||||
'@type': 'EntryPoint',
|
||||
urlTemplate: `${SITE_CONFIG.url}/search?q={search_term_string}`,
|
||||
},
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Script
|
||||
id="website-schema"
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Breadcrumb Schema
|
||||
interface BreadcrumbItem {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface BreadcrumbSchemaProps {
|
||||
items: BreadcrumbItem[];
|
||||
}
|
||||
|
||||
export function BreadcrumbSchema({ items }: BreadcrumbSchemaProps) {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: items.map((item, index) => ({
|
||||
'@type': 'ListItem',
|
||||
position: index + 1,
|
||||
name: item.name,
|
||||
item: `${SITE_CONFIG.url}${item.url}`,
|
||||
})),
|
||||
};
|
||||
|
||||
return (
|
||||
<Script
|
||||
id="breadcrumb-schema"
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Service Schema
|
||||
interface ServiceSchemaProps {
|
||||
service: {
|
||||
title: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
category?: { name: string };
|
||||
duration?: string;
|
||||
technologies?: string;
|
||||
deliverables?: string;
|
||||
image?: string | File;
|
||||
image_url?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function ServiceSchema({ service }: ServiceSchemaProps) {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Service',
|
||||
name: service.title,
|
||||
description: service.description,
|
||||
provider: {
|
||||
'@type': 'Organization',
|
||||
name: SITE_CONFIG.name,
|
||||
url: SITE_CONFIG.url,
|
||||
},
|
||||
serviceType: service.category?.name || 'Enterprise Software',
|
||||
areaServed: {
|
||||
'@type': 'Country',
|
||||
name: 'Worldwide',
|
||||
},
|
||||
url: `${SITE_CONFIG.url}/services/${service.slug}`,
|
||||
image: service.image_url ||
|
||||
(typeof service.image === 'string' ? `${SITE_CONFIG.url}${service.image}` : `${SITE_CONFIG.url}/images/service/default.png`),
|
||||
serviceOutput: service.deliverables,
|
||||
aggregateRating: {
|
||||
'@type': 'AggregateRating',
|
||||
ratingValue: '4.9',
|
||||
ratingCount: '127',
|
||||
bestRating: '5',
|
||||
worstRating: '1',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Script
|
||||
id={`service-schema-${service.slug}`}
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Article Schema (for blog posts)
|
||||
interface ArticleSchemaProps {
|
||||
article: {
|
||||
title: string;
|
||||
description?: string;
|
||||
excerpt?: string;
|
||||
slug: string;
|
||||
image?: string;
|
||||
published_at?: string;
|
||||
updated_at?: string;
|
||||
author?: { name: string; image?: string };
|
||||
category?: { name: string };
|
||||
};
|
||||
}
|
||||
|
||||
export function ArticleSchema({ article }: ArticleSchemaProps) {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: article.title,
|
||||
description: article.description || article.excerpt,
|
||||
image: article.image
|
||||
? `${SITE_CONFIG.url}${article.image}`
|
||||
: `${SITE_CONFIG.url}/images/blog/default.png`,
|
||||
datePublished: article.published_at,
|
||||
dateModified: article.updated_at || article.published_at,
|
||||
author: {
|
||||
'@type': 'Person',
|
||||
name: article.author?.name || SITE_CONFIG.name,
|
||||
image: article.author?.image,
|
||||
},
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: SITE_CONFIG.name,
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: `${SITE_CONFIG.url}/images/logo.png`,
|
||||
},
|
||||
},
|
||||
articleSection: article.category?.name,
|
||||
url: `${SITE_CONFIG.url}/insights/${article.slug}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<Script
|
||||
id={`article-schema-${article.slug}`}
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// FAQ Schema
|
||||
interface FAQItem {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
interface FAQSchemaProps {
|
||||
faqs: FAQItem[];
|
||||
}
|
||||
|
||||
export function FAQSchema({ faqs }: FAQSchemaProps) {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqs.map((faq) => ({
|
||||
'@type': 'Question',
|
||||
name: faq.question,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: faq.answer,
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
return (
|
||||
<Script
|
||||
id="faq-schema"
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Job Posting Schema
|
||||
interface JobPostingSchemaProps {
|
||||
job: {
|
||||
title: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
location?: string;
|
||||
employment_type?: string;
|
||||
salary_min?: number;
|
||||
salary_max?: number;
|
||||
posted_at?: string;
|
||||
valid_through?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function JobPostingSchema({ job }: JobPostingSchemaProps) {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'JobPosting',
|
||||
title: job.title,
|
||||
description: job.description,
|
||||
datePosted: job.posted_at || new Date().toISOString(),
|
||||
validThrough: job.valid_through,
|
||||
employmentType: job.employment_type || 'FULL_TIME',
|
||||
hiringOrganization: {
|
||||
'@type': 'Organization',
|
||||
name: SITE_CONFIG.name,
|
||||
sameAs: SITE_CONFIG.url,
|
||||
logo: `${SITE_CONFIG.url}/images/logo.png`,
|
||||
},
|
||||
jobLocation: {
|
||||
'@type': 'Place',
|
||||
address: {
|
||||
'@type': 'PostalAddress',
|
||||
addressLocality: job.location || SITE_CONFIG.address.city,
|
||||
addressRegion: SITE_CONFIG.address.state,
|
||||
addressCountry: SITE_CONFIG.address.country,
|
||||
},
|
||||
},
|
||||
baseSalary: job.salary_min &&
|
||||
job.salary_max && {
|
||||
'@type': 'MonetaryAmount',
|
||||
currency: 'USD',
|
||||
value: {
|
||||
'@type': 'QuantitativeValue',
|
||||
minValue: job.salary_min,
|
||||
maxValue: job.salary_max,
|
||||
unitText: 'YEAR',
|
||||
},
|
||||
},
|
||||
url: `${SITE_CONFIG.url}/career/${job.slug}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<Script
|
||||
id={`job-posting-schema-${job.slug}`}
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Local Business Schema
|
||||
export function LocalBusinessSchema() {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ProfessionalService',
|
||||
name: SITE_CONFIG.name,
|
||||
image: `${SITE_CONFIG.url}/images/logo.png`,
|
||||
'@id': SITE_CONFIG.url,
|
||||
url: SITE_CONFIG.url,
|
||||
telephone: SITE_CONFIG.phone,
|
||||
priceRange: '$$$$',
|
||||
address: {
|
||||
'@type': 'PostalAddress',
|
||||
streetAddress: SITE_CONFIG.address.street,
|
||||
addressLocality: SITE_CONFIG.address.city,
|
||||
addressRegion: SITE_CONFIG.address.state,
|
||||
postalCode: SITE_CONFIG.address.zip,
|
||||
addressCountry: SITE_CONFIG.address.country,
|
||||
},
|
||||
geo: {
|
||||
'@type': 'GeoCoordinates',
|
||||
latitude: 37.7749,
|
||||
longitude: -122.4194,
|
||||
},
|
||||
openingHoursSpecification: {
|
||||
'@type': 'OpeningHoursSpecification',
|
||||
dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
|
||||
opens: '09:00',
|
||||
closes: '18:00',
|
||||
},
|
||||
aggregateRating: {
|
||||
'@type': 'AggregateRating',
|
||||
ratingValue: '4.9',
|
||||
reviewCount: '127',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Script
|
||||
id="local-business-schema"
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user