Copy optimization

This commit is contained in:
Thimon 2025-10-26 02:05:16 +02:00
parent 1f12fb739b
commit 789678a0c1
23 changed files with 1301 additions and 472 deletions

View File

@ -1,19 +1,16 @@
import type { Metadata } from "next";
import Hero from "@/components/Hero";
import Process from "@/components/Process";
import Pricing from "@/components/Pricing";
import Testimonials from "@/components/Testimonials";
import FAQ from "@/components/FAQ";
import CTA from "@/components/CTA";
import { SERVICE_CATEGORIES } from "@/lib/services";
import { plans } from "@/lib/pricing";
import { site } from "@/lib/site";
import JsonLd from "@/components/JsonLd";
import Link from "next/link";
export const metadata: Metadata = {
title: "Productized IT for SMBs",
description: site.description,
title: "SMB Website Reliability & Deliverability",
description:
"DMARC-aligned email, Cloudflare edge security & speed, tested backups, and uptime watch for SMB sites. Fixed-scope sprints and care plans with real SLAs and before/after proof.",
};
export default function HomePage() {
@ -26,89 +23,142 @@ export default function HomePage() {
sameAs: site.org.sameAs,
};
const faqItems = [
{
q: "How fast can you start?",
a: "Most sprints start within 23 business days after scope confirmation.",
},
{
q: "Do you work under NDA?",
a: "Yes—mutual NDA available on request; we keep credentials least-privilege.",
},
{
q: "Can we switch providers later?",
a: "Yes. Everything is documented; you own the accounts and artifacts. Month-to-month plans with a 30-day cancel policy.",
},
{
q: "Whats your guarantee?",
a: "We show proof of outcomes. If the agreed scope isnt met, we make it right or refund the sprint fee.",
},
{
q: "Do you offer 24/7?",
a: "Yes—Mission-Critical includes 24/7 paging with a 1-hour first response. Essential and Growth cover business hours.",
},
];
const faqLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: [
{
"@type": "Question",
name: "How fast can you start?",
acceptedAnswer: {
"@type": "Answer",
text: "Most sprints start within 23 business days after scope confirmation.",
},
},
{
"@type": "Question",
name: "Do you work under NDA?",
acceptedAnswer: {
"@type": "Answer",
text: "Yes—mutual NDA available on request; we keep credentials least-privilege.",
},
},
{
"@type": "Question",
name: "Can we switch providers later?",
acceptedAnswer: {
"@type": "Answer",
text: "Yes. Everything is documented; you own the accounts and artifacts.",
},
},
],
};
// Service category summaries
const categoryDescriptions: Record<string, string> = {
"infrastructure-devops":
"Reliable servers, containers, and automation—Docker, Kubernetes, Cloudflare, and backups that just work.",
"web-performance":
"Make your website fast, secure, and consistently available. Core Web Vitals, deliverability, and uptime care.",
"dev-platforms":
"Private Git hosting, CI/CD, staging environments, and secrets management—developer efficiency built in.",
"migrations":
"Move from legacy hosting or apps to modern, containerized infrastructure with zero-to-minimal downtime.",
"minecraft":
"Managed Minecraft servers, plugin development, and performance tuning for creators and communities.",
"web-dev":
"Modern websites and headless CMS setups designed for speed, SEO, and maintainability.",
mainEntity: faqItems.map((i) => ({
"@type": "Question",
name: i.q,
acceptedAnswer: { "@type": "Answer", text: i.a },
})),
};
return (
<main 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 />
{/* --- HERO --- */}
<section className="relative overflow-hidden">
<div className="absolute inset-0 pointer-events-none opacity-60 bg-[radial-gradient(50%_50%_at_50%_0%,rgba(99,102,241,0.20),rgba(168,85,247,0.15)_40%,transparent_70%)]" />
<div className="container mx-auto max-w-6xl px-4 py-28 text-center">
<h1 className="text-4xl sm:text-6xl font-extrabold tracking-tight">
<span className="bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 bg-clip-text text-transparent">
Fixes in days. Uptime for months.
</span>
</h1>
<p className="mx-auto mt-6 max-w-3xl text-lg sm:text-xl text-neutral-700 dark:text-neutral-300">
SMB website reliability and deliverabilityimplemented with the{" "}
<span className="font-semibold">Reliability Stack</span>: DMARC-aligned email, Cloudflare edge
security & speed, tested backups, and uptime watch. Flat pricing. Before/after proof.
</p>
{/* --- Service Areas --- */}
<section className="relative overflow-hidden border-b border-neutral-200 dark:border-neutral-800">
<div className="mt-8 flex flex-col sm:flex-row gap-3 justify-center">
<Link
href="/free"
className="inline-flex items-center justify-center rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 px-6 py-3 text-white font-semibold hover:opacity-90 transition"
>
Start a Free Reliability Check
</Link>
<Link
href="/contact"
className="inline-flex items-center justify-center rounded-lg border border-neutral-300 dark:border-neutral-700 px-6 py-3 text-neutral-900 dark:text-white font-semibold hover:bg-neutral-50 dark:hover:bg-neutral-800 transition"
>
Book a 15-min Fit Call
</Link>
</div>
{/* Proof badges */}
<div className="mt-10 grid grid-cols-1 sm:grid-cols-3 gap-4 max-w-4xl mx-auto">
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-4">
<p className="text-2xl font-bold">+38% faster TTFB</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">after Cloudflare tuning</p>
</div>
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-4">
<p className="text-2xl font-bold">DMARC p=reject</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">in 48 hours, spoofing blocked</p>
</div>
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-4">
<p className="text-2xl font-bold">7m 12s restore</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">backup drill to full recovery</p>
</div>
</div>
</div>
</section>
{/* --- RELIABILITY STACK --- */}
<section className="relative overflow-hidden border-y border-neutral-200 dark:border-neutral-800">
<div className="container mx-auto max-w-6xl px-4 py-20 text-center">
<h2 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">
Service Areas
The Reliability Stack
</h2>
<p className="mt-5 text-lg text-neutral-700 dark:text-neutral-300 max-w-2xl mx-auto">
Fixed-scope sprints and managed plans for infrastructure, performance, and development.
Each category leads to specialized services tailored to small and mid-sized teams.
<p className="mt-5 text-lg text-neutral-700 dark:text-neutral-300 max-w-3xl mx-auto">
Four layers that make websites dependable. Implemented in a 13 day sprint, proven with before/after data.
</p>
</div>
<div className="container mx-auto max-w-6xl px-4 pb-20">
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{Object.entries(SERVICE_CATEGORIES).map(([id, cat], i) => (
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{[
{
title: "Email Deliverability",
body:
"Inbox-ready email with SPF/DKIM/DMARC alignment, monitoring, and reports. Stop spoofing and missing leads.",
href: "/services#email-deliverability",
},
{
title: "Cloudflare Edge",
body:
"WAF & bot mitigation, HTTP/3, cache tuning, and origin shields for fewer attacks and faster TTFB.",
href: "/services#cloudflare",
},
{
title: "Backups & Restore",
body:
"Automated backups with scheduled restore tests and a recovery runbook. Know you can recover, fast.",
href: "/services#backups",
},
{
title: "Uptime & Incidents",
body:
"Monitors and SSL watch with clear incident notes and post-mortems. Reduce surprises and MTTR.",
href: "/services#uptime",
},
].map((card) => (
<article
key={id}
className="group relative overflow-hidden rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-8 shadow-sm hover:shadow-lg transition-all duration-300 backdrop-blur"
style={{ animation: `fadeInUp 0.6s ease forwards`, animationDelay: `${i * 80}ms` }}
key={card.title}
className="group relative overflow-hidden rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-8 shadow-sm hover:shadow-lg transition-all duration-300 backdrop-blur text-left"
>
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/0 via-transparent to-purple-100/0 dark:from-blue-900/10 dark:to-purple-900/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<h3 className="text-xl font-semibold tracking-tight text-neutral-900 dark:text-white">
{cat.label}
{card.title}
</h3>
<p className="mt-3 text-sm text-neutral-700 dark:text-neutral-300">
{categoryDescriptions[id] ?? ""}
</p>
<p className="mt-3 text-sm text-neutral-700 dark:text-neutral-300">{card.body}</p>
<Link
href={`/services#${cat.anchor}`}
href={card.href}
className="mt-5 inline-flex items-center text-sm font-medium text-blue-600 hover:underline"
>
Explore services
Explore
</Link>
</article>
))}
@ -125,17 +175,76 @@ export default function HomePage() {
</div>
</section>
{/* --- Process --- */}
<Process />
{/* --- Pricing --- */}
<section className="relative py-24 bg-gradient-to-br from-blue-50/40 via-white to-purple-50/40 dark:from-blue-950/10 dark:to-purple-950/10">
{/* --- HOW IT WORKS (3 STEPS) --- */}
<section className="relative py-24">
<div className="container mx-auto max-w-6xl px-4">
<Pricing plans={plans} />
<div className="text-center">
<h2 className="text-3xl sm:text-5xl font-bold tracking-tight">
From audit to outcomes in 72 hours
</h2>
<p className="mt-4 text-lg text-neutral-700 dark:text-neutral-300 max-w-2xl mx-auto">
Diagnose the risks, implement the fixes, and prove the resultsthen choose the right Care plan.
</p>
</div>
<div className="mt-10 grid gap-6 sm:grid-cols-3">
{[
{
step: "1",
title: "Diagnose",
body:
"DNS/email/edge scan, backup checks, and uptime review. Clear scope and fixed price before we start.",
},
{
step: "2",
title: "Implement",
body:
"DMARC + Cloudflare + backups + monitors. Least-privilege access, change log, and rollback plan.",
},
{
step: "3",
title: "Prove",
body:
"Before/after report, restore test timer, incident notes, and next-step plan you can share internally.",
},
].map((s) => (
<div
key={s.step}
className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-6"
>
<div className="text-sm font-semibold text-blue-600">Step {s.step}</div>
<h3 className="mt-2 text-xl font-semibold">{s.title}</h3>
<p className="mt-2 text-sm text-neutral-700 dark:text-neutral-300">{s.body}</p>
</div>
))}
</div>
<div className="mt-10 flex justify-center">
<Link
href="/contact?sprint=fix"
className="inline-flex items-center rounded-lg bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 px-6 py-3 font-semibold hover:opacity-90 transition"
>
Book a Fix Sprint (490)
</Link>
</div>
</div>
</section>
{/* --- Testimonials --- */}
{/* --- PRICING --- */}
<section className="relative py-24 bg-gradient-to-br from-blue-50/40 via-white to-purple-50/40 dark:from-blue-950/10 dark:to-purple-950/10">
<div className="container mx-auto max-w-6xl px-4">
<Pricing plans={plans} />
<div className="mt-8 text-center">
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Every plan includes DNS/email monitoring, automated backups with restore verification, uptime & SSL
watch, and a quarterly health summary.
</p>
</div>
</div>
</section>
{/* --- TESTIMONIALS --- */}
<section className="relative py-24 border-t border-neutral-200 dark:border-neutral-800">
<div className="container mx-auto max-w-6xl px-4">
<Testimonials />
@ -145,40 +254,33 @@ export default function HomePage() {
{/* --- FAQ --- */}
<section className="relative py-24 bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
<div className="container mx-auto max-w-4xl px-4">
<FAQ
items={[
{
q: "How fast can you start?",
a: "Most sprints start within 23 business days after scope confirmation.",
},
{
q: "Do you work under NDA?",
a: "Yes—mutual NDA available on request; we keep credentials least-privilege.",
},
{
q: "Whats your guarantee?",
a: "We show proof of outcomes. If scope isnt met, we make it right or refund the sprint fee.",
},
]}
/>
<FAQ items={faqItems} />
</div>
</section>
{/* --- Call to Action --- */}
{/* --- CTA --- */}
<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 make your IT infrastructure reliable and scalable?
Ready to make your website reliable and inbox-ready?
</h2>
<p className="mt-4 text-lg text-blue-100">
Talk with Van Hunen IT get a fixed-scope sprint or managed plan that fits your business.
Start with a Fix Sprint or choose a Care plan that fits your business. Well prove the results.
</p>
<Link
href="/contact"
className="mt-8 inline-flex items-center rounded-lg bg-white text-blue-700 font-semibold px-6 py-3 hover:bg-blue-50 transition"
>
Contact Us
</Link>
<div className="mt-8 flex flex-col sm:flex-row gap-3 justify-center">
<Link
href="/free"
className="inline-flex items-center rounded-lg bg-white text-blue-700 font-semibold px-6 py-3 hover:bg-blue-50 transition"
>
Start Free Check
</Link>
<Link
href="/pricing"
className="inline-flex items-center rounded-lg border border-white/70 text-white font-semibold px-6 py-3 hover:bg-white/10 transition"
>
See Pricing & SLA
</Link>
</div>
</div>
</section>

118
web/app/pricing/page.tsx Normal file
View File

@ -0,0 +1,118 @@
// app/pricing/page.tsx
import type { Metadata } from "next";
import PricingTable from "@/components/pricing/PricingTable";
import ComparisonTable from "@/components/pricing/ComparisonTable";
import PlanRecommender from "@/components/pricing/PlanRecommender";
import ROICalculator from "@/components/pricing/ROICalculator";
import FAQ from "@/components/pricing/FAQ";
import Guarantee from "@/components/pricing/Guarantee";
import { Plan } from "@/components/pricing/types";
export const metadata: Metadata = {
title: "Pricing & SLAs — Van Hunen IT",
description:
"Simple plans with real SLAs. Essential, Growth, Mission-Critical. Month-to-month, 30-day cancel, incident credits.",
};
const plans: Plan[] = [
{
id: "essential",
name: "Essential Care",
bestFor: "Solo & micro businesses",
monthlyPrice: 149,
yearlyDiscount: 0.1,
outcomes: ["Inbox-ready email", "99.9% uptime"],
inclusions: [
"SPF/DKIM/DMARC monitoring",
"Automated backups + quarterly restore test",
"Uptime & SSL watch",
"Incident credits",
"Business-hours support (8×5)",
],
sla: "Next-business-day first response (8×5)",
ctaLabel: "Start Essential",
popular: false,
},
{
id: "growth",
name: "Growth Care",
bestFor: "SMB team sites",
monthlyPrice: 299,
yearlyDiscount: 0.1,
outcomes: ["99.95% uptime", "Faster TTFB"],
inclusions: [
"Everything in Essential",
"Cloudflare WAF & bot tuning",
"Monthly Web Vitals report",
"Priority incident handling",
],
sla: "4-hour first response (8×5)",
ctaLabel: "Start Growth",
popular: true,
},
{
id: "mission",
name: "Mission-Critical",
bestFor: "High-traffic & 24/7",
monthlyPrice: 649,
yearlyDiscount: 0.1,
outcomes: ["99.99% uptime", "24/7 on-call"],
inclusions: [
"Everything in Growth",
"24/7 paging",
"Weekly checks",
"DR runbook & drills",
],
sla: "1-hour first response (24/7)",
ctaLabel: "Talk to us",
popular: false,
contactOnly: true,
},
];
export default function PricingPage() {
return (
<main className="mx-auto max-w-7xl px-6 py-16">
<header className="mx-auto max-w-3xl text-center">
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl">
Simple plans with real SLAs
</h1>
<p className="mt-4 text-lg text-gray-600">
Pick the care level that matches your traffic and risk. Month-to-month, 30-day
cancel. Fix Sprint available for urgent issues.
</p>
</header>
{/* Engaging element #1: Billing toggle inside table */}
<section className="mt-12">
<PricingTable plans={plans} />
<p className="mt-3 text-center text-sm text-gray-500">
Prices exclude VAT. Annual billing saves 10%.
</p>
</section>
{/* Engaging element #2: Plan recommender */}
<section className="mt-24">
<PlanRecommender plans={plans} />
</section>
{/* Comparison matrix */}
<section className="mt-24">
<ComparisonTable plans={plans} />
</section>
{/* Engaging element #3: ROI calculator */}
<section className="mt-24">
<ROICalculator plans={plans} />
</section>
<section className="mt-24">
<Guarantee />
</section>
<section className="mt-24">
<FAQ />
</section>
</main>
);
}

View File

@ -1,3 +1,4 @@
// app/services/page.tsx
import { Metadata } from "next";
import { getAllServices, SERVICE_CATEGORIES, type ServiceCategoryId } from "@/lib/services";
import ServiceCard from "@/components/ServiceCard";
@ -6,30 +7,43 @@ import Link from "next/link";
export const revalidate = 86400;
export const metadata: Metadata = {
title: "Services — Van Hunen IT",
title: "Services — Website Reliability for SMBs | Van Hunen IT",
description:
"Explore Van Hunen IT's fixed-scope sprints and managed solutions — from VPS hardening and Docker orchestration to Kubernetes, Cloudflare, Core Web Vitals, and Minecraft operations.",
"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();
const categories: ServiceCategoryId[] = Object.keys(SERVICE_CATEGORIES) as ServiceCategoryId[];
// Emphasize core Reliability first; keep the rest discoverable.
const categories: ServiceCategoryId[] = [
"web-performance",
"infrastructure-devops",
"dev-platforms",
"migrations",
"web-dev",
"minecraft",
];
return (
<main 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">
<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">
Professional IT Services
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 managed plans for{" "}
<strong>VPS</strong>, <strong>Docker</strong>, <strong>Kubernetes</strong>,{" "}
<strong>Cloudflare</strong>, <strong>Core Web Vitals</strong>, and{" "}
<strong>Minecraft</strong>. Clear outcomes, flat pricing, and proof you can keep.
Fixed-scope sprints and care plans for{" "}
<strong>email deliverability</strong>, <strong>Cloudflare edge security &amp; 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
@ -41,6 +55,21 @@ export default function ServicesPage() {
</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>
@ -50,31 +79,33 @@ export default function ServicesPage() {
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={`${id}-heading`}
className={`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`}
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={`${id}-heading`}
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={`/services#${SERVICE_CATEGORIES[id].anchor}`}
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>
@ -101,17 +132,26 @@ export default function ServicesPage() {
<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 optimize your IT infrastructure?
Ready to improve reliability and deliverability?
</h2>
<p className="mt-4 text-lg text-blue-100">
Get in touch to discuss your goals well help you choose the right sprint or managed plan.
Tell us about your site. Well recommend the right Fix Sprint or Care plan and show you the expected
before/after.
</p>
<Link
href="/contact"
className="mt-8 inline-flex items-center rounded-lg bg-white text-blue-700 font-semibold px-6 py-3 hover:bg-blue-50 transition"
>
Contact Us
</Link>
<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>

View File

@ -1,26 +1,68 @@
type QA = { q: string; a: string };
function slugify(input: string) {
return input
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, "")
.trim()
.replace(/\s+/g, "-")
.slice(0, 80);
}
export default function FAQ({ items }: { items: QA[] }) {
return (
<section className="relative py-24 bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
<section
aria-labelledby="faq-heading"
className="relative py-24 bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950"
>
<div className="container mx-auto max-w-4xl px-4">
<h2 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent text-center">
<h2
id="faq-heading"
className="text-3xl sm:text-4xl font-bold tracking-tight bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent text-center"
>
Frequently Asked Questions
</h2>
<div className="mt-10 space-y-4">
{items.map((x, i) => (
<details
key={i}
className="group rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-5 shadow-sm hover:shadow-md transition"
>
<summary className="cursor-pointer text-lg font-medium text-neutral-900 dark:text-white flex items-center justify-between">
{x.q}
<span className="transition-transform group-open:rotate-180"></span>
</summary>
<p className="mt-3 text-sm text-neutral-700 dark:text-neutral-300">{x.a}</p>
</details>
))}
{items.map((x, i) => {
const id = slugify(x.q) || `faq-${i}`;
return (
<details
key={id}
id={id}
className="group rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-5 shadow-sm hover:shadow-md transition"
>
<summary className="cursor-pointer text-lg font-medium text-neutral-900 dark:text-white flex items-center justify-between">
<span>{x.q}</span>
<span className="ml-4 select-none transition-transform group-open:rotate-180" aria-hidden="true">
</span>
</summary>
<div className="mt-3 text-sm text-neutral-700 dark:text-neutral-300 leading-relaxed">
{x.a}
</div>
</details>
);
})}
</div>
<p className="mt-10 text-center text-sm text-neutral-600 dark:text-neutral-400">
Still have questions?{" "}
<a
href="/contact"
className="font-medium text-blue-600 hover:underline"
>
Contact us
</a>{" "}
or{" "}
<a
href="/pricing"
className="font-medium text-blue-600 hover:underline"
>
see Pricing & SLA
</a>
.
</p>
</div>
</section>
);

View File

@ -12,8 +12,9 @@ export default function Header() {
<nav aria-label="Main" className="flex items-center gap-1 text-sm">
{[
{ href: "/services", label: "Services" },
{ href: "/free", label: "Free tools" },
{ href: "/contact", label: "Contact" },
// { href: "/free", label: "Free tools" },
{ href: "/pricing", label: "Pricing" },
{ href: "/contact", label: "Contact" }
].map((item) => (
<Link
key={item.href}

View File

@ -3,45 +3,91 @@ import type { Plan } from "@/lib/pricing";
export default function Pricing({ plans }: { plans: Plan[] }) {
return (
<section className="relative py-24 border-t border-neutral-200 dark:border-neutral-800 bg-gradient-to-b from-white via-neutral-50 to-white dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
<section
aria-labelledby="pricing-heading"
className="relative py-24 border-t border-neutral-200 dark:border-neutral-800 bg-gradient-to-b from-white via-neutral-50 to-white dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950"
>
<div className="container mx-auto max-w-6xl px-4 text-center">
<h2 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
Simple, flat pricing
<h2
id="pricing-heading"
className="text-3xl sm:text-4xl font-bold tracking-tight bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"
>
Simple plans with real SLAs
</h2>
<p className="mt-4 text-neutral-700 dark:text-neutral-300 max-w-2xl mx-auto">
Pick the care level that matches your traffic and risk. Month-to-month, 30-day cancel.
</p>
<div className="mt-12 grid gap-8 sm:grid-cols-3">
{plans.map((p, i) => (
<div
<article
key={p.id}
className={`rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-8 shadow-sm hover:shadow-lg transition backdrop-blur animate-[fadeInUp_0.6s_ease_forwards]`}
aria-label={`${p.name} plan`}
className={[
"relative rounded-2xl border border-neutral-200 dark:border-neutral-800",
p.popular
? "bg-white shadow-lg ring-2 ring-blue-600/20 dark:bg-neutral-900/80"
: "bg-white/70 dark:bg-neutral-900/70 shadow-sm",
"p-8 hover:shadow-lg transition backdrop-blur animate-[fadeInUp_0.6s_ease_forwards]",
].join(" ")}
style={{ animationDelay: `${i * 100}ms` }}
>
{p.popular && (
<div className="mb-3 inline-block rounded-full bg-gradient-to-r from-blue-600 to-purple-600 px-3 py-1 text-xs text-white">
<div
aria-hidden="true"
className="absolute -top-3 right-6 rounded-full bg-gradient-to-r from-blue-600 to-purple-600 px-3 py-1 text-xs text-white"
>
Most popular
</div>
)}
<div className="text-xl font-semibold">{p.name}</div>
<div className="text-xl font-semibold text-neutral-900 dark:text-white">{p.name}</div>
<div className="mt-3 flex items-baseline justify-center gap-1">
<div className="text-4xl font-bold text-neutral-900 dark:text-white">{p.price}</div>
{p.periodicity && (
<div className="text-sm text-neutral-500 dark:text-neutral-400">{p.periodicity}</div>
)}
</div>
<p className="mt-3 text-sm text-neutral-600 dark:text-neutral-300">{p.description}</p>
{p.description && (
<p className="mt-3 text-sm text-neutral-600 dark:text-neutral-300">{p.description}</p>
)}
<ul className="mt-5 space-y-2 text-sm text-neutral-700 dark:text-neutral-300 text-left">
{p.features.map((f, idx) => (
<li key={idx}> {f}</li>
<li key={idx} className="flex items-start gap-2">
<span className="mt-1.5 inline-block h-1.5 w-1.5 rounded-full bg-gradient-to-r from-blue-600 to-purple-600" />
<span>{f}</span>
</li>
))}
</ul>
<Link
href={p.cta.href}
className="mt-8 inline-block w-full rounded-lg bg-gradient-to-r from-blue-600 to-purple-600 text-white py-3 text-sm font-medium hover:opacity-90 transition"
aria-label={p.cta.label}
className={[
"mt-8 inline-block w-full rounded-lg py-3 text-sm font-medium transition",
p.popular
? "bg-gradient-to-r from-blue-600 to-purple-600 text-white hover:opacity-90"
: "border border-neutral-300 dark:border-neutral-700 text-neutral-900 dark:text-white hover:bg-neutral-50 dark:hover:bg-neutral-800",
].join(" ")}
>
{p.cta.label}
</Link>
</div>
<p className="mt-3 text-xs text-neutral-500 dark:text-neutral-400">
Includes DNS/email monitoring, automated backups with restore verification, uptime & SSL watch.
</p>
</article>
))}
</div>
<div className="mt-10 text-center">
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Need 24/7 paging and 1-hour first response? Choose <span className="font-medium">Mission-Critical</span>.
</p>
</div>
</div>
</section>
);

View File

@ -1,12 +1,69 @@
export default function Testimonials() {
// Replace or extend these with real quotes/metrics when available.
const metrics = [
{ title: "+38% faster TTFB", sub: "after Cloudflare tuning" },
{ title: "DMARC p=reject", sub: "in 48 hours, spoofing blocked" },
{ title: "7m 12s restore", sub: "backup drill to full recovery" },
];
const quotes = [
{
quote:
"Our forms finally land in the inbox and the site is measurably faster. The before/after report made it an easy yes.",
name: "Marketing Lead",
company: "B2B Services SMB",
},
{
quote:
"They implemented, tested, and documented everything in two days. We now have backups that actually restore.",
name: "Founder",
company: "E-commerce SME",
},
];
return (
<section className="relative py-24 bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950 text-center">
<h2 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
What clients say
</h2>
<p className="mt-6 text-neutral-600 dark:text-neutral-300">
Testimonials coming soon.
</p>
<section
aria-labelledby="testimonials-heading"
className="relative py-24 bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950"
>
<div className="container mx-auto max-w-6xl px-4">
<h2
id="testimonials-heading"
className="text-center text-3xl sm:text-4xl font-bold tracking-tight bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"
>
Proof we deliver
</h2>
{/* Metric badges */}
<div className="mt-10 grid grid-cols-1 sm:grid-cols-3 gap-4 max-w-4xl mx-auto">
{metrics.map((m) => (
<div
key={m.title}
className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-5 text-center"
>
<p className="text-2xl font-bold text-neutral-900 dark:text-white">{m.title}</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">{m.sub}</p>
</div>
))}
</div>
{/* Quotes */}
<div className="mt-12 grid gap-6 sm:grid-cols-2">
{quotes.map((q, i) => (
<figure
key={i}
className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-6 shadow-sm"
>
<blockquote className="text-neutral-800 dark:text-neutral-200">
{q.quote}
</blockquote>
<figcaption className="mt-4 text-sm text-neutral-600 dark:text-neutral-400">
{q.name}, {q.company}
</figcaption>
</figure>
))}
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,39 @@
// components/pricing/BillingToggle.tsx
"use client";
import clsx from "clsx";
type Billing = "monthly" | "yearly";
export default function BillingToggle({
value,
onChange,
}: {
value: Billing;
onChange: (v: Billing) => void;
}) {
return (
<div className="mx-auto mb-8 flex w-fit items-center gap-2 rounded-full border border-gray-200 p-1">
<button
onClick={() => onChange("monthly")}
className={clsx(
"rounded-full px-4 py-2 text-sm transition",
value === "monthly" ? "bg-gray-900 text-white" : "text-gray-600 hover:bg-gray-100"
)}
aria-pressed={value === "monthly"}
>
Monthly
</button>
<button
onClick={() => onChange("yearly")}
className={clsx(
"rounded-full px-4 py-2 text-sm transition",
value === "yearly" ? "bg-gray-900 text-white" : "text-gray-600 hover:bg-gray-100"
)}
aria-pressed={value === "yearly"}
>
Yearly <span className="ml-1 rounded bg-emerald-100 px-1.5 py-0.5 text-[10px] font-semibold text-emerald-700">Save 10%</span>
</button>
</div>
);
}

View File

@ -0,0 +1,89 @@
// components/pricing/ComparisonTable.tsx
import { Plan } from "./types";
import { CheckIcon, MinusIcon } from "./icons";
const rows: { label: string; keys: Array<keyof Plan | string> }[] = [
{ label: "Email deliverability monitoring", keys: [] },
{ label: "Automated backups", keys: [] },
{ label: "Restore test cadence", keys: [] },
{ label: "Cloudflare WAF & bot tuning", keys: [] },
{ label: "Web Vitals reporting", keys: [] },
{ label: "On-call paging", keys: [] },
{ label: "First response SLA", keys: [] },
{ label: "Uptime target", keys: [] },
];
export default function ComparisonTable({ plans }: { plans: Plan[] }) {
// Hard-code feature mapping for clarity (keeps copy outcome-driven)
const matrix: Record<string, (planId: string) => string | boolean> = {
"Email deliverability monitoring": () => true,
"Automated backups": () => true,
"Restore test cadence": (id) =>
id === "essential" ? "Quarterly" : id === "growth" ? "Quarterly" : "Monthly",
"Cloudflare WAF & bot tuning": (id) => id !== "essential",
"Web Vitals reporting": (id) => (id === "growth" || id === "mission" ? "Monthly" : false),
"On-call paging": (id) => (id === "mission" ? "24/7" : false),
"First response SLA": (id) =>
id === "essential" ? "NBD (8×5)" : id === "growth" ? "4h (8×5)" : "1h (24/7)",
"Uptime target": (id) =>
id === "essential" ? "99.9%" : id === "growth" ? "99.95%" : "99.99%",
};
const features = Object.keys(matrix);
return (
<div className="overflow-hidden rounded-2xl border border-gray-200">
<div className="bg-gray-50 px-6 py-4">
<h2 className="text-xl font-semibold">Whats inside each plan</h2>
<p className="mt-1 text-sm text-gray-600">
Outcome-driven features, not laundry lists.
</p>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-white">
<tr>
<th className="py-3 pl-6 pr-3 text-left text-sm font-semibold text-gray-600">Feature</th>
{plans.map((p) => (
<th key={p.id} className="px-3 py-3 text-left text-sm font-semibold text-gray-600">
{p.name}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-100 bg-white">
{features.map((feature) => (
<tr key={feature}>
<td className="whitespace-nowrap py-3 pl-6 pr-3 text-sm font-medium text-gray-900">
{feature}
</td>
{plans.map((p) => {
const val = matrix[feature](p.id);
const isBool = typeof val === "boolean";
return (
<td key={p.id + feature} className="whitespace-nowrap px-3 py-3 text-sm">
{isBool ? (
val ? (
<CheckIcon className="h-5 w-5 text-emerald-600" />
) : (
<MinusIcon className="h-5 w-5 text-gray-300" />
)
) : (
<span className="rounded-full bg-gray-50 px-2 py-0.5 text-xs text-gray-700">
{val as string}
</span>
)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-gray-50 px-6 py-3 text-xs text-gray-500">
Uptime target refers to target SLO over a rolling 30-day period. Credits apply if response SLAs are missed.
</div>
</div>
);
}

View File

@ -0,0 +1,35 @@
// components/pricing/FAQ.tsx
export default function FAQ() {
const faqs = [
{
q: "Do I need to sign a long contract?",
a: "No. Plans are month-to-month with a 30-day cancel. We also provide an exit plan and documentation.",
},
{
q: "What happens if you miss an SLA?",
a: "We apply incident credits on your next invoice. Credits scale with the breach severity.",
},
{
q: "Are you GDPR compliant?",
a: "Yes. We sign a DPA, use least-privilege access, and keep an audit log. We provide cookie and privacy pages.",
},
{
q: "Can you help outside business hours?",
a: "Mission-Critical includes 24/7 paging. You can also request ad-hoc after-hours support when needed.",
},
];
return (
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<h2 className="text-xl font-semibold">Pricing FAQs</h2>
<dl className="mt-6 space-y-6">
{faqs.map((item) => (
<div key={item.q}>
<dt className="text-sm font-medium text-gray-900">{item.q}</dt>
<dd className="mt-2 text-sm text-gray-700">{item.a}</dd>
</div>
))}
</dl>
</div>
);
}

View File

@ -0,0 +1,18 @@
// components/pricing/Guarantee.tsx
export default function Guarantee() {
return (
<div className="relative overflow-hidden rounded-2xl border border-gray-200 bg-gradient-to-br from-emerald-50 to-white p-6">
<div className="mx-auto max-w-3xl text-center">
<h2 className="text-xl font-semibold">Risk reversed</h2>
<p className="mt-2 text-gray-700">
30-day, no-questions cancel. If we dont deliver your agreed outcomes in the first month,
well complete the sprint free.
</p>
<div className="mt-4 inline-flex items-center gap-3 rounded-xl border border-emerald-200 bg-white px-4 py-2 text-sm font-medium text-emerald-900">
<span className="inline-block rounded-full bg-emerald-100 px-2 py-0.5 text-xs">Guarantee</span>
Incident credits apply if response SLAs are missed.
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,85 @@
// components/pricing/PlanCard.tsx
import { Plan } from "./types";
import { formatEUR, monthlyForYearly } from "./money";
import { CheckIcon } from "./icons";
export default function PlanCard({
plan,
billing,
}: {
plan: Plan;
billing: "monthly" | "yearly";
}) {
const price =
billing === "monthly"
? plan.monthlyPrice
: monthlyForYearly(plan.monthlyPrice, plan.yearlyDiscount);
return (
<div
className={[
"relative flex h-full flex-col rounded-2xl border border-gray-200 bg-white p-6 shadow-sm transition hover:shadow-md",
plan.popular ? "ring-2 ring-emerald-500" : "",
].join(" ")}
>
{plan.popular && (
<div className="absolute -top-3 left-6 rounded-full bg-emerald-600 px-3 py-1 text-xs font-semibold text-white shadow">
Most popular
</div>
)}
<h3 className="text-xl font-semibold">{plan.name}</h3>
<p className="mt-1 text-sm text-gray-500">{plan.bestFor}</p>
<div className="mt-6 flex items-baseline gap-2">
<span className="text-4xl font-bold">{formatEUR(price)}</span>
<span className="text-sm text-gray-500">/mo</span>
</div>
{billing === "yearly" && (
<p className="mt-1 text-xs text-emerald-700">Billed annually (save {Math.round(plan.yearlyDiscount * 100)}%)</p>
)}
<ul className="mt-6 space-y-2 text-sm">
{plan.outcomes.map((o) => (
<li key={o} className="flex items-start gap-2">
<CheckIcon className="mt-0.5 h-5 w-5 text-emerald-600" />
<span className="font-medium">{o}</span>
</li>
))}
</ul>
<div className="mt-4 rounded-lg bg-gray-50 p-3 text-sm">
<p className="font-medium">Includes:</p>
<ul className="mt-2 space-y-1">
{plan.inclusions.map((i) => (
<li key={i} className="flex items-start gap-2">
<CheckIcon className="mt-0.5 h-4 w-4 text-gray-400" />
<span>{i}</span>
</li>
))}
</ul>
</div>
<p className="mt-4 text-sm text-gray-600">
<span className="font-medium">SLA:</span> {plan.sla}
</p>
<div className="mt-6">
<a
href={plan.contactOnly ? "/contact" : `/checkout?plan=${plan.id}&billing=${billing}`}
className={[
"inline-flex w-full items-center justify-center rounded-xl px-4 py-3 text-sm font-semibold transition",
plan.contactOnly
? "border border-gray-300 text-gray-900 hover:bg-gray-50"
: "bg-gray-900 text-white hover:bg-black",
].join(" ")}
aria-label={plan.ctaLabel}
>
{plan.ctaLabel}
</a>
<p className="mt-2 text-center text-xs text-gray-500">
30-day cancel. Incident credits if we miss SLAs.
</p>
</div>
</div>
);
}

View File

@ -0,0 +1,87 @@
// components/pricing/PlanRecommender.tsx
"use client";
import { useMemo, useState } from "react";
import { Plan } from "./types";
export default function PlanRecommender({ plans }: { plans: Plan[] }) {
const [sessions, setSessions] = useState<number>(5000);
const [risk, setRisk] = useState<"low" | "medium" | "high">("medium");
const [hours, setHours] = useState<"business" | "around" | "always">("around");
const recommended = useMemo(() => {
// Simple heuristic: higher sessions + high risk + always-on => mission; else growth; else essential
if (sessions >= 20000 || risk === "high" || hours === "always") return "mission";
if (sessions >= 7000 || risk === "medium") return "growth";
return "essential";
}, [sessions, risk, hours]);
const plan = plans.find((p) => p.id === recommended)!;
return (
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<h2 className="text-xl font-semibold">Not sure where to start?</h2>
<p className="mt-1 text-sm text-gray-600">
Answer three quick questions and well suggest a plan.
</p>
<div className="mt-6 grid gap-6 md:grid-cols-3">
<div>
<label className="block text-sm font-medium text-gray-700">
Monthly sessions (est.)
</label>
<input
type="number"
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
value={sessions}
onChange={(e) => setSessions(Number(e.target.value || 0))}
min={0}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Risk tolerance</label>
<select
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
value={risk}
onChange={(e) => setRisk(e.target.value as any)}
>
<option value="low">Low (occasional issues okay)</option>
<option value="medium">Medium (prefer stability)</option>
<option value="high">High (incidents must be rare)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Support window</label>
<select
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
value={hours}
onChange={(e) => setHours(e.target.value as any)}
>
<option value="business">Business hours</option>
<option value="around">Extended (early/late)</option>
<option value="always">24/7</option>
</select>
</div>
</div>
<div className="mt-6 rounded-xl border border-emerald-200 bg-emerald-50 p-4">
<p className="text-sm font-semibold text-emerald-900">Recommended plan</p>
<p className="mt-1 text-lg font-semibold">
{plan.name} <span className="text-sm font-normal text-emerald-900"> {plan.bestFor}</span>
</p>
<p className="mt-1 text-sm text-emerald-900">
You indicated {sessions.toLocaleString()} sessions/mo, {risk} risk tolerance, and{" "}
{hours === "always" ? "24/7" : hours === "around" ? "extended" : "business-hours"} support.
</p>
<div className="mt-3">
<a
href={plan.contactOnly ? "/contact" : `/checkout?plan=${plan.id}`}
className="inline-flex items-center rounded-lg bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-700"
>
{plan.contactOnly ? "Talk to us" : "Start " + plan.name}
</a>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,22 @@
// components/pricing/PricingTable.tsx
"use client";
import { useState } from "react";
import { Plan } from "./types";
import BillingToggle from "./BillingToggle";
import PlanCard from "./PlanCard";
export default function PricingTable({ plans }: { plans: Plan[] }) {
const [billing, setBilling] = useState<"monthly" | "yearly">("monthly");
return (
<div>
<BillingToggle value={billing} onChange={setBilling} />
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
{plans.map((p) => (
<PlanCard key={p.id} plan={p} billing={billing} />
))}
</div>
</div>
);
}

View File

@ -0,0 +1,147 @@
// components/pricing/ROICalculator.tsx
"use client";
import { useMemo, useState } from "react";
import { Plan } from "./types";
import { formatEUR } from "./money";
function targetUptime(planId: string) {
return planId === "mission" ? 0.9999 : planId === "growth" ? 0.9995 : 0.999;
}
export default function ROICalculator({ plans }: { plans: Plan[] }) {
const [planId, setPlanId] = useState<string>("growth");
const [monthlyLeads, setMonthlyLeads] = useState<number>(40);
const [avgLeadValue, setAvgLeadValue] = useState<number>(150);
const [currentUptime, setCurrentUptime] = useState<number>(0.995); // 99.5%
const [deliverability, setDeliverability] = useState<number>(0.85); // 85%
const plan = plans.find((p) => p.id === planId)!;
const result = useMemo(() => {
const target = targetUptime(planId);
const uptimeGain = Math.max(0, target - currentUptime); // fraction
// assume +10pp absolute deliverability improvement (conservative)
const deliverabilityGain = Math.max(0, 0.1 + (deliverability < 0.9 ? 0 : -0.02));
const recoveredLeads =
monthlyLeads * uptimeGain + monthlyLeads * (deliverabilityGain * (1 - deliverability));
const recoveredRevenue = recoveredLeads * avgLeadValue;
const planCost = plan.monthlyPrice;
const roi = planCost ? (recoveredRevenue - planCost) / planCost : 0;
return {
target,
uptimeGain,
deliverabilityGain,
recoveredLeads,
recoveredRevenue,
planCost,
roi,
};
}, [planId, plan, monthlyLeads, avgLeadValue, currentUptime, deliverability]);
return (
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<h2 className="text-xl font-semibold">Estimate your ROI</h2>
<p className="mt-1 text-sm text-gray-600">
A simple, conservative model based on uptime & deliverability improvements.
</p>
<div className="mt-6 grid gap-6 md:grid-cols-5">
<div className="md:col-span-1">
<label className="block text-sm font-medium text-gray-700">Plan</label>
<select
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
value={planId}
onChange={(e) => setPlanId(e.target.value)}
>
{plans.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</div>
<div className="md:col-span-1">
<label className="block text-sm font-medium text-gray-700">Monthly leads</label>
<input
type="number"
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
min={0}
value={monthlyLeads}
onChange={(e) => setMonthlyLeads(Number(e.target.value || 0))}
/>
</div>
<div className="md:col-span-1">
<label className="block text-sm font-medium text-gray-700">Avg. lead value ()</label>
<input
type="number"
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
min={0}
value={avgLeadValue}
onChange={(e) => setAvgLeadValue(Number(e.target.value || 0))}
/>
</div>
<div className="md:col-span-1">
<label className="block text-sm font-medium text-gray-700">Current uptime</label>
<input
type="number"
step="0.001"
min={0.95}
max={0.9999}
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
value={currentUptime}
onChange={(e) => setCurrentUptime(Number(e.target.value || 0))}
/>
<p className="mt-1 text-xs text-gray-500">e.g., 0.995 = 99.5%</p>
</div>
<div className="md:col-span-1">
<label className="block text-sm font-medium text-gray-700">Inbox rate</label>
<input
type="number"
step="0.01"
min={0.5}
max={1}
className="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2"
value={deliverability}
onChange={(e) => setDeliverability(Number(e.target.value || 0))}
/>
<p className="mt-1 text-xs text-gray-500">e.g., 0.85 = 85%</p>
</div>
</div>
<div className="mt-6 grid gap-6 md:grid-cols-3">
<div className="rounded-xl border border-gray-200 p-4">
<p className="text-sm text-gray-600">Target uptime (plan)</p>
<p className="mt-1 text-2xl font-semibold">{(result.target * 100).toFixed(3)}%</p>
</div>
<div className="rounded-xl border border-gray-200 p-4">
<p className="text-sm text-gray-600">Recovered leads (est.)</p>
<p className="mt-1 text-2xl font-semibold">
{Math.max(0, Math.round(result.recoveredLeads))}
</p>
</div>
<div className="rounded-xl border border-gray-200 p-4">
<p className="text-sm text-gray-600">Monthly impact</p>
<p className="mt-1 text-2xl font-semibold">{formatEUR(Math.max(0, Math.round(result.recoveredRevenue)))}</p>
</div>
</div>
<div className="mt-6 rounded-xl border border-emerald-200 bg-emerald-50 p-4">
<p className="text-sm font-semibold text-emerald-900">Estimated ROI</p>
<p className="mt-1 text-lg font-semibold text-emerald-900">
{result.planCost
? `${Math.round(result.roi * 100)}%`
: "—"}
</p>
<p className="mt-1 text-sm text-emerald-900">
Impact is an estimate; we provide before/after proof during your first month.
</p>
</div>
</div>
);
}

View File

@ -0,0 +1,25 @@
// components/pricing/icons.tsx
import * as React from "react";
export function CheckIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
d="M20 6L9 17l-5-5"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export function MinusIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path d="M5 12h14" stroke="currentColor" strokeWidth={2} strokeLinecap="round" />
</svg>
);
}

View File

@ -0,0 +1,13 @@
// components/pricing/money.ts
export function formatEUR(amount: number) {
return new Intl.NumberFormat("nl-NL", {
style: "currency",
currency: "EUR",
maximumFractionDigits: 0,
}).format(amount);
}
export function monthlyForYearly(baseMonthly: number, discount: number) {
// display as monthly equivalent when billed annually
return Math.round(baseMonthly * (1 - discount));
}

View File

@ -0,0 +1,14 @@
// components/pricing/types.ts
export type Plan = {
id: string;
name: string;
bestFor: string;
monthlyPrice: number; // base monthly
yearlyDiscount: number; // e.g. 0.1 => 10%
outcomes: string[];
inclusions: string[];
sla: string;
ctaLabel: string;
popular?: boolean;
contactOnly?: boolean;
};

View File

@ -1,8 +1,8 @@
version: "3.9"
services:
vanhunen-it-dev:
container_name: vanhunen-it
dev:
container_name: dev
build:
context: .
dockerfile: Dockerfile
@ -19,8 +19,8 @@ services:
networks:
- devnet
vanhunen-it-prod:
container_name: vanhunen-it-prod
prod:
container_name: prod
build:
context: .
dockerfile: Dockerfile

View File

@ -16,26 +16,35 @@ export interface Service {
slug: string;
title: string;
category: ServiceCategoryId;
outcome: string; // one-liner outcome under the H1
who: string[]; // "Who it's for"
outcomes?: string[]; // optional detailed outcomes list
outcome: string; // one-liner outcome under the H1
who: string[]; // "Who it's for"
outcomes?: string[]; // optional detailed outcomes list
deliverables: string[];
timeline: string; // e.g., "12 days"
price: string; // e.g., "€490" or "from €99/mo"
proof: string[]; // what you verify and how you show it
timeline: string; // e.g., "12 days"
price: string; // e.g., "€490" or "from €99/mo"
proof: string[]; // what you verify and how you show it
faq: FaqItem[];
relatedSlugs?: string[]; // internal cross-links
relatedSlugs?: string[]; // internal cross-links
metaTitle: string;
metaDescription: string;
}
export const SERVICE_CATEGORIES: Record<ServiceCategoryId, { label: string; anchor: string; }> = {
"infrastructure-devops": { label: "Infrastructure & DevOps", anchor: "infrastructure-devops" },
"web-performance": { label: "Web Performance & Reliability", anchor: "web-performance" },
"dev-platforms": { label: "Developer Platforms & Tooling", anchor: "dev-platforms" },
"migrations": { label: "Migrations & Refreshes", anchor: "migrations" },
"minecraft": { label: "Minecraft Services", anchor: "minecraft" },
"web-dev": { label: "Web Development", anchor: "web-dev" },
export const SERVICE_CATEGORIES: Record<
ServiceCategoryId,
{ label: string; anchor: string }
> = {
"infrastructure-devops": {
label: "Infrastructure & DevOps",
anchor: "infrastructure-devops",
},
"web-performance": {
label: "Website Reliability & Performance",
anchor: "web-performance",
},
"dev-platforms": { label: "Developer Platforms & Tooling", anchor: "dev-platforms" },
migrations: { label: "Migrations & Refreshes", anchor: "migrations" },
minecraft: { label: "Minecraft Services", anchor: "minecraft" },
"web-dev": { label: "Web Development", anchor: "web-dev" },
};
// ------- 23 services -------
@ -44,7 +53,7 @@ export const SERVICES: Service[] = [
slug: "vps-hardening-care",
title: "VPS Hardening & Care",
category: "infrastructure-devops",
outcome: "Secure, monitored, and backed-up VPS ready for production.",
outcome: "Secure, monitored, and backed-up VPS—foundation of the Reliability Stack™.",
who: [
"SMBs running WordPress/Next.js/apps on a VPS",
"Teams needing a baseline security and backup posture",
@ -61,22 +70,26 @@ export const SERVICES: Service[] = [
"Restore test report and monitoring dashboard screenshots",
],
faq: [
{ q: "Do you need root access?", a: "Yes, temporary privileged access is required to apply hardening, install monitoring, and verify backups. Access is removed post-delivery." },
{ q: "Which OS do you support?", a: "Debian/Ubuntu preferred. We can adapt to AlmaLinux/RHEL-family on request." },
{
q: "Do you need root access?",
a: "Yes, temporary privileged access is required to apply hardening, install monitoring, and verify backups. Access is removed post-delivery.",
},
{
q: "Which OS do you support?",
a: "Debian/Ubuntu preferred. We can adapt to AlmaLinux/RHEL-family on request.",
},
],
relatedSlugs: ["cloudflare-edge-hardening", "backup-disaster-recovery-drill"],
metaTitle: "VPS Hardening & Care — Van Hunen IT",
metaDescription: "Secure your VPS with hardening, monitoring, and tested backups. One-off setup or ongoing care.",
metaDescription:
"Secure your VPS with hardening, monitoring, and tested backups. One-off setup or ongoing care.",
},
{
slug: "dockerize-deploy",
title: "Dockerize & Deploy",
category: "infrastructure-devops",
outcome: "Your app runs in reproducible containers with minimal manual steps.",
who: [
"Teams moving from pets to containers",
"Startups needing consistent envs from dev to prod",
],
who: ["Teams moving from pets to containers", "Startups needing consistent envs from dev to prod"],
deliverables: [
"Dockerfiles & Compose, healthchecks, .env templating",
"Secrets handling, rollouts, runbook",
@ -84,83 +97,60 @@ export const SERVICES: Service[] = [
],
timeline: "23 days",
price: "€690",
proof: [
"Successful build/deploy logs with healthcheck passes",
"Rollback steps validated in a dry-run",
],
proof: ["Successful build/deploy logs with healthcheck passes", "Rollback steps validated in a dry-run"],
faq: [
{ q: "Can you support multiple services?", a: "Yes—compose a multi-service stack with networks, volumes, and per-service healthchecks." },
{ q: "Will you set up CI?", a: "CI/CD can be added. See Git-to-Prod CI/CD." },
],
relatedSlugs: ["git-to-prod-ci-cd", "observability-stack"],
metaTitle: "Dockerize & Deploy — Van Hunen IT",
metaDescription: "Containerize your app with Docker and deploy with confidence—healthchecks, secrets, and runbooks included.",
metaDescription:
"Containerize your app with Docker and deploy with confidence—healthchecks, secrets, and runbooks included.",
},
{
slug: "container-registry-setup",
title: "Private Container Registry Setup",
category: "infrastructure-devops",
outcome: "Secure image storage and CI/CD-friendly workflows.",
who: [
"Teams needing private images with access control",
"Orgs adopting container scanning & retention policies",
],
deliverables: [
"GHCR/Harbor registry, RBAC & tokens",
"Retention & vulnerability scanning",
"CI push/pull integration docs",
],
who: ["Teams needing private images with access control", "Orgs adopting container scanning & retention policies"],
deliverables: ["GHCR/Harbor registry, RBAC & tokens", "Retention & vulnerability scanning", "CI push/pull integration docs"],
timeline: "12 days",
price: "€490",
proof: [
"Policy & RBAC screenshots",
"Pipeline run showing signed/pushed images",
],
proof: ["Policy & RBAC screenshots", "Pipeline run showing signed/pushed images"],
faq: [
{ q: "Do you support Harbor on-prem?", a: "Yes, Harbor on VPS/K8s with TLS and persistence." },
{ q: "Image signing?", a: "Cosign support can be added on request." },
],
relatedSlugs: ["git-to-prod-ci-cd", "dockerize-deploy"],
metaTitle: "Private Container Registry — Van Hunen IT",
metaDescription: "Set up a private, secure container registry with RBAC, retention, and CI integration.",
metaDescription:
"Set up a private, secure container registry with RBAC, retention, and CI integration.",
},
{
slug: "k3s-kubernetes-cluster",
title: "k3s/Kubernetes Cluster in a Day",
category: "infrastructure-devops",
outcome: "Production-ready k3s/K8s with ingress, TLS, and app namespaces.",
who: [
"Projects outgrowing Docker Compose",
"Teams needing multi-app isolation with ingress",
],
deliverables: [
"k3s/K8s install, ingress (Traefik/NGINX), TLS",
"Storage class, namespaces, RBAC",
"Example deployment & runbook",
],
who: ["Projects outgrowing Docker Compose", "Teams needing multi-app isolation with ingress"],
deliverables: ["k3s/K8s install, ingress (Traefik/NGINX), TLS", "Storage class, namespaces, RBAC", "Example deployment & runbook"],
timeline: "12 days",
price: "€890",
proof: [
"kubectl outputs validating health & RBAC",
"Ingress verification and SSL pass",
],
proof: ["kubectl outputs validating health & RBAC", "Ingress verification and SSL pass"],
faq: [
{ q: "Is this managed?", a: "We provision and hand over; optional care add-on available." },
{ q: "On which infra?", a: "Single VPS, multi-node, or cloud—sized to your load." },
],
relatedSlugs: ["observability-stack", "secrets-management", "staging-environment"],
metaTitle: "Kubernetes (k3s) in a Day — Van Hunen IT",
metaDescription: "Get a production-ready k3s/Kubernetes cluster with ingress, TLS, RBAC, and a sample app.",
metaDescription:
"Get a production-ready k3s/Kubernetes cluster with ingress, TLS, RBAC, and a sample app.",
},
{
slug: "git-to-prod-ci-cd",
title: "Git-to-Prod CI/CD",
category: "infrastructure-devops",
outcome: "Push to main → build → test → deploy.",
who: [
"Teams wanting predictable deployment pipelines",
"Shops standardizing environments and rollbacks",
],
who: ["Teams wanting predictable deployment pipelines", "Shops standardizing environments and rollbacks"],
deliverables: [
"Pipelines (GitHub Actions/Woodpecker)",
"Image build & tagging, environment promotion",
@ -168,167 +158,121 @@ export const SERVICES: Service[] = [
],
timeline: "2 days",
price: "€780",
proof: [
"Green pipeline run from commit to deploy",
"Rollback rehearsal recorded in logs",
],
proof: ["Green pipeline run from commit to deploy", "Rollback rehearsal recorded in logs"],
faq: [
{ q: "Do you support monorepos?", a: "Yes—matrix builds and targeted deploys." },
{ q: "Secrets in CI?", a: "We wire secure secrets management per provider." },
],
relatedSlugs: ["dockerize-deploy", "container-registry-setup"],
metaTitle: "CI/CD to Production — Van Hunen IT",
metaDescription: "Automated pipelines from commit to production with tagging, promotion, and rollbacks.",
metaDescription:
"Automated pipelines from commit to production with tagging, promotion, and rollbacks.",
},
{
slug: "observability-stack",
title: "Observability Stack (Prometheus/Grafana/Loki)",
category: "infrastructure-devops",
outcome: "Metrics, logs, and alerts you can act on.",
who: [
"Apps needing visibility and alerting",
"Teams consolidating logs and dashboards",
],
deliverables: [
"Prometheus metrics & alert rules",
"Grafana dashboards for app & infra",
"Loki log aggregation & retention",
],
who: ["Apps needing visibility and alerting", "Teams consolidating logs and dashboards"],
deliverables: ["Prometheus metrics & alert rules", "Grafana dashboards for app & infra", "Loki log aggregation & retention"],
timeline: "12 days",
price: "€740",
proof: [
"Dashboards with baseline SLOs",
"Test alert firing to Slack/Email",
],
proof: ["Dashboards with baseline SLOs", "Test alert firing to Slack/Email"],
faq: [
{ q: "Can you integrate with K8s?", a: "Yes, via exporters and service monitors." },
{ q: "Retention strategy?", a: "Right-sized for your VPS budget and compliance." },
],
relatedSlugs: ["k3s-kubernetes-cluster", "git-to-prod-ci-cd"],
metaTitle: "Observability Stack — Van Hunen IT",
metaDescription: "Prometheus, Grafana, and Loki set up with alerts and actionable dashboards.",
metaDescription:
"Prometheus, Grafana, and Loki set up with alerts and actionable dashboards.",
},
{
slug: "backup-disaster-recovery-drill",
title: "Backup & Disaster-Recovery Drill",
category: "infrastructure-devops",
outcome: "Verified restore path—not just backups.",
who: [
"Sites that never tested restore",
"Teams formalizing RPO/RTO targets",
],
deliverables: [
"Backup plan (files/db), encryption & rotation",
"Restore test with documented steps",
"RPO/RTO notes & recommendations",
],
who: ["Sites that never tested restore", "Teams formalizing RPO/RTO targets"],
deliverables: ["Backup plan (files/db), encryption & rotation", "Restore test with documented steps", "RPO/RTO notes & recommendations"],
timeline: "1 day",
price: "€490",
proof: [
"Restore demonstration on staging",
"Report with timings and gaps",
],
proof: ["Restore demonstration on staging", "Report with timings and gaps"],
faq: [
{ q: "Which databases?", a: "MySQL/MariaDB/Postgres supported; others on request." },
{ q: "Offsite options?", a: "S3-compatible storage or rsync to secondary VPS." },
],
relatedSlugs: ["vps-hardening-care", "website-care-plan"],
metaTitle: "Backup & DR Drill — Van Hunen IT",
metaDescription: "We plan, back up, and verify restores with a documented drill and RPO/RTO notes.",
metaDescription:
"We plan, back up, and verify restores with a documented drill and RPO/RTO notes.",
},
{
slug: "cloudflare-edge-hardening",
title: "Cloudflare Edge Hardening",
category: "infrastructure-devops",
outcome: "Lower TTFB, fewer bad bots, safer origins.",
who: [
"Sites facing spam/bot abuse or high TTFB",
"Teams needing sane edge security fast",
],
deliverables: [
"WAF & bot tuning, page rules, cache keys",
"Origin shielding, HTTP/3, rate limiting",
"TTFB and cache-hit improvements",
],
who: ["Sites facing spam/bot abuse or high TTFB", "Teams needing sane edge security fast"],
deliverables: ["WAF & bot tuning, page rules, cache keys", "Origin shielding, HTTP/3, rate limiting", "TTFB and cache-hit improvements"],
timeline: "1 day",
price: "€420",
proof: [
"Before/after WebPageTest/TTFB screenshots",
"WAF rule set export & notes",
],
proof: ["Before/after WebPageTest/TTFB screenshots", "WAF rule set export & notes"],
faq: [
{ q: "Pro/Business plan required?", a: "We work with Free+ plans; some features need Pro/Business." },
{ q: "Will it break APIs?", a: "Rules are staged and tested with allowlists where needed." },
],
relatedSlugs: ["vps-hardening-care", "core-web-vitals-sprint"],
metaTitle: "Cloudflare Edge Hardening — Van Hunen IT",
metaDescription: "Tune WAF, caching, and HTTP/3 to reduce TTFB and block abusive traffic.",
metaDescription:
"Tune WAF, caching, and HTTP/3 to reduce TTFB and block abusive traffic.",
},
{
slug: "core-web-vitals-sprint",
title: "Core Web Vitals Sprint",
category: "web-performance",
outcome: "CLS/LCP/INP into the green.",
who: [
"Marketing sites and shops with poor CWV",
"Next.js/WordPress teams needing a focused fix",
],
deliverables: [
"Image strategy (WebP/next/image), font loading",
"Script defers, critical CSS, caching headers",
"Before/after CWV report",
],
outcome: "CLS/LCP/INP into the green with measurable before/after.",
who: ["Marketing sites and shops with poor CWV", "Next.js/WordPress teams needing a focused fix"],
deliverables: ["Image strategy (WebP/next/image), font loading", "Script defers, critical CSS, caching headers", "Before/after CWV report"],
timeline: "23 days",
price: "€820",
proof: [
"Lighthouse/CrUX before vs after",
"Largest contentful paint assets diff",
],
proof: ["Lighthouse/CrUX before vs after", "Largest contentful paint assets diff"],
faq: [
{ q: "Will you change design?", a: "Only insofar as needed to stabilize layout and loading." },
{ q: "Third-party scripts?", a: "We reduce impact via defer/async and budgeting." },
],
relatedSlugs: ["cloudflare-edge-hardening", "website-care-plan"],
metaTitle: "Core Web Vitals Sprint — Van Hunen IT",
metaDescription: "Improve LCP/CLS/INP with image, font, and script strategy plus caching.",
metaDescription:
"Improve LCP/CLS/INP with image, font, and script strategy plus caching.",
},
{
slug: "website-care-plan",
title: "Website Care Plan",
category: "web-performance",
outcome: "Updated site with restore-on-demand and uptime eyes on.",
who: [
"SMBs wanting stable updates and monitoring",
"Teams without in-house ops",
],
outcome: "Updates, uptime watch, and verified restores—the Reliability Stack™ on autopilot.",
who: ["SMBs wanting stable updates and monitoring", "Teams without in-house ops"],
deliverables: [
"Updates, uptime monitoring, backups + monthly restore test",
"Incident credits and priority support",
"Security checks & reporting",
],
timeline: "Monthly",
price: "from €99/mo",
proof: [
"Monthly report & restore test evidence",
"Incident notes with timestamps",
],
price: "from €149/mo",
proof: ["Monthly report & restore test evidence", "Incident notes with timestamps"],
faq: [
{ q: "What platforms?", a: "WordPress, Next.js, Node backends; others on request." },
{ q: "SLA?", a: "Incident response windows depend on plan tier." },
],
relatedSlugs: ["backup-disaster-recovery-drill", "core-web-vitals-sprint"],
metaTitle: "Website Care Plan — Van Hunen IT",
metaDescription: "Monthly updates, uptime, and verified restores for peace of mind.",
metaDescription:
"Monthly updates, uptime, and verified restores for peace of mind.",
},
{
slug: "email-deliverability-pack",
title: "Secure Contact & Email Deliverability Pack",
category: "web-performance",
outcome: "Inbox-ready email + safe forms.",
who: [
"Domains landing in spam or failing DMARC",
"Sites receiving contact-form spam",
],
outcome: "Inbox-ready email, spoofing blocked, safer forms.",
who: ["Domains landing in spam or failing DMARC", "Sites receiving contact-form spam"],
deliverables: [
"SPF/DKIM/DMARC config & reports",
"Seed tests, alignment verification",
@ -336,167 +280,117 @@ export const SERVICES: Service[] = [
],
timeline: "12 days",
price: "€520",
proof: [
"Before/after seed test screenshots",
"DMARC alignment & provider screenshots",
],
proof: ["Before/after seed test screenshots", "DMARC alignment & provider screenshots"],
faq: [
{ q: "BIMI?", a: "Supported if you provide a valid SVG and VMC (optional)." },
{ q: "Multiple providers?", a: "Yes—ESP+transactional combos are supported." },
],
relatedSlugs: ["website-care-plan", "cloudflare-edge-hardening"],
metaTitle: "Email Deliverability Pack — Van Hunen IT",
metaDescription: "Fix spam issues with SPF/DKIM/DMARC, verified tests, and safer contact forms.",
metaDescription:
"Fix spam issues with SPF/DKIM/DMARC, verified tests, and safer contact forms.",
},
{
slug: "self-hosted-gitea-sso",
title: "Self-Hosted Git (Gitea) with SSO",
category: "dev-platforms",
outcome: "Private Git with teams and permissions.",
who: [
"Teams needing on-prem/private Git",
"Shops standardizing code workflows",
],
deliverables: [
"Gitea + runner, backup/restore",
"OAuth/SSO, protected branches",
"Repo templates & permissions",
],
who: ["Teams needing on-prem/private Git", "Shops standardizing code workflows"],
deliverables: ["Gitea + runner, backup/restore", "OAuth/SSO, protected branches", "Repo templates & permissions"],
timeline: "1 day",
price: "€460",
proof: [
"Admin settings & SSO validation",
"Backup/restore rehearsal log",
],
proof: ["Admin settings & SSO validation", "Backup/restore rehearsal log"],
faq: [
{ q: "Migrate from GitHub/GitLab?", a: "Yes—repositories and permissions where possible." },
{ q: "Runner support?", a: "Gitea Actions or Woodpecker runners on request." },
],
relatedSlugs: ["git-to-prod-ci-cd", "container-registry-setup"],
metaTitle: "Self-Hosted Gitea with SSO — Van Hunen IT",
metaDescription: "Own your source control with Gitea, SSO, backups, and runners.",
metaDescription:
"Own your source control with Gitea, SSO, backups, and runners.",
},
{
slug: "secrets-management",
title: "Secrets Management (SOPS/age or Sealed-Secrets)",
category: "dev-platforms",
outcome: "Safe secrets in Git and Kubernetes.",
who: [
"Teams committing .env by mistake",
"K8s users needing encrypted manifests",
],
deliverables: [
"SOPS/age or Sealed-Secrets setup",
"Key management & rotation policy",
"Usage examples & policy notes",
],
who: ["Teams committing .env by mistake", "K8s users needing encrypted manifests"],
deliverables: ["SOPS/age or Sealed-Secrets setup", "Key management & rotation policy", "Usage examples & policy notes"],
timeline: "0.51 day",
price: "€380",
proof: [
"Encrypted secrets in repo & decrypt flow",
"Rotation drill notes",
],
proof: ["Encrypted secrets in repo & decrypt flow", "Rotation drill notes"],
faq: [
{ q: "Which to choose?", a: "SOPS for Git-centric flow; Sealed-Secrets for cluster-centric flow." },
{ q: "CI integration?", a: "We wire CI to decrypt securely where needed." },
],
relatedSlugs: ["k3s-kubernetes-cluster", "git-to-prod-ci-cd"],
metaTitle: "Secrets Management — Van Hunen IT",
metaDescription: "Implement SOPS/age or Sealed-Secrets for safe secrets handling in Git/K8s.",
metaDescription:
"Implement SOPS/age or Sealed-Secrets for safe secrets handling in Git/K8s.",
},
{
slug: "staging-environment",
title: "Staging Environment on the Same VPS/Cluster",
category: "dev-platforms",
outcome: "Risk-free previews before prod.",
who: [
"Teams deploying without review",
"Sites needing UAT previews",
],
deliverables: [
"Staging namespace/compose stack",
"Preview URLs & deploy gates",
"Masked data & access controls",
],
who: ["Teams deploying without review", "Sites needing UAT previews"],
deliverables: ["Staging namespace/compose stack", "Preview URLs & deploy gates", "Masked data & access controls"],
timeline: "1 day",
price: "€520",
proof: [
"Preview deployment validation",
"Access restricted and logged",
],
proof: ["Preview deployment validation", "Access restricted and logged"],
faq: [
{ q: "Separate VPS needed?", a: "Not required; we can isolate on the same host if resources allow." },
{ q: "Data masking?", a: "We provide safe anonymization for staging data." },
],
relatedSlugs: ["git-to-prod-ci-cd", "dockerize-deploy"],
metaTitle: "Staging Environment — Van Hunen IT",
metaDescription: "Add a staging environment with preview URLs and deploy gates.",
metaDescription:
"Add a staging environment with preview URLs and deploy gates.",
},
{
slug: "server-app-migration",
title: "Server/App Migration to VPS/Kubernetes",
category: "migrations",
outcome: "Zero-to-minimal downtime move with rollback.",
who: [
"Teams changing hosts or platforms",
"Apps consolidating infra",
],
deliverables: [
"Inventory & plan, containerization if needed",
"DNS/cutover & rollback plan",
"Smoke tests & timed runbook",
],
who: ["Teams changing hosts or platforms", "Apps consolidating infra"],
deliverables: ["Inventory & plan, containerization if needed", "DNS/cutover & rollback plan", "Smoke tests & timed runbook"],
timeline: "24 days",
price: "€1,190",
proof: [
"Cutover timeline & metrics",
"Rollback rehearsal log",
],
proof: ["Cutover timeline & metrics", "Rollback rehearsal log"],
faq: [
{ q: "Can you migrate databases?", a: "Yes—logical or physical migration with validated checks." },
{ q: "Downtime window?", a: "We schedule low-impact windows and offer blue/green where possible." },
],
relatedSlugs: ["legacy-to-container-refresh", "k3s-kubernetes-cluster"],
metaTitle: "Server/App Migration — Van Hunen IT",
metaDescription: "Plan and execute migrations to VPS or Kubernetes with rollback protection.",
metaDescription:
"Plan and execute migrations to VPS or Kubernetes with rollback protection.",
},
{
slug: "legacy-to-container-refresh",
title: "Legacy to Container Refresh",
category: "migrations",
outcome: "From pets to cattle—documented and reproducible.",
who: [
"Legacy apps lacking deployment consistency",
"Teams modernizing delivery",
],
deliverables: [
"Dockerfiles & manifests",
"Healthchecks, backups, docs",
"Operational runbook",
],
who: ["Legacy apps lacking deployment consistency", "Teams modernizing delivery"],
deliverables: ["Dockerfiles & manifests", "Healthchecks, backups, docs", "Operational runbook"],
timeline: "23 days",
price: "€990",
proof: [
"Green healthchecks post-deploy",
"Disaster recovery walk-through",
],
proof: ["Green healthchecks post-deploy", "Disaster recovery walk-through"],
faq: [
{ q: "Unsupported stacks?", a: "We assess feasibility; some apps may need refactors." },
{ q: "Windows workloads?", a: "Case-by-case; Linux recommended for best results." },
],
relatedSlugs: ["dockerize-deploy", "git-to-prod-ci-cd"],
metaTitle: "Legacy to Container Refresh — Van Hunen IT",
metaDescription: "Containerize legacy apps with healthchecks, backups, and docs.",
metaDescription:
"Containerize legacy apps with healthchecks, backups, and docs.",
},
{
slug: "minecraft-managed-server",
title: "Managed Minecraft Server",
category: "minecraft",
outcome: "Fast, stable, and safe server on a dedicated VPS.",
who: [
"Communities, schools, creators",
"Small networks needing reliable ops",
],
who: ["Communities, schools, creators", "Small networks needing reliable ops"],
deliverables: [
"VPS sizing/hardening, Paper/Velocity setup",
"Auto-backups + restore test",
@ -504,200 +398,145 @@ export const SERVICES: Service[] = [
],
timeline: "Setup 1 day · Ongoing monthly",
price: "Starter €49/mo · Pro €99/mo · Network €199/mo (+ VPS)",
proof: [
"TPS baseline & timings report",
"Restore test proof & monitoring",
],
proof: ["TPS baseline & timings report", "Restore test proof & monitoring"],
faq: [
{ q: "Java or Bedrock?", a: "Java by default; Bedrock or Geyser support on request." },
{ q: "Modpacks?", a: "CurseForge/modded supported—resource-dependent." },
],
relatedSlugs: ["minecraft-performance-audit", "minecraft-plugin-development", "minecraft-monetization-pack"],
metaTitle: "Managed Minecraft Server — Van Hunen IT",
metaDescription: "Turnkey Minecraft hosting on a hardened VPS with backups and performance tuning.",
metaDescription:
"Turnkey Minecraft hosting on a hardened VPS with backups and performance tuning.",
},
{
slug: "minecraft-performance-audit",
title: "Minecraft Performance & Stability Audit",
category: "minecraft",
outcome: "Higher TPS, fewer crashes.",
who: [
"Servers with lag or frequent crashes",
"Owners scaling to more players",
],
deliverables: [
"Profiler run (Spark), timings analysis",
"JVM flags & plugin audit",
"Before/after TPS report",
],
who: ["Servers with lag or frequent crashes", "Owners scaling to more players"],
deliverables: ["Profiler run (Spark), timings analysis", "JVM flags & plugin audit", "Before/after TPS report"],
timeline: "1 day",
price: "€390",
proof: [
"Timings & Spark screenshots",
"Updated config diff & TPS before/after",
],
proof: ["Timings & Spark screenshots", "Updated config diff & TPS before/after"],
faq: [
{ q: "Supports Bungee/Velocity?", a: "Yes—networked setups supported." },
{ q: "Player cap increase?", a: "We optimize, then size infra appropriately." },
],
relatedSlugs: ["minecraft-managed-server"],
metaTitle: "Minecraft Performance Audit — Van Hunen IT",
metaDescription: "Fix lag with timings, JVM flags, and plugin optimizations plus TPS reporting.",
metaDescription:
"Fix lag with timings, JVM flags, and plugin optimizations plus TPS reporting.",
},
{
slug: "minecraft-plugin-development",
title: "Custom Minecraft Plugin Development",
category: "minecraft",
outcome: "Features tailored to your server/community.",
who: [
"Servers needing unique mechanics",
"Creators monetizing custom content",
],
deliverables: [
"Spec, plugin build & tests",
"Config & permissions",
"Maintenance window",
],
who: ["Servers needing unique mechanics", "Creators monetizing custom content"],
deliverables: ["Spec, plugin build & tests", "Config & permissions", "Maintenance window"],
timeline: "From 37 days",
price: "€85/hr or fixed from €650",
proof: [
"Feature demo & test suite run",
"Config docs and changelog",
],
proof: ["Feature demo & test suite run", "Config docs and changelog"],
faq: [
{ q: "Source code ownership?", a: "You own it after payment unless agreed otherwise (private repo transfer included)." },
{ q: "API compatibility?", a: "Paper API targeted; cross-version support is scoped case-by-case." },
],
relatedSlugs: ["minecraft-managed-server", "minecraft-monetization-pack"],
metaTitle: "Minecraft Plugin Development — Van Hunen IT",
metaDescription: "Build custom Paper/Spigot plugins with tests, configs, and docs.",
metaDescription:
"Build custom Paper/Spigot plugins with tests, configs, and docs.",
},
{
slug: "minecraft-monetization-pack",
title: "Creator Monetization Pack (Tebex)",
category: "minecraft",
outcome: "Clean store + safe donations.",
who: [
"Servers adding a store",
"Creators formalizing monetization",
],
deliverables: [
"Tebex setup & product catalog",
"Rank automation & receipts",
"Anti-fraud notes & webhooks",
],
who: ["Servers adding a store", "Creators formalizing monetization"],
deliverables: ["Tebex setup & product catalog", "Rank automation & receipts", "Anti-fraud notes & webhooks"],
timeline: "1 day",
price: "€420",
proof: [
"Test purchase flow",
"Webhook logs to grants",
],
proof: ["Test purchase flow", "Webhook logs to grants"],
faq: [
{ q: "Compliance?", a: "We avoid P2W violations and follow platform rules." },
{ q: "Branding?", a: "Store theme aligned with your site and server style." },
],
relatedSlugs: ["minecraft-plugin-development", "minecraft-managed-server"],
metaTitle: "Minecraft Monetization Pack — Van Hunen IT",
metaDescription: "Set up Tebex with products, ranks, and safe donation flows.",
metaDescription:
"Set up Tebex with products, ranks, and safe donation flows.",
},
{
slug: "quick-launch-website",
title: "Quick-Launch Website (Next.js)",
category: "web-dev",
outcome: "Fast, SEO-ready site in days.",
who: [
"SMBs needing a credible web presence fast",
"Consultants/creators launching offers",
],
deliverables: [
"57 sections, forms, OG/Twitter cards",
"Analytics & deploy to your VPS",
"Basic SEO & sitemap",
],
who: ["SMBs needing a credible web presence fast", "Consultants/creators launching offers"],
deliverables: ["57 sections, forms, OG/Twitter cards", "Analytics & deploy to your VPS", "Basic SEO & sitemap"],
timeline: "57 days",
price: "€2,450",
proof: [
"Lighthouse pass for basics",
"Deployed site link & repo handover",
],
proof: ["Lighthouse pass for basics", "Deployed site link & repo handover"],
faq: [
{ q: "Copy & assets?", a: "We provide a brief and templates; you can supply or we refine." },
{ q: "CMS?", a: "Optional—see Headless CMS Setup." },
],
relatedSlugs: ["headless-cms-setup", "core-web-vitals-sprint"],
metaTitle: "Quick-Launch Website (Next.js) — Van Hunen IT",
metaDescription: "Launch a fast, SEO-ready site in days with forms, analytics, and deployment.",
metaDescription:
"Launch a fast, SEO-ready site in days with forms, analytics, and deployment.",
},
{
slug: "headless-cms-setup",
title: "Headless CMS Setup (Ghost/Strapi)",
category: "web-dev",
outcome: "Non-tech content updates without redeploys.",
who: [
"Teams wanting easy publishing",
"Sites separating content from code",
],
deliverables: [
"CMS install & roles",
"Content model & CI/CD",
"Image optimization & docs",
],
who: ["Teams wanting easy publishing", "Sites separating content from code"],
deliverables: ["CMS install & roles", "Content model & CI/CD", "Image optimization & docs"],
timeline: "23 days",
price: "€1,190",
proof: [
"Editor demo & role permissions",
"Publishing pipeline test",
],
proof: ["Editor demo & role permissions", "Publishing pipeline test"],
faq: [
{ q: "Migration from WordPress?", a: "We can import and map key content types." },
{ q: "Auth & SSO?", a: "SSO/OAuth possible depending on CMS chosen." },
],
relatedSlugs: ["quick-launch-website", "website-care-plan"],
metaTitle: "Headless CMS Setup — Van Hunen IT",
metaDescription: "Install and configure Ghost/Strapi with roles, CI/CD, and image optimization.",
metaDescription:
"Install and configure Ghost/Strapi with roles, CI/CD, and image optimization.",
},
{
slug: "security-compliance-baseline",
title: "Security & Compliance Baseline (GDPR-aware)",
category: "web-dev",
outcome: "Reasonable security for small teams.",
who: [
"SMBs formalizing access and logging",
"Teams preparing for audits",
],
deliverables: [
"Password policy, 2FA & least-privilege",
"Audit logging & data retention",
"Incident checklist & drills",
],
who: ["SMBs formalizing access and logging", "Teams preparing for audits"],
deliverables: ["Password policy, 2FA & least-privilege", "Audit logging & data retention", "Incident checklist & drills"],
timeline: "12 days",
price: "€740",
proof: [
"Policy documents & checklists",
"Access review and logging tests",
],
proof: ["Policy documents & checklists", "Access review and logging tests"],
faq: [
{ q: "Covers DPIA?", a: "We provide input; legal sign-off remains with your DPO/counsel." },
{ q: "Tooling?", a: "We match to your stack—SaaS or self-hosted where appropriate." },
],
relatedSlugs: ["vps-hardening-care", "backup-disaster-recovery-drill"],
metaTitle: "Security & Compliance Baseline — Van Hunen IT",
metaDescription: "Implement practical security policies, logging, and incident readiness for SMBs.",
metaDescription:
"Implement practical security policies, logging, and incident readiness for SMBs.",
},
// (Web performance group already added 3; infra/devops group has 7; dev-platforms 3; migrations 2; minecraft 4; web-dev 3)
];
export function getAllServices(): Service[] {
// Keep a stable order by category then title
return [...SERVICES].sort((a, b) =>
a.category === b.category ? a.title.localeCompare(b.title) : a.category.localeCompare(b.category)
a.category === b.category
? a.title.localeCompare(b.title)
: a.category.localeCompare(b.category)
);
}
export function getServiceBySlug(slug: string): Service | undefined {
return SERVICES.find(s => s.slug === slug);
return SERVICES.find((s) => s.slug === slug);
}
export function getServicesByCategory(category: ServiceCategoryId): Service[] {
return getAllServices().filter(s => s.category === category);
return getAllServices().filter((s) => s.category === category);
}

View File

@ -16,6 +16,7 @@
"node": ">=20 <23"
},
"dependencies": {
"clsx": "^2.1.1",
"framer-motion": "^12.23.24",
"next": "14.2.13",
"react": "18.3.1",

View File

@ -8,6 +8,9 @@ importers:
.:
dependencies:
clsx:
specifier: ^2.1.1
version: 2.1.1
framer-motion:
specifier: ^12.23.24
version: 12.23.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -544,6 +547,10 @@ packages:
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@ -2296,6 +2303,8 @@ snapshots:
client-only@0.0.1: {}
clsx@2.1.1: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4