Neue services

This commit is contained in:
2026-03-25 20:07:27 -05:00
parent 42e0971a13
commit bcf9dc541c
85 changed files with 8589 additions and 4832 deletions

View File

@@ -0,0 +1,166 @@
# Product Marketing Context
*Last updated: 2026-03-25*
## Product Overview
**One-liner:** Bay Area IT is a local managed IT and business technology partner for small and mid-sized businesses in Corpus Christi and the Coastal Bend.
**What it does:** The company provides ongoing IT support, help desk, network infrastructure, business email, device support, security basics, backups, and related technology services for local businesses. It also offers adjacent services such as web design, shared drive setup, printer/scanner installation, and hardware procurement. The positioning throughout the site emphasizes reliable day-to-day operations, fast support, and a local partner that can help remotely or on-site.
**Product category:** Managed IT services, IT support, outsourced IT support, IT help desk, business technology services for SMBs.
**Product type:** Local B2B service business.
**Business model:** Service-led business with monthly recurring support plans plus project-based services. Pricing signals on the site suggest transparent monthly pricing for support and per-unit pricing for some offers (example: business email at `$5` per inbox). Primary conversion is a free 20-minute assessment / consultation.
## Target Audience
**Target companies:** Small and mid-sized businesses in Corpus Christi and the broader Coastal Bend, including Portland, Rockport, Aransas Pass, Kingsville, and Port Aransas. Strong fit for offices, service businesses, healthcare-related practices, growing teams, and companies without a full in-house IT department.
**Decision-makers:** Business owners, office managers, operations leaders, and IT-responsible managers at SMBs. Secondary audiences likely include executive assistants, practice managers, and internal technical contacts who influence vendor selection.
**Primary use case:** Outsource day-to-day IT management so the business can reduce downtime, improve security, and keep employees productive without building a full internal IT team.
**Jobs to be done:**
- Keep our systems, email, devices, Wi-Fi, and users working reliably every day.
- Give employees a clear support channel so small issues stop turning into business delays.
- Put practical security, backup, and monitoring in place without overcomplicating the environment.
**Use cases:**
- Ongoing managed IT support for SMBs without in-house IT
- Remote-first help desk with on-site escalation when needed
- Business email setup, migration, and support
- Network cleanup, Wi-Fi improvement, firewall/router/switch management
- New user onboarding, device setup, and account/access support
- Web design and domain/DNS support for small local businesses
## Personas
| Persona | Cares about | Challenge | Value we promise |
|---------|-------------|-----------|------------------|
| Business owner / President | Predictable costs, fewer disruptions, trust in provider, fast escalation | They are tired of acting as part-time IT and losing time to avoidable issues | We take IT ownership off their plate with clear communication, local accountability, and practical support |
| Office manager / Operations lead | Employee productivity, quick fixes, simple process, dependable vendor | Staff lose time to recurring tickets, login issues, printer/email/network friction | We give your team a responsive help desk and structured support workflow that keeps work moving |
| Practice manager / Admin lead | Reliability, compliance-adjacent security basics, smooth communication systems | Email, devices, and access issues disrupt client or patient-facing work | We stabilize the environment and implement simple security and backup foundations |
| Internal technical contact | Competence, clean implementation, sane network/security setup | They need outside help without inheriting chaos or vague recommendations | We provide hands-on implementation, monitoring, and support without unnecessary complexity |
| Financial buyer | Cost control, ROI, avoiding headcount, lower risk | Hiring full-time IT is expensive and overkill for many SMBs | Outsourced IT gives predictable monthly pricing and broader coverage than a single in-house hire |
## Problems & Pain Points
**Core problem:** Business technology has become unpredictable, reactive, and distracting. Employees hit avoidable issues, support requests pile up, and owners or managers end up spending time managing IT instead of running the business.
**Why alternatives fall short:**
- Break-fix support is reactive and does not create ongoing stability
- Remote-only national providers can feel slow, generic, and disconnected from local urgency
- A single in-house generalist is expensive and may not cover the full range of needs
- DIY setups for email, networking, security, and backups create hidden fragility
**What it costs them:** Lost productivity, downtime, delayed customer response, missed emails, security exposure, recurring employee frustration, and management time pulled away from growth.
**Emotional tension:** Frustration, lack of confidence in systems, fear of outages or security incidents, and fatigue from being forced to “figure IT out” internally.
## Competitive Landscape
**Direct:** Local MSPs and IT support providers in Corpus Christi / Coastal Bend — often positioned similarly on help desk, managed IT, and outsourced support; likely to fall short on responsiveness, communication clarity, or service breadth if they are inconsistent.
**Secondary:** Freelance IT technicians, break-fix computer repair shops, and one-person outsourced support providers — cheaper upfront but often reactive, less structured, and weaker on ongoing monitoring, security baselines, and business process support.
**Indirect:** Hiring an internal IT employee, relying on a tech-savvy office employee, or using vendor support from Microsoft/Google/ISP/hardware providers — these options are fragmented, slower for cross-system problems, and usually do not provide clear ownership of the full environment.
## Differentiation
**Key differentiators:**
- Local provider based in Corpus Christi with regional service coverage
- Remote-first support with on-site availability when needed
- Broad business technology scope: help desk, email, networking, devices, security basics, backups, web, DNS/domain support
- Clear, practical positioning instead of overly technical jargon
- Free 20-minute assessment as a low-friction first step
**How we do it differently:** We position the company as a local IT partner that combines fast support, hands-on implementation, and practical business judgment. The site consistently emphasizes reliable operations, simple explanations, and support that fits SMB reality rather than enterprise complexity.
**Why that's better:** SMB buyers get one accountable partner, faster issue resolution, lower management overhead, and support that balances remote speed with local presence.
**Why customers choose us:** They want predictable help, local accountability, straightforward communication, and a provider who can support everyday business operations instead of only isolated technical tasks.
## Objections
| Objection | Response |
|-----------|----------|
| “Were too small to need managed IT.” | Small businesses are often the most disrupted by recurring tech issues because they have less internal coverage. Outsourced support gives you structure and expertise without hiring a full-time team. |
| “We already have someone who helps with IT.” | That can work until issues span email, networking, security, users, and vendors at the same time. We provide broader coverage, clearer ownership, and ongoing support instead of ad hoc fixes. |
| “A remote provider or break-fix shop is cheaper.” | Lower upfront cost usually means slower response, less accountability, and more recurring issues. The sites positioning is around reducing downtime and management distraction, not just closing tickets cheaply. |
**Anti-persona:** Large enterprises with internal IT departments and complex procurement, buyers shopping only on the lowest hourly rate, and organizations that want purely transactional break-fix work with no ongoing relationship.
## Switching Dynamics
**Push:** Recurring downtime, slow systems, unresolved support tickets, weak Wi-Fi, email problems, unclear ownership, and security/backups that feel too risky to ignore.
**Pull:** Local support, fast response, clear communication, monthly support options, practical security baseline, and an easy first step via a free assessment.
**Habit:** Existing vendor relationships, internal workarounds, “good enough” support, reluctance to document the current environment, and the inertia of staying reactive.
**Anxiety:** Fear of disruption during migration, concern about cost, worry that the new provider will overcomplicate things, and uncertainty about whether a local MSP will actually be more responsive.
## Customer Language
**How they describe the problem:**
- “The internet is slow.”
- “The system is glitchy.”
- “Our email stopped working.”
- “Support requests never end.”
- “Im tired of playing part-time IT.”
- “We need reliable IT without hiring in-house staff.”
**How they describe us:**
- “Local IT support”
- “Outsourced IT support”
- “Fast remote assistance with on-site support when needed”
- “Reliable IT partner”
- “Help desk for employees”
**Words to use:** reliable, local, fast response, outsourced IT support, help desk, remote support, on-site when needed, security baseline, backup monitoring, predictable monthly pricing, practical, clear, business-focused.
**Words to avoid:** revolutionary, cutting-edge for its own sake, AI-first, disruption, synergy, enterprise transformation, generic “digital innovation” language.
**Glossary:**
| Term | Meaning |
|------|---------|
| Managed IT / ongoing IT support | Recurring support relationship covering maintenance, issue resolution, and operational stability |
| Help desk | Day-to-day employee support for access, software, device, and common IT issues |
| Security baseline | Practical protection measures like MFA, patching, endpoint protection, and backup basics |
| Remote-first support | Resolving most issues quickly without waiting for an on-site visit |
| On-site support | Physical support for hardware, installations, and network troubleshooting when required |
## Brand Voice
**Tone:** Professional, direct, practical, reassuring.
**Style:** Conversational but credible. Explains technical work in business terms. Avoids jargon-heavy positioning and stays grounded in outcomes like uptime, productivity, and reliability.
**Personality:** Local, dependable, clear, experienced, no-nonsense.
## Proof Points
**Metrics:**
- 25+ years serving the region appears repeatedly across location/service pages
- 30+ local businesses supported appears across multiple pages
- 99.9% uptime and `<2 min` response time appear on the About page
- Assessments can begin within 48 hours
- 24/7 emergency support / monitoring is referenced in contact and CTA copy
**Customers:** No named logos surfaced in the repo copy reviewed. The site references local businesses across the Coastal Bend rather than public logos.
**Testimonials:**
> No direct testimonial quotes were found in the repo sections reviewed. Add real customer quotes here when available.
**Value themes:**
| Theme | Proof |
|-------|-------|
| Local trust | Corpus Christi address, repeated local positioning, service-area pages for Coastal Bend cities |
| Reliability | 99.9% uptime claim, remote-first help desk, proactive maintenance and monitoring language |
| Experience | 25+ years claim across pages; long-standing regional presence |
| Business fit | Positioning for small and mid-sized businesses without in-house IT |
| Speed | Fast response, emergency support, assessments within 48 hours |
## Goals
**Business goal:** Generate qualified local SMB leads for managed IT, help desk, business email, networking, and related technology services.
**Conversion action:** Book a free 20-minute assessment / consultation or submit a contact inquiry / call the office.
**Current metrics:** Not fully established from the repo alone. Repo signals include claims around 30+ businesses supported, 99.9% uptime, fast response, and 48-hour assessment turnaround. Customer-count references conflict in the codebase and should be normalized before using in external collateral.
## Open Questions / Conflicts To Resolve
- Brand name is inconsistent: `Bay Area Affiliates`, `Bay Area IT`, and `Bay Area IT Services` all appear in the repo.
- Customer count is inconsistent: some pages say `30+ local businesses`, while `AboutPage` also says `over 150 businesses`.
- Founding timeline is inconsistent: timeline starts in `2000`, but hero/about copy also says `Since 2010`.
- Pricing clarity is partial: one service has explicit `$5 per inbox` pricing, while broader managed IT pricing is only described as transparent monthly pricing.

15
.dockerignore Normal file
View File

@@ -0,0 +1,15 @@
.git
.gitignore
.agents
node_modules
dist
*.log
Dockerfile
docker-compose.yml
README.md
*.md
bullet
ellipsis
em
left
right

175
App.tsx
View File

