Copy optimization
This commit is contained in:
parent
1f12fb739b
commit
789678a0c1
298
web/app/page.tsx
298
web/app/page.tsx
|
|
@ -1,19 +1,16 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Hero from "@/components/Hero";
|
|
||||||
import Process from "@/components/Process";
|
|
||||||
import Pricing from "@/components/Pricing";
|
import Pricing from "@/components/Pricing";
|
||||||
import Testimonials from "@/components/Testimonials";
|
import Testimonials from "@/components/Testimonials";
|
||||||
import FAQ from "@/components/FAQ";
|
import FAQ from "@/components/FAQ";
|
||||||
import CTA from "@/components/CTA";
|
|
||||||
import { SERVICE_CATEGORIES } from "@/lib/services";
|
|
||||||
import { plans } from "@/lib/pricing";
|
import { plans } from "@/lib/pricing";
|
||||||
import { site } from "@/lib/site";
|
import { site } from "@/lib/site";
|
||||||
import JsonLd from "@/components/JsonLd";
|
import JsonLd from "@/components/JsonLd";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Productized IT for SMBs",
|
title: "SMB Website Reliability & Deliverability",
|
||||||
description: site.description,
|
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() {
|
export default function HomePage() {
|
||||||
|
|
@ -26,89 +23,142 @@ export default function HomePage() {
|
||||||
sameAs: site.org.sameAs,
|
sameAs: site.org.sameAs,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const faqItems = [
|
||||||
|
{
|
||||||
|
q: "How fast can you start?",
|
||||||
|
a: "Most sprints start within 2–3 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: "What’s your guarantee?",
|
||||||
|
a: "We show proof of outcomes. If the agreed scope isn’t 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 = {
|
const faqLd = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "FAQPage",
|
"@type": "FAQPage",
|
||||||
mainEntity: [
|
mainEntity: faqItems.map((i) => ({
|
||||||
{
|
"@type": "Question",
|
||||||
"@type": "Question",
|
name: i.q,
|
||||||
name: "How fast can you start?",
|
acceptedAnswer: { "@type": "Answer", text: i.a },
|
||||||
acceptedAnswer: {
|
})),
|
||||||
"@type": "Answer",
|
|
||||||
text: "Most sprints start within 2–3 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.",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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 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 deliverability—implemented 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 --- */}
|
<div className="mt-8 flex flex-col sm:flex-row gap-3 justify-center">
|
||||||
<section className="relative overflow-hidden border-b border-neutral-200 dark:border-neutral-800">
|
<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">
|
<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">
|
<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>
|
</h2>
|
||||||
<p className="mt-5 text-lg text-neutral-700 dark:text-neutral-300 max-w-2xl mx-auto">
|
<p className="mt-5 text-lg text-neutral-700 dark:text-neutral-300 max-w-3xl mx-auto">
|
||||||
Fixed-scope sprints and managed plans for infrastructure, performance, and development.
|
Four layers that make websites dependable. Implemented in a 1–3 day sprint, proven with before/after data.
|
||||||
Each category leads to specialized services tailored to small and mid-sized teams.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="container mx-auto max-w-6xl px-4 pb-20">
|
<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">
|
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
{Object.entries(SERVICE_CATEGORIES).map(([id, cat], i) => (
|
{[
|
||||||
|
{
|
||||||
|
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
|
<article
|
||||||
key={id}
|
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"
|
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"
|
||||||
style={{ animation: `fadeInUp 0.6s ease forwards`, animationDelay: `${i * 80}ms` }}
|
|
||||||
>
|
>
|
||||||
<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" />
|
<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">
|
<h3 className="text-xl font-semibold tracking-tight text-neutral-900 dark:text-white">
|
||||||
{cat.label}
|
{card.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-3 text-sm text-neutral-700 dark:text-neutral-300">
|
<p className="mt-3 text-sm text-neutral-700 dark:text-neutral-300">{card.body}</p>
|
||||||
{categoryDescriptions[id] ?? ""}
|
|
||||||
</p>
|
|
||||||
<Link
|
<Link
|
||||||
href={`/services#${cat.anchor}`}
|
href={card.href}
|
||||||
className="mt-5 inline-flex items-center text-sm font-medium text-blue-600 hover:underline"
|
className="mt-5 inline-flex items-center text-sm font-medium text-blue-600 hover:underline"
|
||||||
>
|
>
|
||||||
Explore services →
|
Explore →
|
||||||
</Link>
|
</Link>
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
|
|
@ -125,17 +175,76 @@ export default function HomePage() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* --- Process --- */}
|
{/* --- HOW IT WORKS (3 STEPS) --- */}
|
||||||
<Process />
|
<section className="relative py-24">
|
||||||
|
|
||||||
{/* --- 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">
|
<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 results—then 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>
|
</div>
|
||||||
</section>
|
</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">
|
<section className="relative py-24 border-t border-neutral-200 dark:border-neutral-800">
|
||||||
<div className="container mx-auto max-w-6xl px-4">
|
<div className="container mx-auto max-w-6xl px-4">
|
||||||
<Testimonials />
|
<Testimonials />
|
||||||
|
|
@ -145,40 +254,33 @@ export default function HomePage() {
|
||||||
{/* --- FAQ --- */}
|
{/* --- 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">
|
<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">
|
<div className="container mx-auto max-w-4xl px-4">
|
||||||
<FAQ
|
<FAQ items={faqItems} />
|
||||||
items={[
|
|
||||||
{
|
|
||||||
q: "How fast can you start?",
|
|
||||||
a: "Most sprints start within 2–3 business days after scope confirmation.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
q: "Do you work under NDA?",
|
|
||||||
a: "Yes—mutual NDA available on request; we keep credentials least-privilege.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
q: "What’s your guarantee?",
|
|
||||||
a: "We show proof of outcomes. If scope isn’t met, we make it right or refund the sprint fee.",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<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">
|
<div className="container mx-auto max-w-4xl px-6">
|
||||||
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight">
|
<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>
|
</h2>
|
||||||
<p className="mt-4 text-lg text-blue-100">
|
<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. We’ll prove the results.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<div className="mt-8 flex flex-col sm:flex-row gap-3 justify-center">
|
||||||
href="/contact"
|
<Link
|
||||||
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"
|
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"
|
||||||
Contact Us
|
>
|
||||||
</Link>
|
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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
118
web/app/pricing/page.tsx
Normal file
118
web/app/pricing/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// app/services/page.tsx
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { getAllServices, SERVICE_CATEGORIES, type ServiceCategoryId } from "@/lib/services";
|
import { getAllServices, SERVICE_CATEGORIES, type ServiceCategoryId } from "@/lib/services";
|
||||||
import ServiceCard from "@/components/ServiceCard";
|
import ServiceCard from "@/components/ServiceCard";
|
||||||
|
|
@ -6,30 +7,43 @@ import Link from "next/link";
|
||||||
export const revalidate = 86400;
|
export const revalidate = 86400;
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Services — Van Hunen IT",
|
title: "Services — Website Reliability for SMBs | Van Hunen IT",
|
||||||
description:
|
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" },
|
alternates: { canonical: "/services" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ServicesPage() {
|
export default function ServicesPage() {
|
||||||
const services = getAllServices();
|
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 (
|
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 --- */}
|
{/* --- Hero Header --- */}
|
||||||
<section className="relative overflow-hidden border-b border-neutral-200 dark:border-neutral-800">
|
<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">
|
<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">
|
<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>
|
</h1>
|
||||||
<p className="mt-5 text-lg text-neutral-700 dark:text-neutral-300 max-w-2xl mx-auto">
|
<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{" "}
|
Fixed-scope sprints and care plans for{" "}
|
||||||
<strong>VPS</strong>, <strong>Docker</strong>, <strong>Kubernetes</strong>,{" "}
|
<strong>email deliverability</strong>, <strong>Cloudflare edge security & speed</strong>,{" "}
|
||||||
<strong>Cloudflare</strong>, <strong>Core Web Vitals</strong>, and{" "}
|
<strong>backups with restore tests</strong>, and <strong>uptime watch</strong>. Clear outcomes, flat pricing,
|
||||||
<strong>Minecraft</strong>. Clear outcomes, flat pricing, and proof you can keep.
|
and before/after proof you can keep.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-8 flex flex-wrap justify-center gap-3">
|
<div className="mt-8 flex flex-wrap justify-center gap-3">
|
||||||
{categories.map((id) => (
|
{categories.map((id) => (
|
||||||
<a
|
<a
|
||||||
|
|
@ -41,6 +55,21 @@ export default function ServicesPage() {
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -50,31 +79,33 @@ export default function ServicesPage() {
|
||||||
const list = services.filter((s) => s.category === id);
|
const list = services.filter((s) => s.category === id);
|
||||||
if (!list.length) return null;
|
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 (
|
return (
|
||||||
<section
|
<section
|
||||||
key={id}
|
key={id}
|
||||||
id={SERVICE_CATEGORIES[id].anchor}
|
id={SERVICE_CATEGORIES[id].anchor}
|
||||||
aria-labelledby={`${id}-heading`}
|
aria-labelledby={headingId}
|
||||||
className={`relative py-16 scroll-mt-24 ${
|
className={sectionClasses}
|
||||||
index % 2 === 0
|
|
||||||
? "bg-white dark:bg-neutral-900/50"
|
|
||||||
: "bg-neutral-50 dark:bg-neutral-900/30"
|
|
||||||
} rounded-2xl mb-12 shadow-sm`}
|
|
||||||
>
|
>
|
||||||
<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="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="px-4 sm:px-8">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
|
||||||
<h2
|
<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"
|
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}
|
{SERVICE_CATEGORIES[id].label}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<Link
|
<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"
|
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 ↑
|
Jump to top ↑
|
||||||
</Link>
|
</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">
|
<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">
|
<div className="container mx-auto max-w-4xl px-6">
|
||||||
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight">
|
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight">
|
||||||
Ready to optimize your IT infrastructure?
|
Ready to improve reliability and deliverability?
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-lg text-blue-100">
|
<p className="mt-4 text-lg text-blue-100">
|
||||||
Get in touch to discuss your goals — we’ll help you choose the right sprint or managed plan.
|
Tell us about your site. We’ll recommend the right Fix Sprint or Care plan and show you the expected
|
||||||
|
before/after.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<div className="mt-8 flex items-center justify-center gap-3">
|
||||||
href="/contact"
|
<Link
|
||||||
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"
|
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"
|
||||||
Contact Us
|
>
|
||||||
</Link>
|
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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,68 @@
|
||||||
type QA = { q: string; a: string };
|
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[] }) {
|
export default function FAQ({ items }: { items: QA[] }) {
|
||||||
return (
|
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">
|
<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
|
Frequently Asked Questions
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="mt-10 space-y-4">
|
<div className="mt-10 space-y-4">
|
||||||
{items.map((x, i) => (
|
{items.map((x, i) => {
|
||||||
<details
|
const id = slugify(x.q) || `faq-${i}`;
|
||||||
key={i}
|
return (
|
||||||
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"
|
<details
|
||||||
>
|
key={id}
|
||||||
<summary className="cursor-pointer text-lg font-medium text-neutral-900 dark:text-white flex items-center justify-between">
|
id={id}
|
||||||
{x.q}
|
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"
|
||||||
<span className="transition-transform group-open:rotate-180">⌄</span>
|
>
|
||||||
</summary>
|
<summary className="cursor-pointer text-lg font-medium text-neutral-900 dark:text-white flex items-center justify-between">
|
||||||
<p className="mt-3 text-sm text-neutral-700 dark:text-neutral-300">{x.a}</p>
|
<span>{x.q}</span>
|
||||||
</details>
|
<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>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,9 @@ export default function Header() {
|
||||||
<nav aria-label="Main" className="flex items-center gap-1 text-sm">
|
<nav aria-label="Main" className="flex items-center gap-1 text-sm">
|
||||||
{[
|
{[
|
||||||
{ href: "/services", label: "Services" },
|
{ href: "/services", label: "Services" },
|
||||||
{ href: "/free", label: "Free tools" },
|
// { href: "/free", label: "Free tools" },
|
||||||
{ href: "/contact", label: "Contact" },
|
{ href: "/pricing", label: "Pricing" },
|
||||||
|
{ href: "/contact", label: "Contact" }
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
|
|
|
||||||
|
|
@ -3,45 +3,91 @@ import type { Plan } from "@/lib/pricing";
|
||||||
|
|
||||||
export default function Pricing({ plans }: { plans: Plan[] }) {
|
export default function Pricing({ plans }: { plans: Plan[] }) {
|
||||||
return (
|
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">
|
<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">
|
<h2
|
||||||
Simple, flat pricing
|
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>
|
</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">
|
<div className="mt-12 grid gap-8 sm:grid-cols-3">
|
||||||
{plans.map((p, i) => (
|
{plans.map((p, i) => (
|
||||||
<div
|
<article
|
||||||
key={p.id}
|
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` }}
|
style={{ animationDelay: `${i * 100}ms` }}
|
||||||
>
|
>
|
||||||
{p.popular && (
|
{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
|
Most popular
|
||||||
</div>
|
</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="mt-3 flex items-baseline justify-center gap-1">
|
||||||
<div className="text-4xl font-bold text-neutral-900 dark:text-white">{p.price}</div>
|
<div className="text-4xl font-bold text-neutral-900 dark:text-white">{p.price}</div>
|
||||||
{p.periodicity && (
|
{p.periodicity && (
|
||||||
<div className="text-sm text-neutral-500 dark:text-neutral-400">{p.periodicity}</div>
|
<div className="text-sm text-neutral-500 dark:text-neutral-400">{p.periodicity}</div>
|
||||||
)}
|
)}
|
||||||
</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">
|
<ul className="mt-5 space-y-2 text-sm text-neutral-700 dark:text-neutral-300 text-left">
|
||||||
{p.features.map((f, idx) => (
|
{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>
|
</ul>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={p.cta.href}
|
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}
|
{p.cta.label}
|
||||||
</Link>
|
</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>
|
||||||
|
|
||||||
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,69 @@
|
||||||
export default function Testimonials() {
|
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 (
|
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">
|
<section
|
||||||
<h2 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
aria-labelledby="testimonials-heading"
|
||||||
What clients say
|
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"
|
||||||
</h2>
|
>
|
||||||
<p className="mt-6 text-neutral-600 dark:text-neutral-300">
|
<div className="container mx-auto max-w-6xl px-4">
|
||||||
Testimonials coming soon.
|
<h2
|
||||||
</p>
|
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>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
39
web/components/pricing/BillingToggle.tsx
Normal file
39
web/components/pricing/BillingToggle.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
89
web/components/pricing/ComparisonTable.tsx
Normal file
89
web/components/pricing/ComparisonTable.tsx
Normal 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">What’s 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
web/components/pricing/FAQ.tsx
Normal file
35
web/components/pricing/FAQ.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
web/components/pricing/Guarantee.tsx
Normal file
18
web/components/pricing/Guarantee.tsx
Normal 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 don’t deliver your agreed outcomes in the first month,
|
||||||
|
we’ll 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
85
web/components/pricing/PlanCard.tsx
Normal file
85
web/components/pricing/PlanCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
87
web/components/pricing/PlanRecommender.tsx
Normal file
87
web/components/pricing/PlanRecommender.tsx
Normal 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 we’ll 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
web/components/pricing/PricingTable.tsx
Normal file
22
web/components/pricing/PricingTable.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
147
web/components/pricing/ROICalculator.tsx
Normal file
147
web/components/pricing/ROICalculator.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
web/components/pricing/icons.tsx
Normal file
25
web/components/pricing/icons.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
web/components/pricing/money.ts
Normal file
13
web/components/pricing/money.ts
Normal 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));
|
||||||
|
}
|
||||||
14
web/components/pricing/types.ts
Normal file
14
web/components/pricing/types.ts
Normal 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;
|
||||||
|
};
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
version: "3.9"
|
version: "3.9"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
vanhunen-it-dev:
|
dev:
|
||||||
container_name: vanhunen-it
|
container_name: dev
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
|
@ -19,8 +19,8 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- devnet
|
- devnet
|
||||||
|
|
||||||
vanhunen-it-prod:
|
prod:
|
||||||
container_name: vanhunen-it-prod
|
container_name: prod
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
|
|
||||||
|
|
@ -16,26 +16,35 @@ export interface Service {
|
||||||
slug: string;
|
slug: string;
|
||||||
title: string;
|
title: string;
|
||||||
category: ServiceCategoryId;
|
category: ServiceCategoryId;
|
||||||
outcome: string; // one-liner outcome under the H1
|
outcome: string; // one-liner outcome under the H1
|
||||||
who: string[]; // "Who it's for"
|
who: string[]; // "Who it's for"
|
||||||
outcomes?: string[]; // optional detailed outcomes list
|
outcomes?: string[]; // optional detailed outcomes list
|
||||||
deliverables: string[];
|
deliverables: string[];
|
||||||
timeline: string; // e.g., "1–2 days"
|
timeline: string; // e.g., "1–2 days"
|
||||||
price: string; // e.g., "€490" or "from €99/mo"
|
price: string; // e.g., "€490" or "from €99/mo"
|
||||||
proof: string[]; // what you verify and how you show it
|
proof: string[]; // what you verify and how you show it
|
||||||
faq: FaqItem[];
|
faq: FaqItem[];
|
||||||
relatedSlugs?: string[]; // internal cross-links
|
relatedSlugs?: string[]; // internal cross-links
|
||||||
metaTitle: string;
|
metaTitle: string;
|
||||||
metaDescription: string;
|
metaDescription: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SERVICE_CATEGORIES: Record<ServiceCategoryId, { label: string; anchor: string; }> = {
|
export const SERVICE_CATEGORIES: Record<
|
||||||
"infrastructure-devops": { label: "Infrastructure & DevOps", anchor: "infrastructure-devops" },
|
ServiceCategoryId,
|
||||||
"web-performance": { label: "Web Performance & Reliability", anchor: "web-performance" },
|
{ label: string; anchor: string }
|
||||||
"dev-platforms": { label: "Developer Platforms & Tooling", anchor: "dev-platforms" },
|
> = {
|
||||||
"migrations": { label: "Migrations & Refreshes", anchor: "migrations" },
|
"infrastructure-devops": {
|
||||||
"minecraft": { label: "Minecraft Services", anchor: "minecraft" },
|
label: "Infrastructure & DevOps",
|
||||||
"web-dev": { label: "Web Development", anchor: "web-dev" },
|
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 -------
|
// ------- 23 services -------
|
||||||
|
|
@ -44,7 +53,7 @@ export const SERVICES: Service[] = [
|
||||||
slug: "vps-hardening-care",
|
slug: "vps-hardening-care",
|
||||||
title: "VPS Hardening & Care",
|
title: "VPS Hardening & Care",
|
||||||
category: "infrastructure-devops",
|
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: [
|
who: [
|
||||||
"SMBs running WordPress/Next.js/apps on a VPS",
|
"SMBs running WordPress/Next.js/apps on a VPS",
|
||||||
"Teams needing a baseline security and backup posture",
|
"Teams needing a baseline security and backup posture",
|
||||||
|
|
@ -61,22 +70,26 @@ export const SERVICES: Service[] = [
|
||||||
"Restore test report and monitoring dashboard screenshots",
|
"Restore test report and monitoring dashboard screenshots",
|
||||||
],
|
],
|
||||||
faq: [
|
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"],
|
relatedSlugs: ["cloudflare-edge-hardening", "backup-disaster-recovery-drill"],
|
||||||
metaTitle: "VPS Hardening & Care — Van Hunen IT",
|
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",
|
slug: "dockerize-deploy",
|
||||||
title: "Dockerize & Deploy",
|
title: "Dockerize & Deploy",
|
||||||
category: "infrastructure-devops",
|
category: "infrastructure-devops",
|
||||||
outcome: "Your app runs in reproducible containers with minimal manual steps.",
|
outcome: "Your app runs in reproducible containers with minimal manual steps.",
|
||||||
who: [
|
who: ["Teams moving from pets to containers", "Startups needing consistent envs from dev to prod"],
|
||||||
"Teams moving from pets to containers",
|
|
||||||
"Startups needing consistent envs from dev to prod",
|
|
||||||
],
|
|
||||||
deliverables: [
|
deliverables: [
|
||||||
"Dockerfiles & Compose, healthchecks, .env templating",
|
"Dockerfiles & Compose, healthchecks, .env templating",
|
||||||
"Secrets handling, rollouts, runbook",
|
"Secrets handling, rollouts, runbook",
|
||||||
|
|
@ -84,83 +97,60 @@ export const SERVICES: Service[] = [
|
||||||
],
|
],
|
||||||
timeline: "2–3 days",
|
timeline: "2–3 days",
|
||||||
price: "€690",
|
price: "€690",
|
||||||
proof: [
|
proof: ["Successful build/deploy logs with healthcheck passes", "Rollback steps validated in a dry-run"],
|
||||||
"Successful build/deploy logs with healthcheck passes",
|
|
||||||
"Rollback steps validated in a dry-run",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Can you support multiple services?", a: "Yes—compose a multi-service stack with networks, volumes, and per-service healthchecks." },
|
{ 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." },
|
{ 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"],
|
relatedSlugs: ["git-to-prod-ci-cd", "observability-stack"],
|
||||||
metaTitle: "Dockerize & Deploy — Van Hunen IT",
|
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",
|
slug: "container-registry-setup",
|
||||||
title: "Private Container Registry Setup",
|
title: "Private Container Registry Setup",
|
||||||
category: "infrastructure-devops",
|
category: "infrastructure-devops",
|
||||||
outcome: "Secure image storage and CI/CD-friendly workflows.",
|
outcome: "Secure image storage and CI/CD-friendly workflows.",
|
||||||
who: [
|
who: ["Teams needing private images with access control", "Orgs adopting container scanning & retention policies"],
|
||||||
"Teams needing private images with access control",
|
deliverables: ["GHCR/Harbor registry, RBAC & tokens", "Retention & vulnerability scanning", "CI push/pull integration docs"],
|
||||||
"Orgs adopting container scanning & retention policies",
|
|
||||||
],
|
|
||||||
deliverables: [
|
|
||||||
"GHCR/Harbor registry, RBAC & tokens",
|
|
||||||
"Retention & vulnerability scanning",
|
|
||||||
"CI push/pull integration docs",
|
|
||||||
],
|
|
||||||
timeline: "1–2 days",
|
timeline: "1–2 days",
|
||||||
price: "€490",
|
price: "€490",
|
||||||
proof: [
|
proof: ["Policy & RBAC screenshots", "Pipeline run showing signed/pushed images"],
|
||||||
"Policy & RBAC screenshots",
|
|
||||||
"Pipeline run showing signed/pushed images",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Do you support Harbor on-prem?", a: "Yes, Harbor on VPS/K8s with TLS and persistence." },
|
{ 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." },
|
{ q: "Image signing?", a: "Cosign support can be added on request." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["git-to-prod-ci-cd", "dockerize-deploy"],
|
relatedSlugs: ["git-to-prod-ci-cd", "dockerize-deploy"],
|
||||||
metaTitle: "Private Container Registry — Van Hunen IT",
|
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",
|
slug: "k3s-kubernetes-cluster",
|
||||||
title: "k3s/Kubernetes Cluster in a Day",
|
title: "k3s/Kubernetes Cluster in a Day",
|
||||||
category: "infrastructure-devops",
|
category: "infrastructure-devops",
|
||||||
outcome: "Production-ready k3s/K8s with ingress, TLS, and app namespaces.",
|
outcome: "Production-ready k3s/K8s with ingress, TLS, and app namespaces.",
|
||||||
who: [
|
who: ["Projects outgrowing Docker Compose", "Teams needing multi-app isolation with ingress"],
|
||||||
"Projects outgrowing Docker Compose",
|
deliverables: ["k3s/K8s install, ingress (Traefik/NGINX), TLS", "Storage class, namespaces, RBAC", "Example deployment & runbook"],
|
||||||
"Teams needing multi-app isolation with ingress",
|
|
||||||
],
|
|
||||||
deliverables: [
|
|
||||||
"k3s/K8s install, ingress (Traefik/NGINX), TLS",
|
|
||||||
"Storage class, namespaces, RBAC",
|
|
||||||
"Example deployment & runbook",
|
|
||||||
],
|
|
||||||
timeline: "1–2 days",
|
timeline: "1–2 days",
|
||||||
price: "€890",
|
price: "€890",
|
||||||
proof: [
|
proof: ["kubectl outputs validating health & RBAC", "Ingress verification and SSL pass"],
|
||||||
"kubectl outputs validating health & RBAC",
|
|
||||||
"Ingress verification and SSL pass",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Is this managed?", a: "We provision and hand over; optional care add-on available." },
|
{ 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." },
|
{ q: "On which infra?", a: "Single VPS, multi-node, or cloud—sized to your load." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["observability-stack", "secrets-management", "staging-environment"],
|
relatedSlugs: ["observability-stack", "secrets-management", "staging-environment"],
|
||||||
metaTitle: "Kubernetes (k3s) in a Day — Van Hunen IT",
|
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",
|
slug: "git-to-prod-ci-cd",
|
||||||
title: "Git-to-Prod CI/CD",
|
title: "Git-to-Prod CI/CD",
|
||||||
category: "infrastructure-devops",
|
category: "infrastructure-devops",
|
||||||
outcome: "Push to main → build → test → deploy.",
|
outcome: "Push to main → build → test → deploy.",
|
||||||
who: [
|
who: ["Teams wanting predictable deployment pipelines", "Shops standardizing environments and rollbacks"],
|
||||||
"Teams wanting predictable deployment pipelines",
|
|
||||||
"Shops standardizing environments and rollbacks",
|
|
||||||
],
|
|
||||||
deliverables: [
|
deliverables: [
|
||||||
"Pipelines (GitHub Actions/Woodpecker)",
|
"Pipelines (GitHub Actions/Woodpecker)",
|
||||||
"Image build & tagging, environment promotion",
|
"Image build & tagging, environment promotion",
|
||||||
|
|
@ -168,167 +158,121 @@ export const SERVICES: Service[] = [
|
||||||
],
|
],
|
||||||
timeline: "2 days",
|
timeline: "2 days",
|
||||||
price: "€780",
|
price: "€780",
|
||||||
proof: [
|
proof: ["Green pipeline run from commit to deploy", "Rollback rehearsal recorded in logs"],
|
||||||
"Green pipeline run from commit to deploy",
|
|
||||||
"Rollback rehearsal recorded in logs",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Do you support monorepos?", a: "Yes—matrix builds and targeted deploys." },
|
{ q: "Do you support monorepos?", a: "Yes—matrix builds and targeted deploys." },
|
||||||
{ q: "Secrets in CI?", a: "We wire secure secrets management per provider." },
|
{ q: "Secrets in CI?", a: "We wire secure secrets management per provider." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["dockerize-deploy", "container-registry-setup"],
|
relatedSlugs: ["dockerize-deploy", "container-registry-setup"],
|
||||||
metaTitle: "CI/CD to Production — Van Hunen IT",
|
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",
|
slug: "observability-stack",
|
||||||
title: "Observability Stack (Prometheus/Grafana/Loki)",
|
title: "Observability Stack (Prometheus/Grafana/Loki)",
|
||||||
category: "infrastructure-devops",
|
category: "infrastructure-devops",
|
||||||
outcome: "Metrics, logs, and alerts you can act on.",
|
outcome: "Metrics, logs, and alerts you can act on.",
|
||||||
who: [
|
who: ["Apps needing visibility and alerting", "Teams consolidating logs and dashboards"],
|
||||||
"Apps needing visibility and alerting",
|
deliverables: ["Prometheus metrics & alert rules", "Grafana dashboards for app & infra", "Loki log aggregation & retention"],
|
||||||
"Teams consolidating logs and dashboards",
|
|
||||||
],
|
|
||||||
deliverables: [
|
|
||||||
"Prometheus metrics & alert rules",
|
|
||||||
"Grafana dashboards for app & infra",
|
|
||||||
"Loki log aggregation & retention",
|
|
||||||
],
|
|
||||||
timeline: "1–2 days",
|
timeline: "1–2 days",
|
||||||
price: "€740",
|
price: "€740",
|
||||||
proof: [
|
proof: ["Dashboards with baseline SLOs", "Test alert firing to Slack/Email"],
|
||||||
"Dashboards with baseline SLOs",
|
|
||||||
"Test alert firing to Slack/Email",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Can you integrate with K8s?", a: "Yes, via exporters and service monitors." },
|
{ 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." },
|
{ q: "Retention strategy?", a: "Right-sized for your VPS budget and compliance." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["k3s-kubernetes-cluster", "git-to-prod-ci-cd"],
|
relatedSlugs: ["k3s-kubernetes-cluster", "git-to-prod-ci-cd"],
|
||||||
metaTitle: "Observability Stack — Van Hunen IT",
|
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",
|
slug: "backup-disaster-recovery-drill",
|
||||||
title: "Backup & Disaster-Recovery Drill",
|
title: "Backup & Disaster-Recovery Drill",
|
||||||
category: "infrastructure-devops",
|
category: "infrastructure-devops",
|
||||||
outcome: "Verified restore path—not just backups.",
|
outcome: "Verified restore path—not just backups.",
|
||||||
who: [
|
who: ["Sites that never tested restore", "Teams formalizing RPO/RTO targets"],
|
||||||
"Sites that never tested restore",
|
deliverables: ["Backup plan (files/db), encryption & rotation", "Restore test with documented steps", "RPO/RTO notes & recommendations"],
|
||||||
"Teams formalizing RPO/RTO targets",
|
|
||||||
],
|
|
||||||
deliverables: [
|
|
||||||
"Backup plan (files/db), encryption & rotation",
|
|
||||||
"Restore test with documented steps",
|
|
||||||
"RPO/RTO notes & recommendations",
|
|
||||||
],
|
|
||||||
timeline: "1 day",
|
timeline: "1 day",
|
||||||
price: "€490",
|
price: "€490",
|
||||||
proof: [
|
proof: ["Restore demonstration on staging", "Report with timings and gaps"],
|
||||||
"Restore demonstration on staging",
|
|
||||||
"Report with timings and gaps",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Which databases?", a: "MySQL/MariaDB/Postgres supported; others on request." },
|
{ q: "Which databases?", a: "MySQL/MariaDB/Postgres supported; others on request." },
|
||||||
{ q: "Offsite options?", a: "S3-compatible storage or rsync to secondary VPS." },
|
{ q: "Offsite options?", a: "S3-compatible storage or rsync to secondary VPS." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["vps-hardening-care", "website-care-plan"],
|
relatedSlugs: ["vps-hardening-care", "website-care-plan"],
|
||||||
metaTitle: "Backup & DR Drill — Van Hunen IT",
|
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",
|
slug: "cloudflare-edge-hardening",
|
||||||
title: "Cloudflare Edge Hardening",
|
title: "Cloudflare Edge Hardening",
|
||||||
category: "infrastructure-devops",
|
category: "infrastructure-devops",
|
||||||
outcome: "Lower TTFB, fewer bad bots, safer origins.",
|
outcome: "Lower TTFB, fewer bad bots, safer origins.",
|
||||||
who: [
|
who: ["Sites facing spam/bot abuse or high TTFB", "Teams needing sane edge security fast"],
|
||||||
"Sites facing spam/bot abuse or high TTFB",
|
deliverables: ["WAF & bot tuning, page rules, cache keys", "Origin shielding, HTTP/3, rate limiting", "TTFB and cache-hit improvements"],
|
||||||
"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",
|
timeline: "1 day",
|
||||||
price: "€420",
|
price: "€420",
|
||||||
proof: [
|
proof: ["Before/after WebPageTest/TTFB screenshots", "WAF rule set export & notes"],
|
||||||
"Before/after WebPageTest/TTFB screenshots",
|
|
||||||
"WAF rule set export & notes",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Pro/Business plan required?", a: "We work with Free+ plans; some features need Pro/Business." },
|
{ 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." },
|
{ q: "Will it break APIs?", a: "Rules are staged and tested with allowlists where needed." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["vps-hardening-care", "core-web-vitals-sprint"],
|
relatedSlugs: ["vps-hardening-care", "core-web-vitals-sprint"],
|
||||||
metaTitle: "Cloudflare Edge Hardening — Van Hunen IT",
|
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",
|
slug: "core-web-vitals-sprint",
|
||||||
title: "Core Web Vitals Sprint",
|
title: "Core Web Vitals Sprint",
|
||||||
category: "web-performance",
|
category: "web-performance",
|
||||||
outcome: "CLS/LCP/INP into the green.",
|
outcome: "CLS/LCP/INP into the green with measurable before/after.",
|
||||||
who: [
|
who: ["Marketing sites and shops with poor CWV", "Next.js/WordPress teams needing a focused fix"],
|
||||||
"Marketing sites and shops with poor CWV",
|
deliverables: ["Image strategy (WebP/next/image), font loading", "Script defers, critical CSS, caching headers", "Before/after CWV report"],
|
||||||
"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: "2–3 days",
|
timeline: "2–3 days",
|
||||||
price: "€820",
|
price: "€820",
|
||||||
proof: [
|
proof: ["Lighthouse/CrUX before vs after", "Largest contentful paint assets diff"],
|
||||||
"Lighthouse/CrUX before vs after",
|
|
||||||
"Largest contentful paint assets diff",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Will you change design?", a: "Only insofar as needed to stabilize layout and loading." },
|
{ 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." },
|
{ q: "Third-party scripts?", a: "We reduce impact via defer/async and budgeting." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["cloudflare-edge-hardening", "website-care-plan"],
|
relatedSlugs: ["cloudflare-edge-hardening", "website-care-plan"],
|
||||||
metaTitle: "Core Web Vitals Sprint — Van Hunen IT",
|
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",
|
slug: "website-care-plan",
|
||||||
title: "Website Care Plan",
|
title: "Website Care Plan",
|
||||||
category: "web-performance",
|
category: "web-performance",
|
||||||
outcome: "Updated site with restore-on-demand and uptime eyes on.",
|
outcome: "Updates, uptime watch, and verified restores—the Reliability Stack™ on autopilot.",
|
||||||
who: [
|
who: ["SMBs wanting stable updates and monitoring", "Teams without in-house ops"],
|
||||||
"SMBs wanting stable updates and monitoring",
|
|
||||||
"Teams without in-house ops",
|
|
||||||
],
|
|
||||||
deliverables: [
|
deliverables: [
|
||||||
"Updates, uptime monitoring, backups + monthly restore test",
|
"Updates, uptime monitoring, backups + monthly restore test",
|
||||||
"Incident credits and priority support",
|
"Incident credits and priority support",
|
||||||
"Security checks & reporting",
|
"Security checks & reporting",
|
||||||
],
|
],
|
||||||
timeline: "Monthly",
|
timeline: "Monthly",
|
||||||
price: "from €99/mo",
|
price: "from €149/mo",
|
||||||
proof: [
|
proof: ["Monthly report & restore test evidence", "Incident notes with timestamps"],
|
||||||
"Monthly report & restore test evidence",
|
|
||||||
"Incident notes with timestamps",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "What platforms?", a: "WordPress, Next.js, Node backends; others on request." },
|
{ q: "What platforms?", a: "WordPress, Next.js, Node backends; others on request." },
|
||||||
{ q: "SLA?", a: "Incident response windows depend on plan tier." },
|
{ q: "SLA?", a: "Incident response windows depend on plan tier." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["backup-disaster-recovery-drill", "core-web-vitals-sprint"],
|
relatedSlugs: ["backup-disaster-recovery-drill", "core-web-vitals-sprint"],
|
||||||
metaTitle: "Website Care Plan — Van Hunen IT",
|
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",
|
slug: "email-deliverability-pack",
|
||||||
title: "Secure Contact & Email Deliverability Pack",
|
title: "Secure Contact & Email Deliverability Pack",
|
||||||
category: "web-performance",
|
category: "web-performance",
|
||||||
outcome: "Inbox-ready email + safe forms.",
|
outcome: "Inbox-ready email, spoofing blocked, safer forms.",
|
||||||
who: [
|
who: ["Domains landing in spam or failing DMARC", "Sites receiving contact-form spam"],
|
||||||
"Domains landing in spam or failing DMARC",
|
|
||||||
"Sites receiving contact-form spam",
|
|
||||||
],
|
|
||||||
deliverables: [
|
deliverables: [
|
||||||
"SPF/DKIM/DMARC config & reports",
|
"SPF/DKIM/DMARC config & reports",
|
||||||
"Seed tests, alignment verification",
|
"Seed tests, alignment verification",
|
||||||
|
|
@ -336,167 +280,117 @@ export const SERVICES: Service[] = [
|
||||||
],
|
],
|
||||||
timeline: "1–2 days",
|
timeline: "1–2 days",
|
||||||
price: "€520",
|
price: "€520",
|
||||||
proof: [
|
proof: ["Before/after seed test screenshots", "DMARC alignment & provider screenshots"],
|
||||||
"Before/after seed test screenshots",
|
|
||||||
"DMARC alignment & provider screenshots",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "BIMI?", a: "Supported if you provide a valid SVG and VMC (optional)." },
|
{ q: "BIMI?", a: "Supported if you provide a valid SVG and VMC (optional)." },
|
||||||
{ q: "Multiple providers?", a: "Yes—ESP+transactional combos are supported." },
|
{ q: "Multiple providers?", a: "Yes—ESP+transactional combos are supported." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["website-care-plan", "cloudflare-edge-hardening"],
|
relatedSlugs: ["website-care-plan", "cloudflare-edge-hardening"],
|
||||||
metaTitle: "Email Deliverability Pack — Van Hunen IT",
|
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",
|
slug: "self-hosted-gitea-sso",
|
||||||
title: "Self-Hosted Git (Gitea) with SSO",
|
title: "Self-Hosted Git (Gitea) with SSO",
|
||||||
category: "dev-platforms",
|
category: "dev-platforms",
|
||||||
outcome: "Private Git with teams and permissions.",
|
outcome: "Private Git with teams and permissions.",
|
||||||
who: [
|
who: ["Teams needing on-prem/private Git", "Shops standardizing code workflows"],
|
||||||
"Teams needing on-prem/private Git",
|
deliverables: ["Gitea + runner, backup/restore", "OAuth/SSO, protected branches", "Repo templates & permissions"],
|
||||||
"Shops standardizing code workflows",
|
|
||||||
],
|
|
||||||
deliverables: [
|
|
||||||
"Gitea + runner, backup/restore",
|
|
||||||
"OAuth/SSO, protected branches",
|
|
||||||
"Repo templates & permissions",
|
|
||||||
],
|
|
||||||
timeline: "1 day",
|
timeline: "1 day",
|
||||||
price: "€460",
|
price: "€460",
|
||||||
proof: [
|
proof: ["Admin settings & SSO validation", "Backup/restore rehearsal log"],
|
||||||
"Admin settings & SSO validation",
|
|
||||||
"Backup/restore rehearsal log",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Migrate from GitHub/GitLab?", a: "Yes—repositories and permissions where possible." },
|
{ q: "Migrate from GitHub/GitLab?", a: "Yes—repositories and permissions where possible." },
|
||||||
{ q: "Runner support?", a: "Gitea Actions or Woodpecker runners on request." },
|
{ q: "Runner support?", a: "Gitea Actions or Woodpecker runners on request." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["git-to-prod-ci-cd", "container-registry-setup"],
|
relatedSlugs: ["git-to-prod-ci-cd", "container-registry-setup"],
|
||||||
metaTitle: "Self-Hosted Gitea with SSO — Van Hunen IT",
|
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",
|
slug: "secrets-management",
|
||||||
title: "Secrets Management (SOPS/age or Sealed-Secrets)",
|
title: "Secrets Management (SOPS/age or Sealed-Secrets)",
|
||||||
category: "dev-platforms",
|
category: "dev-platforms",
|
||||||
outcome: "Safe secrets in Git and Kubernetes.",
|
outcome: "Safe secrets in Git and Kubernetes.",
|
||||||
who: [
|
who: ["Teams committing .env by mistake", "K8s users needing encrypted manifests"],
|
||||||
"Teams committing .env by mistake",
|
deliverables: ["SOPS/age or Sealed-Secrets setup", "Key management & rotation policy", "Usage examples & policy notes"],
|
||||||
"K8s users needing encrypted manifests",
|
|
||||||
],
|
|
||||||
deliverables: [
|
|
||||||
"SOPS/age or Sealed-Secrets setup",
|
|
||||||
"Key management & rotation policy",
|
|
||||||
"Usage examples & policy notes",
|
|
||||||
],
|
|
||||||
timeline: "0.5–1 day",
|
timeline: "0.5–1 day",
|
||||||
price: "€380",
|
price: "€380",
|
||||||
proof: [
|
proof: ["Encrypted secrets in repo & decrypt flow", "Rotation drill notes"],
|
||||||
"Encrypted secrets in repo & decrypt flow",
|
|
||||||
"Rotation drill notes",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Which to choose?", a: "SOPS for Git-centric flow; Sealed-Secrets for cluster-centric flow." },
|
{ 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." },
|
{ q: "CI integration?", a: "We wire CI to decrypt securely where needed." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["k3s-kubernetes-cluster", "git-to-prod-ci-cd"],
|
relatedSlugs: ["k3s-kubernetes-cluster", "git-to-prod-ci-cd"],
|
||||||
metaTitle: "Secrets Management — Van Hunen IT",
|
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",
|
slug: "staging-environment",
|
||||||
title: "Staging Environment on the Same VPS/Cluster",
|
title: "Staging Environment on the Same VPS/Cluster",
|
||||||
category: "dev-platforms",
|
category: "dev-platforms",
|
||||||
outcome: "Risk-free previews before prod.",
|
outcome: "Risk-free previews before prod.",
|
||||||
who: [
|
who: ["Teams deploying without review", "Sites needing UAT previews"],
|
||||||
"Teams deploying without review",
|
deliverables: ["Staging namespace/compose stack", "Preview URLs & deploy gates", "Masked data & access controls"],
|
||||||
"Sites needing UAT previews",
|
|
||||||
],
|
|
||||||
deliverables: [
|
|
||||||
"Staging namespace/compose stack",
|
|
||||||
"Preview URLs & deploy gates",
|
|
||||||
"Masked data & access controls",
|
|
||||||
],
|
|
||||||
timeline: "1 day",
|
timeline: "1 day",
|
||||||
price: "€520",
|
price: "€520",
|
||||||
proof: [
|
proof: ["Preview deployment validation", "Access restricted and logged"],
|
||||||
"Preview deployment validation",
|
|
||||||
"Access restricted and logged",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Separate VPS needed?", a: "Not required; we can isolate on the same host if resources allow." },
|
{ 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." },
|
{ q: "Data masking?", a: "We provide safe anonymization for staging data." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["git-to-prod-ci-cd", "dockerize-deploy"],
|
relatedSlugs: ["git-to-prod-ci-cd", "dockerize-deploy"],
|
||||||
metaTitle: "Staging Environment — Van Hunen IT",
|
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",
|
slug: "server-app-migration",
|
||||||
title: "Server/App Migration to VPS/Kubernetes",
|
title: "Server/App Migration to VPS/Kubernetes",
|
||||||
category: "migrations",
|
category: "migrations",
|
||||||
outcome: "Zero-to-minimal downtime move with rollback.",
|
outcome: "Zero-to-minimal downtime move with rollback.",
|
||||||
who: [
|
who: ["Teams changing hosts or platforms", "Apps consolidating infra"],
|
||||||
"Teams changing hosts or platforms",
|
deliverables: ["Inventory & plan, containerization if needed", "DNS/cutover & rollback plan", "Smoke tests & timed runbook"],
|
||||||
"Apps consolidating infra",
|
|
||||||
],
|
|
||||||
deliverables: [
|
|
||||||
"Inventory & plan, containerization if needed",
|
|
||||||
"DNS/cutover & rollback plan",
|
|
||||||
"Smoke tests & timed runbook",
|
|
||||||
],
|
|
||||||
timeline: "2–4 days",
|
timeline: "2–4 days",
|
||||||
price: "€1,190",
|
price: "€1,190",
|
||||||
proof: [
|
proof: ["Cutover timeline & metrics", "Rollback rehearsal log"],
|
||||||
"Cutover timeline & metrics",
|
|
||||||
"Rollback rehearsal log",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Can you migrate databases?", a: "Yes—logical or physical migration with validated checks." },
|
{ 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." },
|
{ q: "Downtime window?", a: "We schedule low-impact windows and offer blue/green where possible." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["legacy-to-container-refresh", "k3s-kubernetes-cluster"],
|
relatedSlugs: ["legacy-to-container-refresh", "k3s-kubernetes-cluster"],
|
||||||
metaTitle: "Server/App Migration — Van Hunen IT",
|
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",
|
slug: "legacy-to-container-refresh",
|
||||||
title: "Legacy to Container Refresh",
|
title: "Legacy to Container Refresh",
|
||||||
category: "migrations",
|
category: "migrations",
|
||||||
outcome: "From pets to cattle—documented and reproducible.",
|
outcome: "From pets to cattle—documented and reproducible.",
|
||||||
who: [
|
who: ["Legacy apps lacking deployment consistency", "Teams modernizing delivery"],
|
||||||
"Legacy apps lacking deployment consistency",
|
deliverables: ["Dockerfiles & manifests", "Healthchecks, backups, docs", "Operational runbook"],
|
||||||
"Teams modernizing delivery",
|
|
||||||
],
|
|
||||||
deliverables: [
|
|
||||||
"Dockerfiles & manifests",
|
|
||||||
"Healthchecks, backups, docs",
|
|
||||||
"Operational runbook",
|
|
||||||
],
|
|
||||||
timeline: "2–3 days",
|
timeline: "2–3 days",
|
||||||
price: "€990",
|
price: "€990",
|
||||||
proof: [
|
proof: ["Green healthchecks post-deploy", "Disaster recovery walk-through"],
|
||||||
"Green healthchecks post-deploy",
|
|
||||||
"Disaster recovery walk-through",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Unsupported stacks?", a: "We assess feasibility; some apps may need refactors." },
|
{ q: "Unsupported stacks?", a: "We assess feasibility; some apps may need refactors." },
|
||||||
{ q: "Windows workloads?", a: "Case-by-case; Linux recommended for best results." },
|
{ q: "Windows workloads?", a: "Case-by-case; Linux recommended for best results." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["dockerize-deploy", "git-to-prod-ci-cd"],
|
relatedSlugs: ["dockerize-deploy", "git-to-prod-ci-cd"],
|
||||||
metaTitle: "Legacy to Container Refresh — Van Hunen IT",
|
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",
|
slug: "minecraft-managed-server",
|
||||||
title: "Managed Minecraft Server",
|
title: "Managed Minecraft Server",
|
||||||
category: "minecraft",
|
category: "minecraft",
|
||||||
outcome: "Fast, stable, and safe server on a dedicated VPS.",
|
outcome: "Fast, stable, and safe server on a dedicated VPS.",
|
||||||
who: [
|
who: ["Communities, schools, creators", "Small networks needing reliable ops"],
|
||||||
"Communities, schools, creators",
|
|
||||||
"Small networks needing reliable ops",
|
|
||||||
],
|
|
||||||
deliverables: [
|
deliverables: [
|
||||||
"VPS sizing/hardening, Paper/Velocity setup",
|
"VPS sizing/hardening, Paper/Velocity setup",
|
||||||
"Auto-backups + restore test",
|
"Auto-backups + restore test",
|
||||||
|
|
@ -504,200 +398,145 @@ export const SERVICES: Service[] = [
|
||||||
],
|
],
|
||||||
timeline: "Setup 1 day · Ongoing monthly",
|
timeline: "Setup 1 day · Ongoing monthly",
|
||||||
price: "Starter €49/mo · Pro €99/mo · Network €199/mo (+ VPS)",
|
price: "Starter €49/mo · Pro €99/mo · Network €199/mo (+ VPS)",
|
||||||
proof: [
|
proof: ["TPS baseline & timings report", "Restore test proof & monitoring"],
|
||||||
"TPS baseline & timings report",
|
|
||||||
"Restore test proof & monitoring",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Java or Bedrock?", a: "Java by default; Bedrock or Geyser support on request." },
|
{ q: "Java or Bedrock?", a: "Java by default; Bedrock or Geyser support on request." },
|
||||||
{ q: "Modpacks?", a: "CurseForge/modded supported—resource-dependent." },
|
{ q: "Modpacks?", a: "CurseForge/modded supported—resource-dependent." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["minecraft-performance-audit", "minecraft-plugin-development", "minecraft-monetization-pack"],
|
relatedSlugs: ["minecraft-performance-audit", "minecraft-plugin-development", "minecraft-monetization-pack"],
|
||||||
metaTitle: "Managed Minecraft Server — Van Hunen IT",
|
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",
|
slug: "minecraft-performance-audit",
|
||||||
title: "Minecraft Performance & Stability Audit",
|
title: "Minecraft Performance & Stability Audit",
|
||||||
category: "minecraft",
|
category: "minecraft",
|
||||||
outcome: "Higher TPS, fewer crashes.",
|
outcome: "Higher TPS, fewer crashes.",
|
||||||
who: [
|
who: ["Servers with lag or frequent crashes", "Owners scaling to more players"],
|
||||||
"Servers with lag or frequent crashes",
|
deliverables: ["Profiler run (Spark), timings analysis", "JVM flags & plugin audit", "Before/after TPS report"],
|
||||||
"Owners scaling to more players",
|
|
||||||
],
|
|
||||||
deliverables: [
|
|
||||||
"Profiler run (Spark), timings analysis",
|
|
||||||
"JVM flags & plugin audit",
|
|
||||||
"Before/after TPS report",
|
|
||||||
],
|
|
||||||
timeline: "1 day",
|
timeline: "1 day",
|
||||||
price: "€390",
|
price: "€390",
|
||||||
proof: [
|
proof: ["Timings & Spark screenshots", "Updated config diff & TPS before/after"],
|
||||||
"Timings & Spark screenshots",
|
|
||||||
"Updated config diff & TPS before/after",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Supports Bungee/Velocity?", a: "Yes—networked setups supported." },
|
{ q: "Supports Bungee/Velocity?", a: "Yes—networked setups supported." },
|
||||||
{ q: "Player cap increase?", a: "We optimize, then size infra appropriately." },
|
{ q: "Player cap increase?", a: "We optimize, then size infra appropriately." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["minecraft-managed-server"],
|
relatedSlugs: ["minecraft-managed-server"],
|
||||||
metaTitle: "Minecraft Performance Audit — Van Hunen IT",
|
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",
|
slug: "minecraft-plugin-development",
|
||||||
title: "Custom Minecraft Plugin Development",
|
title: "Custom Minecraft Plugin Development",
|
||||||
category: "minecraft",
|
category: "minecraft",
|
||||||
outcome: "Features tailored to your server/community.",
|
outcome: "Features tailored to your server/community.",
|
||||||
who: [
|
who: ["Servers needing unique mechanics", "Creators monetizing custom content"],
|
||||||
"Servers needing unique mechanics",
|
deliverables: ["Spec, plugin build & tests", "Config & permissions", "Maintenance window"],
|
||||||
"Creators monetizing custom content",
|
|
||||||
],
|
|
||||||
deliverables: [
|
|
||||||
"Spec, plugin build & tests",
|
|
||||||
"Config & permissions",
|
|
||||||
"Maintenance window",
|
|
||||||
],
|
|
||||||
timeline: "From 3–7 days",
|
timeline: "From 3–7 days",
|
||||||
price: "€85/hr or fixed from €650",
|
price: "€85/hr or fixed from €650",
|
||||||
proof: [
|
proof: ["Feature demo & test suite run", "Config docs and changelog"],
|
||||||
"Feature demo & test suite run",
|
|
||||||
"Config docs and changelog",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Source code ownership?", a: "You own it after payment unless agreed otherwise (private repo transfer included)." },
|
{ 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." },
|
{ q: "API compatibility?", a: "Paper API targeted; cross-version support is scoped case-by-case." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["minecraft-managed-server", "minecraft-monetization-pack"],
|
relatedSlugs: ["minecraft-managed-server", "minecraft-monetization-pack"],
|
||||||
metaTitle: "Minecraft Plugin Development — Van Hunen IT",
|
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",
|
slug: "minecraft-monetization-pack",
|
||||||
title: "Creator Monetization Pack (Tebex)",
|
title: "Creator Monetization Pack (Tebex)",
|
||||||
category: "minecraft",
|
category: "minecraft",
|
||||||
outcome: "Clean store + safe donations.",
|
outcome: "Clean store + safe donations.",
|
||||||
who: [
|
who: ["Servers adding a store", "Creators formalizing monetization"],
|
||||||
"Servers adding a store",
|
deliverables: ["Tebex setup & product catalog", "Rank automation & receipts", "Anti-fraud notes & webhooks"],
|
||||||
"Creators formalizing monetization",
|
|
||||||
],
|
|
||||||
deliverables: [
|
|
||||||
"Tebex setup & product catalog",
|
|
||||||
"Rank automation & receipts",
|
|
||||||
"Anti-fraud notes & webhooks",
|
|
||||||
],
|
|
||||||
timeline: "1 day",
|
timeline: "1 day",
|
||||||
price: "€420",
|
price: "€420",
|
||||||
proof: [
|
proof: ["Test purchase flow", "Webhook logs to grants"],
|
||||||
"Test purchase flow",
|
|
||||||
"Webhook logs to grants",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Compliance?", a: "We avoid P2W violations and follow platform rules." },
|
{ q: "Compliance?", a: "We avoid P2W violations and follow platform rules." },
|
||||||
{ q: "Branding?", a: "Store theme aligned with your site and server style." },
|
{ q: "Branding?", a: "Store theme aligned with your site and server style." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["minecraft-plugin-development", "minecraft-managed-server"],
|
relatedSlugs: ["minecraft-plugin-development", "minecraft-managed-server"],
|
||||||
metaTitle: "Minecraft Monetization Pack — Van Hunen IT",
|
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",
|
slug: "quick-launch-website",
|
||||||
title: "Quick-Launch Website (Next.js)",
|
title: "Quick-Launch Website (Next.js)",
|
||||||
category: "web-dev",
|
category: "web-dev",
|
||||||
outcome: "Fast, SEO-ready site in days.",
|
outcome: "Fast, SEO-ready site in days.",
|
||||||
who: [
|
who: ["SMBs needing a credible web presence fast", "Consultants/creators launching offers"],
|
||||||
"SMBs needing a credible web presence fast",
|
deliverables: ["5–7 sections, forms, OG/Twitter cards", "Analytics & deploy to your VPS", "Basic SEO & sitemap"],
|
||||||
"Consultants/creators launching offers",
|
|
||||||
],
|
|
||||||
deliverables: [
|
|
||||||
"5–7 sections, forms, OG/Twitter cards",
|
|
||||||
"Analytics & deploy to your VPS",
|
|
||||||
"Basic SEO & sitemap",
|
|
||||||
],
|
|
||||||
timeline: "5–7 days",
|
timeline: "5–7 days",
|
||||||
price: "€2,450",
|
price: "€2,450",
|
||||||
proof: [
|
proof: ["Lighthouse pass for basics", "Deployed site link & repo handover"],
|
||||||
"Lighthouse pass for basics",
|
|
||||||
"Deployed site link & repo handover",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Copy & assets?", a: "We provide a brief and templates; you can supply or we refine." },
|
{ q: "Copy & assets?", a: "We provide a brief and templates; you can supply or we refine." },
|
||||||
{ q: "CMS?", a: "Optional—see Headless CMS Setup." },
|
{ q: "CMS?", a: "Optional—see Headless CMS Setup." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["headless-cms-setup", "core-web-vitals-sprint"],
|
relatedSlugs: ["headless-cms-setup", "core-web-vitals-sprint"],
|
||||||
metaTitle: "Quick-Launch Website (Next.js) — Van Hunen IT",
|
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",
|
slug: "headless-cms-setup",
|
||||||
title: "Headless CMS Setup (Ghost/Strapi)",
|
title: "Headless CMS Setup (Ghost/Strapi)",
|
||||||
category: "web-dev",
|
category: "web-dev",
|
||||||
outcome: "Non-tech content updates without redeploys.",
|
outcome: "Non-tech content updates without redeploys.",
|
||||||
who: [
|
who: ["Teams wanting easy publishing", "Sites separating content from code"],
|
||||||
"Teams wanting easy publishing",
|
deliverables: ["CMS install & roles", "Content model & CI/CD", "Image optimization & docs"],
|
||||||
"Sites separating content from code",
|
|
||||||
],
|
|
||||||
deliverables: [
|
|
||||||
"CMS install & roles",
|
|
||||||
"Content model & CI/CD",
|
|
||||||
"Image optimization & docs",
|
|
||||||
],
|
|
||||||
timeline: "2–3 days",
|
timeline: "2–3 days",
|
||||||
price: "€1,190",
|
price: "€1,190",
|
||||||
proof: [
|
proof: ["Editor demo & role permissions", "Publishing pipeline test"],
|
||||||
"Editor demo & role permissions",
|
|
||||||
"Publishing pipeline test",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Migration from WordPress?", a: "We can import and map key content types." },
|
{ q: "Migration from WordPress?", a: "We can import and map key content types." },
|
||||||
{ q: "Auth & SSO?", a: "SSO/OAuth possible depending on CMS chosen." },
|
{ q: "Auth & SSO?", a: "SSO/OAuth possible depending on CMS chosen." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["quick-launch-website", "website-care-plan"],
|
relatedSlugs: ["quick-launch-website", "website-care-plan"],
|
||||||
metaTitle: "Headless CMS Setup — Van Hunen IT",
|
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",
|
slug: "security-compliance-baseline",
|
||||||
title: "Security & Compliance Baseline (GDPR-aware)",
|
title: "Security & Compliance Baseline (GDPR-aware)",
|
||||||
category: "web-dev",
|
category: "web-dev",
|
||||||
outcome: "Reasonable security for small teams.",
|
outcome: "Reasonable security for small teams.",
|
||||||
who: [
|
who: ["SMBs formalizing access and logging", "Teams preparing for audits"],
|
||||||
"SMBs formalizing access and logging",
|
deliverables: ["Password policy, 2FA & least-privilege", "Audit logging & data retention", "Incident checklist & drills"],
|
||||||
"Teams preparing for audits",
|
|
||||||
],
|
|
||||||
deliverables: [
|
|
||||||
"Password policy, 2FA & least-privilege",
|
|
||||||
"Audit logging & data retention",
|
|
||||||
"Incident checklist & drills",
|
|
||||||
],
|
|
||||||
timeline: "1–2 days",
|
timeline: "1–2 days",
|
||||||
price: "€740",
|
price: "€740",
|
||||||
proof: [
|
proof: ["Policy documents & checklists", "Access review and logging tests"],
|
||||||
"Policy documents & checklists",
|
|
||||||
"Access review and logging tests",
|
|
||||||
],
|
|
||||||
faq: [
|
faq: [
|
||||||
{ q: "Covers DPIA?", a: "We provide input; legal sign-off remains with your DPO/counsel." },
|
{ 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." },
|
{ q: "Tooling?", a: "We match to your stack—SaaS or self-hosted where appropriate." },
|
||||||
],
|
],
|
||||||
relatedSlugs: ["vps-hardening-care", "backup-disaster-recovery-drill"],
|
relatedSlugs: ["vps-hardening-care", "backup-disaster-recovery-drill"],
|
||||||
metaTitle: "Security & Compliance Baseline — Van Hunen IT",
|
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[] {
|
export function getAllServices(): Service[] {
|
||||||
// Keep a stable order by category then title
|
// Keep a stable order by category then title
|
||||||
return [...SERVICES].sort((a, b) =>
|
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 {
|
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[] {
|
export function getServicesByCategory(category: ServiceCategoryId): Service[] {
|
||||||
return getAllServices().filter(s => s.category === category);
|
return getAllServices().filter((s) => s.category === category);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
"node": ">=20 <23"
|
"node": ">=20 <23"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^12.23.24",
|
"framer-motion": "^12.23.24",
|
||||||
"next": "14.2.13",
|
"next": "14.2.13",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
clsx:
|
||||||
|
specifier: ^2.1.1
|
||||||
|
version: 2.1.1
|
||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^12.23.24
|
specifier: ^12.23.24
|
||||||
version: 12.23.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
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:
|
client-only@0.0.1:
|
||||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||||
|
|
||||||
|
clsx@2.1.1:
|
||||||
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
|
|
@ -2296,6 +2303,8 @@ snapshots:
|
||||||
|
|
||||||
client-only@0.0.1: {}
|
client-only@0.0.1: {}
|
||||||
|
|
||||||
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user