Complete SEO overhaul

This commit is contained in:
Timo Knuth 2026-01-22 15:17:20 +01:00
parent e5c5503e08
commit 42e0971a13
37 changed files with 4319 additions and 1217 deletions

31
App.tsx
View File

@ -12,6 +12,10 @@ import AboutPage from './src/pages/AboutPage';
import ServicesPage from './src/pages/ServicesPage'; import ServicesPage from './src/pages/ServicesPage';
import BlogPage from './src/pages/BlogPage'; import BlogPage from './src/pages/BlogPage';
import ContactPage from './src/pages/ContactPage'; 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';
// Register GSAP plugins globally // Register GSAP plugins globally
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin); gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
@ -69,6 +73,33 @@ const AppContent: React.FC = () => {
<Route path="/services" element={<ServicesPage />} /> <Route path="/services" element={<ServicesPage />} />
<Route path="/blog" element={<BlogPage />} /> <Route path="/blog" element={<BlogPage />} />
<Route path="/contact" element={<ContactPage />} /> <Route path="/contact" element={<ContactPage />} />
{/* SEO Location Pages */}
{locationData.map((data) => (
<Route
key={data.slug}
path={`/${data.slug}`}
element={<LocationPage data={data} />}
/>
))}
{/* SEO Service Pages */}
{serviceData.map((data) => (
<Route
key={data.slug}
path={`/${data.slug}`}
element={<ServicePage data={data} />}
/>
))}
{/* Authority Blog Posts */}
{blogPostData.map((data) => (
<Route
key={data.slug}
path={`/${data.slug}`}
element={<BlogPostPage data={data} />}
/>
))}
</Routes> </Routes>
</main> </main>
<Footer /> <Footer />

View File

@ -0,0 +1,43 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { locationData } from '../src/data/seoData';
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">
<div className="max-w-4xl mx-auto">
<h2 className="font-display text-3xl md:text-4xl font-bold mb-6 text-gray-900 dark:text-white">
Areas We Serve Local IT Support Across the Coastal Bend
</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.
</p>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-12">
{locationData.map((loc) => (
<Link
key={loc.slug}
to={`/${loc.slug}`}
className="p-4 bg-white dark:bg-white/10 rounded-lg shadow-sm hover:shadow-md transition-all text-gray-800 dark:text-gray-200 font-medium hover:text-black dark:hover:text-white"
>
{loc.city}
</Link>
))}
</div>
<p className="text-lg text-gray-600 dark:text-gray-400">
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>
</div>
</div>
</section>
);
};
export default AreasWeServe;

65
components/FAQ.tsx Normal file
View File

@ -0,0 +1,65 @@
import React from 'react';
import { motion } from 'framer-motion';
import { FAQItem } from '../src/data/seoData';
interface FAQProps {
items: FAQItem[];
}
const FAQ: React.FC<FAQProps> = ({ items }) => {
if (!items || items.length === 0) return null;
return (
<section className="py-24 px-6 bg-white dark:bg-black">
<div className="max-w-3xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="font-display text-3xl md:text-4xl font-bold mb-4 text-gray-900 dark:text-white">
Frequently Asked Questions
</h2>
<p className="text-gray-600 dark:text-gray-400">
Common questions about our IT services.
</p>
</motion.div>
<div className="space-y-6">
{items.map((faq, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
className="p-6 rounded-2xl bg-gray-50 dark:bg-white/5 border border-gray-100 dark:border-white/10"
>
<h3 className="font-bold text-lg mb-2 text-gray-900 dark:text-white">{faq.question}</h3>
<p className="text-gray-600 dark:text-gray-300">{faq.answer}</p>
</motion.div>
))}
</div>
</div>
{/* JSON-LD for FAQ Page */}
<script type="application/ld+json" dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": items.map(faq => ({
"@type": "Question",
"name": faq.question,
"acceptedAnswer": {
"@type": "Answer",
"text": faq.answer
}
}))
})
}} />
</section>
);
};
export default FAQ;

68
components/SEO.tsx Normal file
View File