@@ -1,65 +1,93 @@
import React, { useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route, useLocation } from 'react-router-dom';
import Lenis from '@studio-freight/lenis';
import React, { Suspense, useEffect, useRef } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate, useLocation } from 'react-router-dom';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { ScrollToPlugin } from 'gsap/ScrollToPlugin';
import Navbar from './components/Navbar';
import Footer from './components/Footer';
import BackToTop from './components/BackToTop';
import HomePage from './src/pages/HomePage';
import AboutPage from './src/pages/AboutPage';
import ServicesPage from './src/pages/ServicesPage';
import BlogPage from './src/pages/BlogPage';
import ContactPage from './src/pages/ContactPage';
import LocationPage from './src/pages/LocationPage';
import ServicePage from './src/pages/ServicePage';
import BlogPostPage from './src/pages/BlogPostPage';
import { locationData, serviceData, blogPostData } from './src/data/seoData';
import { BlogSeoRoute, LocationSeoRoute, ServiceSeoRoute, blogRoutes, locationRoutes, serviceRoutes, legacyRedirects } from './src/routes/seoRoutes';
// Register GSAP plugins globally
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
import HomePage from './src/pages/HomePage';
const AboutPage = React.lazy(() => import('./src/pages/AboutPage'));
const ServicesPage = React.lazy(() => import('./src/pages/ServicesPage'));
const BlogPage = React.lazy(() => import('./src/pages/BlogPage'));
const ContactPage = React.lazy(() => import('./src/pages/ContactPage'));
const LocationsPage = React.lazy(() => import('./src/pages/LocationsPage'));
const PrivacyPolicyPage = React.lazy(() => import('./src/pages/PrivacyPolicyPage'));
const TermsOfServicePage = React.lazy(() => import('./src/pages/TermsOfServicePage'));
// Grain Overlay Component
const GrainOverlay = () => (
<div className="fixed inset-0 w-full h-full pointer-events-none z-50 opacity-50 dark:opacity-100 bg-grain mix-blend-overlay"></div>
);
const RouteFallback = () => (
<div className="min-h-[50vh] flex items-center justify-center px-6 text-sm text-gray-500 dark:text-gray-400">
Loading page...
</div>
);
const AppContent: React.FC = () => {
const location = useLocation();
const lenisRef = useRef<any>(null);
useEffect(() => {
// Initialize Lenis for smooth scrolling
const lenis = new Lenis({
duration: 1.2,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
direction: 'vertical',
gestureDirection: 'vertical',
smooth: true,
smoothTouch: false,
touchMultiplier: 2,
} as any);
if (typeof window === 'undefined') {
return;
}
// Synchronize Lenis with GSAP ScrollTrigger
lenis.on('scroll', ScrollTrigger.update);
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
const finePointer = window.matchMedia('(pointer: fine) and (hover: hover)');
const ticker = (time: number) => {
lenis.raf(time * 1000);
};
if (prefersReducedMotion.matches || !finePointer.matches) {
return;
}
// Use GSAP ticker for smoother animation loop integration
gsap.ticker.add(ticker);
let active = true;
let ticker: ((time: number) => void) | null = null;
// Disable lag smoothing to prevent jumps
gsap.ticker.lagSmoothing(0);
void import('@studio-freight/lenis').then(({ default: Lenis }) => {
if (!active) {
return;
}
// Reset scroll on route change
lenis.scrollTo(0, { immediate: true });
const lenis = new Lenis({
duration: 1.2,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
direction: 'vertical',
gestureDirection: 'vertical',
smooth: true,
smoothTouch: false,
touchMultiplier: 2,
} as any);
lenisRef.current = lenis;
lenis.on('scroll', ScrollTrigger.update);
ticker = (time: number) => {
lenis.raf(time * 1000);
};
gsap.ticker.add(ticker);
gsap.ticker.lagSmoothing(0);
});
return () => {
gsap.ticker.remove(ticker);
lenis.destroy();
active = false;
if (ticker) {
gsap.ticker.remove(ticker);
}
lenisRef.current?.destroy();
lenisRef.current = null;
};
}, []);
useEffect(() => {
lenisRef.current?.scrollTo(0, { immediate: true });
}, [location.pathname]);
return (
@@ -67,40 +95,55 @@ const AppContent: React.FC = () => {
<GrainOverlay />
<Navbar />
<main>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/services" element={<ServicesPage />} />
<Route path="/blog" element={<BlogPage />} />
<Route path="/contact" element={<ContactPage />} />
<Suspense fallback={<RouteFallback />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/services" element={<ServicesPage />} />
<Route path="/blog" element={<BlogPage />} />
<Route path="/contact" element={<ContactPage />} />
<Route path="/locations" element={<LocationsPage />} />
<Route path="/privacy-policy" element={<PrivacyPolicyPage />} />
<Route path="/terms-of-service" element={<TermsOfServicePage />} />
{/* SEO Location Pages */}
{locationData.map((data) => (
<Route
key={data.slug}
path={`/${data.slug}`}
element={<LocationPage data={data} />}
/>
))}
{/* SEO Location Pages */}
{locationRoutes.map((data) => (
<React.Fragment key={data.slug}>
<Route
path={data.path}
element={<LocationSeoRoute slug={data.slug} />}
/>
</React.Fragment>
))}
{/* SEO Service Pages */}
{serviceData.map((data) => (
<Route
key={data.slug}
path={`/${data.slug}`}
element={<ServicePage data={data} />}
/>
))}
{/* SEO Service Pages */}
{serviceRoutes.map((data) => (
<React.Fragment key={data.slug}>
<Route
path={data.path}
element={<ServiceSeoRoute slug={data.slug} />}
/>
</React.Fragment>
))}
{/* Authority Blog Posts */}
{blogPostData.map((data) => (
<Route
key={data.slug}
path={`/${data.slug}`}
element={<BlogPostPage data={data} />}
/>
))}
</Routes>
{/* Authority Blog Posts */}
{blogRoutes.map((data) => (
<React.Fragment key={data.slug}>
<Route
path={data.path}
element={<BlogSeoRoute slug={data.slug} />}
/>
</React.Fragment>
))}
{/* Legacy URL redirects — preserve old flat URLs */}
{legacyRedirects.map(({ from, to }) => (
<React.Fragment key={from}>
<Route path={from} element={<Navigate to={to} replace />} />
</React.Fragment>
))}
</Routes>
</Suspense>
</main>
<Footer />
<BackToTop />

22
Caddyfile Normal file
View File

@@ -0,0 +1,22 @@
# Caddy inside the Docker container — listens on :80
# The host Caddy reverse-proxies to this container and handles HTTPS.
:80 {
root * /srv
encode zstd gzip
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy "geolocation=(), microphone=(), camera=()"
-Server
}
# Long-term caching for hashed assets
@assets path /assets/* /images/* /logo.svg /public/*
header @assets Cache-Control "public, max-age=31536000, immutable"
# SPA fallback: prerendered routes get their own index.html, rest falls back
try_files {path} {path}/index.html /index.html
file_server
}

18
Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM caddy:2.8-alpine
COPY Caddyfile /etc/caddy/Caddyfile
COPY --from=build /app/dist /srv
EXPOSE 80
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]

298
README.md
View File

@@ -1,53 +1,293 @@
# Bay Area Affiliates
# Bay Area IT — Website
Ein modernes React-Projekt erstellt mit Vite, TypeScript und Framer Motion.
Marketing website for **Bay Area IT**, an IT service provider based in Corpus Christi, TX.
Built with React 19, Vite 6, TypeScript, and Tailwind CSS. Deployed as a static site served by Caddy inside Docker.
## Voraussetzungen
---
- Node.js (Version 18 oder höher empfohlen)
- npm (wird mit Node.js installiert)
## Table of Contents
## Installation
- [Tech Stack](#tech-stack)
- [Project Structure](#project-structure)
- [Local Development](#local-development)
- [Build & Preview](#build--preview)
- [Deployment (Docker + Caddy)](#deployment-docker--caddy)
- [Adding Content](#adding-content)
- [Scripts Reference](#scripts-reference)
Klone das Repository und installiere die Abhängigkeiten:
---
```bash
npm install
## Tech Stack
| Layer | Technology |
|---|---|
| UI Framework | React 19 |
| Build Tool | Vite 6 |
| Language | TypeScript 5 |
| Styling | Tailwind CSS 3 |
| Routing | React Router 7 |
| Animations | Framer Motion 12, GSAP 3 |
| Smooth Scroll | Lenis |
| Web Server | Caddy 2 (inside Docker) |
| Containerization | Docker + Docker Compose |
---
## Project Structure
```
bayarea/
├── components/ # Shared UI components
│ ├── Navbar.tsx
│ ├── Footer.tsx
│ ├── Hero.tsx
│ ├── Services.tsx
│ ├── Blog.tsx
│ ├── CTA.tsx
│ ├── Testimonials.tsx
│ ├── Process.tsx
│ ├── AreasWeServe.tsx
│ ├── SEO.tsx # Head/meta tag injection
│ ├── Breadcrumb.tsx
│ └── LoadingScreen.tsx
├── src/
│ ├── pages/ # Route-level page components
│ │ ├── HomePage.tsx
│ │ ├── AboutPage.tsx
│ │ ├── ServicesPage.tsx
│ │ ├── ServicePage.tsx # Dynamic: /services/:slug
│ │ ├── BlogPage.tsx
│ │ ├── BlogPostPage.tsx # Dynamic: /blog/:slug
│ │ ├── LocationPage.tsx # Dynamic: /locations/:slug
│ │ ├── LocationsPage.tsx
│ │ ├── ContactPage.tsx
│ │ ├── PrivacyPolicyPage.tsx
│ │ ├── TermsOfServicePage.tsx
│ │ └── LegalPage.tsx
│ ├── data/
│ │ └── seoData.ts # All location, service, and blog post data
│ ├── routes/ # Route definitions
│ └── index.css # Global styles
├── scripts/
│ ├── prerender-routes.ts # Generates per-route index.html files
│ ├── prune-dist-assets.mjs # Removes unused assets from dist/
│ ├── generate-sitemap.ts # Generates public/sitemap.xml
│ ├── generate-robots.ts # Generates public/robots.txt
│ └── optimize-images.mjs # Converts images to WebP
├── public/
│ ├── assets/services/ # Service page images (.webp)
│ ├── images/blog/ # Blog post images (.webp)
│ ├── sitemap.xml
│ └── robots.txt
├── Dockerfile # Multi-stage: Node build → Caddy serve
├── docker-compose.yml # Runs the container on localhost:8080
├── Caddyfile # Caddy config inside the container
├── caddy-host.snippet # Paste this into your host Caddy config
├── tailwind.config.cjs
├── postcss.config.cjs
├── vite.config.ts
└── tsconfig.json
```
## Entwicklung
---
Starte den lokalen Entwicklungsserver:
## Local Development
### Prerequisites
- Node.js 22+ (or use [nvm](https://github.com/nvm-sh/nvm))
- npm
### Setup
```bash
# Install dependencies
npm install
# Start the dev server
npm run dev
```
Die Anwendung ist dann unter `http://localhost:5173` erreichbar.
The app runs at **http://localhost:3012**
## Build
Hot Module Replacement (HMR) is enabled. If port 3012 is taken, Vite picks the next available port automatically.
Erstelle eine optimierte Version für die Produktion:
---
## Build & Preview
```bash
# Production build (Vite + prerender + asset pruning)
npm run build
```
Die erzeugten Dateien befinden sich im `dist`-Verzeichnis.
## Vorschau
Teste den Produktions-Build lokal:
```bash
# Preview the production build locally
npm run preview
```
## Technologien
The build pipeline runs three steps in sequence:
- [React](https://react.dev/)
- [Vite](https://vitejs.dev/)
- [TypeScript](https://www.typescriptlang.org/)
- [Framer Motion](https://www.framer.com/motion/) (Animationen)
- [GSAP](https://gsap.com/) (Animationen)
- [Lenis](https://github.com/studio-freight/lenis) (Smooth Scrolling)
1. **`vite build`** — bundles and outputs to `dist/`
2. **`prune-dist-assets.mjs`** — removes any unreferenced files from `dist/assets/`
3. **`prerender-routes.ts`** — writes a unique `index.html` per route into `dist/` with correct `<title>`, `<meta>`, canonical URLs, Open Graph tags, and JSON-LD schema
The result is a fully static site where every URL has its own HTML file — no server-side rendering required.
---
## Deployment (Docker + Caddy)
This project uses a **two-Caddy setup**:
```
Internet
└── Host Caddy (HTTPS, port 443)
└── Docker container (internal port 80 → host localhost:8080)
└── Caddy inside container
└── /srv (built static files)
```
### Step 1 — Build and start the container
On your server, copy the project files (or clone the repo), then run:
```bash
docker compose up -d --build
```
This builds the image (Node 22 compiles the app, Caddy serves it) and starts the container.
The app is now available at **http://localhost:8080** — only reachable from the server itself.
To verify:
```bash
curl -I http://localhost:8080
# HTTP/1.1 200 OK
```
### Step 2 — Configure the host Caddy
Add the following to your host Caddy config (usually `/etc/caddy/Caddyfile`).
A ready-to-paste version is in `caddy-host.snippet`:
```caddy
bayareait.services, www.bayareait.services {
encode zstd gzip
reverse_proxy localhost:8080
@www host www.bayareait.services
redir @www https://bayareait.services{uri} permanent
}
```
Replace `bayareait.services` with your actual domain.
### Step 3 — Reload host Caddy
```bash
sudo systemctl reload caddy
# or
caddy reload --config /etc/caddy/Caddyfile
```
Caddy automatically obtains and renews an SSL certificate from Let's Encrypt — no manual certificate management needed.
### Updating the site
```bash
# Pull latest changes, rebuild, restart
git pull
docker compose up -d --build
```
Zero-downtime: Docker Compose replaces the old container while the new one starts.
### Useful Docker commands
```bash
# View logs
docker compose logs -f
# Stop the container
docker compose down
# Rebuild without cache
docker compose build --no-cache
docker compose up -d
```
---
## Adding Content
All page content is driven by **`src/data/seoData.ts`**. No new page files needed for standard locations, services, or blog posts — just add a new entry to the right array.
### Add a location page
```ts
// src/data/seoData.ts → locationData array
{
city: "Sinton",
slug: "locations/it-support-sinton",
title: "IT Support Sinton, TX | Bay Area IT",
description: "...",
h1: "IT Support for Businesses in Sinton, TX",
keywords: ["IT support Sinton", "..."],
content: `<p>...</p>`,
faq: [
{ question: "...", answer: "..." }
]
}
```
### Add a service page
```ts
// src/data/seoData.ts → serviceData array
{
id: "9",
slug: "services/cloud-backup",
title: "Cloud Backup Services | Bay Area IT",
description: "...",
h1: "Cloud Backup for Corpus Christi Businesses",
keywords: ["cloud backup", "..."],
content: `<p>...</p>`,
faq: []
}
```
### Add a blog post
```ts
// src/data/seoData.ts → blogPostData array
{
slug: "blog/your-post-slug",
title: "Your Post Title | Bay Area IT",
description: "...",
h1: "Your Post Heading",
keywords: ["..."],
content: `<p>...</p>`,
date: "2026-03-25",
image: "/images/blog/your-image.webp"
}
```
After adding content, rebuild:
```bash
npm run build
# or on the server:
docker compose up -d --build
```
---
## Scripts Reference
| Script | Command | Description |
|---|---|---|
| Dev server | `npm run dev` | Starts Vite dev server on port 3012 |
| Production build | `npm run build` | Vite build + asset pruning + prerendering |
| Preview build | `npm run preview` | Serves `dist/` locally via Vite |
| Generate sitemap | `npm run generate:seo` | Writes `public/sitemap.xml` and `public/robots.txt` |
| Optimize images | `npm run optimize:images` | Converts images in `public/` to WebP |
| Prerender only | `npm run prerender:routes` | Re-runs route prerendering on existing `dist/` |

0
bullet Normal file
View File

12
caddy-host.snippet Normal file
View File

@@ -0,0 +1,12 @@
# Paste this block into your HOST Caddy config (e.g. /etc/caddy/Caddyfile)
# Replace the domain and adjust the port if you changed it in docker-compose.yml
bayareait.services, www.bayareait.services {
encode zstd gzip
reverse_proxy localhost:8080
# Optional: redirect www → non-www
@www host www.bayareait.services
redir @www https://bayareait.services{uri} permanent
}

240
competitor-alternatives.md Normal file
View File

@@ -0,0 +1,240 @@
# Competitor Alternatives Plan
## Context
- Product: Bay Area IT
- Category: Managed IT services for SMBs
- Market: Corpus Christi and the Coastal Bend
- Primary goal: Generate qualified local leads for managed IT and outsourced IT support
- Primary CTA: Book a free 20-minute IT assessment
## Positioning Summary
Bay Area IT should frame itself as the practical local alternative to reactive break-fix shops, generic remote-only MSPs, and the cost of hiring a full in-house IT person too early. The strongest angle is not "more features." It is clearer ownership, faster help, better day-to-day reliability, and local accountability for small and mid-sized businesses.
## Recommended Page Set
### Priority 1
1. `outsourced-it-support-vs-in-house-it.md`
2. `break-fix-it-support-alternative.md`
3. `managed-it-services-vs-freelance-it-support.md`
### Priority 2
1. `best-it-support-companies-corpus-christi.md`
2. `remote-it-support-vs-local-it-support.md`
3. `msp-vs-internal-it-manager.md`
### Priority 3
1. `bay-area-it-vs-national-it-provider.md`
2. `computer-repair-shop-vs-managed-it-services.md`
3. `best-managed-it-service-alternatives-coastal-bend.md`
## Why These Pages Matter
These pages match the real evaluation paths local SMB buyers use:
- They compare outsourced IT against hiring.
- They look for alternatives after frustration with reactive support.
- They compare local accountability against cheaper but fragmented options.
- They often search by problem first, not by a known competitor brand.
## Core Messaging Angles
### Bay Area IT is best for
- SMBs that want one accountable technology partner
- Teams losing time to recurring issues, slow machines, email problems, and network friction
- Businesses that need remote-first support with on-site help when needed
- Owners and office managers who are tired of acting as part-time IT
### Bay Area IT is not the best fit for
- Large enterprises with full internal IT departments
- Buyers who only want ad hoc hourly repair
- Organizations seeking the absolute lowest-cost option with no ongoing support relationship
## Common Switch Triggers
- Slow computers and outdated systems reduce productivity
- Support requests keep piling up without a clear owner
- Email, Wi-Fi, printer, and access issues interrupt normal work
- Current provider is reactive, vague, or hard to reach
- Internal staff are spending too much time on IT work
## Page Template 1: Break-Fix IT Support Alternative
### Suggested URL
`/break-fix-it-support-alternative`
### Title Tag
Break-Fix IT Support Alternative for Corpus Christi Businesses
### Meta Description
Looking for a better alternative to break-fix IT support? See how Bay Area IT helps Corpus Christi businesses reduce downtime with ongoing support, monitoring, and fast local help.
### H1
The Local Alternative to Break-Fix IT Support
### TL;DR
Break-fix support can be fine for isolated issues, but it usually leaves SMBs stuck in a cycle of recurring downtime, surprise costs, and no clear ownership. Bay Area IT is a better fit for businesses that want ongoing support, faster response, and a partner focused on keeping systems stable before problems escalate.
### Section Outline
#### Why businesses look for an alternative
Many small businesses start with break-fix support because it feels simpler and cheaper. The problem is that it only starts working after something is already broken. That means slow computers, email issues, weak Wi-Fi, and recurring employee tickets keep coming back, while managers keep losing time coordinating repairs.
#### Bay Area IT as the alternative
Bay Area IT provides ongoing support instead of isolated repair visits. That includes day-to-day help desk coverage, practical security baselines, device and user support, network help, and a local team that can step in remotely or on-site. The difference is not just fixing issues faster. It is reducing how often they happen in the first place.
#### Comparison snapshot
| Category | Bay Area IT | Break-Fix Provider |
|---|---|---|
| Support model | Ongoing monthly support | Reactive per-incident support |
| Ownership | One accountable partner | Usually ticket-by-ticket |
| Cost structure | Predictable recurring pricing | Variable and harder to forecast |
| Prevention | Monitoring and maintenance | Limited preventive work |
| Business fit | SMBs that want reliability | SMBs with very occasional issues |
#### Who should switch
Bay Area IT is the better choice if your team depends on email, cloud apps, printers, Wi-Fi, shared drives, and employee devices every day and cannot afford repeated disruption. Break-fix is still reasonable for very small environments that truly only need occasional repair.
#### CTA
Book a free 20-minute assessment and see whether ongoing IT support would reduce downtime for your team.
## Page Template 2: Outsourced IT Support vs In-House IT
### Suggested URL
`/outsourced-it-support-vs-in-house-it`
### Title Tag
Outsourced IT Support vs In-House IT for Small Businesses
### Meta Description
Comparing outsourced IT support to hiring in-house IT? See the tradeoffs in cost, coverage, speed, and business fit for Corpus Christi small businesses.
### H1
Outsourced IT Support vs In-House IT: Which Is Better for a Growing SMB?
### TL;DR
For many small and mid-sized businesses, outsourced IT is the more practical option because it provides broader coverage at a lower fixed cost than hiring a full-time internal employee too early. In-house IT becomes stronger when the business has enough scale, internal complexity, and daily demand to justify dedicated full-time ownership.
### Comparison Table
| Category | Bay Area IT / Outsourced IT | In-House IT |
|---|---|---|
| Cost | Usually lower and more predictable for SMBs | Higher salary, benefits, and hiring overhead |
| Coverage | Broader skill coverage across support areas | Depends on one person's strengths |
| Speed | Fast remote help with local escalation | Strong if fully staffed and available |
| Scalability | Easier to ramp without adding headcount | Requires hiring and management |
| Best for | SMBs without a mature IT department | Larger or more complex environments |
### Key Sections
#### Cost comparison
Most SMBs underestimate the full cost of internal IT. Salary is only the starting point. Hiring, benefits, downtime risk during turnover, and tool overhead all add up quickly. Outsourced support works best when a company needs coverage across many day-to-day IT functions but does not need a full internal team yet.
#### Breadth vs depth
An internal IT hire can be valuable, but one person rarely covers everything equally well across support, networking, email, vendors, security basics, onboarding, backups, and documentation. Bay Area IT should position breadth and consistency as a major advantage here.
#### Best-fit summary
Choose outsourced IT if you want dependable support without taking on full-time headcount. Choose in-house IT when your environment is large enough to justify dedicated internal ownership every day.
## Page Template 3: Managed IT Services vs Freelance IT Support
### Suggested URL
`/managed-it-services-vs-freelance-it-support`
### Title Tag
Managed IT Services vs Freelance IT Support
### Meta Description
Freelance IT support can work for occasional help, but managed IT services offer more structure, accountability, and continuity for growing small businesses.
### H1
Managed IT Services vs Freelance IT Support
### TL;DR
Freelance IT support can be affordable and flexible, but it often depends on one person's availability and process. Managed IT services are a better fit for businesses that want reliable response, recurring maintenance, clearer documentation, and less operational risk.
### Key Differentiators
- Process and coverage over one-person availability
- Ongoing support instead of ad hoc fixes
- Better continuity if one technician is unavailable
- Stronger fit for teams with multiple users and recurring support needs
## Reusable Proof Themes
- Local presence in Corpus Christi
- Regional service across the Coastal Bend
- Remote-first support with on-site help when needed
- Broad scope across help desk, email, networking, devices, backups, and security basics
- Low-friction CTA through a free 20-minute assessment
## Honest Weaknesses to Acknowledge
- Bay Area IT is not the lowest-cost option if a business only needs one-off repairs
- Businesses with complex enterprise compliance requirements may need a larger specialist provider
- No public customer-switching quotes are currently documented in the repo
## Content Gaps To Fill Before Publishing
1. Confirm the canonical brand name: Bay Area IT, Bay Area IT Services, or Bay Area Affiliates.
2. Normalize proof points that currently conflict in the repo, especially years in business and customer count.
3. Add at least 2 real switching testimonials.
4. Document migration/onboarding process for new managed IT clients.
5. Add concrete pricing guidance or example monthly plan ranges if the business is comfortable publishing them.
## Competitor Data Template
```yaml
competitor:
name:
type:
geography:
target_audience:
pricing_model:
strengths: []
weaknesses: []
best_for: []
not_ideal_for: []
common_complaints: []
migration_notes: []
comparison_angle:
```
## CTA Options
- Book your free 20-minute IT assessment
- Talk to a local IT partner about your current setup
- See if managed IT support would reduce downtime for your team
## Recommended Next Step
Turn the first three priority pages into publishable landing-page copy, then add local proof and real switching testimonials before rollout.

View File

@@ -3,6 +3,10 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { locationData } from '../src/data/seoData';
const cityLocations = locationData.filter(loc =>
loc.slug.startsWith('locations/it-support-')
);
const AreasWeServe: React.FC = () => {
return (
<section className="py-24 px-6 bg-gray-50 dark:bg-white/5 mx-auto text-center border-t border-gray-200 dark:border-white/10">
@@ -12,11 +16,11 @@ const AreasWeServe: React.FC = () => {
</h2>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-12 leading-relaxed">
We provide professional IT support and IT services for businesses throughout Corpus Christi and the surrounding Coastal Bend area.
Our team supports local companies with business IT support, outsourced IT services, and help desk solutions, delivered remotely or on-site when needed.
Our team supports local companies with outsourced IT services and help desk solutions, delivered remotely or on-site when needed.
</p>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-12">
{locationData.map((loc) => (
{cityLocations.map((loc) => (
<Link
key={loc.slug}
to={`/${loc.slug}`}
@@ -31,8 +35,8 @@ const AreasWeServe: React.FC = () => {
Not sure if your location is covered? <Link to="/contact" className="underline hover:text-black dark:hover:text-white transition-colors">Contact us today</Link> to discuss your IT needs.
</p>
<div className="mt-6">
<Link to="/it-support-corpus-christi" className="text-sm text-gray-500 hover:text-gray-800 dark:hover:text-gray-300 transition-colors">
Get local IT support in Corpus Christi and nearby areas
<Link to="/locations" className="text-sm text-gray-500 hover:text-gray-800 dark:hover:text-gray-300 transition-colors">
View all service areas
</Link>
</div>
</div>

View File

@@ -2,32 +2,21 @@ import React, { useRef, useLayoutEffect } from 'react';
import { motion } from 'framer-motion';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { Link } from 'react-router-dom';
import { blogPostData } from '../src/data/seoData';
gsap.registerPlugin(ScrollTrigger);
const posts = [
{
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuARalmRkuoZMBAbavGQgx4a-JhLgXBJ6JSD0U4vycdwaGGV3d-ffUFrdbx2lIbKrYCmS100i7VJ0w5cDHITXYV6w1-pSUPHKL7Jik__TWOIYOnq_4ND5ri7l8SQoaJdjJK9jhYvtxdxrZm6j8t8BNAjvPTaUdUDo4C7QVqcx1KbGvup6cpF8vY1LJ82S_5OMAZ6JgH0rK5bvWpqD3WqPhtqJCUB6d_1gUvluKjotwnNQ03t1dSYV8HOtRrLE83j6i_wgL4GZ0XTsMZb',
date: 'Jan 10, 2026',
category: 'Performance',
title: 'Upgrade Your HDD to SSD for Enhanced Performance',
excerpt: 'In today\'s fast-paced digital world, the performance of your computer can make a significant difference in productivity...'
},
{
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCz5lTYjY4RNXubQlrA-BtLIGR3nUY8ULkD9omwT5FShfdMrbMgS5dDCyfN3xiB5WC7T3vjNvyvVbvnD0G1zBpbNTjfOYyhmAEfno7Hf5W1sm-KYRXYrLGQq-c6TkLgEf0i9JGNvuFZ6edcenr2o39dCzIPXcp_z9XWOIzp7kBX2EydNPLJoRofVYuSTmEA1y0_xh4sdiRy1PykRASGLhKfN19_XLNuwyTBVKYISY7cHc-An69eZpAfhrvngu3E47rU6KuQS0k3QXBZ',
date: 'Jan 5, 2026',
category: 'Security',
title: 'Secure Your Corporate Network Access with WireGuard VPN',
excerpt: 'The safest way to access your corporate network remotely is through a secure VPN connection...'
},
{
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCl5iOhTsCqcHnho89DkoLh0DYeuvef0pdp8k26NKzcAq7YPvWbAYARg9mCIvqGTxQGradp8zvscuuibskpz4W_nEzQQO1z7lgwKJ1Xxiw_yQOyXMLfoRNLTHXzqFUH8Q5daCAfYTb7Zl3sFjB7k8i44D6TGolzqrN05Db27Abf2TWDDzHpVSrNml4zddvxholHFxMzqDeSzQ5p77SLDSFNaYBZGR2lEdN2V9O0GzMqxbOjFmBGMW48nlrEDLDzYGv_gWI3RSqNqBl-',
date: 'Dec 28, 2025',
category: 'Infrastructure',
title: 'Virtualizing Windows Machines: Future-Proof Your Corporate Network',
excerpt: 'In October 2025, Microsoft will end support for Windows 10. Learn how virtualization can help you prepare...'
}
];
const posts = blogPostData
.filter((post) => !post.redirect)
.slice(0, 3)
.map((post) => ({
image: post.image || '/images/blog/business-email-comparison-new.png',
category: post.category === 'authority' ? 'IT Insights' : 'Local Services',
title: post.h1,
excerpt: post.description,
href: `/${post.slug}`,
}));
const Blog: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
@@ -73,13 +62,12 @@ const Blog: React.FC = () => {
Stay updated <span className="text-gray-400 dark:text-gray-600">with our latest news and articles.</span>
</h2>
</div>
<motion.a
href="#"
<Link
to="/blog"
className="hidden md:inline-flex items-center text-sm font-medium text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
whileHover={{ x: 5 }}
>
View all posts <span className="material-symbols-outlined text-sm ml-1">arrow_forward</span>
</motion.a>
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
@@ -91,8 +79,9 @@ const Blog: React.FC = () => {
viewport={{ once: true }}
transition={{ duration: 0.5, delay: i * 0.1 }}
whileHover={{ y: -8 }}
className="group cursor-pointer"
className="group"
>
<Link to={post.href} className="block">
<div className="h-64 rounded-xl overflow-hidden mb-6 relative shadow-lg">
<div
ref={el => { if (el) imagesRef.current.push(el); }}
@@ -101,6 +90,9 @@ const Blog: React.FC = () => {
<img
src={post.image}
alt={post.title}
loading="lazy"
decoding="async"
fetchPriority="low"
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
/>
</div>
@@ -110,8 +102,6 @@ const Blog: React.FC = () => {
</div>
</div>
<div className="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400 mb-3">
<span>{post.date}</span>
<span className="w-1 h-1 rounded-full bg-gray-400"></span>
<span className="text-blue-600 dark:text-blue-400 font-medium">{post.category}</span>
</div>
<h3 className="text-xl font-display font-bold text-gray-900 dark:text-white mb-2 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
@@ -120,6 +110,7 @@ const Blog: React.FC = () => {
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
{post.excerpt}
</p>
</Link>
</motion.article>
))}
</div>

35
components/Breadcrumb.tsx Normal file
View File

@@ -0,0 +1,35 @@
import React from 'react';
import { Link } from 'react-router-dom';
interface BreadcrumbItem {
label: string;
to?: string;
}
interface BreadcrumbProps {
items: BreadcrumbItem[];
}
const Breadcrumb: React.FC<BreadcrumbProps> = ({ items }) => {
return (
<nav aria-label="breadcrumb" className="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1.5">
{items.map((item, i) => (
<React.Fragment key={i}>
{i > 0 && <span className="text-gray-400 dark:text-gray-600">/</span>}
{item.to ? (
<Link
to={item.to}
className="hover:text-gray-900 dark:hover:text-white transition-colors"
>
{item.label}
</Link>
) : (
<span className="text-gray-900 dark:text-white font-medium">{item.label}</span>
)}
</React.Fragment>
))}
</nav>
);
};
export default Breadcrumb;

View File

@@ -21,7 +21,7 @@ const CTA: React.FC = () => {
transition={{ delay: 0.1 }}
className="text-xl text-gray-600 dark:text-gray-300 mb-12 leading-relaxed"
>
Join 150+ Coastal Bend businesses that trust us with their technology. Get started with a free 20-minute assessment.
Join 30+ Coastal Bend businesses that trust us with their technology. Get started with a free 20-minute assessment.
</motion.p>
<motion.div
@@ -43,6 +43,12 @@ const CTA: React.FC = () => {
>
Send a message
</Link>
<a
href="tel:+13617658400"
className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors mt-2"
>
Or call us: (361) 765-8400
</a>
</motion.div>
<motion.div

View File

@@ -1,68 +1,109 @@
import React from 'react';
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
const Footer: React.FC = () => {
return (
<footer className="bg-background-light dark:bg-background-dark border-t border-gray-200 dark:border-white/10 pt-16 pb-8">
<div className="max-w-7xl mx-auto px-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-16">
<div className="grid grid-cols-1 md:grid-cols-5 gap-12 mb-16">
<div className="col-span-1 md:col-span-2">
<div className="flex items-center gap-2 mb-6">
<span className="material-symbols-outlined text-xl text-gray-900 dark:text-white">dns</span>
<span className="font-display font-bold text-lg tracking-tight text-gray-900 dark:text-white">Bay Area Affiliates</span>
<span className="font-display font-bold text-lg tracking-tight text-gray-900 dark:text-white">Bay Area IT</span>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-xs mb-6">
Providing reliable IT services and solutions to the Coastal Bend community for over 25 years.
Providing reliable IT services and practical technology support to the Coastal Bend community for over 25 years.
</p>
<div className="flex gap-4">
{['X', 'in', 'fb'].map((social) => (
<motion.a
key={social}
href="#"
whileHover={{ y: -5, borderColor: "#ffffff", color: "#ffffff" }}
className="w-8 h-8 flex items-center justify-center rounded border border-gray-300 dark:border-white/20 text-gray-600 dark:text-gray-400 transition-colors"
>
<span className="text-xs font-bold">{social}</span>
</motion.a>
))}
<div className="flex flex-wrap gap-3 text-sm text-gray-600 dark:text-gray-400">
<a href="tel:+13617658400" className="transition-colors hover:text-gray-900 dark:hover:text-white">
(361) 765-8400
</a>
<a href="mailto:info@bayareaaffiliates.com" className="transition-colors hover:text-gray-900 dark:hover:text-white">
info@bayareaaffiliates.com
</a>
</div>
</div>
<div>
<h4 className="text-sm font-bold text-gray-900 dark:text-white mb-6 uppercase tracking-wider">Navigation</h4>
<h4 className="text-sm font-bold text-gray-900 dark:text-white mb-6 uppercase tracking-wider">Services</h4>
<ul className="space-y-4 text-sm text-gray-600 dark:text-gray-400">
{['Services', 'Features', 'Blog', 'Contact'].map((item) => (
<li key={item}>
<motion.a
href="#"
whileHover={{ x: 5, color: "#ffffff" }}
className="inline-block transition-colors"
>
{item}
</motion.a>
{[
{ label: 'IT Help Desk', to: '/services/it-help-desk' },
{ label: 'Computer Support', to: '/services/computer-support' },
{ label: 'Business Email', to: '/services/business-email-services' },
{ label: 'Domain & DNS', to: '/services/domain-registration-dns-support' },
{ label: 'Web Design', to: '/services/web-design-corpus-christi' },
].map((item) => (
<li key={item.label}>
<motion.div whileHover={{ x: 5 }} className="inline-block">
<Link to={item.to} className="transition-colors hover:text-gray-900 dark:hover:text-white">
{item.label}
</Link>
</motion.div>
</li>
))}
</ul>
</div>
<div>
<h4 className="text-sm font-bold text-gray-900 dark:text-white mb-6 uppercase tracking-wider">Contact</h4>
<h4 className="text-sm font-bold text-gray-900 dark:text-white mb-6 uppercase tracking-wider">Areas We Serve</h4>
<ul className="space-y-4 text-sm text-gray-600 dark:text-gray-400">
<li>support@bayareaaffiliates.com</li>
{[
{ label: 'Corpus Christi', to: '/locations/it-support-corpus-christi' },
{ label: 'Portland, TX', to: '/locations/it-support-portland-tx' },
{ label: 'Rockport, TX', to: '/locations/it-support-rockport-tx' },
{ label: 'Aransas Pass', to: '/locations/it-support-aransas-pass-tx' },
{ label: 'Kingsville', to: '/locations/it-support-kingsville-tx' },
].map((item) => (
<li key={item.label}>
<motion.div whileHover={{ x: 5 }} className="inline-block">
<Link to={item.to} className="transition-colors hover:text-gray-900 dark:hover:text-white">
{item.label}
</Link>
</motion.div>
</li>
))}
</ul>
</div>
<div>
<h4 className="text-sm font-bold text-gray-900 dark:text-white mb-6 uppercase tracking-wider">Company</h4>
<ul className="space-y-4 text-sm text-gray-600 dark:text-gray-400">
{[
{ label: 'About', to: '/about' },
{ label: 'Blog', to: '/blog' },
{ label: 'Contact', to: '/contact' },
{ label: 'Privacy Policy', to: '/privacy-policy' },
{ label: 'Terms of Service', to: '/terms-of-service' },
].map((item) => (
<li key={item.label}>
<motion.div whileHover={{ x: 5 }} className="inline-block">
<Link to={item.to} className="transition-colors hover:text-gray-900 dark:hover:text-white">
{item.label}
</Link>
</motion.div>
</li>
))}
<li>
<a href="mailto:info@bayareaaffiliates.com" className="hover:text-gray-900 dark:hover:text-white transition-colors">info@bayareaaffiliates.com</a>
</li>
<li>(361) 765-8400</li>
<li>1001 Blucher St, Corpus Christi, TX 78401</li>
<li><motion.a whileHover={{ x: 5, color: "#ffffff" }} href="#" className="inline-block transition-colors">FAQ</motion.a></li>
</ul>
</div>
</div>
<div className="border-t border-gray-200 dark:border-white/10 pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-xs text-gray-500 dark:text-gray-600">
© 2026 Bay Area Affiliates, Inc. All rights reserved.
© 2026 Bay Area IT. All rights reserved.
</p>
<div className="flex gap-6">
<motion.a whileHover={{ color: "#ffffff" }} href="#" className="text-xs text-gray-500 dark:text-gray-600 transition-colors">Privacy Policy</motion.a>
<motion.a whileHover={{ color: "#ffffff" }} href="#" className="text-xs text-gray-500 dark:text-gray-600 transition-colors">Terms of Service</motion.a>
<div className="flex flex-wrap items-center justify-center gap-4 text-xs text-gray-500 dark:text-gray-600">
<Link to="/privacy-policy" className="transition-colors hover:text-gray-900 dark:hover:text-white">
Privacy Policy
</Link>
<Link to="/terms-of-service" className="transition-colors hover:text-gray-900 dark:hover:text-white">
Terms of Service
</Link>
</div>
</div>
</div>

View File

@@ -1,7 +1,8 @@
import React, { useRef, useLayoutEffect } from 'react';
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { motion, useMotionTemplate, useMotionValue, useReducedMotion } from 'framer-motion';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import heroBg from '../src/assets/hero-bg.webp';
gsap.registerPlugin(ScrollTrigger);
@@ -11,14 +12,43 @@ const Hero: React.FC = () => {
const parallaxWrapperRef = useRef<HTMLDivElement>(null);
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const prefersReducedMotion = useReducedMotion();
const [isInteractive, setIsInteractive] = useState(false);
const maskImage = useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`;
const webkitMaskImage = useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`;
useEffect(() => {
if (prefersReducedMotion || typeof window === 'undefined') {
setIsInteractive(false);
return;
}
const mediaQuery = window.matchMedia('(pointer: fine) and (hover: hover)');
const updateState = () => setIsInteractive(mediaQuery.matches);
updateState();
if (typeof mediaQuery.addEventListener === 'function') {
mediaQuery.addEventListener('change', updateState);
return () => mediaQuery.removeEventListener('change', updateState);
}
mediaQuery.addListener(updateState);
return () => mediaQuery.removeListener(updateState);
}, [prefersReducedMotion]);
const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => {
if (!isInteractive) return;
const { left, top } = currentTarget.getBoundingClientRect();
mouseX.set(clientX - left);
mouseY.set(clientY - top + 75);
};
useLayoutEffect(() => {
if (!isInteractive) {
return;
}
const ctx = gsap.context(() => {
// Parallax Background
gsap.to(parallaxWrapperRef.current, {
@@ -39,13 +69,13 @@ const Hero: React.FC = () => {
);
}, containerRef);
return () => ctx.revert();
}, []);
return () => ctx.revert();
}, [isInteractive]);
return (
<section
ref={containerRef}
onMouseMove={handleMouseMove}
onMouseMove={isInteractive ? handleMouseMove : undefined}
className="relative min-h-screen flex items-center justify-center overflow-hidden pt-20 group"
>
@@ -55,19 +85,23 @@ const Hero: React.FC = () => {
<img
alt="Abstract dark technology background"
className="w-full h-full object-cover opacity-90 dark:opacity-70 brightness-75 contrast-150"
src="/src/assets/hero-bg.png"
src={heroBg}
loading="eager"
decoding="async"
fetchPriority="high"
/>
{/* Highlight Layer - Only visible via mask */}
<motion.img
style={{
maskImage: useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`,
WebkitMaskImage: useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`,
}}
alt=""
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 brightness-150 contrast-150 filter saturate-150"
src="/src/assets/hero-bg.png"
/>
{isInteractive && (
<motion.img
style={{ maskImage, WebkitMaskImage: webkitMaskImage }}
alt=""
aria-hidden="true"
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 brightness-150 contrast-150 filter saturate-150"
src={heroBg}
loading="lazy"
decoding="async"
/>
)}
</div>
<div className="absolute inset-0 bg-gradient-to-t from-background-light via-transparent to-transparent dark:from-background-dark dark:via-transparent dark:to-transparent"></div>
<div className="absolute inset-0 bg-gradient-to-b from-background-light/50 dark:from-background-dark/50 to-transparent"></div>
@@ -76,7 +110,7 @@ const Hero: React.FC = () => {
<div className="relative z-10 text-center max-w-4xl px-6">
<div className="hero-stagger flex items-center justify-center gap-2 mb-6">
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
<span className="text-xs uppercase tracking-[0.2em] text-gray-600 dark:text-gray-400 font-medium">Established 1998</span>
<span className="text-xs uppercase tracking-[0.2em] text-gray-600 dark:text-gray-400 font-medium">Serving the Coastal Bend since 2000</span>
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
</div>
@@ -86,7 +120,7 @@ const Hero: React.FC = () => {
</h1>
<p className="hero-stagger text-lg md:text-xl text-gray-600 dark:text-gray-300 max-w-2xl mx-auto mb-10 font-light leading-relaxed">
Providing top-notch Computer & Networking solutions to the Coastal Bend community.
Local IT support, help desk coverage, networking, email, and practical technology support for Coastal Bend businesses.
</p>
<div className="hero-stagger flex flex-col sm:flex-row items-center justify-center gap-4">

View File

@@ -0,0 +1,111 @@
import React, { useEffect, useRef } from 'react';
import gsap from 'gsap';
interface LoadingScreenProps {
onComplete: () => void;
}
const LoadingScreen: React.FC<LoadingScreenProps> = ({ onComplete }) => {
const containerRef = useRef<HTMLDivElement>(null);
const textRef = useRef<HTMLDivElement>(null);
const progressRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Hide scrolling on the body while loading
document.body.style.overflow = 'hidden';
const tl = gsap.timeline({
onComplete: () => {
document.body.style.overflow = '';
onComplete();
},
});
const chars = textRef.current?.querySelectorAll('.char');
// 1. Initial state: progress bar width 0
gsap.set(progressRef.current, { scaleX: 0, transformOrigin: 'left center' });
if (chars) {
gsap.set(chars, { yPercent: 100, opacity: 0 });
}
// 2. Animate the progress bar and reveal text simultaneously
tl.to(progressRef.current, {
scaleX: 1,
duration: 1.5,
ease: 'power3.inOut',
})
.to(
chars || [],
{
yPercent: 0,
opacity: 1,
duration: 0.8,
stagger: 0.05,
ease: 'power4.out',
},
'<0.5' // Start this animation 0.5s into the progress bar loading
)
// 3. Keep it visible briefly
.to({}, { duration: 0.3 })
// 4. Slide out the individual characters upwards
.to(
chars || [],
{
yPercent: -100,
opacity: 0,
duration: 0.5,
stagger: 0.02,
ease: 'power3.in',
}
)
// 5. Fade out progress line
.to(progressRef.current, { opacity: 0, duration: 0.3 }, '<')
// 6. Slide the whole curtain up to reveal the homepage
.to(containerRef.current, {
yPercent: -100,
duration: 1,
ease: 'expo.inOut',
});
return () => {
tl.kill();
document.body.style.overflow = '';
};
}, [onComplete]);
const text = 'BAY AREA IT';
return (
<div
ref={containerRef}
className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-surface-dark dark:bg-background-dark text-primary"
>
<div className="relative overflow-hidden mb-8">
<h1
ref={textRef}
className="font-display text-4xl md:text-6xl font-bold tracking-widest flex overflow-hidden"
>
{text.split('').map((char, index) => (
<span
key={index}
className={`char inline-block opacity-0 ${char === ' ' ? 'w-4' : ''}`}
>
{char}
</span>
))}
</h1>
</div>
{/* Sleek Minimalist Loading Line */}
<div className="w-48 md:w-64 h-[2px] bg-white/10 rounded-full overflow-hidden">
<div
ref={progressRef}
className="h-full w-full bg-primary origin-left scale-x-0"
></div>
</div>
</div>
);
};
export default LoadingScreen;

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { motion } from 'framer-motion';
const Navbar: React.FC = () => {
const location = useLocation();
@@ -12,13 +11,12 @@ const Navbar: React.FC = () => {
>
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
<Link to="/" className="flex items-center gap-2">
<motion.div
whileHover={{ rotate: 180 }}
transition={{ duration: 0.5 }}
>
<span className="material-symbols-outlined text-xl dark:text-white text-black">dns</span>
</motion.div>
<span className="font-display font-bold text-lg tracking-tight">Bay Area Affiliates</span>
<img
src="/logo.svg"
alt="Bay Area IT logo"
className="h-8 w-8"
/>
<span className="font-display font-bold text-lg tracking-tight">Bay Area IT</span>
</Link>
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-gray-600 dark:text-gray-400">
@@ -28,19 +26,20 @@ const Navbar: React.FC = () => {
to={`/${item.toLowerCase()}`}
className="hover:text-black dark:hover:text-white transition-colors relative group px-2 py-1"
>
<motion.span
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="inline-block"
>
<span className="inline-block">
{item}
</motion.span>
</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-black dark:bg-white transition-all duration-300 ease-out group-hover:w-full"></span>
</Link>
))}
</div>
{/* Client Portal button removed */}
<Link
to="/contact"
className="hidden md:inline-flex items-center px-4 py-1.5 bg-black dark:bg-white text-white dark:text-black rounded-full text-sm font-medium hover:bg-gray-800 dark:hover:bg-gray-100 transition-colors"
>
Get IT Support
</Link>
</div>
</nav>
);

View File

@@ -1,15 +1,42 @@
import React, { useLayoutEffect, useRef } from 'react';
import { motion } from 'framer-motion';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import processIllustration from '../src/assets/process-illustration.webp';
gsap.registerPlugin(ScrollTrigger);
const Process: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
const imgRef = useRef<HTMLImageElement>(null);
const prefersReducedMotion = useReducedMotion();
const [supportsFinePointer, setSupportsFinePointer] = useState(false);
const shouldAnimate = !prefersReducedMotion && supportsFinePointer;
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
const mediaQuery = window.matchMedia('(pointer: fine) and (hover: hover)');
const updateState = () => setSupportsFinePointer(mediaQuery.matches);
updateState();
if (typeof mediaQuery.addEventListener === 'function') {
mediaQuery.addEventListener('change', updateState);
return () => mediaQuery.removeEventListener('change', updateState);
}
mediaQuery.addListener(updateState);
return () => mediaQuery.removeListener(updateState);
}, []);
useLayoutEffect(() => {
if (!shouldAnimate) {
return;
}
const ctx = gsap.context((self) => {
// Dramatic Zoom Animation
if (containerRef.current && imgRef.current) {
@@ -51,17 +78,20 @@ const Process: React.FC = () => {
}, containerRef);
return () => ctx.revert();
}, []);
}, [shouldAnimate]);
return (
<section ref={containerRef} className="relative w-full" style={{ clipPath: 'inset(0)' }}>
{/* Fixed Background Image - constrained to this section via clip-path */}
<div className="fixed inset-0 w-full h-screen z-0 overflow-hidden">
<div className={`${shouldAnimate ? 'fixed inset-0 w-full h-screen' : 'absolute inset-0 w-full h-full'} z-0 overflow-hidden`}>
<img
ref={imgRef}
alt="Modern server rack infrastructure"
className="w-full h-full object-cover opacity-80 will-change-transform origin-center"
src="/src/assets/process-illustration.png"
className={`w-full h-full object-cover opacity-80 origin-center ${shouldAnimate ? 'will-change-transform' : ''}`}
src={processIllustration}
loading="lazy"
decoding="async"
fetchPriority="low"
/>
{/* Gradient overlay for text readability */}
<div className="absolute inset-0 bg-gradient-to-r from-black/50 via-black/30 to-black/60" />

View File

@@ -7,14 +7,16 @@ interface SEOProps {
keywords?: string[];
canonicalUrl?: string;
schema?: object; // JSON-LD schema
ogImage?: string;
ogType?: string;
}
const SEO: React.FC<SEOProps> = ({ title, description, keywords, canonicalUrl, schema }) => {
const SEO: React.FC<SEOProps> = ({ title, description, keywords, canonicalUrl, schema, ogImage, ogType }) => {
useEffect(() => {
// Update Title
document.title = title;
// Helper to set meta tag
// Helper to set meta tag (name attribute)
const setMetaTag = (name: string, content: string) => {
let element = document.querySelector(`meta[name="${name}"]`);
if (!element) {
@@ -25,6 +27,17 @@ const SEO: React.FC<SEOProps> = ({ title, description, keywords, canonicalUrl, s
element.setAttribute('content', content);
};
// Helper to set OG tag (property attribute)
const setOgTag = (property: string, content: string) => {
let element = document.querySelector(`meta[property="${property}"]`);
if (!element) {
element = document.createElement('meta');
element.setAttribute('property', property);
document.head.appendChild(element);
}
element.setAttribute('content', content);
};
// Update Meta Description
setMetaTag('description', description);
@@ -44,6 +57,19 @@ const SEO: React.FC<SEOProps> = ({ title, description, keywords, canonicalUrl, s
link.setAttribute('href', canonicalUrl);
}
// Open Graph Tags
setOgTag('og:title', title);
setOgTag('og:description', description);
setOgTag('og:type', ogType || 'website');
setOgTag('og:site_name', 'Bay Area IT');
if (canonicalUrl) setOgTag('og:url', canonicalUrl);
if (ogImage) setOgTag('og:image', ogImage);
// Twitter Card Tags
setMetaTag('twitter:card', 'summary');
setMetaTag('twitter:title', title);
setMetaTag('twitter:description', description);
// Inject Schema
if (schema) {
const scriptId = 'seo-schema-script';
@@ -57,10 +83,7 @@ const SEO: React.FC<SEOProps> = ({ title, description, keywords, canonicalUrl, s
script.textContent = JSON.stringify(schema);
}
// Cleanup function not strictly necessary for single page app navigation
// unless we want to remove specific tags on unmount, but usually we just overwrite them.
}, [title, description, keywords, canonicalUrl, schema]);
}, [title, description, keywords, canonicalUrl, schema, ogImage, ogType]);
return null;
};

View File

@@ -2,101 +2,77 @@ import React, { useState, useRef, useLayoutEffect, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { Link } from 'react-router-dom';
gsap.registerPlugin(ScrollTrigger);
const servicesData = [
{
id: 1,
category: 'IT Infrastructure',
title: 'Windows 11 Transition',
description: 'Upgrade to Windows 11 before October 2025 to ensure continued security support and take advantage of the latest features.',
icon: 'desktop_windows',
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBMpd_cFINnFibfNErBs8OVAAyDQYTRXix88YH91QImuGi11XGwlY_QUB2R9htcC1h_fTXUeftdEieGT-oi5p5TBjpAyW-86mSsXu-rqhRTBsJlAGuE37bxJES4DUayktXIToEcF-M4PyXdyyTPIYtpYrxK18b2-sPwMzuzCL0LpgJwd5EoYxAkrJQ7W4eBrIG2e9Cw9sY0dJpXJy-TRgwBG0nk-S7W4Y0s3U9w--AzE4fcUimeGMqWwdCncU5tnETmkrkDNFiCyKSA'
category: 'Web Services',
title: 'Web Design',
description: 'Professional websites with domain registration and DNS support to give your business a clean, reliable online presence.',
icon: 'language',
image: '/assets/services/business-it.webp',
href: '/services/web-design-corpus-christi'
},
{
id: 2,
category: 'Web Services',
title: 'Web Services',
description: 'Web design, domain registration, email services, and more to establish and enhance your online presence.',
icon: 'language',
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCxibXNCB5mU7MdWE5znMWnQUc9-d2ZoYF7LXK1CMssnvaFz2ZsGzyxXMbqDmely-UfxapqILD5-Exeo1wlQZKg8T2MK4vjlyAMaehoJoqTy2hHh8rxj46i8CKb4-ILL2JswBc98nJt_Fo1DfcDH0dHH5Zz6H4R2Jm1deViSW8Sp2zNp1sTc4eRHy1URiSRQFcr1C8rca6dKiuNDuyDiUmmesqHobXGItaBeFjJC-0OatWpKbr0zF-Y5qvk9Yl5FY2KUcDY9AcTfelu'
title: 'Bay Area Email Services',
description: 'Enterprise cloud email with 99.99% uptime, local Texas support, 25 GB mailboxes, and business-grade delivery for $5 per inbox.',
icon: 'mail',
image: '/assets/services/business-it.webp',
href: '/services/business-email-services'
},
{
id: 3,
category: 'IT Infrastructure',
title: 'Performance Upgrades',
description: 'Enhance your desktops and laptops with SSDs, maintain your Windows installations, and achieve dramatic performance boosts.',
icon: 'speed',
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBs2fGGwp4EkMarA9Uvy7IOqyW0Pzxzt-94Bsr8Tkbem4uHPq-vMEmGgKuEmds2zKwPrw2nVcvL3MjjKYWieLSLh5pVUbbK6T9aDxt2xhvo4trARZobhzoQCJfI-r6aGW_aqfwC5XxOr9VA3YdnNnYEgkfW_TWrUWYa6mD8X0KdVG3sLimA8p7qWxIqUzFFV82twn60rP4OwLdIsc6t1OGnJzjemxL1Aw05aDo6Ckfr0a1oZ2kD4xKeTkG--zUhezvXB9I03l6f3b46'
title: 'Printer & Scanner Installation',
description: 'Professional installation and configuration of printers and scanners to ensure seamless integration into your workflow.',
icon: 'print',
image: '/assets/services/printer-scanner.webp',
href: '/services'
},
{
id: 4,
category: 'IT Infrastructure',
title: 'Printer & Scanner Installation',
description: 'Professional installation and configuration of printers and scanners to ensure seamless integration into your workflow.',
icon: 'print',
image: '/assets/services/printer-scanner.png'
},
{
id: 5,
category: 'IT Infrastructure',
title: 'New/Refurbished Desktop Hardware',
description: 'Supply and installation of new or refurbished desktop hardware, tailored to meet your business requirements.',
icon: 'computer',
image: '/assets/services/desktop-hardware.png'
image: '/assets/services/desktop-hardware.webp',
href: '/services/computer-support'
},
{
id: 6,
category: 'Security',
title: 'VPN Setup',
description: 'Configure Virtual Private Networks to allow secure remote access to your internal network from anywhere.',
icon: 'vpn_lock',
image: '/assets/services/vpn-setup.png'
},
{
id: 7,
id: 5,
category: 'Networking',
title: 'Network Infrastructure Support',
description: 'Robust network solutions to ensure connectivity, security, and efficiency, including routers, access points, and switches.',
icon: 'lan',
image: '/assets/services/network-infrastructure.png'
image: '/assets/services/network-infrastructure.webp',
href: '/services'
},
{
id: 8,
id: 6,
category: 'Networking',
title: 'Network Attached Storage',
description: 'Selection, setup, and maintenance of Network Attached Storage solutions to provide scalable and reliable data storage.',
title: 'Shared Drive',
description: 'Setup and management of shared drive solutions so your team can store, access, and organize files reliably.',
icon: 'storage',
image: '/assets/services/nas-storage.png'
image: '/assets/services/nas-storage.webp',
href: '/services'
},
{
id: 9,
category: 'IT Infrastructure',
title: 'Business IT Support',
description: 'Comprehensive IT support for businesses, including help desk, maintenance, and strategic planning.',
icon: 'business_center',
image: '/assets/services/business-it.png'
},
{
id: 10,
id: 7,
category: 'IT Infrastructure',
title: 'IT Help Desk',
description: 'Fast and reliable help desk support for employees, resolving technical issues remotely or on-site.',
icon: 'support_agent',
image: '/assets/services/help-desk.png'
},
{
id: 11,
category: 'IT Infrastructure',
title: 'Managed IT Services',
description: 'Proactive monitoring, security, and management of your entire IT infrastructure for a fixed monthly fee.',
icon: 'admin_panel_settings',
image: '/assets/services/managed-it.png'
image: '/assets/services/help-desk.webp',
href: '/services/it-help-desk'
}
];
const categories = ['All', 'IT Infrastructure', 'Web Services', 'Security', 'Networking'];
const categories = ['All', 'IT Infrastructure', 'Web Services', 'Networking'];
interface ServicesProps {
preview?: boolean;
@@ -115,7 +91,7 @@ const Services: React.FC<ServicesProps> = ({ preview = false, featuredIds }) =>
// Filter services based on category first (unless in restricted view with specific IDs, where we might want to ignore category or just show the specific ones)
const filteredByCategory = activeCategory === 'All'
? servicesData
: servicesData.filter(s => s.category === activeCategory || (activeCategory === 'Web Development' && s.category === 'Security'));
: servicesData.filter(s => s.category === activeCategory);
const displayedServices = useMemo(() => {
if (isRestrictedView) {
@@ -178,7 +154,7 @@ const Services: React.FC<ServicesProps> = ({ preview = false, featuredIds }) =>
className="grid grid-cols-1 md:grid-cols-3 gap-6"
>
<AnimatePresence mode="popLayout">
{displayedServices.map((service) => (
{displayedServices.map((service, index) => (
<motion.div
key={service.id}
layout
@@ -194,6 +170,9 @@ const Services: React.FC<ServicesProps> = ({ preview = false, featuredIds }) =>
<img
src={service.image}
alt={service.title}
loading={isRestrictedView ? 'lazy' : index < 3 ? 'eager' : 'lazy'}
decoding="async"
fetchPriority={isRestrictedView ? 'low' : index < 3 ? 'high' : 'low'}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110 opacity-100"
/>
<div className="absolute inset-0 bg-gradient-to-t from-gray-50 dark:from-[#161616] to-transparent pointer-events-none"></div>
@@ -211,13 +190,13 @@ const Services: React.FC<ServicesProps> = ({ preview = false, featuredIds }) =>
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed mb-3">
{service.description}
</p>
<a href="/services" className="inline-flex items-center text-xs font-bold uppercase tracking-wide text-gray-900 dark:text-white group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors">
<Link to={service.href} className="inline-flex items-center text-xs font-bold uppercase tracking-wide text-gray-900 dark:text-white group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors">
Learn More <motion.span
className="material-symbols-outlined text-xs ml-1"
animate={{ x: [0, 5, 0] }}
transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut", repeatDelay: 1 }}
>arrow_forward</motion.span>
</a>
</Link>
</div>
</motion.div>
))}

View File

@@ -23,7 +23,7 @@ const Testimonials: React.FC = () => {
</div>
<blockquote className="text-xl md:text-2xl font-medium leading-relaxed text-gray-900 dark:text-white mb-8 relative z-10">
"Bay Area Affiliates transformed our IT infrastructure completely. Their proactive approach means we rarely have downtime, and when issues do arise, they're resolved quickly. Our team can focus on patient care instead of tech problems."
"Bay Area IT transformed our IT infrastructure completely. Their proactive approach means we rarely have downtime, and when issues do arise, they're resolved quickly. Our team can focus on patient care instead of tech problems."
</blockquote>
<div className="flex items-center gap-4 relative z-10">

9
docker-compose.yml Normal file
View File

@@ -0,0 +1,9 @@
services:
web:
build:
context: .
dockerfile: Dockerfile
image: bay-area-affiliates:latest
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"

0
ellipsis Normal file
View File

0
em Normal file
View File

38
fix-encoding.mjs Normal file
View File

@@ -0,0 +1,38 @@
import { readFileSync, writeFileSync } from 'fs';
const content = readFileSync('src/data/seoData.ts', 'utf8');
// These are the mojibake sequences (UTF-8 bytes misread as Latin-1)
// and their correct Unicode replacements
const replacements = [
['â€"', '—'], // em dash
['â€"', ''], // en dash (different byte sequence)
['’', '\u2019'], // right single quote / apostrophe
['‘', '\u2018'], // left single quote
['“', '\u201c'], // left double quote
['â€', '\u201d'], // right double quote
['…', '…'], // ellipsis
['•', '•'], // bullet
];
let fixed = content;
for (const [bad, good] of replacements) {
const count = fixed.split(bad).length - 1;
if (count > 0) console.log(`Replacing ${count}x: "${bad}" → "${good}"`);
fixed = fixed.split(bad).join(good);
}
// Check for any remaining â sequences
const remaining = fixed.match(/â[^\w\s'"]{1,3}/g);
if (remaining) {
console.log('Remaining unhandled sequences:', [...new Set(remaining)]);
} else {
console.log('No remaining mojibake found.');
}
if (content !== fixed) {
writeFileSync('src/data/seoData.ts', fixed, 'utf8');
console.log('File saved successfully.');
} else {
console.log('No changes needed.');
}

View File

@@ -3,85 +3,15 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bay Area Affiliates, Inc. - Modern IT Solutions</title>
<title>Bay Area IT - Managed IT Services in Corpus Christi</title>
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet" />
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
<script>
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
primary: "#FFFFFF",
"background-light": "#F3F4F6",
"background-dark": "#0a0a0a",
"surface-dark": "#121212",
"text-muted": "#888888",
},
fontFamily: {
display: ['"Space Grotesk"', 'sans-serif'],
sans: ['"Inter"', 'sans-serif'],
},
borderRadius: {
DEFAULT: "4px",
'lg': "8px",
'xl': "12px",
},
backgroundImage: {
'grain': "url('data:image/svg+xml,%3Csvg viewBox=%220 0 200 200%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22noiseFilter%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%220.65%22 numOctaves=%223%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23noiseFilter)%22 opacity=%220.05%22/%3E%3C/svg%3E')",
}
},
},
};
</script>
<style>
html {
height: auto;
overflow-y: auto;
}
body {
background-color: #0a0a0a;
color: white;
overflow-x: hidden;
width: 100%;
min-height: 100vh;
}
.lenis.lenis-smooth {
scroll-behavior: auto !important;
}
.lenis.lenis-smooth [data-lenis-prevent] {
overscroll-behavior: contain;
}
.lenis.lenis-stopped {
overflow: hidden;
}
.lenis.lenis-scrolling iframe {
pointer-events: none;
}
/* Custom Scrollbar */
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>
</head>
<body class="bg-background-light dark:bg-background-dark text-gray-900 dark:text-white font-sans antialiased selection:bg-white selection:text-black transition-colors duration-300">
<div id="root"></div>
<script type="module" src="/index.tsx"></script>

View File

@@ -1,6 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './src/index.css';
const rootElement = document.getElementById('root');
if (!rootElement) {

0
left Normal file
View File

1558
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,11 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build": "tsx scripts/generate-sitemap.ts && tsx scripts/generate-robots.ts && vite build && node scripts/prune-dist-assets.mjs && tsx scripts/prerender-routes.ts",
"preview": "vite preview",
"generate:seo": "npx tsx scripts/generate-sitemap.ts && npx tsx scripts/generate-robots.ts"
"generate:seo": "tsx scripts/generate-sitemap.ts && tsx scripts/generate-robots.ts",
"optimize:images": "node scripts/optimize-images.mjs",
"prerender:routes": "tsx scripts/prerender-routes.ts"
},
"dependencies": {
"@studio-freight/lenis": "^1.0.42",
@@ -18,8 +20,14 @@
"react-router-dom": "^7.12.0"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^22.14.0",
"@vitejs/plugin-react": "^5.0.0",
"autoprefixer": "^10.4.27",
"postcss": "^8.5.8",
"sharp": "^0.34.5",
"tailwindcss": "^3.4.17",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
"vite": "^6.2.0"

6
postcss.config.cjs Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

41
public/logo.svg Normal file
View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="40"
height="40"
viewBox="0 0 10.583333 10.583333"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1">
<g
id="g2"
transform="translate(-59.849599,-62.962344)">
<g
id="g3"
transform="matrix(2.4622373,0,0,2.4622373,-103.30064,-98.368198)">
<path
id="rect4"
style="opacity:0.92;fill:#000080;fill-opacity:0;stroke-width:0.0110529;-inkscape-stroke:none;paint-order:markers fill stroke"
d="m 69.557843,68.36669 v -1.921262 h -1.960031 v 1.445588 c 0,0.26351 0.212068,0.475674 0.475578,0.475674 z" />
<path
id="path1"
style="opacity:0.92;fill:#000080;stroke-width:0.0110529;-inkscape-stroke:none;paint-order:markers fill stroke"
d="m 68.07339,65.851028 c -0.26351,0 -0.475578,0.212067 -0.475578,0.475579 v 0.118821 h 1.960031 v 1.921262 h 0.10113 c 0.26351,0 0.475578,-0.212164 0.475578,-0.475674 v -1.564409 c 0,-0.263512 -0.212068,-0.475579 -0.475578,-0.475579 z" />
<path
id="rect14"
style="opacity:0.92;fill:#0000ff;fill-opacity:0;stroke-width:0.0110529;-inkscape-stroke:none;paint-order:markers fill stroke"
d="m 68.587351,69.424584 v -1.960128 h -1.99822 v 1.484453 c 0,0.26351 0.212067,0.475675 0.475577,0.475675 z" />
<path
id="path2"
style="opacity:0.92;fill:#0000ff;stroke-width:0.0110529;-inkscape-stroke:none;paint-order:markers fill stroke"
d="m 67.064708,66.908921 c -0.26351,0 -0.475577,0.212068 -0.475577,0.475578 v 0.07996 h 1.99822 v 1.960128 h 0.06294 c 0.263512,0 0.475579,-0.212165 0.475579,-0.475675 v -1.56441 c 0,-0.26351 -0.212067,-0.475578 -0.475579,-0.475578 z" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -2,145 +2,133 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://bayareait.services</loc>
<lastmod>2026-01-22</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://bayareait.services/services</loc>
<lastmod>2026-01-22</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://bayareait.services/blog</loc>
<lastmod>2026-01-22</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://bayareait.services/contact</loc>
<lastmod>2026-01-22</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://bayareait.services/about</loc>
<lastmod>2026-01-22</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://bayareait.services/it-support-corpus-christi</loc>
<lastmod>2026-01-22</lastmod>
<loc>https://bayareait.services/locations/it-support-corpus-christi</loc>
<lastmod>2026-03-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/it-support-portland-tx</loc>
<lastmod>2026-01-22</lastmod>
<loc>https://bayareait.services/locations/it-support-portland-tx</loc>
<lastmod>2026-03-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/it-support-rockport-tx</loc>
<lastmod>2026-01-22</lastmod>
<loc>https://bayareait.services/locations/it-support-rockport-tx</loc>
<lastmod>2026-03-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/it-support-aransas-pass-tx</loc>
<lastmod>2026-01-22</lastmod>
<loc>https://bayareait.services/locations/it-support-aransas-pass-tx</loc>
<lastmod>2026-03-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/it-support-kingsville-tx</loc>
<lastmod>2026-01-22</lastmod>
<loc>https://bayareait.services/locations/it-support-kingsville-tx</loc>
<lastmod>2026-03-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/business-it-support</loc>
<lastmod>2026-01-22</lastmod>
<loc>https://bayareait.services/services/web-design-corpus-christi</loc>
<lastmod>2026-03-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/it-help-desk</loc>
<lastmod>2026-01-22</lastmod>
<loc>https://bayareait.services/services/business-email-corpus-christi</loc>
<lastmod>2026-03-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/computer-support</loc>
<lastmod>2026-01-22</lastmod>
<loc>https://bayareait.services/services/it-help-desk</loc>
<lastmod>2026-03-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/managed-it-services-corpus-christi</loc>
<lastmod>2026-01-22</lastmod>
<loc>https://bayareait.services/services/computer-support</loc>
<lastmod>2026-03-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/services/business-email-services</loc>
<lastmod>2026-03-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/services/domain-registration-dns-support</loc>
<lastmod>2026-03-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/blog/it-support-small-business-corpus-christi</loc>
<lastmod>2026-01-22</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/outsourced-it-support-corpus-christi</loc>
<lastmod>2026-01-22</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/it-service-vs-inhouse-it</loc>
<lastmod>2026-01-22</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/common-it-problems-businesses-corpus-christi</loc>
<lastmod>2026-01-22</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/it-support-cost-corpus-christi</loc>
<lastmod>2026-01-22</lastmod>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/it-support-corpus-christi-blog</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/it-support-portland-tx-blog</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/it-support-rockport-tx-blog</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/it-support-aransas-pass-blog</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://bayareait.services/blog/it-support-kingsville-tx-blog</loc>
<lastmod>2026-01-22</lastmod>
<loc>https://bayareait.services/blog/business-email-vs-google-workspace-vs-microsoft-365</loc>
<lastmod>2026-03-25</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>

0
right Normal file
View File

View File

@@ -1,4 +1,3 @@
import fs from 'fs';
import path from 'path';
@@ -17,12 +16,11 @@ Sitemap: ${BASE_URL}/sitemap.xml
const robots = generateRobots();
const outputPath = path.resolve(process.cwd(), 'public/robots.txt');
// Ensure public directory exists
const publicDir = path.dirname(outputPath);
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true });
}
fs.writeFileSync(outputPath, robots);
console.log(`Robots.txt generated at ${outputPath}`);
console.log(`Robots.txt generated at ${outputPath}`);

View File

@@ -1,13 +1,9 @@
import fs from 'fs';
import path from 'path';
import { locationData, serviceData, blogPostData } from '../src/data/seoData';
const BASE_URL = process.env.BASE_URL || 'https://bayareait.services';
/**
* Generates the sitemap.xml content
*/
const generateSitemap = () => {
const currentDate = new Date().toISOString().split('T')[0];
@@ -15,13 +11,15 @@ const generateSitemap = () => {
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
`;
// Static Pages
const staticPages = [
'',
'/locations',
'/services',
'/blog',
'/contact',
'/about'
'/about',
'/privacy-policy',
'/terms-of-service'
];
staticPages.forEach(page => {
@@ -34,7 +32,6 @@ const generateSitemap = () => {
`;
});
// Location Pages
locationData.forEach(page => {
xml += ` <url>
<loc>${BASE_URL}/${page.slug}</loc>
@@ -45,7 +42,6 @@ const generateSitemap = () => {
`;
});
// Service Pages
serviceData.forEach(page => {
xml += ` <url>
<loc>${BASE_URL}/${page.slug}</loc>
@@ -56,10 +52,9 @@ const generateSitemap = () => {
`;
});
// Blog Posts
blogPostData.forEach(post => {
blogPostData.filter(post => !('redirect' in post) || !post.redirect).forEach(post => {
xml += ` <url>
<loc>${BASE_URL}/blog/${post.slug}</loc>
<loc>${BASE_URL}/${post.slug}</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
@@ -71,15 +66,13 @@ const generateSitemap = () => {
return xml;
};
// Write to public/sitemap.xml
const sitemap = generateSitemap();
const outputPath = path.resolve(process.cwd(), 'public/sitemap.xml');
// Ensure public directory exists
const publicDir = path.dirname(outputPath);
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true });
}
fs.writeFileSync(outputPath, sitemap);
console.log(`Sitemap generated at ${outputPath}`);
console.log(`Sitemap generated at ${outputPath}`);

View File

@@ -0,0 +1,61 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import sharp from 'sharp';
const roots = ['public', path.join('src', 'assets')];
async function walk(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
const files = await Promise.all(
entries.map(async (entry) => {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
return walk(fullPath);
}
return fullPath;
}),
);
return files.flat();
}
function pickWidth(filePath) {
const normalized = filePath.replace(/\\/g, '/');
if (normalized.includes('/images/blog/')) return 1280;
if (normalized.endsWith('/hero-bg.png')) return 1600;
if (normalized.endsWith('/process-illustration.png')) return 1600;
if (normalized.includes('/assets/services/')) return 1200;
return 1280;
}
async function optimize(filePath) {
const targetPath = filePath.replace(/\.png$/i, '.webp');
const width = pickWidth(filePath);
const image = sharp(filePath, { animated: false }).rotate();
const metadata = await image.metadata();
const pipeline =
metadata.width && metadata.width > width ? image.resize({ width, withoutEnlargement: true }) : image;
await pipeline.webp({ quality: 76, effort: 6 }).toFile(targetPath);
}
const pngFiles = (
await Promise.all(
roots.map(async (root) => {
try {
return await walk(root);
} catch {
return [];
}
}),
)
)
.flat()
.filter((filePath) => filePath.toLowerCase().endsWith('.png'));
await Promise.all(pngFiles.map((filePath) => optimize(filePath)));
console.log(`Optimized ${pngFiles.length} PNG files to WebP.`);

206
scripts/prerender-routes.ts Normal file
View File

@@ -0,0 +1,206 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { blogPostData, locationData, serviceData } from '../src/data/seoData';
type RouteMeta = {
route: string;
title: string;
description: string;
canonicalUrl: string;
keywords?: string[];
schema?: Record<string, unknown>;
};
const DIST_DIR = path.resolve(process.cwd(), 'dist');
const BASE_URL = 'https://bayareait.services';
const DEFAULT_OG_IMAGE = `${BASE_URL}/logo.svg`;
const staticRoutes: RouteMeta[] = [
{
route: '/',
title: 'IT Service & IT Support for Businesses in Corpus Christi, TX',
description:
'Reliable IT support and IT services for businesses in Corpus Christi, TX. Fast response, outsourced IT support and help desk solutions.',
canonicalUrl: `${BASE_URL}/`,
keywords: ['IT Service', 'IT Support', 'Corpus Christi', 'IT Help Desk'],
schema: {
'@context': 'https://schema.org',
'@type': 'ITService',
name: 'Bay Area IT',
url: BASE_URL,
telephone: '+1-361-765-8400',
areaServed: ['Corpus Christi', 'Portland', 'Rockport', 'Aransas Pass', 'Kingsville'],
},
},
{
route: '/about',
title: 'About Bay Area IT | Local IT Support in Corpus Christi',
description:
'Learn about Bay Area IT, a local IT partner serving Corpus Christi and the Coastal Bend with practical support, reliable service, and over 25 years of experience.',
canonicalUrl: `${BASE_URL}/about`,
schema: {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'Bay Area IT',
url: BASE_URL,
},
},
{
route: '/services',
title: 'IT Services | Bay Area IT Support, Email, Networking and Web',
description:
'Explore Bay Area IT services for Corpus Christi businesses, including help desk support, business email, networking, hardware, web design, and day-to-day IT support.',
canonicalUrl: `${BASE_URL}/services`,
},
{
route: '/blog',
title: 'Blog | Bay Area IT Insights for Corpus Christi Businesses',
description:
'Read practical IT guidance for Corpus Christi and Coastal Bend businesses, from managed IT support and costs to business email and local service coverage.',
canonicalUrl: `${BASE_URL}/blog`,
},
{
route: '/contact',
title: 'Contact Bay Area IT | Free IT Assessment in Corpus Christi',
description:
'Talk to Bay Area IT about managed IT support, help desk coverage, business email, networking, and technology support across Corpus Christi and the Coastal Bend.',
canonicalUrl: `${BASE_URL}/contact`,
},
{
route: '/locations',
title: 'IT Support Service Areas - Corpus Christi & Coastal Bend, TX',
description:
'Bay Area IT provides IT support and IT services throughout the Coastal Bend. View all cities we serve in the Corpus Christi area.',
canonicalUrl: `${BASE_URL}/locations`,
},
{
route: '/privacy-policy',
title: 'Privacy Policy | Bay Area IT',
description:
'Read how Bay Area IT collects, uses, and protects information submitted through this website.',
canonicalUrl: `${BASE_URL}/privacy-policy`,
},
{
route: '/terms-of-service',
title: 'Terms of Service | Bay Area IT',
description:
'Review the Bay Area IT terms covering use of this website and our IT support services.',
canonicalUrl: `${BASE_URL}/terms-of-service`,
},
];
const dynamicRoutes: RouteMeta[] = [
...locationData.map((item) => ({
route: `/${item.slug}`,
title: item.title,
description: item.description,
canonicalUrl: `${BASE_URL}/${item.slug}`,
keywords: item.keywords,
schema: {
'@context': 'https://schema.org',
'@type': 'LocalBusiness',
name: 'Bay Area IT',
url: `${BASE_URL}/${item.slug}`,
areaServed: item.city,
},
})),
...serviceData.map((item) => ({
route: `/${item.slug}`,
title: item.title,
description: item.description,
canonicalUrl: `${BASE_URL}/${item.slug}`,
keywords: item.keywords,
schema: {
'@context': 'https://schema.org',
'@type': 'Service',
name: item.h1,
provider: {
'@type': 'Organization',
name: 'Bay Area IT',
},
url: `${BASE_URL}/${item.slug}`,
},
})),
...blogPostData
.filter((item) => !item.redirect)
.map((item) => ({
route: `/${item.slug}`,
title: item.title,
description: item.description,
canonicalUrl: `${BASE_URL}/${item.slug}`,
keywords: item.keywords,
schema: {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: item.h1,
description: item.description,
url: `${BASE_URL}/${item.slug}`,
publisher: {
'@type': 'Organization',
name: 'Bay Area IT',
},
},
})),
];
function escapeHtml(value: string) {
return value
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function buildHead(meta: RouteMeta) {
const keywords = meta.keywords?.length
? `\n <meta name="keywords" content="${escapeHtml(meta.keywords.join(', '))}" />`
: '';
const schema = meta.schema
? `\n <script type="application/ld+json">${JSON.stringify(meta.schema)}</script>`
: '';
return ` <title>${escapeHtml(meta.title)}</title>
<meta name="description" content="${escapeHtml(meta.description)}" />${keywords}
<link rel="canonical" href="${meta.canonicalUrl}" />
<meta property="og:title" content="${escapeHtml(meta.title)}" />
<meta property="og:description" content="${escapeHtml(meta.description)}" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Bay Area IT" />
<meta property="og:url" content="${meta.canonicalUrl}" />
<meta property="og:image" content="${DEFAULT_OG_IMAGE}" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="${escapeHtml(meta.title)}" />
<meta name="twitter:description" content="${escapeHtml(meta.description)}" />${schema}`;
}
function injectHead(template: string, meta: RouteMeta) {
const withoutTitle = template.replace(/<title>[\s\S]*?<\/title>/i, '');
return withoutTitle.replace('</head>', `${buildHead(meta)}\n </head>`);
}
async function writeRouteHtml(template: string, meta: RouteMeta) {
const html = injectHead(template, meta);
const cleanRoute = meta.route === '/' ? '' : meta.route.replace(/^\/+/, '');
const filePath =
meta.route === '/'
? path.join(DIST_DIR, 'index.html')
: path.join(DIST_DIR, cleanRoute, 'index.html');
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, html, 'utf8');
}
async function main() {
const templatePath = path.join(DIST_DIR, 'index.html');
const template = await fs.readFile(templatePath, 'utf8');
const allRoutes = [...staticRoutes, ...dynamicRoutes];
await Promise.all(allRoutes.map((meta) => writeRouteHtml(template, meta)));
console.log(`Prerendered ${allRoutes.length} route HTML files.`);
}
main().catch((error) => {
console.error('Failed to prerender route HTML files.');
console.error(error);
process.exit(1);
});

View File

@@ -0,0 +1,42 @@
import fs from 'node:fs/promises';
import path from 'node:path';
const distDir = 'dist';
async function walk(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
const files = await Promise.all(
entries.map(async (entry) => {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
return walk(fullPath);
}
return fullPath;
}),
);
return files.flat();
}
try {
const files = await walk(distDir);
const pngFiles = files.filter((filePath) => filePath.toLowerCase().endsWith('.png'));
let removed = 0;
for (const pngFile of pngFiles) {
const webpFile = pngFile.replace(/\.png$/i, '.webp');
try {
await fs.access(webpFile);
await fs.unlink(pngFile);
removed += 1;
} catch {
// Keep PNGs that have no optimized WebP counterpart.
}
}
console.log(`Pruned ${removed} PNG files from dist.`);
} catch {
console.log('No dist directory to prune.');
}

BIN
src/assets/hero-bg.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

File diff suppressed because it is too large Load Diff

63
src/index.css Normal file
View File

@@ -0,0 +1,63 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
height: auto;
overflow-y: auto;
}
body {
background-color: #0a0a0a;
color: white;
overflow-x: hidden;
width: 100%;
min-height: 100vh;
}
}
@layer utilities {
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
}
.lenis.lenis-smooth {
scroll-behavior: auto !important;
}
.lenis.lenis-smooth [data-lenis-prevent] {
overscroll-behavior: contain;
}
.lenis.lenis-stopped {
overflow: hidden;
}
.lenis.lenis-scrolling iframe {
pointer-events: none;
}

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { motion, useInView, useSpring, useTransform, useScroll, useMotionValueEvent } from 'framer-motion';
import Contact from '../../components/Contact';
import SEO from '../../components/SEO';
const Counter = ({ value }: { value: number }) => {
const ref = useRef(null);
@@ -40,13 +41,6 @@ const AboutPage: React.FC = () => {
window.scrollTo(0, 0);
}, []);
const stats = [
{ label: 'Businesses served', value: '150+' },
{ label: 'Uptime achieved', value: '99.9%' },
{ label: 'Years of service', value: '15+' },
{ label: 'Response time', value: '<2min' },
];
const values = [
{
title: 'Security-First',
@@ -66,13 +60,20 @@ const AboutPage: React.FC = () => {
];
const timeline = [
{ year: '2010', title: 'Founded in Corpus Christi', desc: 'Started with a mission to bring enterprise-level IT solutions to local businesses.' },
{ year: '2000', title: 'Founded in Corpus Christi', desc: 'Started with a mission to bring enterprise-level IT solutions to local businesses.' },
{ year: '2015', title: 'Expanded Service Portfolio', desc: 'Added cloud services and advanced networking to serve growing businesses.' },
{ year: '2020', title: 'Remote Work Transformation', desc: 'Helped 100+ businesses transition to secure remote work during the pandemic.' },
{ year: '2024', title: 'Leading the Coastal Bend', desc: 'Now serving 150+ businesses with modern, reliable IT infrastructure.' },
{ year: '2020', title: 'Remote Work Transformation', desc: 'Helped local businesses strengthen remote access, security, and day-to-day support during a disruptive period.' },
{ year: '2024', title: 'Leading the Coastal Bend', desc: 'Now supporting 30+ local businesses with practical, reliable IT infrastructure.' },
];
return (
<>
<SEO
title="About Bay Area IT | Local IT Support in Corpus Christi"
description="Learn about Bay Area IT, a local IT partner serving Corpus Christi and the Coastal Bend with practical support, reliable service, and over 25 years of experience."
keywords={['about Bay Area IT', 'Corpus Christi IT company', 'local IT support Coastal Bend']}
canonicalUrl="https://bayareait.services/about"
/>
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.2),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
@@ -92,7 +93,7 @@ const AboutPage: React.FC = () => {
transition={{ delay: 0.1 }}
className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto leading-relaxed"
>
Since 2010, we've been helping businesses in Corpus Christi and surrounding communities build reliable, secure technology foundations that drive growth.
Since 2000, we've been helping businesses in Corpus Christi and surrounding communities build reliable, secure technology foundations that keep work moving.
</motion.p>
</div>
</section>
@@ -104,13 +105,13 @@ const AboutPage: React.FC = () => {
<h2 className="font-display text-3xl font-bold mb-8 text-gray-900 dark:text-white">Our Story</h2>
<div className="prose dark:prose-invert max-w-none text-lg text-gray-600 dark:text-gray-300 space-y-6">
<p>
Bay Area Affiliates was founded with a simple belief: local businesses deserve the same level of IT expertise and reliability as large corporations, but with the personal touch that only comes from working with your neighbors.
Bay Area IT was founded with a simple belief: local businesses deserve dependable technology support without enterprise complexity, vague communication, or reactive chaos.
</p>
<p>
Over the years, we've watched the Coastal Bend grow and change. We've helped businesses navigate technology challenges, from the transition to cloud computing to the rapid shift to remote work. Through it all, we've maintained our commitment to clear communication, reliable solutions, and exceptional service.
</p>
<p>
Today, we're proud to serve over 150 businesses across the region, from Corpus Christi to the smallest coastal communities. Our team combines deep technical expertise with real-world business understanding to deliver IT solutions that actually work for our clients.
Today, we're proud to support 30+ local businesses across the region. Our team combines deep technical experience with real-world business judgment to deliver IT support that is clear, practical, and reliable.
</p>
</div>
</div>
@@ -121,9 +122,9 @@ const AboutPage: React.FC = () => {
<div className="absolute inset-0 bg-[radial-gradient(ellipse_60%_50%_at_50%_-20%,rgba(255,255,255,0.03),rgba(255,255,255,0))] pointer-events-none"></div>
<div className="max-w-7xl mx-auto grid grid-cols-2 md:grid-cols-4 gap-8 text-center relative z-10">
{[
{ label: 'Businesses served', value: 150, suffix: '+' },
{ label: 'Businesses served', value: 30, suffix: '+' },
{ label: 'Uptime achieved', value: 99.9, suffix: '%' },
{ label: 'Years of service', value: 15, suffix: '+' },
{ label: 'Years of service', value: 25, suffix: '+' },
{ label: 'Response time', value: 2, prefix: '<', suffix: 'min' },
].map((stat, index) => (
<div key={index} className="p-4">
@@ -239,6 +240,7 @@ const AboutPage: React.FC = () => {
<Contact />
</div>
</>
);
};

View File

@@ -2,14 +2,35 @@ import React, { useEffect } from 'react';
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
import Contact from '../../components/Contact';
import SEO from '../../components/SEO';
import { blogPostData } from '../data/seoData';
const cardVariants = {
hidden: { opacity: 0, y: 20 },
visible: (index: number) => ({
opacity: 1,
y: 0,
transition: {
duration: 0.45,
delay: Math.min(index * 0.06, 0.3),
ease: 'easeOut',
},
}),
};
const BlogPage: React.FC = () => {
useEffect(() => {
window.scrollTo(0, 0);
}, []);
return (
<>
<SEO
title="Blog | Bay Area IT Insights for Corpus Christi Businesses"
description="Read practical IT guidance for Corpus Christi and Coastal Bend businesses, from managed IT support and costs to business email and local service coverage."
keywords={['Corpus Christi IT blog', 'managed IT insights', 'business IT support articles']}
canonicalUrl="https://bayareait.services/blog"
/>
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.2),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
<div className="absolute top-[300px] left-1/2 -translate-x-1/2 w-[800px] h-[800px] bg-gray-100/50 dark:bg-white/5 rounded-full blur-[120px] pointer-events-none" />
@@ -24,16 +45,17 @@ const BlogPage: React.FC = () => {
<section className="py-16 px-6 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.05),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.05),rgba(255,255,255,0))]">
<div className="max-w-5xl mx-auto space-y-16">
{blogPostData.map((post) => (
{blogPostData.filter(post => !post.redirect).map((post, index) => (
<Link
key={post.id}
to={`/${post.slug}`}
className="block"
>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
initial="hidden"
animate="visible"
custom={index}
variants={cardVariants}
whileHover={{ y: -5 }}
className="group grid md:grid-cols-2 gap-0 bg-white dark:bg-[#161616] rounded-3xl overflow-hidden shadow-lg border border-gray-100 dark:border-white/5 hover:shadow-2xl hover:shadow-blue-900/10 transition-all duration-300"
>
@@ -41,6 +63,9 @@ const BlogPage: React.FC = () => {
<img
src={post.image || '/images/blog/default.png'}
alt={post.h1}
loading={index === 0 ? 'eager' : 'lazy'}
decoding="async"
fetchPriority={index === 0 ? 'high' : 'low'}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className="absolute top-4 left-4">
@@ -75,6 +100,7 @@ const BlogPage: React.FC = () => {
<Contact />
</div>
</>
);
};

View File

@@ -1,58 +1,17 @@
import React, { useEffect, useRef, useLayoutEffect } from 'react';
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
import React, { useEffect } from 'react';
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import SEO from '../../components/SEO';
import Services from '../../components/Services';
import CTA from '../../components/CTA';
import AreasWeServe from '../../components/AreasWeServe';
import { BlogPostData } from '../data/seoData';
gsap.registerPlugin(ScrollTrigger);
interface BlogPostPageProps {
data: BlogPostData;
}
const BlogPostPage: React.FC<BlogPostPageProps> = ({ data }) => {
const containerRef = useRef<HTMLDivElement>(null);
const parallaxWrapperRef = useRef<HTMLDivElement>(null);
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => {
const { left, top } = currentTarget.getBoundingClientRect();
mouseX.set(clientX - left);
mouseY.set(clientY - top + 75);
};
useLayoutEffect(() => {
const ctx = gsap.context(() => {
// Parallax Background
if (parallaxWrapperRef.current) {
gsap.to(parallaxWrapperRef.current, {
yPercent: 30,
ease: "none",
scrollTrigger: {
trigger: containerRef.current,
start: "top top",
end: "bottom top",
scrub: true
}
});
}
// Text Stagger Animation
gsap.fromTo(".hero-stagger",
{ y: 50, opacity: 0 },
{ y: 0, opacity: 1, duration: 1, stagger: 0.2, ease: "power3.out", delay: 0.2 }
);
}, containerRef);
return () => ctx.revert();
}, []);
useEffect(() => {
window.scrollTo(0, 0);
}, []);
@@ -71,54 +30,24 @@ const BlogPostPage: React.FC<BlogPostPageProps> = ({ data }) => {
title={data.title}
description={data.description}
keywords={data.keywords}
canonicalUrl={window.location.href}
canonicalUrl={`https://bayareait.services/${data.slug}`}
/>
<div className="min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
{/* Hero Section */}
<section
ref={containerRef}
onMouseMove={handleMouseMove}
className="relative min-h-screen flex items-center justify-center overflow-hidden pt-20 group"
>
{/* Parallax Background */}
<div className="absolute inset-0 z-0 pointer-events-none">
<div ref={parallaxWrapperRef} className="absolute w-full h-[120%] -top-[10%] left-0">
{/* Base Layer */}
<img
alt="Abstract dark technology background"
className="w-full h-full object-cover opacity-90 dark:opacity-70 brightness-75 contrast-150"
src="/src/assets/hero-bg.png"
/>
{/* Highlight Layer */}
<motion.img
style={{
maskImage: useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`,
WebkitMaskImage: useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`,
}}
alt=""
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 brightness-150 contrast-150 filter saturate-150"
src="/src/assets/hero-bg.png"
/>
</div>
<div className="absolute inset-0 bg-gradient-to-t from-background-light via-transparent to-transparent dark:from-background-dark dark:via-transparent dark:to-transparent"></div>
<div className="absolute inset-0 bg-gradient-to-b from-background-light/50 dark:from-background-dark/50 to-transparent"></div>
</div>
{/* Hero Content */}
<div className="relative z-10 text-center max-w-4xl px-6">
<div className="min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden pt-32 pb-16">
{/* Clean Hero Section */}
<article className="max-w-4xl mx-auto px-6">
<header className="mb-12 text-center">
{/* Breadcrumbs */}
<nav className="hero-stagger mb-8 text-sm">
<ol className="flex items-center gap-2 text-gray-600 dark:text-gray-400 justify-center">
<nav className="mb-8 text-sm">
<ol className="flex items-center gap-2 text-gray-500 dark:text-gray-400 justify-center">
<li>
<Link to="/" className="hover:text-gray-900 dark:hover:text-white transition-colors">
<Link to="/" className="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
Home
</Link>
</li>
<span className="material-symbols-outlined text-xs">chevron_right</span>
<li>
<Link to="/blog" className="hover:text-gray-900 dark:hover:text-white transition-colors">
<Link to="/blog" className="hover:text-blue-600 dark:hover:text-blue-400 transition-colors">
Blog
</Link>
</li>
@@ -127,83 +56,62 @@ const BlogPostPage: React.FC<BlogPostPageProps> = ({ data }) => {
</ol>
</nav>
<div className="hero-stagger flex items-center justify-center gap-2 mb-6">
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
<span className="text-xs uppercase tracking-[0.2em] text-gray-600 dark:text-gray-400 font-medium">
<div className="flex items-center justify-center gap-2 mb-6">
<span className="h-px w-8 bg-blue-600/30 dark:bg-blue-400/30"></span>
<span className="text-xs uppercase tracking-[0.2em] text-blue-600 dark:text-blue-400 font-bold">
{category}
</span>
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
<span className="h-px w-8 bg-blue-600/30 dark:bg-blue-400/30"></span>
</div>
<h1 className="hero-stagger font-display text-4xl md:text-6xl lg:text-7xl font-medium tracking-tighter leading-[1.1] mb-8 text-gray-900 dark:text-white">
<h1 className="font-display text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-tight mb-6 text-gray-900 dark:text-white">
{data.h1}
</h1>
{/* Meta Info */}
<div className="hero-stagger flex items-center gap-6 text-gray-600 dark:text-gray-400 mb-8 justify-center">
<div className="flex items-center gap-6 text-gray-500 dark:text-gray-400 mb-10 justify-center">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-sm">schedule</span>
<span>5 min read</span>
</div>
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-sm">calendar_today</span>
<span>January 2025</span>
<span className="material-symbols-outlined text-sm">location_on</span>
<span>Coastal Bend business guide</span>
</div>
</div>
{/* Featured Image */}
{data.image && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.4, duration: 0.8 }}
className="hero-stagger rounded-2xl overflow-hidden border border-gray-200 dark:border-white/10 shadow-2xl mb-8 max-w-md mx-auto"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="rounded-3xl overflow-hidden border border-gray-100 dark:border-white/5 shadow-xl md:mb-16 mb-8 max-w-4xl mx-auto"
>
<img
src={data.image}
alt={data.h1}
className="w-full h-auto max-h-64 object-cover"
loading="eager"
decoding="async"
fetchPriority="high"
className="w-full h-auto max-h-[500px] object-cover"
/>
</motion.div>
)}
</header>
<div className="hero-stagger flex flex-col sm:flex-row items-center justify-center gap-4">
<motion.a
href="/contact"
className="px-8 py-3 bg-white dark:bg-white text-black dark:text-black rounded-full font-medium shadow-xl"
whileHover={{ scale: 1.05, backgroundColor: "#3b82f6", color: "#ffffff" }}
whileTap={{ scale: 0.95 }}
>
Get IT Support
</motion.a>
<motion.a
href="/it-support-corpus-christi"
className="px-8 py-3 bg-white/10 dark:bg-white/10 backdrop-blur-sm border-2 border-white/40 dark:border-white/40 text-white dark:text-white rounded-full font-medium shadow-xl"
whileHover={{ scale: 1.05, backgroundColor: "rgba(255,255,255,0.2)", borderColor: "#ffffff" }}
whileTap={{ scale: 0.95 }}
>
View All Services
</motion.a>
{/* Main Content Section */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2, duration: 0.6 }}
className="bg-white dark:bg-[#111] rounded-[2rem] p-8 md:p-12 lg:p-16 shadow-lg border border-gray-100 dark:border-white/5 mb-16"
>
<div className="prose prose-lg dark:prose-invert max-w-none prose-headings:font-display prose-headings:font-bold prose-h2:text-3xl prose-h2:mb-6 prose-h2:mt-12 prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-4 prose-p:leading-relaxed prose-li:leading-relaxed prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-a:no-underline hover:prose-a:underline prose-img:rounded-xl prose-img:shadow-md">
<div dangerouslySetInnerHTML={{ __html: data.content }} />
</div>
</div>
</section>
{/* Main Content Section */}
<section className="px-6 py-16 relative">
<div className="max-w-4xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="bg-white/80 dark:bg-white/5 backdrop-blur-xl rounded-3xl p-12 md:p-16 shadow-2xl border border-gray-100 dark:border-white/10"
>
<div className="prose prose-lg md:prose-xl dark:prose-invert max-w-none prose-headings:font-display prose-h2:text-3xl prose-h2:mb-6 prose-h2:mt-12 prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-4 prose-p:leading-relaxed prose-li:leading-relaxed prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-a:no-underline hover:prose-a:underline prose-strong:text-gray-900 dark:prose-strong:text-white">
<div dangerouslySetInnerHTML={{ __html: data.content }} />
</div>
</motion.div>
</div>
</section>
</motion.div>
</article>
{/* CTA Section */}
<section className="px-6 py-16">
@@ -213,7 +121,7 @@ const BlogPostPage: React.FC<BlogPostPageProps> = ({ data }) => {
viewport={{ once: true }}
className="max-w-4xl mx-auto"
>
<div className="p-12 bg-gradient-to-br from-blue-50 to-gray-50 dark:from-blue-950/30 dark:to-gray-950/30 rounded-3xl border border-blue-100 dark:border-blue-900/50 shadow-xl text-center">
<div className="p-10 md:p-14 bg-gradient-to-br from-blue-50 to-gray-50 dark:from-blue-950/20 dark:to-gray-900/20 rounded-[2.5rem] border border-blue-100/50 dark:border-blue-900/30 shadow-xl text-center">
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-6 block">
{category === 'Local Services' ? 'location_on' : 'insights'}
</span>
@@ -222,14 +130,14 @@ const BlogPostPage: React.FC<BlogPostPageProps> = ({ data }) => {
? 'Ready to Get IT Support in Your Area?'
: 'Need Expert IT Support for Your Business?'}
</h2>
<p className="text-lg text-gray-700 dark:text-gray-300 mb-8 max-w-2xl mx-auto">
<p className="text-lg text-gray-600 dark:text-gray-400 mb-8 max-w-2xl mx-auto">
{category === 'Local Services'
? 'Contact us today to learn how we can help your business with reliable IT support and managed services.'
: 'Let us handle your IT needs so you can focus on growing your business. Get a free consultation today.'}
</p>
<Link
to="/contact"
className="inline-flex items-center gap-3 px-10 py-5 bg-black dark:bg-white text-white dark:text-black rounded-full font-bold text-lg transition-all hover:scale-105 shadow-2xl hover:shadow-3xl"
className="inline-flex items-center gap-3 px-10 py-4 bg-blue-600 hover:bg-blue-700 text-white rounded-full font-bold text-lg transition-all hover:scale-105 shadow-xl hover:shadow-blue-500/25"
>
Get Started
<span className="material-symbols-outlined">arrow_forward</span>
@@ -240,7 +148,7 @@ const BlogPostPage: React.FC<BlogPostPageProps> = ({ data }) => {
{/* Related Content Grid */}
<section className="px-6 py-16">
<div className="max-w-6xl mx-auto">
<div className="max-w-5xl mx-auto">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
@@ -256,15 +164,17 @@ const BlogPostPage: React.FC<BlogPostPageProps> = ({ data }) => {
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.1 }}
className="p-8 bg-white dark:bg-white/5 rounded-2xl border border-gray-100 dark:border-white/10 hover:shadow-xl transition-shadow"
className="p-8 bg-white dark:bg-[#111] rounded-3xl border border-gray-100 dark:border-white/5 hover:shadow-xl transition-shadow"
>
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
verified_user
</span>
<div className="w-14 h-14 bg-blue-50 dark:bg-blue-900/20 rounded-2xl flex items-center justify-center mb-6">
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-3xl">
verified_user
</span>
</div>
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
Proven Expertise
</h3>
<p className="text-gray-600 dark:text-gray-300">
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
Years of experience serving businesses across the Coastal Bend with comprehensive IT solutions.
</p>
</motion.div>
@@ -274,16 +184,18 @@ const BlogPostPage: React.FC<BlogPostPageProps> = ({ data }) => {
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.2 }}
className="p-8 bg-white dark:bg-white/5 rounded-2xl border border-gray-100 dark:border-white/10 hover:shadow-xl transition-shadow"
className="p-8 bg-white dark:bg-[#111] rounded-3xl border border-gray-100 dark:border-white/5 hover:shadow-xl transition-shadow"
>
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
support_agent
</span>
<div className="w-14 h-14 bg-blue-50 dark:bg-blue-900/20 rounded-2xl flex items-center justify-center mb-6">
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-3xl">
support_agent
</span>
</div>
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
24/7 Support
</h3>
<p className="text-gray-600 dark:text-gray-300">
Round-the-clock monitoring and support to keep your business running smoothly at all times.
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
Remote-first support and practical escalation to keep work moving when issues appear.
</p>
</motion.div>
@@ -292,15 +204,17 @@ const BlogPostPage: React.FC<BlogPostPageProps> = ({ data }) => {
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.3 }}
className="p-8 bg-white dark:bg-white/5 rounded-2xl border border-gray-100 dark:border-white/10 hover:shadow-xl transition-shadow"
className="p-8 bg-white dark:bg-[#111] rounded-3xl border border-gray-100 dark:border-white/5 hover:shadow-xl transition-shadow"
>
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
handshake
</span>
<div className="w-14 h-14 bg-blue-50 dark:bg-blue-900/20 rounded-2xl flex items-center justify-center mb-6">
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-3xl">
handshake
</span>
</div>
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
Local Partnership
</h3>
<p className="text-gray-600 dark:text-gray-300">
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
A trusted local partner who understands your community and business needs.
</p>
</motion.div>

View File

@@ -1,5 +1,6 @@
import React, { useEffect } from 'react';
import { motion } from 'framer-motion';
import SEO from '../../components/SEO';
const ContactPage: React.FC = () => {
useEffect(() => {
@@ -13,6 +14,13 @@ const ContactPage: React.FC = () => {
];
return (
<>
<SEO
title="Contact Bay Area IT | Free IT Assessment in Corpus Christi"
description="Talk to Bay Area IT about managed IT support, help desk coverage, business email, networking, and technology support across Corpus Christi and the Coastal Bend."
keywords={['contact Bay Area IT', 'Corpus Christi IT consultation', 'managed IT assessment']}
canonicalUrl="https://bayareait.services/contact"
/>
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(0,0,0,0.2),rgba(0,0,0,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
<div className="absolute bottom-0 right-0 w-[500px] h-[500px] bg-gray-100/50 dark:bg-white/5 rounded-full blur-[100px] pointer-events-none" />
@@ -201,6 +209,7 @@ const ContactPage: React.FC = () => {
</div>
</section>
</div>
</>
);
};

View File

@@ -1,4 +1,5 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import LoadingScreen from '../../components/LoadingScreen';
import Hero from '../../components/Hero';
import Mission from '../../components/Mission';
import Services from '../../components/Services';
@@ -12,26 +13,37 @@ import AreasWeServe from '../../components/AreasWeServe';
import { locationData } from '../data/seoData';
const HomePage: React.FC = () => {
const [isLoading, setIsLoading] = useState(() => {
if (typeof window !== 'undefined') {
return !sessionStorage.getItem('home_loaded');
}
return false;
});
useEffect(() => {
window.scrollTo(0, 0);
}, []);
if (isLoading) {
sessionStorage.setItem('home_loaded', 'true');
}
}, [isLoading]);
// Enhanced LocalBusiness Schema per SEO plan
const schema = {
"@context": "https://schema.org",
"@type": "ITService",
"name": "Bay Area IT Services",
"image": "https://bayarea-cc.com/logo.png",
"@id": "https://bayarea-cc.com",
"url": "https://bayarea-cc.com",
"telephone": "+1-361-XXX-XXXX", // TODO: Replace with actual phone
"name": "Bay Area IT",
"image": "https://bayareait.services/logo.svg",
"@id": "https://bayareait.services",
"url": "https://bayareait.services",
"telephone": "+1-361-765-8400",
"priceRange": "$$",
"address": {
"@type": "PostalAddress",
"streetAddress": "[YOUR STREET]", // TODO: Add actual address
"streetAddress": "1001 Blucher St",
"addressLocality": "Corpus Christi",
"addressRegion": "TX",
"postalCode": "[YOUR ZIP]", // TODO: Add actual ZIP
"postalCode": "78401",
"addressCountry": "US"
},
"geo": {
@@ -48,7 +60,8 @@ const HomePage: React.FC = () => {
],
"serviceType": [
"IT Support",
"Business IT Support",
"IT Help Desk",
"Computer Support",
"Outsourced IT Services",
"Computer Network Support",
"Cyber Security"
@@ -69,11 +82,12 @@ const HomePage: React.FC = () => {
return (
<>
{isLoading && <LoadingScreen onComplete={() => setIsLoading(false)} />}
<SEO
title="IT Service & IT Support for Businesses in Corpus Christi, TX"
description="Reliable IT support and IT services for businesses in Corpus Christi, TX. Fast response, outsourced IT support & help desk solutions. Call now."
keywords={["IT Service", "IT Support", "Corpus Christi", "Business IT Support"]}
canonicalUrl={window.location.href}
keywords={["IT Service", "IT Support", "Corpus Christi", "IT Help Desk"]}
canonicalUrl="https://bayareait.services/"
schema={schema}
/>
<Hero />

121
src/pages/LegalPage.tsx Normal file
View File

@@ -0,0 +1,121 @@
import React, { useEffect } from 'react';
import { motion } from 'framer-motion';
import SEO from '../../components/SEO';
interface LegalSection {
title: string;
body: string[];
}
interface LegalPageProps {
title: string;
description: string;
canonicalUrl: string;
eyebrow: string;
intro: string;
sections: LegalSection[];
}
const LegalPage: React.FC<LegalPageProps> = ({
title,
description,
canonicalUrl,
eyebrow,
intro,
sections,
}) => {
useEffect(() => {
window.scrollTo(0, 0);
}, []);
return (
<>
<SEO
title={title}
description={description}
canonicalUrl={canonicalUrl}
keywords={['Bay Area IT legal', 'privacy policy', 'terms of service']}
/>
<div className="pt-20 min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
<div className="absolute top-0 left-0 right-0 h-[720px] bg-[radial-gradient(ellipse_75%_50%_at_50%_-20%,rgba(255,255,255,0.08),rgba(255,255,255,0))] pointer-events-none" />
<div className="absolute top-32 left-1/2 -translate-x-1/2 w-[760px] h-[420px] bg-white/5 rounded-full blur-[120px] pointer-events-none" />
<section className="px-6 py-20 border-b border-white/10 relative z-10">
<div className="max-w-4xl mx-auto">
<motion.p
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
className="text-xs uppercase tracking-[0.35em] text-gray-500 mb-6"
>
{eyebrow}
</motion.p>
<motion.h1
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.05 }}
className="font-display text-4xl md:text-6xl font-bold text-gray-900 dark:text-white mb-6"
>
{title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="max-w-3xl text-lg md:text-xl leading-relaxed text-gray-600 dark:text-gray-300"
>
{intro}
</motion.p>
</div>
</section>
<section className="px-6 py-16 md:py-20 relative z-10">
<div className="max-w-4xl mx-auto">
<div className="grid gap-6">
{sections.map((section, index) => (
<motion.article
key={section.title}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-10%' }}
transition={{ delay: index * 0.04 }}
className="rounded-[2rem] border border-white/10 bg-white/5 backdrop-blur-sm p-8 md:p-10 shadow-[0_30px_80px_rgba(0,0,0,0.18)]"
>
<h2 className="font-display text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-5">
{section.title}
</h2>
<div className="space-y-4 text-base md:text-lg leading-relaxed text-gray-600 dark:text-gray-300">
{section.body.map((paragraph, paragraphIndex) => {
const [before, after] = paragraph.split('info@bayareaaffiliates.com');
return (
<p key={`${section.title}-${paragraphIndex}`}>
{after !== undefined ? (
<>
{before}
<a
href="mailto:info@bayareaaffiliates.com"
className="text-gray-900 dark:text-white underline decoration-white/30 underline-offset-4 transition-opacity hover:opacity-75"
>
info@bayareaaffiliates.com
</a>
{after}
</>
) : (
paragraph
)}
</p>
);
})}
</div>
</motion.article>
))}
</div>
</div>
</section>
</div>
</>
);
};
export default LegalPage;

View File

@@ -1,14 +1,16 @@
import React, { useEffect, useRef, useLayoutEffect } from 'react';
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { motion, useMotionTemplate, useMotionValue, useReducedMotion } from 'framer-motion';
import { Link } from 'react-router-dom';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import SEO from '../../components/SEO';
import Breadcrumb from '../../components/Breadcrumb';
import Services from '../../components/Services';
import CTA from '../../components/CTA';
import FAQ from '../../components/FAQ';
import AreasWeServe from '../../components/AreasWeServe';
import { LocationData } from '../data/seoData';
import heroBg from '../assets/hero-bg.webp';
gsap.registerPlugin(ScrollTrigger);
@@ -21,14 +23,43 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
const parallaxWrapperRef = useRef<HTMLDivElement>(null);
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const prefersReducedMotion = useReducedMotion();
const [isInteractive, setIsInteractive] = useState(false);
const maskImage = useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`;
const webkitMaskImage = useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`;
useEffect(() => {
if (prefersReducedMotion || typeof window === 'undefined') {
setIsInteractive(false);
return;
}
const mediaQuery = window.matchMedia('(pointer: fine) and (hover: hover)');
const updateState = () => setIsInteractive(mediaQuery.matches);
updateState();
if (typeof mediaQuery.addEventListener === 'function') {
mediaQuery.addEventListener('change', updateState);
return () => mediaQuery.removeEventListener('change', updateState);
}
mediaQuery.addListener(updateState);
return () => mediaQuery.removeListener(updateState);
}, [prefersReducedMotion]);
const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => {
if (!isInteractive) return;
const { left, top } = currentTarget.getBoundingClientRect();
mouseX.set(clientX - left);
mouseY.set(clientY - top + 75);
};
useLayoutEffect(() => {
if (!isInteractive) {
return;
}
const ctx = gsap.context(() => {
// Parallax Background
if (parallaxWrapperRef.current) {
@@ -52,7 +83,7 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
}, containerRef);
return () => ctx.revert();
}, []);
}, [isInteractive]);
useEffect(() => {
window.scrollTo(0, 0);
@@ -61,8 +92,8 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
const schema = {
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": "Bay Area IT Services",
"url": window.location.href,
"name": "Bay Area IT",
"url": `https://bayareait.services/${data.slug}`,
"areaServed": {
"@type": "City",
"name": data.city
@@ -75,7 +106,7 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
title={data.title}
description={data.description}
keywords={data.keywords}
canonicalUrl={window.location.href}
canonicalUrl={`https://bayareait.services/${data.slug}`}
schema={schema}
/>
@@ -83,7 +114,7 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
{/* Hero Section */}
<section
ref={containerRef}
onMouseMove={handleMouseMove}
onMouseMove={isInteractive ? handleMouseMove : undefined}
className="relative min-h-[90vh] flex items-center justify-center overflow-hidden pt-20 group"
>
{/* Parallax Background */}
@@ -93,19 +124,23 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
<img
alt="Abstract dark technology background"
className="w-full h-full object-cover opacity-90 dark:opacity-70 brightness-75 contrast-150"
src="/src/assets/hero-bg.png"
src={heroBg}
loading="eager"
decoding="async"
fetchPriority="high"
/>
{/* Highlight Layer */}
<motion.img
style={{
maskImage: useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`,
WebkitMaskImage: useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`,
}}
alt=""
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 brightness-150 contrast-150 filter saturate-150"
src="/src/assets/hero-bg.png"
/>
{isInteractive && (
<motion.img
style={{ maskImage, WebkitMaskImage: webkitMaskImage }}
alt=""
aria-hidden="true"
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 brightness-150 contrast-150 filter saturate-150"
src={heroBg}
loading="lazy"
decoding="async"
/>
)}
</div>
<div className="absolute inset-0 bg-gradient-to-t from-background-light via-transparent to-transparent dark:from-background-dark dark:via-transparent dark:to-transparent"></div>
<div className="absolute inset-0 bg-gradient-to-b from-background-light/50 dark:from-background-dark/50 to-transparent"></div>
@@ -142,7 +177,7 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
Get IT Support
</motion.a>
<motion.a
href="/it-support-corpus-christi"
href="/services"
className="px-8 py-3 bg-white/10 dark:bg-white/10 backdrop-blur-sm border-2 border-white/40 dark:border-white/40 text-white dark:text-white rounded-full font-medium shadow-xl"
whileHover={{ scale: 1.05, backgroundColor: "rgba(255,255,255,0.2)", borderColor: "#ffffff" }}
whileTap={{ scale: 0.95 }}
@@ -156,6 +191,13 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
{/* Main Content Section */}
<section className="px-6 py-16 relative">
<div className="max-w-4xl mx-auto">
<div className="mb-6">
<Breadcrumb items={[
{ label: 'Home', to: '/' },
{ label: 'Locations', to: '/locations' },
{ label: data.city },
]} />
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
@@ -190,7 +232,7 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
</h3>
<p className="text-lg text-gray-700 dark:text-gray-300 leading-relaxed">
We are based in Corpus Christi and proudly serve businesses throughout the entire surrounding area.
Learn more about our <Link to="/it-support-corpus-christi" className="text-blue-600 dark:text-blue-400 font-bold hover:underline">IT support in Corpus Christi</Link> and our commitment to local businesses.
Learn more about our <Link to="/locations/it-support-corpus-christi" className="text-blue-600 dark:text-blue-400 font-bold hover:underline">IT support in Corpus Christi</Link> and our commitment to local businesses.
</p>
</div>
</div>
@@ -200,7 +242,7 @@ const LocationPage: React.FC<LocationPageProps> = ({ data }) => {
)}
{/* Services Section */}
<Services featuredIds={[10, 9, 11]} />
<Services featuredIds={[6, 4, 3]} />
{/* Areas We Serve & CTA */}
<AreasWeServe />

106
src/pages/LocationsPage.tsx Normal file
View File

@@ -0,0 +1,106 @@
import React, { useEffect } from 'react';
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
import SEO from '../../components/SEO';
import CTA from '../../components/CTA';
import { locationData } from '../data/seoData';
const cityLocations = locationData.filter((loc) =>
loc.slug.startsWith('locations/it-support-'),
);
const LocationsPage: React.FC = () => {
useEffect(() => {
window.scrollTo(0, 0);
}, []);
return (
<>
<SEO
title="IT Support Service Areas - Corpus Christi & Coastal Bend, TX"
description="Bay Area IT provides IT support and IT services throughout the Coastal Bend. View all cities we serve in the Corpus Christi area."
keywords={['IT support Corpus Christi', 'IT services Coastal Bend', 'local IT support Texas']}
canonicalUrl="https://bayareait.services/locations"
/>
<div className="min-h-screen bg-background-light dark:bg-background-dark pt-20">
<section className="py-20 px-6 border-b border-gray-200 dark:border-white/10">
<div className="max-w-4xl mx-auto text-center">
<span className="text-xs font-semibold uppercase tracking-widest text-gray-500 dark:text-gray-500 mb-3 block">
Service Areas
</span>
<h1 className="font-display text-4xl md:text-5xl font-bold mb-6 text-gray-900 dark:text-white tracking-tight">
IT Support Across the{' '}
<span className="text-gray-400 dark:text-gray-500">Coastal Bend</span>
</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-2xl mx-auto leading-relaxed">
We provide professional IT support and managed IT services for businesses
throughout Corpus Christi and the surrounding area. Remote-first, on-site
when needed.
</p>
</div>
</section>
<section className="py-20 px-6">
<div className="max-w-5xl mx-auto">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{cityLocations.map((loc, i) => (
<motion.div
key={loc.slug}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: i * 0.07 }}
>
<Link
to={`/${loc.slug}`}
className="group block p-8 bg-white dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-2xl hover:border-gray-400 dark:hover:border-white/30 hover:shadow-xl transition-all duration-300"
>
<div className="flex items-center justify-between mb-4">
<span className="material-symbols-outlined text-3xl text-gray-400 dark:text-gray-500 group-hover:text-gray-700 dark:group-hover:text-gray-300 transition-colors">
location_on
</span>
<span className="material-symbols-outlined text-sm text-gray-400 dark:text-gray-600 group-hover:text-gray-700 dark:group-hover:text-white group-hover:translate-x-1 transition-all duration-200">
arrow_forward
</span>
</div>
<h2 className="font-display text-xl font-bold text-gray-900 dark:text-white mb-2">
{loc.city}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 leading-relaxed">
{loc.description}
</p>
</Link>
</motion.div>
))}
</div>
</div>
</section>
<section className="py-16 px-6 bg-gray-50 dark:bg-white/5 border-t border-gray-200 dark:border-white/10">
<div className="max-w-4xl mx-auto text-center">
<h2 className="font-display text-2xl md:text-3xl font-bold mb-4 text-gray-900 dark:text-white">
Based in Corpus Christi. Serving the Region.
</h2>
<p className="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto mb-8 leading-relaxed">
Our team is locally based with over 25 years supporting Coastal Bend businesses.
Most issues are resolved remotely for speed. On-site support is available across
all cities we serve.
</p>
<p className="text-gray-500 dark:text-gray-400">
Not seeing your city?{' '}
<Link to="/contact" className="text-gray-900 dark:text-white font-medium underline hover:no-underline transition-all">
Contact us
</Link>{' '}
and we may still be able to help.
</p>
</div>
</section>
<CTA />
</div>
</>
);
};
export default LocationsPage;

View File

@@ -0,0 +1,55 @@
import React from 'react';
import LegalPage from './LegalPage';
const sections = [
{
title: 'Information We Collect',
body: [
'We may collect personal information such as your name, email address, phone number, and other relevant details when you contact us for our IT support services.',
],
},
{
title: 'How We Use Your Information',
body: [
'Your information is used to provide and improve our services, respond to your inquiries, and communicate important updates.',
'We do not sell or rent your personal information to third parties.',
],
},
{
title: 'Data Security',
body: [
'We implement a variety of security measures to maintain the safety of your personal information. However, no method of transmission over the internet or electronic storage is 100% secure.',
],
},
{
title: 'Your Consent',
body: [
'By using our website and services, you consent to this Privacy Policy.',
],
},
{
title: 'Changes to This Privacy Policy',
body: [
'We reserve the right to update this Privacy Policy at any time. Any changes will be effective immediately upon posting on this page.',
],
},
{
title: 'Contact Us',
body: [
'If you have any questions regarding this Privacy Policy, please contact us at info@bayareaaffiliates.com.',
],
},
];
const PrivacyPolicyPage: React.FC = () => (
<LegalPage
title="Privacy Policy"
description="Read the Bay Area Affiliates, Inc. Privacy Policy to understand how we collect, use, and protect your information."
canonicalUrl="https://bayareait.services/privacy-policy"
eyebrow="Bay Area Affiliates, Inc."
intro="At Bay Area Affiliates, Inc., we are committed to protecting your privacy. This page explains how we collect, use, disclose, and safeguard information when you visit our website and use our services."
sections={sections}
/>
);
export default PrivacyPolicyPage;

View File

@@ -1,14 +1,16 @@
import React, { useEffect, useRef, useLayoutEffect } from 'react';
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { motion, useMotionTemplate, useMotionValue, useReducedMotion } from 'framer-motion';
import { Link } from 'react-router-dom';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import SEO from '../../components/SEO';
import Breadcrumb from '../../components/Breadcrumb';
import Services from '../../components/Services';
import CTA from '../../components/CTA';
import FAQ from '../../components/FAQ';
import AreasWeServe from '../../components/AreasWeServe';
import { ServiceData } from '../data/seoData';
import heroBg from '../assets/hero-bg.webp';
gsap.registerPlugin(ScrollTrigger);
@@ -21,14 +23,43 @@ const ServicePage: React.FC<ServicePageProps> = ({ data }) => {
const parallaxWrapperRef = useRef<HTMLDivElement>(null);
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const prefersReducedMotion = useReducedMotion();
const [isInteractive, setIsInteractive] = useState(false);
const maskImage = useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`;
const webkitMaskImage = useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`;
useEffect(() => {
if (prefersReducedMotion || typeof window === 'undefined') {
setIsInteractive(false);
return;
}
const mediaQuery = window.matchMedia('(pointer: fine) and (hover: hover)');
const updateState = () => setIsInteractive(mediaQuery.matches);
updateState();
if (typeof mediaQuery.addEventListener === 'function') {
mediaQuery.addEventListener('change', updateState);
return () => mediaQuery.removeEventListener('change', updateState);
}
mediaQuery.addListener(updateState);
return () => mediaQuery.removeListener(updateState);
}, [prefersReducedMotion]);
const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => {
if (!isInteractive) return;
const { left, top } = currentTarget.getBoundingClientRect();
mouseX.set(clientX - left);
mouseY.set(clientY - top + 75);
};
useLayoutEffect(() => {
if (!isInteractive) {
return;
}
const ctx = gsap.context(() => {
// Parallax Background
if (parallaxWrapperRef.current) {
@@ -52,7 +83,7 @@ const ServicePage: React.FC<ServicePageProps> = ({ data }) => {
}, containerRef);
return () => ctx.revert();
}, []);
}, [isInteractive]);
useEffect(() => {
window.scrollTo(0, 0);
@@ -64,14 +95,14 @@ const ServicePage: React.FC<ServicePageProps> = ({ data }) => {
title={data.title}
description={data.description}
keywords={data.keywords}
canonicalUrl={window.location.href}
canonicalUrl={`https://bayareait.services/${data.slug}`}
/>
<div className="min-h-screen bg-background-light dark:bg-background-dark relative overflow-x-hidden">
{/* Hero Section */}
<section
ref={containerRef}
onMouseMove={handleMouseMove}
onMouseMove={isInteractive ? handleMouseMove : undefined}
className="relative min-h-[90vh] flex items-center justify-center overflow-hidden pt-20 group"
>
{/* Parallax Background */}
@@ -81,19 +112,23 @@ const ServicePage: React.FC<ServicePageProps> = ({ data }) => {
<img
alt="Abstract dark technology background"
className="w-full h-full object-cover opacity-90 dark:opacity-70 brightness-75 contrast-150"
src="/src/assets/hero-bg.png"
src={heroBg}
loading="eager"
decoding="async"
fetchPriority="high"
/>
{/* Highlight Layer */}
<motion.img
style={{
maskImage: useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`,
WebkitMaskImage: useMotionTemplate`radial-gradient(100px circle at ${mouseX}px ${mouseY}px, black, transparent)`,
}}
alt=""
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 brightness-150 contrast-150 filter saturate-150"
src="/src/assets/hero-bg.png"
/>
{isInteractive && (
<motion.img
style={{ maskImage, WebkitMaskImage: webkitMaskImage }}
alt=""
aria-hidden="true"
className="absolute inset-0 w-full h-full object-cover mix-blend-screen opacity-100 brightness-150 contrast-150 filter saturate-150"
src={heroBg}
loading="lazy"
decoding="async"
/>
)}
</div>
<div className="absolute inset-0 bg-gradient-to-t from-background-light via-transparent to-transparent dark:from-background-dark dark:via-transparent dark:to-transparent"></div>
<div className="absolute inset-0 bg-gradient-to-b from-background-light/50 dark:from-background-dark/50 to-transparent"></div>
@@ -130,7 +165,7 @@ const ServicePage: React.FC<ServicePageProps> = ({ data }) => {
Get Started
</motion.a>
<motion.a
href="/it-support-corpus-christi"
href="/services"
className="px-8 py-3 bg-white/10 dark:bg-white/10 backdrop-blur-sm border-2 border-white/40 dark:border-white/40 text-white dark:text-white rounded-full font-medium shadow-xl"
whileHover={{ scale: 1.05, backgroundColor: "rgba(255,255,255,0.2)", borderColor: "#ffffff" }}
whileTap={{ scale: 0.95 }}
@@ -144,6 +179,13 @@ const ServicePage: React.FC<ServicePageProps> = ({ data }) => {
{/* Main Content Section */}
<section className="px-6 py-16 relative">
<div className="max-w-4xl mx-auto">
<div className="mb-6">
<Breadcrumb items={[
{ label: 'Home', to: '/' },
{ label: 'Services', to: '/services' },
{ label: data.h1 },
]} />
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}

View File

@@ -1,72 +1,52 @@
import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence, useScroll, useTransform, useMotionValueEvent } from 'framer-motion';
import Contact from '../../components/Contact';
import SEO from '../../components/SEO';
const services = [
{
id: 1,
title: 'Windows 11 Transition',
description: 'Upgrade to Windows 11 before October 2025 to ensure continued security support and take advantage of the latest features.',
challenge: 'Running outdated operating systems leaves your business vulnerable to security threats and compatibility issues.',
approach: 'We manage the entire migration process, from hardware compatibility checks to software deployment and user training.',
title: 'Bay Area Email Services',
description: 'Our flagship service: enterprise cloud email with 99.99% uptime, premium deliverability, local Corpus Christi support, and 25 GB mailboxes for $5 per inbox.',
challenge: 'Missed emails, spam-folder delivery, and server downtime quietly cost businesses leads, approvals, and customer trust.',
approach: 'We use a serverless AWS-powered email architecture with S3 buffering for inbound mail and Amazon SES for outbound reputation, then manage DNS and migration for you locally.',
deliverables: [
'Hardware compatibility assessment',
'Windows 11 deployment and configuration',
'Application compatibility testing',
'Security policy implementation',
'User training sessions'
'25 GB business mailbox for $5 per inbox',
'99.99% uptime backed by cloud architecture',
'S3 email buffering so incoming mail is never lost',
'Amazon SES sending for strong inbox placement',
'DNS setup, migration, forwarding, and auto-replies'
],
needs: [
'Current device inventory',
'Software list',
'User schedule for upgrades'
'Your domain access',
'Mailbox count and user list',
'Current email provider details'
],
icon: 'desktop_windows',
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBMpd_cFINnFibfNErBs8OVAAyDQYTRXix88YH91QImuGi11XGwlY_QUB2R9htcC1h_fTXUeftdEieGT-oi5p5TBjpAyW-86mSsXu-rqhRTBsJlAGuE37bxJES4DUayktXIToEcF-M4PyXdyyTPIYtpYrxK18b2-sPwMzuzCL0LpgJwd5EoYxAkrJQ7W4eBrIG2e9Cw9sY0dJpXJy-TRgwBG0nk-S7W4Y0s3U9w--AzE4fcUimeGMqWwdCncU5tnETmkrkDNFiCyKSA'
icon: 'mail',
image: '/assets/services/business-it.webp'
},
{
id: 2,
title: 'Web Services',
description: 'Web design, domain registration, email services, and more to establish and enhance your online presence.',
challenge: 'A poor online presence can cost you customers and credibility in a digital-first world.',
approach: 'We build professional, responsive websites and manage your digital identity to attract and retain customers.',
title: 'Shared Drive',
description: 'Setup and management of shared drive solutions so your team can store, access, and organize files reliably.',
challenge: 'Data growth requires scalable storage that is accessible yet secure from loss.',
approach: 'We deploy shared drive solutions that centralize your files and make them easy for your team to access securely.',
deliverables: [
'Custom website design & development',
'Domain registration & management',
'Professional email setup (M365/Google)',
'SEO optimization basics',
'Hosting & maintenance'
'Shared drive setup and configuration',
'Folder structure and permissions',
'User permission management',
'Remote access configuration',
'Backup integration'
],
needs: [
'Brand guidelines / Logo',
'Content & copy',
'Domain access (if existing)'
'Capacity requirements',
'Access patterns'
],
icon: 'language',
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuCxibXNCB5mU7MdWE5znMWnQUc9-d2ZoYF7LXK1CMssnvaFz2ZsGzyxXMbqDmely-UfxapqILD5-Exeo1wlQZKg8T2MK4vjlyAMaehoJoqTy2hHh8rxj46i8CKb4-ILL2JswBc98nJt_Fo1DfcDH0dHH5Zz6H4R2Jm1deViSW8Sp2zNp1sTc4eRHy1URiSRQFcr1C8rca6dKiuNDuyDiUmmesqHobXGItaBeFjJC-0OatWpKbr0zF-Y5qvk9Yl5FY2KUcDY9AcTfelu'
icon: 'storage',
image: ''
},
{
id: 3,
title: 'Performance Upgrades',
description: 'Enhance your desktops and laptops with SSDs, maintain your Windows installations, and achieve dramatic performance boosts.',
challenge: 'Slow computers kill productivity and frustrate employees, leading to wasted time.',
approach: 'We breathe new life into existing hardware with cost-effective upgrades and optimizations.',
deliverables: [
'SSD installation & cloning',
'RAM upgrades',
'System cleanup & optimization',
'Thermal paste replacement',
'Benchmark reporting'
],
needs: [
'Access to devices',
'Data backup confirmation'
],
icon: 'speed',
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBs2fGGwp4EkMarA9Uvy7IOqyW0Pzxzt-94Bsr8Tkbem4uHPq-vMEmGgKuEmds2zKwPrw2nVcvL3MjjKYWieLSLh5pVUbbK6T9aDxt2xhvo4trARZobhzoQCJfI-r6aGW_aqfwC5XxOr9VA3YdnNnYEgkfW_TWrUWYa6mD8X0KdVG3sLimA8p7qWxIqUzFFV82twn60rP4OwLdIsc6t1OGnJzjemxL1Aw05aDo6Ckfr0a1oZ2kD4xKeTkG--zUhezvXB9I03l6f3b46'
},
{
id: 4,
title: 'Printer & Scanner Installation',
description: 'Professional installation and configuration of printers and scanners to ensure seamless integration into your workflow.',
challenge: 'Printer connectivity issues are a leading cause of office support tickets and downtime.',
@@ -85,6 +65,27 @@ const services = [
icon: 'print',
image: ''
},
{
id: 4,
title: 'Web Design',
description: 'Professional website design backed by domain registration and DNS support, so your online presence looks credible and works reliably.',
challenge: 'If your site looks dated, loads poorly, or your domain setup is messy, customers lose trust before they ever contact you.',
approach: 'We build clean business websites and handle the domain and DNS layer correctly, so your site, forms, and connected services stay dependable.',
deliverables: [
'Custom website design & development',
'Domain registration & management',
'DNS setup and support',
'SEO optimization basics',
'Hosting & maintenance'
],
needs: [
'Brand guidelines / Logo',
'Content & copy',
'Domain access (if existing)'
],
icon: 'language',
image: '/assets/services/managed-it.webp'
},
{
id: 5,
title: 'New/Refurbished Desktop Hardware',
@@ -107,26 +108,6 @@ const services = [
},
{
id: 6,
title: 'VPN Setup',
description: 'Configure Virtual Private Networks to allow secure remote access to your internal network from anywhere.',
challenge: 'Remote work requires secure access to internal resources without exposing your network to threats.',
approach: 'We implement robust VPN solutions like WireGuard or OpenVPN for secure, encrypted remote connectivity.',
deliverables: [
'VPN server configuration',
'Client software deployment',
'Access control lists',
'Connection testing',
'User guides'
],
needs: [
'Public IP / DNS details',
'User list'
],
icon: 'vpn_lock',
image: ''
},
{
id: 7,
title: 'Network Infrastructure Support',
description: 'Robust network solutions to ensure connectivity, security, and efficiency, including routers, access points, and switches.',
challenge: 'A weak network backbone leads to slow speeds, dropped calls, and security holes.',
@@ -146,23 +127,23 @@ const services = [
image: ''
},
{
id: 8,
title: 'Network Attached Storage',
description: 'Selection, setup, and maintenance of Network Attached Storage solutions to provide scalable and reliable data storage.',
challenge: 'Data growth requires scalable storage that is accessible yet secure from loss.',
approach: 'We deploy NAS solutions that centralize your data with redundancy and easy access for your team.',
id: 7,
title: 'IT Help Desk',
description: 'Fast and reliable help desk support for employees, resolving technical issues remotely or on-site.',
challenge: 'When employees do not have a clear support channel, small technical issues quickly turn into company-wide delays.',
approach: 'We provide a structured help desk workflow that resolves day-to-day IT issues quickly and keeps your team productive.',
deliverables: [
'NAS hardware selection & setup',
'RAID configuration',
'User permission management',
'Remote access configuration',
'Backup integration'
'Remote troubleshooting and issue resolution',
'User account and access support',
'Software and email assistance',
'Escalation path for on-site issues',
'Clear communication on ticket status'
],
needs: [
'Capacity requirements',
'Access patterns'
'Primary support contacts',
'User and device overview'
],
icon: 'storage',
icon: 'support_agent',
image: ''
}
];
@@ -312,6 +293,13 @@ const ServicesPage: React.FC = () => {
}, []);
return (
<>
<SEO
title="IT Services | Bay Area IT Support, Email, Networking and Web"
description="Explore Bay Area IT services for Corpus Christi businesses, including help desk support, business email, networking, hardware, web design, and day-to-day IT support."
keywords={['IT services Corpus Christi', 'help desk services', 'business email setup', 'network support']}
canonicalUrl="https://bayareait.services/services"
/>
<div className="pt-20 min-h-screen bg-[#0a0a0a] relative overflow-x-hidden">
{/* Gradient for Services Page */}
<div className="absolute top-0 left-0 right-0 h-[800px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.15),rgba(255,255,255,0))] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(255,255,255,0.25),rgba(255,255,255,0))] pointer-events-none" />
@@ -321,10 +309,10 @@ const ServicesPage: React.FC = () => {
<div className="max-w-4xl mx-auto text-center">
<span className="text-white/60 font-bold tracking-widest uppercase text-sm mb-3 block">Expertise</span>
<h1 className="font-display text-4xl md:text-5xl font-bold mb-6 text-white">
Complete IT solutions for <br /><span className="text-gray-500">your business</span>
Reliable business technology <br /><span className="text-gray-500">that keeps communication moving</span>
</h1>
<p className="text-xl text-gray-400 max-w-2xl mx-auto">
From desktop support to enterprise infrastructure, we provide the technology foundation your business needs to thrive.
From enterprise-grade email to day-to-day infrastructure and web support, we help businesses stay reachable, credible, and operational.
</p>
</div>
</section>
@@ -341,7 +329,7 @@ const ServicesPage: React.FC = () => {
className="absolute left-4 md:left-1/2 md:-ml-[0.5px] top-0 w-px bg-white origin-top shadow-[0_0_40px_2px_rgba(255,255,255,1)] drop-shadow-[0_0_10px_rgba(255,255,255,1)] -translate-x-1/2 md:translate-x-0"
></motion.div>
<div className="space-y-64 pb-64">
<div className="space-y-40 pb-40">
{services.map((service, index) => (
<div
key={service.id}
@@ -369,8 +357,8 @@ const ServicesPage: React.FC = () => {
{/* Content Card */}
<motion.div
initial={{ opacity: 0, scale: 0.9, filter: "blur(10px)" }}
whileInView={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.5, ease: "easeOut" }}
className={`md:w-1/2 ${index % 2 === 0 ? 'md:pr-24 pl-12' : 'md:pl-24 pl-12'}`}
@@ -409,6 +397,7 @@ const ServicesPage: React.FC = () => {
)}
</AnimatePresence>
</div>
</>
);
};

View File

@@ -0,0 +1,69 @@
import React from 'react';
import LegalPage from './LegalPage';
const sections = [
{
title: 'Service Description',
body: [
'Bay Area Affiliates, Inc. provides comprehensive IT support services, including hardware and software support, network infrastructure, server repair, remote support, web services, virtualization solutions, and more.',
],
},
{
title: 'User Responsibilities',
body: [
'Users are responsible for maintaining the confidentiality of their account information and for all activities that occur under their account.',
'Users agree to provide accurate and complete information when using our services.',
],
},
{
title: 'Payment Terms',
body: [
'All services rendered by Bay Area Affiliates, Inc. are subject to payment terms agreed upon in the service contract. Late payments may incur additional fees.',
],
},
{
title: 'Limitation of Liability',
body: [
'Bay Area Affiliates, Inc. shall not be liable for any indirect, incidental, or consequential damages arising from the use of our services.',
'Our total liability is limited to the amount paid for the services rendered.',
],
},
{
title: 'Termination',
body: [
'Either party may terminate the service agreement at any time with written notice. Upon termination, users must cease using all services provided by Bay Area Affiliates, Inc.',
],
},
{
title: 'Governing Law',
body: [
'These Terms of Service are governed by and construed in accordance with the laws of the State of Texas, without regard to its conflict of law principles.',
],
},
{
title: 'Changes to Terms',
body: [
'Bay Area Affiliates, Inc. reserves the right to modify these Terms of Service at any time. Any changes will be effective immediately upon posting on our website.',
'Continued use of our services constitutes acceptance of the updated terms.',
],
},
{
title: 'Contact Us',
body: [
'If you have any questions about these Terms of Service, please contact us at info@bayareaaffiliates.com.',
],
},
];
const TermsOfServicePage: React.FC = () => (
<LegalPage
title="Terms of Service"
description="Review the Bay Area Affiliates, Inc. Terms of Service for the use of our website and IT support services."
canonicalUrl="https://bayareait.services/terms-of-service"
eyebrow="Bay Area Affiliates, Inc."
intro="By accessing our website and using our IT support services, you agree to comply with and be bound by the following Terms of Service."
sections={sections}
/>
);
export default TermsOfServicePage;

154
src/routes/seoRoutes.tsx Normal file
View File

@@ -0,0 +1,154 @@
import React, { lazy, useEffect, useState } from 'react';
import { Navigate } from 'react-router-dom';
import type { LocationData, ServiceData, BlogPostData } from '../data/seoData';
const LazyLocationPage = lazy(() => import('../pages/LocationPage'));
const LazyServicePage = lazy(() => import('../pages/ServicePage'));
const LazyBlogPostPage = lazy(() => import('../pages/BlogPostPage'));
type SeoRouteState<T> = {
data: T | null;
error: string | null;
};
const routeFallback = (
<div className="min-h-[40vh] flex items-center justify-center px-6 text-sm text-gray-500 dark:text-gray-400">
Loading page...
</div>
);
function createSeoRoute<T extends { slug: string }>(
loadData: () => Promise<{
locationData?: LocationData[];
serviceData?: ServiceData[];
blogPostData?: BlogPostData[];
}>,
selectData: (module: Awaited<ReturnType<typeof loadData>>, slug: string) => T | undefined,
Page: React.LazyExoticComponent<React.ComponentType<{ data: T }>>,
) {
return function SeoRoute({ slug }: { slug: string }) {
const [state, setState] = useState<SeoRouteState<T>>({
data: null,
error: null,
});
useEffect(() => {
let active = true;
setState({ data: null, error: null });
loadData()
.then((module) => {
if (!active) return;
const nextData = selectData(module, slug);
setState({
data: nextData ?? null,
error: nextData ? null : `No SEO data found for ${slug}`,
});
})
.catch((error: unknown) => {
if (!active) return;
setState({
data: null,
error: error instanceof Error ? error.message : 'Failed to load route data',
});
});
return () => {
active = false;
};
}, [slug]);
if (state.error) {
return (
<div className="min-h-[40vh] flex items-center justify-center px-6 text-center">
<div>
<p className="text-sm uppercase tracking-widest text-gray-500 dark:text-gray-400 mb-2">
Route unavailable
</p>
<p className="text-lg text-gray-900 dark:text-white">{state.error}</p>
</div>
</div>
);
}
if (!state.data) {
return routeFallback;
}
return <Page data={state.data} />;
};
}
const loadSeoData = () => import('../data/seoData');
// Location pages — city IT support pages under /locations/
export const locationRoutes = [
{ path: '/locations/it-support-corpus-christi', slug: 'locations/it-support-corpus-christi' },
{ path: '/locations/it-support-portland-tx', slug: 'locations/it-support-portland-tx' },
{ path: '/locations/it-support-rockport-tx', slug: 'locations/it-support-rockport-tx' },
{ path: '/locations/it-support-aransas-pass-tx', slug: 'locations/it-support-aransas-pass-tx' },
{ path: '/locations/it-support-kingsville-tx', slug: 'locations/it-support-kingsville-tx' },
// Service+location hybrid pages (stored in locationData, rendered via LocationPage)
{ path: '/services/web-design-corpus-christi', slug: 'services/web-design-corpus-christi' },
{ path: '/services/business-email-corpus-christi', slug: 'services/business-email-corpus-christi' },
];
// Service pages — all under /services/
export const serviceRoutes = [
{ path: '/services/it-help-desk', slug: 'services/it-help-desk' },
{ path: '/services/computer-support', slug: 'services/computer-support' },
{ path: '/services/business-email-services', slug: 'services/business-email-services' },
{ path: '/services/domain-registration-dns-support', slug: 'services/domain-registration-dns-support' },
];
// Authority blog posts only — location posts redirect to their location pages
export const blogRoutes = [
{ path: '/blog/it-support-small-business-corpus-christi', slug: 'blog/it-support-small-business-corpus-christi' },
{ path: '/blog/outsourced-it-support-corpus-christi', slug: 'blog/outsourced-it-support-corpus-christi' },
{ path: '/blog/it-service-vs-inhouse-it', slug: 'blog/it-service-vs-inhouse-it' },
{ path: '/blog/common-it-problems-businesses-corpus-christi', slug: 'blog/common-it-problems-businesses-corpus-christi' },
{ path: '/blog/it-support-cost-corpus-christi', slug: 'blog/it-support-cost-corpus-christi' },
{ path: '/blog/business-email-vs-google-workspace-vs-microsoft-365', slug: 'blog/business-email-vs-google-workspace-vs-microsoft-365' },
];
// Old flat-URL redirects — 301-equivalent for any links pointing to old paths
export const legacyRedirects = [
{ from: '/it-support-corpus-christi', to: '/locations/it-support-corpus-christi' },
{ from: '/it-support-portland-tx', to: '/locations/it-support-portland-tx' },
{ from: '/it-support-rockport-tx', to: '/locations/it-support-rockport-tx' },
{ from: '/it-support-aransas-pass-tx', to: '/locations/it-support-aransas-pass-tx' },
{ from: '/it-support-kingsville-tx', to: '/locations/it-support-kingsville-tx' },
{ from: '/it-help-desk', to: '/services/it-help-desk' },
{ from: '/computer-support', to: '/services/computer-support' },
{ from: '/business-email-services', to: '/services/business-email-services' },
{ from: '/domain-registration-dns-support', to: '/services/domain-registration-dns-support' },
{ from: '/web-design-corpus-christi', to: '/services/web-design-corpus-christi' },
{ from: '/business-email-corpus-christi', to: '/services/business-email-corpus-christi' },
// Location blog posts → their canonical location pages
{ from: '/blog/it-support-corpus-christi-blog', to: '/locations/it-support-corpus-christi' },
{ from: '/blog/it-support-portland-tx-blog', to: '/locations/it-support-portland-tx' },
{ from: '/blog/it-support-rockport-tx-blog', to: '/locations/it-support-rockport-tx' },
{ from: '/blog/it-support-aransas-pass-blog', to: '/locations/it-support-aransas-pass-tx' },
{ from: '/blog/it-support-kingsville-tx-blog', to: '/locations/it-support-kingsville-tx' },
];
export const LocationSeoRoute = createSeoRoute<LocationData>(
loadSeoData,
(module, slug) => module.locationData.find((item) => item.slug === slug),
LazyLocationPage,
);
export const ServiceSeoRoute = createSeoRoute<ServiceData>(
loadSeoData,
(module, slug) => module.serviceData.find((item) => item.slug === slug),
LazyServicePage,
);
export const BlogSeoRoute = createSeoRoute<BlogPostData>(
loadSeoData,
(module, slug) => module.blogPostData.find((item) => item.slug === slug),
LazyBlogPostPage,
);

38
tailwind.config.cjs Normal file
View File

@@ -0,0 +1,38 @@
const forms = require('@tailwindcss/forms');
const typography = require('@tailwindcss/typography');
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./index.html',
'./App.tsx',
'./components/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
darkMode: 'class',
theme: {
extend: {
colors: {
primary: '#FFFFFF',
'background-light': '#F3F4F6',
'background-dark': '#0a0a0a',
'surface-dark': '#121212',
'text-muted': '#888888',
},
fontFamily: {
display: ['"Space Grotesk"', 'sans-serif'],
sans: ['"Inter"', 'sans-serif'],
},
borderRadius: {
DEFAULT: '4px',
lg: '8px',
xl: '12px',
},
backgroundImage: {
grain:
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.05'/%3E%3C/svg%3E\")",
},
},
},
plugins: [forms, typography],
};

View File

@@ -1,9 +1,8 @@
import path from 'path';
import { defineConfig, loadEnv } from 'vite';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, '.', '');
export default defineConfig(() => {
return {
server: {
port: 3012,
@@ -16,10 +15,6 @@ export default defineConfig(({ mode }) => {
},
},
plugins: [react()],
define: {
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),