diff --git a/POLICY_API_SETUP.md b/POLICY_API_SETUP.md
deleted file mode 100644
index 28e256d6..00000000
--- a/POLICY_API_SETUP.md
+++ /dev/null
@@ -1,444 +0,0 @@
-# Policy API Setup - Complete Guide
-
-## š Summary
-
-Successfully created a comprehensive Policy Management System with:
-- **3 Policy Types**: Privacy Policy, Terms of Use, Support Policy
-- **Professional Enterprise Content**: 13-15 sections per policy with detailed legal and operational information
-- **Full CRUD API**: RESTful API endpoints for managing policies
-- **Dynamic Frontend**: React-based policy viewer with loading states and error handling
-
----
-
-## š What Was Created
-
-### Backend (Django)
-
-#### 1. **Models** (`backend/policies/models.py`)
-- `Policy`: Main policy document with metadata (type, title, version, effective date)
-- `PolicySection`: Individual sections within each policy
-
-#### 2. **API Views** (`backend/policies/views.py`)
-- `PolicyViewSet`: RESTful viewset for policy CRUD operations
-- Endpoints support filtering by type
-- Retrieve by ID or policy type
-
-#### 3. **Serializers** (`backend/policies/serializers.py`)
-- `PolicySerializer`: Full policy with all sections
-- `PolicyListSerializer`: Simplified list view
-- `PolicySectionSerializer`: Individual section data
-
-#### 4. **Admin Interface** (`backend/policies/admin.py`)
-- Full admin panel integration
-- Inline section editing
-- List filters and search functionality
-
-#### 5. **Management Command** (`backend/policies/management/commands/populate_policies.py`)
-- Command: `python manage.py populate_policies`
-- Creates/updates all 3 policies with professional content
-- 42 total sections across all policies
-
-### Frontend (Next.js)
-
-#### 1. **API Service** (`lib/api/policyService.ts`)
-- `getPolicies()`: Fetch all policies
-- `getPolicyByType(type)`: Fetch specific policy
-- `getPolicyById(id)`: Fetch by ID
-
-#### 2. **React Hook** (`lib/hooks/usePolicy.ts`)
-- `usePolicies()`: Hook for all policies
-- `usePolicy(type)`: Hook for specific policy
-- `usePolicyById(id)`: Hook for policy by ID
-
-#### 3. **Policy Page** (`app/policy/page.tsx`)
-- Dynamic page showing policy content
-- Query parameter: `?type=privacy|terms|support`
-- Loading and error states
-- Responsive design
-- Styled with inline styles
-
-#### 4. **Support Center Integration** (`components/pages/support/SupportCenterHero.tsx`)
-- Added 3 new clickable cards:
- - Privacy Policy ā `/policy?type=privacy`
- - Terms of Use ā `/policy?type=terms`
- - Support Policy ā `/policy?type=support`
-
----
-
-## š Setup Complete!
-
-### What Was Done:
-
-```bash
-# 1. Created migrations
-python manage.py makemigrations policies
-ā
Created: policies/migrations/0001_initial.py
-
-# 2. Applied migrations
-python manage.py migrate policies
-ā
Created database tables
-
-# 3. Populated with content
-python manage.py populate_policies
-ā
Privacy Policy: 13 sections
-ā
Terms of Use: 14 sections
-ā
Support Policy: 15 sections
-```
-
----
-
-## š” API Endpoints
-
-### Base URL
-```
-http://localhost:8000/api/policies/
-```
-
-### Available Endpoints
-
-#### 1. **List All Policies**
-```
-GET /api/policies/
-```
-**Response:**
-```json
-[
- {
- "id": 1,
- "type": "privacy",
- "title": "Privacy Policy",
- "slug": "privacy",
- "description": "Our commitment to protecting your privacy and personal data",
- "last_updated": "2025-10-08",
- "version": "2.1"
- },
- ...
-]
-```
-
-#### 2. **Get Specific Policy by Type**
-```
-GET /api/policies/privacy/
-GET /api/policies/terms/
-GET /api/policies/support/
-```
-**Response:**
-```json
-{
- "id": 1,
- "type": "privacy",
- "title": "Privacy Policy",
- "slug": "privacy",
- "description": "Our commitment to protecting your privacy and personal data",
- "last_updated": "2025-10-08",
- "version": "2.1",
- "effective_date": "2025-10-08",
- "sections": [
- {
- "id": 1,
- "heading": "1. Introduction and Scope",
- "content": "GNX Software Solutions...",
- "order": 1
- },
- ...
- ]
-}
-```
-
-#### 3. **Get Policy by ID**
-```
-GET /api/policies/{id}/
-```
-
----
-
-## šØ Frontend Usage
-
-### 1. **Direct Navigation**
-```typescript
-// Link to policies from anywhere
-Privacy Policy
-Terms of Use
-Support Policy
-```
-
-### 2. **Using the Hook**
-```typescript
-import { usePolicy } from '@/lib/hooks/usePolicy';
-
-function MyComponent() {
- const { data: policy, isLoading, error } = usePolicy('privacy');
-
- if (isLoading) return
Loading...
;
- if (error) return Error: {error.message}
;
-
- return (
-
-
{policy.title}
- {policy.sections.map(section => (
-
-
{section.heading}
-
{section.content}
-
- ))}
-
- );
-}
-```
-
-### 3. **Support Center Cards**
-The support center hero now has 6 cards:
-- **Top Row** (Opens Modals):
- - Submit Tickets
- - Knowledge Base
- - Track Status
-- **Bottom Row** (Navigates to Policy Page):
- - Privacy Policy
- - Terms of Use
- - Support Policy
-
----
-
-## š Policy Content Overview
-
-### Privacy Policy (13 Sections)
-1. Introduction and Scope
-2. Information We Collect
-3. How We Collect Information
-4. Use of Information
-5. Information Sharing and Disclosure
-6. Data Security
-7. Data Retention
-8. Your Rights and Choices
-9. International Data Transfers
-10. Cookies and Tracking Technologies
-11. Children's Privacy
-12. Changes to This Privacy Policy
-13. Contact Information
-
-### Terms of Use (14 Sections)
-1. Acceptance of Terms
-2. Service Description and Scope
-3. User Accounts and Registration
-4. Acceptable Use Policy
-5. Intellectual Property Rights
-6. Service Level Agreements
-7. Payment Terms and Billing
-8. Term and Termination
-9. Disclaimer of Warranties
-10. Limitation of Liability
-11. Dispute Resolution and Governing Law
-12. Modifications to Terms
-13. General Provisions
-14. Contact Information
-
-### Support Policy (15 Sections)
-1. Support Overview and Commitment
-2. Support Coverage and Eligibility
-3. Support Channels and Access Methods
-4. Business Hours and Availability
-5. Priority Levels and Response Times
-6. Support Request Submission Requirements
-7. Support Scope and Covered Activities
-8. Exclusions and Limitations
-9. Escalation Procedures
-10. Knowledge Base and Self-Service Resources
-11. Customer Responsibilities
-12. Scheduled Maintenance and Updates
-13. Service Level Credits and Remedies
-14. Feedback and Continuous Improvement
-15. Contact Information and Resources
-
----
-
-## š§ Admin Panel
-
-Access the admin panel to manage policies:
-
-```
-http://localhost:8000/admin/policies/
-```
-
-**Features:**
-- ā
Create/Edit/Delete policies
-- ā
Inline section editing
-- ā
Version management
-- ā
Effective date tracking
-- ā
Active/Inactive toggle
-- ā
Search and filtering
-
----
-
-## šÆ Features
-
-### Backend
-- ā
RESTful API with Django REST Framework
-- ā
Versioned policies
-- ā
Effective date tracking
-- ā
Ordered sections
-- ā
Active/Inactive status
-- ā
Full admin interface
-- ā
Management command for easy population
-
-### Frontend
-- ā
Dynamic policy loading from API
-- ā
Loading states with spinner
-- ā
Error handling with user-friendly messages
-- ā
Responsive design (mobile-friendly)
-- ā
Version and date display
-- ā
Smooth animations
-- ā
SEO-friendly structure
-- ā
Integration with support center
-
----
-
-## š¦ Testing
-
-### Test the API
-```bash
-# List all policies
-curl http://localhost:8000/api/policies/
-
-# Get privacy policy
-curl http://localhost:8000/api/policies/privacy/
-
-# Get terms of use
-curl http://localhost:8000/api/policies/terms/
-
-# Get support policy
-curl http://localhost:8000/api/policies/support/
-```
-
-### Test the Frontend
-1. Navigate to: `http://localhost:3000/policy?type=privacy`
-2. Navigate to: `http://localhost:3000/policy?type=terms`
-3. Navigate to: `http://localhost:3000/policy?type=support`
-4. Navigate to: `http://localhost:3000/support-center`
-5. Click on any of the 6 feature cards
-
----
-
-## š Database Schema
-
-```sql
--- Policy Table
-CREATE TABLE policies_policy (
- id INTEGER PRIMARY KEY,
- type VARCHAR(50) UNIQUE,
- title VARCHAR(200),
- slug VARCHAR(100) UNIQUE,
- description TEXT,
- last_updated DATE,
- version VARCHAR(20),
- is_active BOOLEAN,
- effective_date DATE,
- created_at TIMESTAMP,
- updated_at TIMESTAMP
-);
-
--- PolicySection Table
-CREATE TABLE policies_policysection (
- id INTEGER PRIMARY KEY,
- policy_id INTEGER REFERENCES policies_policy(id),
- heading VARCHAR(300),
- content TEXT,
- order INTEGER,
- is_active BOOLEAN,
- created_at TIMESTAMP,
- updated_at TIMESTAMP
-);
-```
-
----
-
-## š Updating Policies
-
-### Via Management Command
-```bash
-# Re-run to update all policies
-cd backend
-../venv/bin/python manage.py populate_policies
-```
-
-### Via Admin Panel
-1. Go to `http://localhost:8000/admin/policies/policy/`
-2. Click on the policy to edit
-3. Modify sections inline
-4. Save changes
-
-### Via API (Programmatic)
-```python
-from policies.models import Policy, PolicySection
-
-# Get policy
-policy = Policy.objects.get(type='privacy')
-
-# Add new section
-PolicySection.objects.create(
- policy=policy,
- heading="New Section",
- content="Section content...",
- order=14
-)
-```
-
----
-
-## šØ Customization
-
-### Styling
-The policy page uses inline styles. To customize:
-- Edit `/app/policy/page.tsx`
-- Modify the `
+
+ );
+ }
+
+ // Standard layout with explicit dimensions
+ return (
+
+
+
+
+ );
+}
+
+/**
+ * Usage Examples:
+ *
+ * 1. Hero Image (Priority Loading):
+ *
+ *
+ * 2. Service Card Image (Lazy Loading):
+ *
+ *
+ * 3. Background Image (Fill):
+ *
+ *
+ * 4. Logo (High Priority):
+ *
+ */
+
diff --git a/gnx-react/components/shared/ProtectedImage.tsx b/gnx-react/components/shared/ProtectedImage.tsx
new file mode 100644
index 00000000..a7011ba7
--- /dev/null
+++ b/gnx-react/components/shared/ProtectedImage.tsx
@@ -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 (
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
e.preventDefault()}
+ onDragStart={(e) => e.preventDefault()}
+ style={{
+ WebkitUserSelect: 'none',
+ MozUserSelect: 'none',
+ msUserSelect: 'none',
+ userSelect: 'none',
+ pointerEvents: 'none'
+ }}
+ />
+ {children}
+
+ );
+}
+
+
diff --git a/gnx-react/components/shared/banners/ServiceDetailsBanner.tsx b/gnx-react/components/shared/banners/ServiceDetailsBanner.tsx
index 7750658f..acea59a7 100644
--- a/gnx-react/components/shared/banners/ServiceDetailsBanner.tsx
+++ b/gnx-react/components/shared/banners/ServiceDetailsBanner.tsx
@@ -309,19 +309,6 @@ const ServiceDetailsBanner = ({ service }: ServiceDetailsBannerProps) => {
)}
- {service.formatted_price && (
-
-
-
-
-
-
- Starting From
- {service.formatted_price}
-
-
-
- )}
{service.featured && (
@@ -355,38 +342,20 @@ const ServiceDetailsBanner = ({ service }: ServiceDetailsBannerProps) => {
diff --git a/gnx-react/components/shared/layout/footer/Footer.tsx b/gnx-react/components/shared/layout/footer/Footer.tsx
index 7379bf59..908658ab 100644
--- a/gnx-react/components/shared/layout/footer/Footer.tsx
+++ b/gnx-react/components/shared/layout/footer/Footer.tsx
@@ -243,40 +243,19 @@ const Footer = () => {
diff --git a/gnx-react/components/shared/layout/header/Header.tsx b/gnx-react/components/shared/layout/header/Header.tsx
index 6eca117c..df238620 100644
--- a/gnx-react/components/shared/layout/header/Header.tsx
+++ b/gnx-react/components/shared/layout/header/Header.tsx
@@ -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 */}
- {navigationData.map((item, index) =>
- item.submenu ? (
+ {navigationData.map((item) =>
+ item.title === "Support Center" ? null : item.submenu ? (
!isMobile && setOpenDropdown(index)}
+ key={item.id}
+ onMouseEnter={() => !isMobile && setOpenDropdown(item.id)}
onMouseLeave={() => !isMobile && setOpenDropdown(null)}
>
isMobile && handleDropdownToggle(index)}
+ onClick={() => isMobile && handleDropdownToggle(item.id)}
>
{item.title}
{item.title === "Services" && servicesLoading && (
ā³
)}
-
+
{item.title === "Services" && servicesLoading ? (
Loading services...
@@ -218,7 +218,10 @@ const Header = () => {
) : (
-
+
{
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
>
{item.title}
@@ -150,13 +157,13 @@ const OffcanvasMenu = ({
Get in Touch
Ready to transform your business?
@@ -164,7 +171,7 @@ const OffcanvasMenu = ({
@@ -173,25 +180,7 @@ const OffcanvasMenu = ({
-
-
-
-
-
-
-
-
-
-
diff --git a/gnx-react/components/shared/seo/StructuredData.tsx b/gnx-react/components/shared/seo/StructuredData.tsx
new file mode 100644
index 00000000..0a2509ca
--- /dev/null
+++ b/gnx-react/components/shared/seo/StructuredData.tsx
@@ -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 (
+
+ );
+}
+
+// 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 (
+
+ );
+}
+
+// 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 (
+
+ );
+}
+
+// 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 (
+
+ );
+}
+
+// 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 (
+
+ );
+}
+
+// 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 (
+
+ );
+}
+
+// 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 (
+
+ );
+}
+
+// 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 (
+
+ );
+}
+
diff --git a/gnx-react/lib/config/api.ts b/gnx-react/lib/config/api.ts
index 2c68072b..c4a816d3 100644
--- a/gnx-react/lib/config/api.ts
+++ b/gnx-react/lib/config/api.ts
@@ -1,9 +1,17 @@
/**
* API Configuration
* Centralized configuration for API endpoints
+ *
+ * In Development: Calls backend directly at http://localhost:8000
+ * In Production: Uses Next.js rewrites/nginx proxy at /api (internal network only)
*/
-export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
+// Production: Use relative URLs (nginx proxy)
+// Development: Use full backend URL
+const isProduction = process.env.NODE_ENV === 'production';
+export const API_BASE_URL = isProduction
+ ? '' // Use relative URLs in production (proxied by nginx)
+ : (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000');
export const API_CONFIG = {
// Django API Base URL
diff --git a/gnx-react/lib/seo/metadata.ts b/gnx-react/lib/seo/metadata.ts
new file mode 100644
index 00000000..d49b0155
--- /dev/null
+++ b/gnx-react/lib/seo/metadata.ts
@@ -0,0 +1,297 @@
+import { Metadata } from 'next';
+
+// Site Configuration
+export const SITE_CONFIG = {
+ name: 'GNX Soft',
+ shortName: 'GNX',
+ description: 'Leading enterprise software development company providing custom solutions, data replication, incident management, AI-powered business intelligence, and comprehensive system integrations for modern businesses.',
+ url: process.env.NEXT_PUBLIC_SITE_URL || 'https://gnxsoft.com',
+ ogImage: '/images/og-image.png',
+ email: 'info@gnxsoft.com',
+ phone: '+359 896 13 80 30',
+ address: {
+ street: 'Tsar Simeon I, 56',
+ city: 'Burgas',
+ state: 'BG',
+ zip: '8000',
+ country: 'Bulgaria',
+ },
+ social: {
+ linkedin: 'https://www.linkedin.com/company/gnxtech',
+ github: 'https://github.com/gnxtech',
+ },
+ businessHours: 'Monday - Friday: 9:00 AM - 6:00 PM PST',
+ foundedYear: 2020,
+};
+
+// Default SEO Configuration
+export const DEFAULT_SEO = {
+ title: `${SITE_CONFIG.name} | Enterprise Software Development & IT Solutions`,
+ description: SITE_CONFIG.description,
+ keywords: [
+ 'Enterprise Software Development',
+ 'Custom Software Development',
+ 'Data Replication Services',
+ 'Incident Management SaaS',
+ 'AI Business Intelligence',
+ 'Backend Engineering',
+ 'Frontend Engineering',
+ 'Systems Integration',
+ 'External Systems Integration',
+ 'Payment Terminal Integration',
+ 'ERP Integration',
+ 'Cloud Platform Integration',
+ 'Fiscal Printer Integration',
+ 'Enterprise Technology Solutions',
+ 'Digital Transformation',
+ 'Software Consulting',
+ 'API Development',
+ 'Microservices Architecture',
+ 'Cloud Migration',
+ 'DevOps Services',
+ ],
+};
+
+// Generate metadata for pages
+interface PageMetadataProps {
+ title?: string;
+ description?: string;
+ keywords?: string[];
+ image?: string;
+ url?: string;
+ type?: 'website' | 'article' | 'product' | 'service';
+ publishedTime?: string;
+ modifiedTime?: string;
+ author?: string;
+ noindex?: boolean;
+ nofollow?: boolean;
+}
+
+export function generateMetadata({
+ title,
+ description,
+ keywords = [],
+ image,
+ url,
+ type = 'website',
+ publishedTime,
+ modifiedTime,
+ author,
+ noindex = false,
+ nofollow = false,
+}: PageMetadataProps = {}): Metadata {
+ const pageTitle = title
+ ? `${title} | ${SITE_CONFIG.name}`
+ : DEFAULT_SEO.title;
+ const pageDescription = description || DEFAULT_SEO.description;
+ const pageImage = image
+ ? `${SITE_CONFIG.url}${image}`
+ : `${SITE_CONFIG.url}${SITE_CONFIG.ogImage}`;
+ const pageUrl = url ? `${SITE_CONFIG.url}${url}` : SITE_CONFIG.url;
+ const allKeywords = [...DEFAULT_SEO.keywords, ...keywords];
+
+ const metadata: Metadata = {
+ title: pageTitle,
+ description: pageDescription,
+ keywords: allKeywords,
+ authors: [
+ {
+ name: author || SITE_CONFIG.name,
+ url: SITE_CONFIG.url,
+ },
+ ],
+ creator: SITE_CONFIG.name,
+ publisher: SITE_CONFIG.name,
+ icons: {
+ icon: '/images/logo-light.png',
+ shortcut: '/images/logo-light.png',
+ apple: '/images/logo-light.png',
+ },
+ formatDetection: {
+ email: false,
+ address: false,
+ telephone: false,
+ },
+ metadataBase: new URL(SITE_CONFIG.url),
+ alternates: {
+ canonical: pageUrl,
+ },
+ openGraph: {
+ type: type === 'article' ? 'article' : 'website',
+ locale: 'en_US',
+ url: pageUrl,
+ siteName: SITE_CONFIG.name,
+ title: pageTitle,
+ description: pageDescription,
+ images: [
+ {
+ url: pageImage,
+ width: 1200,
+ height: 630,
+ alt: title || SITE_CONFIG.name,
+ },
+ ],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: pageTitle,
+ description: pageDescription,
+ images: [pageImage],
+ },
+ robots: {
+ index: !noindex,
+ follow: !nofollow,
+ googleBot: {
+ index: !noindex,
+ follow: !nofollow,
+ 'max-video-preview': -1,
+ 'max-image-preview': 'large',
+ 'max-snippet': -1,
+ },
+ },
+ verification: {
+ google: process.env.NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION,
+ yandex: process.env.NEXT_PUBLIC_YANDEX_VERIFICATION,
+ },
+ };
+
+ // Add article-specific metadata
+ if (type === 'article' && (publishedTime || modifiedTime)) {
+ metadata.openGraph = {
+ ...metadata.openGraph,
+ type: 'article',
+ publishedTime,
+ modifiedTime,
+ authors: [author || SITE_CONFIG.name],
+ };
+ }
+
+ return metadata;
+}
+
+// Service-specific metadata generator
+export function generateServiceMetadata(service: {
+ title: string;
+ description: string;
+ short_description?: string;
+ slug: string;
+ category?: { name: string };
+ technologies?: string;
+ duration?: string;
+}) {
+ const keywords = [
+ service.title,
+ `${service.title} Services`,
+ service.category?.name || 'Enterprise Services',
+ 'Custom Development',
+ 'Enterprise Solutions',
+ ];
+
+ if (service.technologies) {
+ const techs = service.technologies.split(',').map((t) => t.trim());
+ keywords.push(...techs);
+ }
+
+ return generateMetadata({
+ title: service.title,
+ description: service.short_description || service.description,
+ keywords,
+ url: `/services/${service.slug}`,
+ type: 'service' as any,
+ });
+}
+
+// Blog-specific metadata generator
+export function generateBlogMetadata(post: {
+ title: string;
+ description?: string;
+ excerpt?: string;
+ slug: string;
+ image?: string;
+ published_at?: string;
+ updated_at?: string;
+ author?: { name: string };
+ category?: { name: string };
+ tags?: string[];
+}) {
+ const keywords = [
+ post.category?.name || 'Technology',
+ 'Blog',
+ 'Tech Insights',
+ ...(post.tags || []),
+ ];
+
+ return generateMetadata({
+ title: post.title,
+ description: post.description || post.excerpt,
+ keywords,
+ image: post.image,
+ url: `/insights/${post.slug}`,
+ type: 'article',
+ publishedTime: post.published_at,
+ modifiedTime: post.updated_at,
+ author: post.author?.name,
+ });
+}
+
+// Case Study metadata generator
+export function generateCaseStudyMetadata(caseStudy: {
+ title: string;
+ description?: string;
+ excerpt?: string;
+ slug: string;
+ image?: string;
+ client_name?: string;
+ industry?: string;
+ technologies?: string;
+}) {
+ const keywords = [
+ 'Case Study',
+ caseStudy.client_name || '',
+ caseStudy.industry || '',
+ 'Success Story',
+ 'Client Project',
+ ];
+
+ if (caseStudy.technologies) {
+ const techs = caseStudy.technologies.split(',').map((t) => t.trim());
+ keywords.push(...techs);
+ }
+
+ return generateMetadata({
+ title: caseStudy.title,
+ description: caseStudy.description || caseStudy.excerpt,
+ keywords: keywords.filter(Boolean),
+ image: caseStudy.image,
+ url: `/case-study/${caseStudy.slug}`,
+ type: 'article',
+ });
+}
+
+// Career metadata generator
+export function generateCareerMetadata(job: {
+ title: string;
+ description?: string;
+ slug: string;
+ location?: string;
+ department?: string;
+ employment_type?: string;
+}) {
+ const keywords = [
+ job.title,
+ 'Career',
+ 'Job Opening',
+ job.department || '',
+ job.location || '',
+ job.employment_type || '',
+ 'Join Our Team',
+ ].filter(Boolean);
+
+ return generateMetadata({
+ title: `${job.title} - Careers`,
+ description: job.description,
+ keywords,
+ url: `/career/${job.slug}`,
+ });
+}
+
diff --git a/gnx-react/next.config.js b/gnx-react/next.config.js
index 2c5bb9c1..610b14ed 100644
--- a/gnx-react/next.config.js
+++ b/gnx-react/next.config.js
@@ -33,11 +33,133 @@ const nextConfig = {
// pathname: '/media/**',
// },
],
+ formats: ['image/avif', 'image/webp'],
+ deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
+ imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
+ minimumCacheTTL: 60,
},
sassOptions: {
includePaths: ['./public/styles', './node_modules'],
quietDeps: true, // Suppress deprecation warnings from dependencies
},
+ // Compiler optimizations
+ compiler: {
+ removeConsole: process.env.NODE_ENV === 'production' ? {
+ exclude: ['error', 'warn'],
+ } : false,
+ },
+ // Compression
+ compress: true,
+ // Production optimizations
+ productionBrowserSourceMaps: false,
+ // Performance optimizations (swcMinify removed - default in Next.js 15)
+ // Enterprise Security Headers
+ async headers() {
+ return [
+ {
+ source: '/:path*',
+ headers: [
+ // Security Headers
+ {
+ key: 'X-DNS-Prefetch-Control',
+ value: 'on'
+ },
+ {
+ key: 'Strict-Transport-Security',
+ value: 'max-age=63072000; includeSubDomains; preload'
+ },
+ {
+ key: 'X-Frame-Options',
+ value: 'SAMEORIGIN'
+ },
+ {
+ key: 'X-Content-Type-Options',
+ value: 'nosniff'
+ },
+ {
+ key: 'X-XSS-Protection',
+ value: '1; mode=block'
+ },
+ {
+ key: 'Referrer-Policy',
+ value: 'strict-origin-when-cross-origin'
+ },
+ {
+ key: 'Permissions-Policy',
+ value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()'
+ },
+ {
+ key: 'Content-Security-Policy',
+ value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: http://localhost:8000 http://localhost:8080; font-src 'self' data:; connect-src 'self' http://localhost:8000 https://www.google-analytics.com; frame-src 'self' https://www.google.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'"
+ },
+ // Performance Headers
+ {
+ key: 'Cache-Control',
+ value: 'public, max-age=31536000, immutable'
+ },
+ ],
+ },
+ // Static assets caching
+ {
+ source: '/images/:path*',
+ headers: [
+ {
+ key: 'Cache-Control',
+ value: 'public, max-age=31536000, immutable',
+ },
+ ],
+ },
+ {
+ source: '/icons/:path*',
+ headers: [
+ {
+ key: 'Cache-Control',
+ value: 'public, max-age=31536000, immutable',
+ },
+ ],
+ },
+ // API responses - no cache for dynamic content
+ {
+ source: '/api/:path*',
+ headers: [
+ {
+ key: 'Cache-Control',
+ value: 'no-store, must-revalidate',
+ },
+ ],
+ },
+ ]
+ },
+ // Redirects for SEO
+ async redirects() {
+ return [
+ // Redirect trailing slashes
+ {
+ source: '/:path+/',
+ destination: '/:path+',
+ permanent: true,
+ },
+ ]
+ },
+ // Rewrites for API proxy (Production: routes /api to backend through nginx)
+ async rewrites() {
+ // In development, proxy to Django backend
+ // In production, nginx handles this
+ if (process.env.NODE_ENV === 'development') {
+ return [
+ {
+ source: '/api/:path*',
+ destination: `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'}/api/:path*`,
+ },
+ {
+ source: '/media/:path*',
+ destination: `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'}/media/:path*`,
+ },
+ ]
+ }
+ // In production, these are handled by nginx reverse proxy
+ return []
+ },
}
module.exports = nextConfig
diff --git a/gnx-react/public/data/offcanvas-data.ts b/gnx-react/public/data/offcanvas-data.ts
index fcbc749b..6d9705c8 100644
--- a/gnx-react/public/data/offcanvas-data.ts
+++ b/gnx-react/public/data/offcanvas-data.ts
@@ -99,12 +99,22 @@ export const OffcanvasData = [
},
{
id: 11,
- title: "Contact Us",
- path: "/contact-us",
+ title: "Support Center",
+ path: "/support-center",
parent_id: null,
display_order: 7,
submenu: null,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z"
+ },
+ {
+ id: 12,
+ title: "Contact Us",
+ path: "/contact-us",
+ parent_id: null,
+ display_order: 8,
+ submenu: null,
+ created_at: "2024-01-01T00:00:00Z",
+ updated_at: "2024-01-01T00:00:00Z"
}
];
diff --git a/gnx-react/public/styles/main.scss b/gnx-react/public/styles/main.scss
index c709cf95..46b9af71 100644
--- a/gnx-react/public/styles/main.scss
+++ b/gnx-react/public/styles/main.scss
@@ -36,6 +36,7 @@
// modern CSS framework (Bootstrap replacement)
@use "./utilities/modern-framework";
+@use "./utilities/content-protection";
/* ==============
========= css table of contents =========
diff --git a/gnx-react/public/styles/utilities/_content-protection.scss b/gnx-react/public/styles/utilities/_content-protection.scss
new file mode 100644
index 00000000..9c655b7d
--- /dev/null
+++ b/gnx-react/public/styles/utilities/_content-protection.scss
@@ -0,0 +1,123 @@
+// ============================================================================
+// Content Protection Styles
+// ============================================================================
+// Prevents content copying, text selection, and image downloading
+
+// Disable text selection globally
+.content-protected {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+
+ // Prevent tap highlight on mobile
+ -webkit-tap-highlight-color: transparent;
+
+ // Disable image dragging
+ img {
+ -webkit-user-drag: none;
+ -khtml-user-drag: none;
+ -moz-user-drag: none;
+ -o-user-drag: none;
+ user-drag: none;
+ pointer-events: none;
+ }
+
+ // Protect all text content
+ * {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ }
+}
+
+// Allow selection for input fields and textareas
+.content-protected input,
+.content-protected textarea,
+.content-protected [contenteditable="true"] {
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ -ms-user-select: text;
+ user-select: text;
+}
+
+// Image protection with overlay
+.protected-image-wrapper {
+ position: relative;
+ display: inline-block;
+
+ img {
+ display: block;
+ -webkit-user-drag: none;
+ -khtml-user-drag: none;
+ -moz-user-drag: none;
+ -o-user-drag: none;
+ user-drag: none;
+ pointer-events: none;
+ }
+
+ // Transparent overlay to prevent right-click on images
+ &::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: transparent;
+ pointer-events: auto;
+ z-index: 1;
+ }
+}
+
+// Watermark overlay (optional - can be enabled per image)
+.watermarked-image {
+ position: relative;
+
+ &::before {
+ content: 'GNX Soft';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%) rotate(-45deg);
+ font-size: 3rem;
+ font-weight: bold;
+ color: rgba(255, 255, 255, 0.15);
+ pointer-events: none;
+ z-index: 2;
+ white-space: nowrap;
+ }
+}
+
+// Hide on print (prevent print screen)
+@media print {
+ .no-print {
+ display: none !important;
+ }
+
+ body::after {
+ content: 'This content is protected and cannot be printed. Visit gnxsoft.com';
+ display: block;
+ text-align: center;
+ padding: 50px;
+ font-size: 20px;
+ }
+}
+
+// Additional protection for code blocks
+.content-protected pre,
+.content-protected code {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+// Blur content when dev tools are open (optional)
+.devtools-open {
+ filter: blur(5px);
+ pointer-events: none;
+}
+
+
diff --git a/gnx-react/requirements.txt b/gnx-react/requirements.txt
index 0991a6e4..b277c410 100644
--- a/gnx-react/requirements.txt
+++ b/gnx-react/requirements.txt
@@ -3,7 +3,6 @@ djangorestframework==3.14.0
django-cors-headers==4.3.1
python-decouple==3.8
Pillow==10.1.0
-psycopg2-binary==2.9.9
celery==5.3.4
redis==5.0.1
django-environ==0.11.2
diff --git a/nginx-gnxsoft.conf b/nginx-gnxsoft.conf
new file mode 100644
index 00000000..9dbd7bd1
--- /dev/null
+++ b/nginx-gnxsoft.conf
@@ -0,0 +1,195 @@
+# Production Nginx Configuration for GNX Soft
+# Place this in /etc/nginx/sites-available/gnxsoft
+#
+# DEPLOYMENT NOTES:
+# 1. Frontend: Next.js production build runs on port 3000
+# - Build: npm run build
+# - Start: npm start (or use PM2: pm2 start npm --name "gnxsoft-frontend" -- start)
+# 2. Backend: Django runs on port 8000 (internal only)
+# - Use Gunicorn: gunicorn gnx.wsgi:application --bind 127.0.0.1:8000
+# - Or PM2: pm2 start gunicorn --name "gnxsoft-backend" -- gnx.wsgi:application --bind 127.0.0.1:8000
+
+# Frontend - Public facing (Next.js Production Server)
+upstream frontend {
+ server 127.0.0.1:3000;
+ keepalive 64;
+}
+
+# Backend - Internal only (Django)
+upstream backend_internal {
+ server 127.0.0.1:8000;
+ keepalive 64;
+}
+
+# Redirect HTTP to HTTPS
+server {
+ listen 80;
+ listen [::]:80;
+ server_name gnxsoft.com www.gnxsoft.com;
+
+ # Let's Encrypt validation
+ location /.well-known/acme-challenge/ {
+ root /var/www/certbot;
+ }
+
+ # Redirect all other traffic to HTTPS
+ location / {
+ return 301 https://$server_name$request_uri;
+ }
+}
+
+# HTTPS Frontend
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name gnxsoft.com www.gnxsoft.com;
+
+ # SSL Configuration
+ ssl_certificate /etc/letsencrypt/live/gnxsoft.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/gnxsoft.com/privkey.pem;
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
+ ssl_prefer_server_ciphers on;
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_timeout 10m;
+
+ # Security Headers
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+ add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;
+
+ # Rate Limiting Zones
+ limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
+ limit_req_zone $binary_remote_addr zone=general_limit:10m rate=100r/s;
+ limit_req_status 429;
+
+ # Client settings
+ client_max_body_size 10M;
+ client_body_timeout 30s;
+ client_header_timeout 30s;
+
+ # Logging
+ access_log /var/log/nginx/gnxsoft_access.log;
+ error_log /var/log/nginx/gnxsoft_error.log warn;
+
+ # Root location - Frontend (Next.js)
+ location / {
+ limit_req zone=general_limit burst=50 nodelay;
+
+ proxy_pass http://frontend;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_cache_bypass $http_upgrade;
+
+ # Timeouts
+ proxy_connect_timeout 60s;
+ proxy_send_timeout 60s;
+ proxy_read_timeout 60s;
+ }
+
+ # API Proxy - Frontend talks to backend ONLY through this internal proxy
+ # Backend port 8000 is BLOCKED from internet by firewall
+ location /api/ {
+ limit_req zone=api_limit burst=20 nodelay;
+
+ # Internal proxy to backend (127.0.0.1:8000)
+ # Backend is NOT accessible from public internet
+ proxy_pass http://backend_internal/api/;
+ proxy_http_version 1.1;
+
+ # Backend sees request as coming from localhost
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP 127.0.0.1;
+ proxy_set_header X-Forwarded-For 127.0.0.1;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # Hide backend server info
+ proxy_hide_header X-Powered-By;
+ proxy_hide_header Server;
+
+ # Timeouts
+ proxy_connect_timeout 30s;
+ proxy_send_timeout 30s;
+ proxy_read_timeout 30s;
+
+ # CORS headers (if needed)
+ add_header Access-Control-Allow-Origin "https://gnxsoft.com" always;
+ add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
+ add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
+ add_header Access-Control-Allow-Credentials "true" always;
+
+ # Handle preflight requests
+ if ($request_method = 'OPTIONS') {
+ return 204;
+ }
+ }
+
+ # Media files (served by nginx directly for better performance)
+ location /media/ {
+ alias /var/www/gnxsoft/media/;
+ expires 30d;
+ add_header Cache-Control "public, immutable";
+ access_log off;
+
+ # Security
+ location ~ \.(php|py|pl|sh)$ {
+ deny all;
+ }
+ }
+
+ # Static files (served by nginx directly)
+ location /static/ {
+ alias /var/www/gnxsoft/static/;
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ access_log off;
+ }
+
+ # Next.js static files
+ location /_next/static/ {
+ proxy_pass http://frontend;
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ access_log off;
+ }
+
+ # Deny access to hidden files
+ location ~ /\. {
+ deny all;
+ access_log off;
+ log_not_found off;
+ }
+
+ # Deny access to backend admin (extra security)
+ location /admin {
+ deny all;
+ return 404;
+ }
+
+ # Health check endpoint
+ location /health {
+ access_log off;
+ return 200 "OK\n";
+ add_header Content-Type text/plain;
+ }
+}
+
+# ==============================================================================
+# IMPORTANT SECURITY NOTES:
+# ==============================================================================
+# 1. Backend runs on 127.0.0.1:8000 (internal only)
+# 2. Firewall BLOCKS external access to port 8000
+# 3. Only nginx can reach backend (internal network)
+# 4. Public internet can ONLY access nginx (ports 80, 443)
+# 5. All API calls go through nginx proxy (/api/* ā 127.0.0.1:8000/api/*)
+# 6. Backend IP whitelist middleware ensures only localhost requests
+# ==============================================================================
+