160 lines
6.6 KiB
TypeScript
160 lines
6.6 KiB
TypeScript
// app/services/page.tsx
|
||
import { Metadata } from "next";
|
||
import { getAllServices, SERVICE_CATEGORIES, type ServiceCategoryId } from "@/lib/services";
|
||
import ServiceCard from "@/components/ServiceCard";
|
||
import Link from "next/link";
|
||
|
||
export const revalidate = 86400;
|
||
|
||
export const metadata: Metadata = {
|
||
title: "Services — Website Reliability for SMBs | Van Hunen IT",
|
||
description:
|
||
"Fixed-scope sprints and care plans for SMB website reliability: email deliverability (DMARC), Cloudflare edge security & speed, tested backups, and uptime watch.",
|
||
alternates: { canonical: "/services" },
|
||
};
|
||
|
||
export default function ServicesPage() {
|
||
const services = getAllServices();
|
||
|
||
// Emphasize core Reliability first; keep the rest discoverable.
|
||
const categories: ServiceCategoryId[] = [
|
||
"web-performance",
|
||
"infrastructure-devops",
|
||
"dev-platforms",
|
||
"migrations",
|
||
"web-dev",
|
||
"minecraft",
|
||
];
|
||
|
||
return (
|
||
<main
|
||
id="top"
|
||
className="relative isolate min-h-screen bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950"
|
||
>
|
||
{/* --- Hero Header --- */}
|
||
<section className="relative overflow-hidden border-b border-neutral-200 dark:border-neutral-800">
|
||
<div className="container mx-auto max-w-6xl px-4 py-20 text-center">
|
||
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 bg-clip-text text-transparent">
|
||
Services that make websites reliable
|
||
</h1>
|
||
<p className="mt-5 text-lg text-neutral-700 dark:text-neutral-300 max-w-2xl mx-auto">
|
||
Fixed-scope sprints and care plans for{" "}
|
||
<strong>email deliverability</strong>, <strong>Cloudflare edge security & speed</strong>,{" "}
|
||
<strong>backups with restore tests</strong>, and <strong>uptime watch</strong>. Clear outcomes, flat pricing,
|
||
and before/after proof you can keep.
|
||
</p>
|
||
|
||
<div className="mt-8 flex flex-wrap justify-center gap-3">
|
||
{categories.map((id) => (
|
||
<a
|
||
key={id}
|
||
href={`#${SERVICE_CATEGORIES[id].anchor}`}
|
||
className="rounded-full border border-neutral-300 dark:border-neutral-700 px-4 py-1.5 text-sm font-medium text-neutral-700 dark:text-neutral-300 hover:border-blue-500 hover:text-blue-600 dark:hover:text-blue-400 transition"
|
||
>
|
||
{SERVICE_CATEGORIES[id].label}
|
||
</a>
|
||
))}
|
||
</div>
|
||
|
||
<div className="mt-8 flex items-center justify-center gap-3">
|
||
<Link
|
||
href="/pricing"
|
||
className="inline-flex items-center rounded-lg bg-blue-600 px-5 py-2.5 text-white font-semibold hover:bg-blue-500 transition"
|
||
>
|
||
See pricing
|
||
</Link>
|
||
<Link
|
||
href="/contact"
|
||
className="inline-flex items-center rounded-lg border border-neutral-300 dark:border-neutral-700 px-5 py-2.5 font-semibold text-neutral-800 dark:text-neutral-200 hover:bg-neutral-50 dark:hover:bg-neutral-900/40 transition"
|
||
>
|
||
Book a 15-min Fit Call
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* --- Service Categories --- */}
|
||
<div className="container mx-auto max-w-6xl px-4 py-16">
|
||
{categories.map((id, index) => {
|
||
const list = services.filter((s) => s.category === id);
|
||
if (!list.length) return null;
|
||
|
||
const headingId = `${id}-heading`;
|
||
const sectionClasses = `relative py-16 scroll-mt-24 ${
|
||
index % 2 === 0
|
||
? "bg-white dark:bg-neutral-900/50"
|
||
: "bg-neutral-50 dark:bg-neutral-900/30"
|
||
} rounded-2xl mb-12 shadow-sm`;
|
||
|
||
return (
|
||
<section
|
||
key={id}
|
||
id={SERVICE_CATEGORIES[id].anchor}
|
||
aria-labelledby={headingId}
|
||
className={sectionClasses}
|
||
>
|
||
<div className="absolute inset-0 -z-10 bg-gradient-to-br from-blue-50/10 via-transparent to-purple-50/10 dark:from-blue-950/10 dark:to-purple-950/10 rounded-2xl" />
|
||
<div className="px-4 sm:px-8">
|
||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
|
||
<h2
|
||
id={headingId}
|
||
className="text-2xl sm:text-3xl font-bold tracking-tight bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"
|
||
>
|
||
{SERVICE_CATEGORIES[id].label}
|
||
</h2>
|
||
<Link
|
||
href="#top"
|
||
className="mt-3 sm:mt-0 inline-flex items-center text-sm font-medium text-blue-600 hover:underline dark:text-blue-400"
|
||
aria-label="Jump to top"
|
||
>
|
||
Jump to top ↑
|
||
</Link>
|
||
</div>
|
||
|
||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||
{list.map((svc, i) => (
|
||
<div
|
||
key={svc.slug}
|
||
className="animate-[fadeInUp_0.6s_ease_forwards]"
|
||
style={{ animationDelay: `${i * 80}ms` }}
|
||
>
|
||
<ServiceCard svc={svc} />
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* --- CTA footer --- */}
|
||
<section className="relative py-24 text-center bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white">
|
||
<div className="container mx-auto max-w-4xl px-6">
|
||
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight">
|
||
Ready to improve reliability and deliverability?
|
||
</h2>
|
||
<p className="mt-4 text-lg text-blue-100">
|
||
Tell us about your site. We’ll recommend the right Fix Sprint or Care plan and show you the expected
|
||
before/after.
|
||
</p>
|
||
<div className="mt-8 flex items-center justify-center gap-3">
|
||
<Link
|
||
href="/pricing"
|
||
className="inline-flex items-center rounded-lg bg-white text-blue-700 font-semibold px-6 py-3 hover:bg-blue-50 transition"
|
||
>
|
||
Compare plans
|
||
</Link>
|
||
<Link
|
||
href="/contact"
|
||
className="inline-flex items-center rounded-lg border border-white/80 text-white font-semibold px-6 py-3 hover:bg-white/10 transition"
|
||
>
|
||
Contact us
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
);
|
||
}
|