@ -0,0 +1,68 @@
import React, { useEffect } from 'react';
interface SEOProps {
title: string;
description: string;
keywords?: string[];
canonicalUrl?: string;
schema?: object; // JSON-LD schema
}
const SEO: React.FC<SEOProps> = ({ title, description, keywords, canonicalUrl, schema }) => {
useEffect(() => {
// Update Title
document.title = title;
// Helper to set meta tag
const setMetaTag = (name: string, content: string) => {
let element = document.querySelector(`meta[name="${name}"]`);
if (!element) {
element = document.createElement('meta');
element.setAttribute('name', name);
document.head.appendChild(element);
}
element.setAttribute('content', content);
};
// Update Meta Description
setMetaTag('description', description);
// Update Keywords
if (keywords && keywords.length > 0) {
setMetaTag('keywords', keywords.join(', '));
}
// Update Canonical
if (canonicalUrl) {
let link = document.querySelector('link[rel="canonical"]');
if (!link) {
link = document.createElement('link');
link.setAttribute('rel', 'canonical');
document.head.appendChild(link);
}
link.setAttribute('href', canonicalUrl);
}
// Inject Schema
if (schema) {
const scriptId = 'seo-schema-script';
let script = document.getElementById(scriptId);
if (!script) {
script = document.createElement('script');
script.id = scriptId;
script.setAttribute('type', 'application/ld+json');
document.head.appendChild(script);
}
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]);
return null;
};
export default SEO;

View File

@ -1,4 +1,4 @@
import React, { useState, useRef, useLayoutEffect } from 'react'; import React, { useState, useRef, useLayoutEffect, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import gsap from 'gsap'; import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger'; import { ScrollTrigger } from 'gsap/ScrollTrigger';
@ -69,45 +69,68 @@ const servicesData = [
description: 'Selection, setup, and maintenance of Network Attached Storage solutions to provide scalable and reliable data storage.', description: 'Selection, setup, and maintenance of Network Attached Storage solutions to provide scalable and reliable data storage.',
icon: 'storage', icon: 'storage',
image: '/assets/services/nas-storage.png' image: '/assets/services/nas-storage.png'
},
{
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,
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'
} }
]; ];
const categories = ['All', 'IT Infrastructure', 'Web Services', 'Security', 'Networking']; const categories = ['All', 'IT Infrastructure', 'Web Services', 'Security', 'Networking'];
const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => { interface ServicesProps {
preview?: boolean;
featuredIds?: number[];
}
const Services: React.FC<ServicesProps> = ({ preview = false, featuredIds }) => {
const [activeCategory, setActiveCategory] = useState('All'); const [activeCategory, setActiveCategory] = useState('All');
const [showAll, setShowAll] = useState(false);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const imagesRef = useRef<(HTMLDivElement | null)[]>([]);
// Reset refs on render to handle filtering updates // Determine if we should be in "preview mode" (showing only a subset)
imagesRef.current = []; // This applies if preview is true OR if featuredIds are provided and we haven't clicked "Show More"
const isRestrictedView = (preview || featuredIds) && !showAll;
const filteredServices = activeCategory === 'All' // 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
: servicesData.filter(s => s.category === activeCategory || (activeCategory === 'Web Development' && s.category === 'Security')); : servicesData.filter(s => s.category === activeCategory || (activeCategory === 'Web Development' && s.category === 'Security'));
const displayedServices = preview ? servicesData.slice(0, 3) : filteredServices; const displayedServices = useMemo(() => {
if (isRestrictedView) {
useLayoutEffect(() => { if (featuredIds && featuredIds.length > 0) {
const ctx = gsap.context(() => { // Sort the services to match the order of featuredIds
imagesRef.current.forEach((imgWrapper) => { return featuredIds
if (!imgWrapper) return; .map(id => servicesData.find(s => s.id === id))
.filter((s): s is typeof servicesData[0] => s !== undefined);
gsap.to(imgWrapper, { }
yPercent: 30, // Fallback to first 3 if no IDs but preview is true
ease: "none", return servicesData.slice(0, 3);
scrollTrigger: { }
trigger: imgWrapper.closest('.group'), // Show all (filtered by category)
start: "top bottom", return filteredByCategory;
end: "bottom top", }, [isRestrictedView, featuredIds, filteredByCategory]);
scrub: true
}
});
});
}, containerRef);
return () => ctx.revert();
}, [filteredServices]);
return ( return (
<motion.section <motion.section
@ -127,32 +150,35 @@ const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => {
</h2> </h2>
</div> </div>
<div className="flex gap-6 mb-12 border-b border-gray-200 dark:border-white/10 text-sm font-medium overflow-x-auto pb-2 no-scrollbar"> {/* Categories - Hide in restricted view to keep it clean, or keep it? User said "mach nur das 3 services angezeigt werden". usually categories are for the full list. */}
{categories.map((cat) => ( {!isRestrictedView && (
<button <div className="flex gap-6 mb-12 border-b border-gray-200 dark:border-white/10 text-sm font-medium overflow-x-auto pb-2 no-scrollbar">
key={cat} {categories.map((cat) => (
onClick={() => setActiveCategory(cat)} <button
className={`pb-2 whitespace-nowrap transition-colors relative ${activeCategory === cat key={cat}
? 'text-gray-900 dark:text-white' onClick={() => setActiveCategory(cat)}
: 'text-gray-500 dark:text-gray-500 hover:text-gray-800 dark:hover:text-gray-300' className={`pb-2 whitespace-nowrap transition-colors relative ${activeCategory === cat
}`} ? 'text-gray-900 dark:text-white'
> : 'text-gray-500 dark:text-gray-500 hover:text-gray-800 dark:hover:text-gray-300'
{cat} }`}
{activeCategory === cat && ( >
<motion.div {cat}
layoutId="activeTab" {activeCategory === cat && (
className="absolute bottom-0 left-0 right-0 h-0.5 bg-black dark:bg-white" <motion.div
/> layoutId="activeTab"
)} className="absolute bottom-0 left-0 right-0 h-0.5 bg-black dark:bg-white"
</button> />
))} )}
</div> </button>
))}
</div>
)}
<div <div
className="grid grid-cols-1 md:grid-cols-3 gap-6" className="grid grid-cols-1 md:grid-cols-3 gap-6"
> >
<AnimatePresence mode="popLayout"> <AnimatePresence mode="popLayout">
{filteredServices.map((service, index) => ( {displayedServices.map((service) => (
<motion.div <motion.div
key={service.id} key={service.id}
layout layout
@ -164,34 +190,28 @@ const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => {
className="group relative bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl overflow-hidden hover:border-gray-300 dark:hover:border-white/30 hover:shadow-2xl transition-all duration-300" className="group relative bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl overflow-hidden hover:border-gray-300 dark:hover:border-white/30 hover:shadow-2xl transition-all duration-300"
> >
{/* Image Container */} {/* Image Container */}
<div className="h-64 bg-gray-200 dark:bg-black/40 overflow-hidden relative"> <div className="h-40 bg-gray-200 dark:bg-black/40 overflow-hidden relative">
{/* Parallax Wrapper */} <img
<div src={service.image}
ref={el => { if (el) imagesRef.current.push(el); }} alt={service.title}
className="w-full h-[140%] -mt-[20%]" className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110 opacity-100"
> />
<img
src={service.image}
alt={service.title}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110 opacity-100"
/>
</div>
<div className="absolute inset-0 bg-gradient-to-t from-gray-50 dark:from-[#161616] to-transparent pointer-events-none"></div> <div className="absolute inset-0 bg-gradient-to-t from-gray-50 dark:from-[#161616] to-transparent pointer-events-none"></div>
</div> </div>
<div className="p-6 relative"> <div className="p-4 relative">
<motion.div <motion.div
className="w-10 h-10 rounded-full bg-white dark:bg-white/10 flex items-center justify-center mb-4 border border-gray-200 dark:border-white/10" className="w-8 h-8 rounded-full bg-white dark:bg-white/10 flex items-center justify-center mb-3 border border-gray-200 dark:border-white/10"
whileHover={{ rotate: 360, backgroundColor: "#171717", color: "#ffffff", borderColor: "#171717" }} whileHover={{ rotate: 360, backgroundColor: "#171717", color: "#ffffff", borderColor: "#171717" }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
> >
<span className="material-symbols-outlined text-sm text-gray-900 dark:text-white group-hover:text-white">{service.icon}</span> <span className="material-symbols-outlined text-sm text-gray-900 dark:text-white group-hover:text-white">{service.icon}</span>
</motion.div> </motion.div>
<h3 className="font-display text-xl font-bold text-gray-900 dark:text-white mb-2">{service.title}</h3> <h3 className="font-display text-lg font-bold text-gray-900 dark:text-white mb-2">{service.title}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed mb-4"> <p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed mb-3">
{service.description} {service.description}
</p> </p>
<a 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"> <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">
Learn More <motion.span Learn More <motion.span
className="material-symbols-outlined text-xs ml-1" className="material-symbols-outlined text-xs ml-1"
animate={{ x: [0, 5, 0] }} animate={{ x: [0, 5, 0] }}
@ -204,16 +224,18 @@ const Services: React.FC<{ preview?: boolean }> = ({ preview = false }) => {
</AnimatePresence> </AnimatePresence>
</div> </div>
{preview && ( {isRestrictedView && (
<div className="mt-12 text-center"> <div className="mt-12 text-center">
<a <button
href="/services" onClick={() => setShowAll(true)}
className="inline-flex items-center gap-2 px-8 py-3 bg-black dark:bg-white text-white dark:text-black rounded-full font-medium hover:bg-gray-800 dark:hover:bg-gray-200 transition-colors" className="inline-flex items-center gap-2 px-8 py-3 bg-black dark:bg-white text-white dark:text-black rounded-full font-medium hover:bg-gray-800 dark:hover:bg-gray-200 transition-colors"
> >
View all services <span className="material-symbols-outlined text-sm">arrow_forward</span> Show More Services <span className="material-symbols-outlined text-sm">expand_more</span>
</a> </button>
</div> </div>
)} )}
{/* If we are showing all and originally had a restricted view, maybe show a "Show Less" but user didn't ask for it. The user said "then all are shown". */}
</div> </div>
</motion.section> </motion.section>
); );

View File

@ -65,6 +65,22 @@
.lenis.lenis-scrolling iframe { .lenis.lenis-scrolling iframe {
pointer-events: none; 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> </style>
<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"> <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> <div id="root"></div>

528
package-lock.json generated
View File

@ -18,6 +18,7 @@
"devDependencies": { "devDependencies": {
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react": "^5.0.0",
"tsx": "^4.21.0",
"typescript": "~5.8.2", "typescript": "~5.8.2",
"vite": "^6.2.0" "vite": "^6.2.0"
} }
@ -1475,6 +1476,19 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/get-tsconfig": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/gsap": { "node_modules/gsap": {
"version": "3.14.2", "version": "3.14.2",
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz", "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz",
@ -1690,6 +1704,16 @@
"react-dom": ">=18" "react-dom": ">=18"
} }
}, },
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.55.1", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
@ -1790,6 +1814,510 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/android-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/esbuild": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.2",
"@esbuild/android-arm": "0.27.2",
"@esbuild/android-arm64": "0.27.2",
"@esbuild/android-x64": "0.27.2",
"@esbuild/darwin-arm64": "0.27.2",
"@esbuild/darwin-x64": "0.27.2",
"@esbuild/freebsd-arm64": "0.27.2",
"@esbuild/freebsd-x64": "0.27.2",
"@esbuild/linux-arm": "0.27.2",
"@esbuild/linux-arm64": "0.27.2",
"@esbuild/linux-ia32": "0.27.2",
"@esbuild/linux-loong64": "0.27.2",
"@esbuild/linux-mips64el": "0.27.2",
"@esbuild/linux-ppc64": "0.27.2",
"@esbuild/linux-riscv64": "0.27.2",
"@esbuild/linux-s390x": "0.27.2",
"@esbuild/linux-x64": "0.27.2",
"@esbuild/netbsd-arm64": "0.27.2",
"@esbuild/netbsd-x64": "0.27.2",
"@esbuild/openbsd-arm64": "0.27.2",
"@esbuild/openbsd-x64": "0.27.2",
"@esbuild/openharmony-arm64": "0.27.2",
"@esbuild/sunos-x64": "0.27.2",
"@esbuild/win32-arm64": "0.27.2",
"@esbuild/win32-ia32": "0.27.2",
"@esbuild/win32-x64": "0.27.2"
}
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.8.3", "version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",

View File

@ -6,7 +6,8 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview",
"generate:seo": "npx tsx scripts/generate-sitemap.ts && npx tsx scripts/generate-robots.ts"
}, },
"dependencies": { "dependencies": {
"@studio-freight/lenis": "^1.0.42", "@studio-freight/lenis": "^1.0.42",
@ -19,6 +20,7 @@
"devDependencies": { "devDependencies": {
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react": "^5.0.0",
"tsx": "^4.21.0",
"typescript": "~5.8.2", "typescript": "~5.8.2",
"vite": "^6.2.0" "vite": "^6.2.0"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 KiB

6
public/robots.txt Normal file
View File

@ -0,0 +1,6 @@
User-agent: *
Allow: /
Disallow: /admin
Disallow: /api
Sitemap: https://bayareait.services/sitemap.xml

147
public/sitemap.xml Normal file
View File

@ -0,0 +1,147 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://bayareait.services</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://bayareait.services/services</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://bayareait.services/blog</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://bayareait.services/contact</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://bayareait.services/about</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://bayareait.services/it-support-corpus-christi</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/it-support-portland-tx</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/it-support-rockport-tx</loc>
<lastmod>2026-01-22</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>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/it-support-kingsville-tx</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/business-it-support</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/it-help-desk</loc>
<lastmod>2026-01-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://bayareait.services/computer-support</loc>
<lastmod>2026-01-22</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>
<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>
<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>
<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>
<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>
<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>
<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>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
</urlset>

View File

@ -0,0 +1,28 @@
import fs from 'fs';
import path from 'path';
const BASE_URL = process.env.BASE_URL || 'https://bayareait.services';
const generateRobots = () => {
const content = `User-agent: *
Allow: /
Disallow: /admin
Disallow: /api
Sitemap: ${BASE_URL}/sitemap.xml
`;
return content;
};
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}`);

View File

@ -0,0 +1,85 @@
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];
let xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
`;
// Static Pages
const staticPages = [
'',
'/services',
'/blog',
'/contact',
'/about'
];
staticPages.forEach(page => {
xml += ` <url>
<loc>${BASE_URL}${page}</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>monthly</changefreq>
<priority>${page === '' ? '1.0' : '0.8'}</priority>
</url>
`;
});
// Location Pages
locationData.forEach(page => {
xml += ` <url>
<loc>${BASE_URL}/${page.slug}</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
`;
});
// Service Pages
serviceData.forEach(page => {
xml += ` <url>
<loc>${BASE_URL}/${page.slug}</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
`;
});
// Blog Posts
blogPostData.forEach(post => {
xml += ` <url>
<loc>${BASE_URL}/blog/${post.slug}</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
`;
});
xml += `</urlset>`;
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}`);

1251
src/data/seoData.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,36 +1,8 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
import Contact from '../../components/Contact'; import Contact from '../../components/Contact';
import { blogPostData } from '../data/seoData';
const posts = [
{
id: 1,
title: 'Upgrade your HDD to SSD for a big speed boost',
excerpt: 'A practical checklist for Corpus Christi business owners considering SSD upgrades, including before/after performance comparisons and cost analysis.',
image: '/assets/services/desktop-hardware.png',
category: 'Hardware',
readTime: '5 min read',
date: 'Jan 15, 2026'
},
{
id: 2,
title: 'Secure your corporate network access with WireGuard VPN',
excerpt: 'Learn why Corpus Christi businesses are switching to WireGuard VPN for faster, more secure remote access, and how to implement it properly in the Coastal Bend.',
image: '/assets/services/vpn-setup.png',
category: 'Security',
readTime: '7 min read',
date: 'Jan 10, 2026'
},
{
id: 3,
title: 'What comprehensive IT support looks like for SMBs',
excerpt: 'Understanding the full scope of managed IT services for Corpus Christi small businesses: from hardware and network infrastructure to virtualization and helpdesk support.',
image: '/assets/services/network-infrastructure.png',
category: 'Strategy',
readTime: '6 min read',
date: 'Jan 05, 2026'
}
];
const BlogPage: React.FC = () => { const BlogPage: React.FC = () => {
useEffect(() => { useEffect(() => {
@ -52,46 +24,51 @@ 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))]"> <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"> <div className="max-w-5xl mx-auto space-y-16">
{posts.map((post) => ( {blogPostData.map((post) => (
<motion.div <Link
key={post.id} key={post.id}
initial={{ opacity: 0, y: 20 }} to={`/${post.slug}`}
whileInView={{ opacity: 1, y: 0 }} className="block"
viewport={{ once: true, margin: "-100px" }}
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"
> >
<div className="h-64 md:h-auto overflow-hidden relative"> <motion.div
<img initial={{ opacity: 0, y: 20 }}
src={post.image} whileInView={{ opacity: 1, y: 0 }}
alt={post.title} viewport={{ once: true, margin: "-100px" }}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105" 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"
<div className="absolute top-4 left-4"> >
<span className="px-3 py-1 bg-black/70 backdrop-blur-md text-white text-xs font-bold rounded-full border border-white/20"> <div className="h-64 md:h-auto overflow-hidden relative">
{post.category} <img
</span> src={post.image || '/images/blog/default.png'}
alt={post.h1}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className="absolute top-4 left-4">
<span className="px-3 py-1 bg-black/70 backdrop-blur-md text-white text-xs font-bold rounded-full border border-white/20">
{post.category === 'authority' ? 'IT Insights' : 'Local Services'}
</span>
</div>
</div> </div>
</div> <div className="p-8 md:p-12 flex flex-col justify-center">
<div className="p-8 md:p-12 flex flex-col justify-center"> <div className="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400 mb-4">
<div className="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400 mb-4"> <span>Jan 2026</span>
<span>{post.date}</span> <span className="w-1 h-1 rounded-full bg-gray-300 dark:bg-gray-600"></span>
<span className="w-1 h-1 rounded-full bg-gray-300 dark:bg-gray-600"></span> <span>8 min read</span>
<span>{post.readTime}</span> </div>
<h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{post.h1}
</h2>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed mb-8">
{post.description}
</p>
<div className="mt-auto">
<span className="inline-flex items-center gap-2 font-bold text-blue-600 dark:text-white group-hover:gap-3 transition-all">
Read article <span className="material-symbols-outlined text-sm">arrow_forward</span>
</span>
</div>
</div> </div>
<h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors"> </motion.div>
{post.title} </Link>
</h2>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed mb-8">
{post.excerpt}
</p>
<div className="mt-auto">
<span className="inline-flex items-center gap-2 font-bold text-blue-600 dark:text-white group-hover:gap-3 transition-all">
Read article <span className="material-symbols-outlined text-sm">arrow_forward</span>
</span>
</div>
</div>
</motion.div>
))} ))}
</div> </div>
</section> </section>

322
src/pages/BlogPostPage.tsx Normal file
View File

@ -0,0 +1,322 @@
import React, { useEffect, useRef, useLayoutEffect } from 'react';
import { motion, useMotionTemplate, useMotionValue } 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);
}, []);
const category = data.slug.includes('corpus-christi-blog') ||
data.slug.includes('portland-tx') ||
data.slug.includes('rockport-tx') ||
data.slug.includes('aransas-pass') ||
data.slug.includes('kingsville-tx')
? 'Local Services'
: 'IT Insights';
return (
<>
<SEO
title={data.title}
description={data.description}
keywords={data.keywords}
canonicalUrl={window.location.href}
/>
<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">
{/* 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">
<li>
<Link to="/" className="hover:text-gray-900 dark:hover:text-white 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">
Blog
</Link>
</li>
<span className="material-symbols-outlined text-xs">chevron_right</span>
<li className="text-gray-900 dark:text-white font-medium">{category}</li>
</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">
{category}
</span>
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></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">
{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-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>
</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"
>
<img
src={data.image}
alt={data.h1}
className="w-full h-auto max-h-64 object-cover"
/>
</motion.div>
)}
<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>
</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>
{/* CTA Section */}
<section className="px-6 py-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
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">
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-6 block">
{category === 'Local Services' ? 'location_on' : 'insights'}
</span>
<h2 className="font-display text-3xl font-bold mb-4 text-gray-900 dark:text-white">
{category === 'Local Services'
? '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">
{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"
>
Get Started
<span className="material-symbols-outlined">arrow_forward</span>
</Link>
</div>
</motion.div>
</section>
{/* Related Content Grid */}
<section className="px-6 py-16">
<div className="max-w-6xl mx-auto">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="font-display text-4xl font-bold mb-12 text-center text-gray-900 dark:text-white"
>
Why Choose Bay Area IT?
</motion.h2>
<div className="grid md:grid-cols-3 gap-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
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"
>
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
verified_user
</span>
<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">
Years of experience serving businesses across the Coastal Bend with comprehensive IT solutions.
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
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"
>
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
support_agent
</span>
<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>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
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"
>
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
handshake
</span>
<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">
A trusted local partner who understands your community and business needs.
</p>
</motion.div>
</div>
</div>
</section>
{/* Services Section */}
<Services preview={true} />
{/* Areas We Serve & CTA */}
<AreasWeServe />
<CTA />
</div>
</>
);
};
export default BlogPostPage;

View File

@ -6,20 +6,84 @@ import Process from '../../components/Process';
import Blog from '../../components/Blog'; import Blog from '../../components/Blog';
import Testimonials from '../../components/Testimonials'; import Testimonials from '../../components/Testimonials';
import CTA from '../../components/CTA'; import CTA from '../../components/CTA';
import SEO from '../../components/SEO';
import FAQ from '../../components/FAQ';
import AreasWeServe from '../../components/AreasWeServe';
import { locationData } from '../data/seoData';
const HomePage: React.FC = () => { const HomePage: React.FC = () => {
useEffect(() => { useEffect(() => {
window.scrollTo(0, 0); window.scrollTo(0, 0);
}, []); }, []);
// 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
"priceRange": "$$",
"address": {
"@type": "PostalAddress",
"streetAddress": "[YOUR STREET]", // TODO: Add actual address
"addressLocality": "Corpus Christi",
"addressRegion": "TX",
"postalCode": "[YOUR ZIP]", // TODO: Add actual ZIP
"addressCountry": "US"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 27.800583,
"longitude": -97.39638
},
"areaServed": [
{ "@type": "City", "name": "Corpus Christi" },
{ "@type": "City", "name": "Portland" },
{ "@type": "City", "name": "Rockport" },
{ "@type": "City", "name": "Aransas Pass" },
{ "@type": "City", "name": "Kingsville" }
],
"serviceType": [
"IT Support",
"Business IT Support",
"Outsourced IT Services",
"Computer Network Support",
"Cyber Security"
],
"openingHoursSpecification": {
"@type": "OpeningHoursSpecification",
"dayOfWeek": [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday"
],
"opens": "08:00",
"closes": "18:00"
}
};
return ( return (
<> <>
<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}
schema={schema}
/>
<Hero /> <Hero />
<Mission /> <Mission />
<Services preview={true} /> <Services preview={true} />
<Process /> <Process />
<Blog /> <Blog />
<Testimonials /> <Testimonials />
<AreasWeServe />
<FAQ items={locationData[0].faq} />
<CTA /> <CTA />
</> </>
); );

214
src/pages/LocationPage.tsx Normal file
View File

@ -0,0 +1,214 @@
import React, { useEffect, useRef, useLayoutEffect } from 'react';
import { motion, useMotionTemplate, useMotionValue } 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 FAQ from '../../components/FAQ';
import AreasWeServe from '../../components/AreasWeServe';
import { LocationData } from '../data/seoData';
gsap.registerPlugin(ScrollTrigger);
interface LocationPageProps {
data: LocationData;
}
const LocationPage: React.FC<LocationPageProps> = ({ 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);
}, []);
const schema = {
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": "Bay Area IT Services",
"url": window.location.href,
"areaServed": {
"@type": "City",
"name": data.city
}
};
return (
<>
<SEO
title={data.title}
description={data.description}
keywords={data.keywords}
canonicalUrl={window.location.href}
schema={schema}
/>
<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-[90vh] 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="hero-stagger flex items-center justify-center gap-2 mb-4">
<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">
{data.city}, Texas IT Support
</span>
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
</div>
<h1 className="hero-stagger font-display text-4xl md:text-5xl lg:text-6xl font-medium tracking-tighter leading-[1.1] mb-5 text-gray-900 dark:text-white">
{data.h1.split(' ').slice(0, -2).join(' ')}<br />
<span className="text-gray-500 dark:text-gray-500">
{data.h1.split(' ').slice(-2).join(' ')}
</span>
</h1>
<p className="hero-stagger text-base md:text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto mb-6 font-light leading-relaxed">
{data.description}
</p>
<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>
</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-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">
<div dangerouslySetInnerHTML={{ __html: data.content }} />
</div>
</motion.div>
</div>
</section>
{/* Internal Linking Card (if not Corpus Christi) */}
{data.city !== "Corpus Christi" && (
<section className="px-6 py-12">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="max-w-4xl mx-auto"
>
<div className="p-10 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">
<div className="flex items-start gap-4">
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-4xl">
info
</span>
<div>
<h3 className="font-display text-2xl font-bold mb-3 text-gray-900 dark:text-white">
Serving the Coastal Bend
</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.
</p>
</div>
</div>
</div>
</motion.div>
</section>
)}
{/* Services Section */}
<Services featuredIds={[10, 9, 11]} />
{/* Areas We Serve & CTA */}
<AreasWeServe />
<FAQ items={data.faq} />
<CTA />
</div>
</>
);
};
export default LocationPage;

232
src/pages/ServicePage.tsx Normal file
View File

@ -0,0 +1,232 @@
import React, { useEffect, useRef, useLayoutEffect } from 'react';
import { motion, useMotionTemplate, useMotionValue } 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 FAQ from '../../components/FAQ';
import AreasWeServe from '../../components/AreasWeServe';
import { ServiceData } from '../data/seoData';
gsap.registerPlugin(ScrollTrigger);
interface ServicePageProps {
data: ServiceData;
}
const ServicePage: React.FC<ServicePageProps> = ({ 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);
}, []);
return (
<>
<SEO
title={data.title}
description={data.description}
keywords={data.keywords}
canonicalUrl={window.location.href}
/>
<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-[90vh] 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="hero-stagger flex items-center justify-center gap-2 mb-4">
<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">
Professional IT Services
</span>
<span className="h-px w-8 bg-gray-400 dark:bg-gray-500"></span>
</div>
<h1 className="hero-stagger font-display text-4xl md:text-5xl lg:text-6xl font-medium tracking-tighter leading-[1.1] mb-5 text-gray-900 dark:text-white">
{data.h1.split(' ').slice(0, -2).join(' ')}<br />
<span className="text-gray-500 dark:text-gray-500">
{data.h1.split(' ').slice(-2).join(' ')}
</span>
</h1>
<p className="hero-stagger text-base md:text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto mb-6 font-light leading-relaxed">
{data.description}
</p>
<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 Started
</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>
</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-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">
<div dangerouslySetInnerHTML={{ __html: data.content }} />
</div>
</motion.div>
</div>
</section>
{/* Feature Highlight Section */}
<section className="px-6 py-16">
<div className="max-w-6xl mx-auto grid md:grid-cols-3 gap-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
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"
>
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
speed
</span>
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
Fast Response
</h3>
<p className="text-gray-600 dark:text-gray-300">
Quick resolution of IT issues to minimize downtime and keep your business running smoothly.
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
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"
>
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
verified_user
</span>
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
Proactive Security
</h3>
<p className="text-gray-600 dark:text-gray-300">
Advanced security measures and monitoring to protect your business from cyber threats.
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
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"
>
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400 text-5xl mb-4 block">
support_agent
</span>
<h3 className="font-display text-xl font-bold mb-3 text-gray-900 dark:text-white">
Expert Team
</h3>
<p className="text-gray-600 dark:text-gray-300">
Experienced IT professionals dedicated to providing exceptional service and support.
</p>
</motion.div>
</div>
</section>
{/* Services Section */}
<Services featuredIds={data.relatedServices} />
{/* Areas We Serve & CTA */}
<AreasWeServe />
<FAQ items={data.faq} />
<CTA />
</div>
</>
);
};
export default ServicePage;

View File

@ -206,6 +206,7 @@ const ServiceModal: React.FC<{ service: typeof services[0] | null; onClose: () =
exit={{ scale: 0.9, opacity: 0 }} exit={{ scale: 0.9, opacity: 0 }}
className="bg-[#1a1a1a] border border-white/10 rounded-2xl w-full max-w-4xl max-h-[90vh] overflow-y-auto relative shadow-2xl custom-scrollbar" className="bg-[#1a1a1a] border border-white/10 rounded-2xl w-full max-w-4xl max-h-[90vh] overflow-y-auto relative shadow-2xl custom-scrollbar"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
data-lenis-prevent
> >
{/* Close Button - Sticky and distinct */} {/* Close Button - Sticky and distinct */}
<button <button