Minecraft & VPS added

This commit is contained in:
Thimon 2025-10-26 17:52:17 +01:00
parent 789678a0c1
commit 95b3ea4383
18 changed files with 2999 additions and 752 deletions

View File

@ -19,7 +19,33 @@ export default function ContactPage() {
function RequestForm() { function RequestForm() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const prefillType = searchParams.get("type") as RequestType | null;
// Accept both ?type= and ?plan= to allow links from multiple pages
const rawType = (searchParams.get("type") || searchParams.get("plan")) as string | null;
type RequestType =
| "recommendation"
| "vps"
| "website-care"
| "development"
| "minecraft-hosting"
| "plugin"
| "support"
| "partnership";
const isRequestType = (v: any): v is RequestType =>
[
"recommendation",
"vps",
"website-care",
"development",
"minecraft-hosting",
"plugin",
"support",
"partnership",
].includes(v);
const prefillType: RequestType | "" = rawType && isRequestType(rawType) ? rawType : "";
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -30,29 +56,46 @@ function RequestForm() {
domain: "", domain: "",
company: "", company: "",
message: "", message: "",
hosting: "", hosting: "", // reused for Minecraft edition (Java/Bedrock/etc.)
concern: "", concern: "",
}); });
type RequestType =
| "audit"
| "consultation"
| "support"
| "tool"
| "partnership";
const requestTypes: { id: RequestType; label: string; desc: string; icon: string }[] = [ const requestTypes: { id: RequestType; label: string; desc: string; icon: string }[] = [
{ {
id: "audit", id: "recommendation",
label: "Free Audit", label: "Get My Recommendation",
desc: "Request a free website, DNS or performance check.", desc: "Tell us about your stack/server. Well propose the right plan and next steps.",
icon: "🧠", icon: "🧭",
}, },
{ {
id: "consultation", id: "vps",
label: "Consultation / Quote", label: "Managed VPS Hosting",
desc: "Discuss a project, hosting setup, or optimization.", desc: "Hardened, monitored VPS with restore-tested backups.",
icon: "⚙️", icon: "🖥️",
},
{
id: "website-care",
label: "Managed Website Hosting & Care",
desc: "Updates, uptime, security checks, monthly restore tests.",
icon: "🛡️",
},
{
id: "development",
label: "Website Development",
desc: "Fast, SEO-ready Next.js site with deployment.",
icon: "⚡",
},
{
id: "minecraft-hosting",
label: "Managed Minecraft Hosting",
desc: "Lag-free server on a hardened VPS with backups and tuning.",
icon: "⛏️",
},
{
id: "plugin",
label: "Minecraft Plugin Development",
desc: "Custom Paper/Spigot plugins with tests and docs.",
icon: "🔌",
}, },
{ {
id: "support", id: "support",
@ -60,12 +103,6 @@ function RequestForm() {
desc: "Report an issue or request hands-on help.", desc: "Report an issue or request hands-on help.",
icon: "🛠️", icon: "🛠️",
}, },
{
id: "tool",
label: "Tool Follow-Up",
desc: "Continue from one of our free tools or reports.",
icon: "📊",
},
{ {
id: "partnership", id: "partnership",
label: "Partnership / Collaboration", label: "Partnership / Collaboration",
@ -107,20 +144,61 @@ function RequestForm() {
} }
}; };
const submitLabel =
form.type === "recommendation"
? "Get My Recommendation"
: form.type === "plugin"
? "Request Plugin Quote"
: form.type === "minecraft-hosting"
? "Launch My Minecraft Server"
: form.type === "development"
? "Request Website Proposal"
: form.type === "vps" || form.type === "website-care"
? "Send Technical Details"
: "Submit Request";
const showDomainInput =
form.type === "recommendation" ||
form.type === "vps" ||
form.type === "website-care" ||
form.type === "development" ||
form.type === "support";
const showMinecraftEdition =
form.type === "minecraft-hosting" || form.type === "plugin";
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 --- */}
<section className="border-b border-neutral-200 dark:border-neutral-800"> <section className="border-b border-neutral-200 dark:border-neutral-800">
<div className="container mx-auto max-w-4xl px-6 py-16 text-center"> <div className="container mx-auto max-w-4xl px-6 py-16 text-center">
<h1 className="text-4xl sm:text-5xl font-bold tracking-tight bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 bg-clip-text text-transparent"> <h1 className="text-4xl sm:text-5xl font-bold tracking-tight bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 bg-clip-text text-transparent">
Start a Request Talk to an Engineer
</h1> </h1>
<p className="mt-4 text-neutral-700 dark:text-neutral-300 text-lg"> <p className="mt-4 text-neutral-700 dark:text-neutral-300 text-lg">
Choose what youd like to do audits, consultations, or support. Tell us about your stack or server. Well recommend a plan and outline next stepsno lock-in.
Well guide you through the right steps and get back within one business day.
</p> </p>
{/* Trust microcopy */}
<div className="mt-8 grid grid-cols-1 sm:grid-cols-3 gap-4">
{[
{ t: "Mutual NDA", s: "Available on request" },
{ t: "Least-Privilege Access", s: "Credentials scoped to the task" },
{ t: "Month-to-Month", s: "30-day cancel policy" },
].map((item) => (
<div
key={item.t}
className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-4"
>
<div className="text-sm font-semibold">{item.t}</div>
<div className="text-xs text-neutral-600 dark:text-neutral-400">{item.s}</div>
</div>
))}
</div>
</div> </div>
</section> </section>
{/* --- FORM --- */}
<section className="container mx-auto max-w-3xl px-6 py-12"> <section className="container mx-auto max-w-3xl px-6 py-12">
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{step === 1 && ( {step === 1 && (
@ -132,7 +210,7 @@ function RequestForm() {
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
> >
<h2 className="text-2xl font-semibold text-neutral-900 dark:text-white mb-6 text-center"> <h2 className="text-2xl font-semibold text-neutral-900 dark:text-white mb-6 text-center">
What kind of request do you have? What do you need help with?
</h2> </h2>
<div className="grid gap-6 sm:grid-cols-2"> <div className="grid gap-6 sm:grid-cols-2">
{requestTypes.map((t) => ( {requestTypes.map((t) => (
@ -200,8 +278,7 @@ function RequestForm() {
/> />
</div> </div>
{(form.type === "audit" || form.type === "consultation") && ( {showDomainInput && (
<>
<div> <div>
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200"> <label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
Website or Domain Website or Domain
@ -215,7 +292,10 @@ function RequestForm() {
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
</div> </div>
)}
{/* Concern selector for scoping */}
{(form.type !== "" && form.type !== "partnership") && (
<div> <div>
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200"> <label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
Main Concern Main Concern
@ -231,10 +311,33 @@ function RequestForm() {
<option value="security">Security</option> <option value="security">Security</option>
<option value="dns">DNS / Email Issues</option> <option value="dns">DNS / Email Issues</option>
<option value="hosting">Hosting Migration</option> <option value="hosting">Hosting Migration</option>
<option value="minecraft">Minecraft Performance</option>
<option value="plugin">Plugin Feature / Bug</option>
<option value="other">Other</option> <option value="other">Other</option>
</select> </select>
</div> </div>
</> )}
{/* Minecraft edition for hosting/plugin */}
{showMinecraftEdition && (
<div>
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
Minecraft Edition
</label>
<select
name="hosting"
value={form.hosting}
onChange={handleChange}
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select one</option>
<option value="java">Java (Paper/Spigot)</option>
<option value="bedrock">Bedrock (Geyser supported)</option>
<option value="forge">Modpack / Forge</option>
<option value="fabric">Fabric</option>
<option value="other">Other / Not sure</option>
</select>
</div>
)} )}
<div> <div>
@ -246,7 +349,15 @@ function RequestForm() {
rows={4} rows={4}
value={form.message} value={form.message}
onChange={handleChange} onChange={handleChange}
placeholder="Describe your request..." placeholder={
form.type === "plugin"
? "Describe the plugin features, commands/permissions, and any existing code…"
: form.type === "development"
? "Describe pages/sections, brand goals, timeline, and examples you like…"
: form.type === "minecraft-hosting"
? "Player count, peak times, current host/specs, and performance issues…"
: "Describe your request..."
}
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
</div> </div>
@ -265,9 +376,14 @@ function RequestForm() {
disabled={loading} disabled={loading}
className="rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white font-medium px-6 py-2 hover:opacity-90 transition" className="rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white font-medium px-6 py-2 hover:opacity-90 transition"
> >
{loading ? "Submitting..." : "Submit Request"} {loading ? "Submitting..." : submitLabel}
</button> </button>
</div> </div>
{/* GDPR-friendly footer note */}
<p className="mt-4 text-xs text-neutral-600 dark:text-neutral-400 text-center">
We use your details only to respond to your request. No unsolicited marketing. EU/GDPR-friendly.
</p>
</motion.form> </motion.form>
)} )}
</AnimatePresence> </AnimatePresence>

View File

@ -0,0 +1,385 @@
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { toast } from "sonner";
type PlanId = "starter" | "pro" | "network";
type Edition = "java" | "bedrock" | "modded";
type ModLoader = "paper" | "spigot" | "velocity" | "forge" | "fabric" | "";
type Region = "eu-nl" | "eu-de" | "us-east" | "us-west";
// ✅ Allow readonly arrays for safe assignment from const literals
type Plan = {
id: PlanId;
name: string;
price: string;
specs: string;
typical: string;
features: readonly string[];
};
type RegionOpt = { id: Region; label: string };
export default function DeployWizard({
plans,
regions,
siteHostname,
}: {
plans: readonly Plan[];
regions: readonly RegionOpt[];
siteHostname: string;
}) {
const router = useRouter();
const [step, setStep] = useState<number>(1);
const [loading, setLoading] = useState<boolean>(false);
const [form, setForm] = useState<{
plan: PlanId | "";
edition: Edition | "";
modLoader: ModLoader;
version: string;
region: Region | "";
serverName: string;
subdomain: string;
email: string;
acceptEula: boolean;
notes: string;
}>({
plan: "",
edition: "",
modLoader: "",
version: "latest",
region: "",
serverName: "",
subdomain: "",
email: "",
acceptEula: false,
notes: "",
});
const set = <K extends keyof typeof form>(key: K, value: (typeof form)[K]) =>
setForm((f) => ({ ...f, [key]: value }));
// Preselect plan from hash/query if present
if (typeof window !== "undefined" && !form.plan) {
const hash = window.location.hash;
const url = new URL(window.location.href);
const qp = url.searchParams.get("plan") as PlanId | null;
const fromHash = /plan=(starter|pro|network)/.exec(hash || "")?.[1] as PlanId | undefined;
const initial = qp || fromHash;
if (initial) set("plan", initial);
}
const canContinue1 = !!form.plan;
const canContinue2 =
!!form.edition &&
!!form.region &&
!!form.version &&
(form.edition !== "modded" || !!form.modLoader);
const canSubmit =
canContinue2 &&
!!form.serverName &&
!!form.email &&
!!form.subdomain &&
form.acceptEula;
const handleSubmit = async () => {
if (!canSubmit) return;
setLoading(true);
try {
const res = await fetch("/api/minecraft/create-server", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-secret-token": process.env.NEXT_PUBLIC_FORM_SECRET || "",
},
body: JSON.stringify(form),
});
if (!res.ok) throw new Error("Provisioning failed");
toast.success("Server provisioning started! Check your email for details.");
router.push("/minecraft/success");
} catch (err) {
console.error(err);
toast.error("Something went wrong. Please try again.");
} finally {
setLoading(false);
}
};
return (
<div className="mt-8 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 shadow-sm p-6">
<AnimatePresence mode="wait">
{step === 1 && (
<motion.div
key="mc-step1"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.25 }}
>
<h3 className="text-xl font-semibold text-neutral-900 dark:text-white">
1) Choose a Plan
</h3>
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
You can change plans later. Network is customcontact us for sizing.
</p>
<div className="mt-5 grid gap-4 sm:grid-cols-3">
{plans.map((p) => (
<button
key={p.id}
onClick={() => set("plan", p.id)}
className={`text-left rounded-xl border p-4 transition ${
form.plan === p.id
? "border-blue-600 bg-blue-50/50 dark:bg-blue-950/30"
: "border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60"
}`}
>
<div className="flex items-baseline justify-between">
<div className="font-semibold">{p.name}</div>
<div className="text-sm font-bold">{p.price}</div>
</div>
<div className="mt-1 text-xs text-neutral-600 dark:text-neutral-400">
{p.specs}
</div>
<div className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
{p.typical}
</div>
</button>
))}
</div>
<div className="mt-6 flex justify-end">
<button
disabled={!canContinue1}
onClick={() => setStep(2)}
className="rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white font-medium px-5 py-2 disabled:opacity-50"
>
Continue
</button>
</div>
</motion.div>
)}
{step === 2 && (
<motion.div
key="mc-step2"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.25 }}
>
<h3 className="text-xl font-semibold text-neutral-900 dark:text-white">
2) Configure
</h3>
<div className="mt-4 grid gap-4 sm:grid-cols-2">
{/* Edition */}
<div>
<label className="block text-sm font-medium mb-1">Edition</label>
<select
value={form.edition}
onChange={(e) => set("edition", e.target.value as Edition)}
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2"
>
<option value="">Select one</option>
<option value="java">Java (Paper/Spigot)</option>
<option value="bedrock">Bedrock (Geyser supported)</option>
<option value="modded">Modded (Forge/Fabric)</option>
</select>
</div>
{/* Mod loader */}
<div>
<label className="block text-sm font-medium mb-1">Mod Loader</label>
<select
value={form.modLoader}
onChange={(e) => set("modLoader", e.target.value as ModLoader)}
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2"
disabled={form.edition === "bedrock"}
>
<option value="">None / Default</option>
<option value="paper">Paper</option>
<option value="spigot">Spigot</option>
<option value="velocity">Velocity (proxy)</option>
<option value="forge">Forge</option>
<option value="fabric">Fabric</option>
</select>
<p className="mt-1 text-xs text-neutral-500">
Velocity is recommended for multi-server networks.
</p>
</div>
{/* Version */}
<div>
<label className="block text-sm font-medium mb-1">Version</label>
<input
type="text"
value={form.version}
onChange={(e) => set("version", e.target.value)}
placeholder="latest or e.g. 1.20.6"
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2"
/>
</div>
{/* Region */}
<div>
<label className="block text-sm font-medium mb-1">Region</label>
<select
value={form.region}
onChange={(e) => set("region", e.target.value as Region)}
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2"
>
<option value="">Select region</option>
{regions.map((r) => (
<option key={r.id} value={r.id}>
{r.label}
</option>
))}
</select>
</div>
</div>
<div className="mt-6 flex justify-between">
<button
onClick={() => setStep(1)}
className="text-sm text-neutral-600 dark:text-neutral-400 hover:underline"
>
Back
</button>
<button
disabled={!canContinue2}
onClick={() => setStep(3)}
className="rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white font-medium px-5 py-2 disabled:opacity-50"
>
Continue
</button>
</div>
</motion.div>
)}
{step === 3 && (
<motion.div
key="mc-step3"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.25 }}
>
<h3 className="text-xl font-semibold text-neutral-900 dark:text-white">
3) Details & Provision
</h3>
<div className="mt-4 grid gap-4">
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium mb-1">Server Name</label>
<input
type="text"
value={form.serverName}
onChange={(e) => set("serverName", e.target.value)}
placeholder="My Community Server"
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Contact Email</label>
<input
type="email"
value={form.email}
onChange={(e) => set("email", e.target.value)}
placeholder="you@example.com"
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2"
/>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium mb-1">Subdomain</label>
<div className="flex items-center gap-2">
<input
type="text"
value={form.subdomain}
onChange={(e) =>
set(
"subdomain",
e.target.value.replace(/[^a-z0-9-]/gi, "").toLowerCase()
)
}
placeholder="myserver"
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2"
/>
<span className="text-sm text-neutral-600 dark:text-neutral-400">
.{siteHostname}
</span>
</div>
<p className="mt-1 text-xs text-neutral-500">
Well map this to your server on provision.
</p>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Notes (optional)
</label>
<input
type="text"
value={form.notes}
onChange={(e) => set("notes", e.target.value)}
placeholder="Plugins to preinstall, SFTP user, etc."
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2"
/>
</div>
</div>
<label className="mt-2 inline-flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={form.acceptEula}
onChange={(e) => set("acceptEula", e.target.checked)}
className="h-4 w-4"
/>
I accept the{" "}
<a
href="https://www.minecraft.net/en-us/eula"
target="_blank"
rel="noreferrer"
className="underline"
>
Minecraft EULA
</a>
.
</label>
</div>
<div className="mt-6 flex justify-between items-center">
<button
onClick={() => setStep(2)}
className="text-sm text-neutral-600 dark:text-neutral-400 hover:underline"
>
Back
</button>
<button
onClick={handleSubmit}
disabled={!canSubmit || loading}
className="rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white font-medium px-6 py-2 disabled:opacity-50"
>
{loading ? "Provisioning..." : "Provision Server"}
</button>
</div>
<p className="mt-3 text-xs text-neutral-600 dark:text-neutral-400">
Well send panel access and SFTP credentials to your email. All plans include
backups and monitoring. Modpacks & networks depend on resources and may
require custom sizing.
</p>
</motion.div>
)}
</AnimatePresence>
</div>
);
}

328
web/app/minecraft/page.tsx Normal file
View File

@ -0,0 +1,328 @@
import type { Metadata } from "next";
import Link from "next/link";
import JsonLd from "@/components/JsonLd";
import { site } from "@/lib/site";
import DeployWizard from "./DeployWizard";
export const metadata: Metadata = {
title: "Managed Minecraft Server Hosting & Plugins | Van Hunen IT",
description:
"Lag-free, stable Minecraft hosting on a hardened VPS—backups, tuning, and monitoring included. Deploy a server in minutes or request custom plugin development.",
};
// Shared data (passed to the client wizard & used for SEO JSON-LD)
const plans = [
{
id: "starter",
name: "Starter",
price: "€19/mo",
specs: "2 vCPU • 4 GB RAM • 40 GB SSD",
typical: "Up to ~20 players (optimized)",
features: [
"Paper/Spigot or Bedrock",
"Daily backups + restore test",
"Basic tuning & monitoring",
],
},
{
id: "pro",
name: "Pro",
price: "€39/mo",
specs: "3 vCPU • 8 GB RAM • 80 GB SSD",
typical: "Up to ~60 players (optimized)",
features: [
"Paper/Velocity or Bedrock + Geyser",
"Backups (2x/day) + restore test",
"Advanced tuning & monitoring",
],
},
{
id: "network",
name: "Network",
price: "Custom",
specs: "Dedicated VPS spec",
typical: "Networks & modpacks",
features: [
"Velocity/Bungee multi-server",
"Backups (hourly) + restore test",
"Custom tuning & SLA",
],
},
] as const;
const regions = [
{ id: "eu-nl", label: "EU — Netherlands" },
{ id: "eu-de", label: "EU — Germany" },
{ id: "us-east", label: "US — East" },
{ id: "us-west", label: "US — West" },
] as const;
const faqItems = [
{
q: "Do you support both Java and Bedrock?",
a: "Yes. Java with Paper/Spigot or Velocity, and Bedrock with Geyser support. Modpacks (Forge/Fabric) are supported on resource-appropriate plans.",
},
{
q: "How do backups work?",
a: "Automated backups are included with scheduled restore tests. We keep notes and timing so you know recovery is proven.",
},
{
q: "Can I migrate from another host?",
a: "Yes. We can migrate files, plugins, and worlds. Well review your current setup and tune performance on the new server.",
},
{
q: "Do you offer DDoS protection?",
a: "We apply baseline hardening and provider-level network protections. For advanced scenarios, well discuss options during setup.",
},
{
q: "What about uptime and incidents?",
a: "We monitor resources and uptime, document incidents with notes, and offer SLAs depending on your plan.",
},
];
const serviceLd = {
"@context": "https://schema.org",
"@type": "Service",
serviceType: "Managed Minecraft Server Hosting & Plugin Development",
provider: {
"@type": "Organization",
name: site.name,
url: site.url,
logo: site.org.logo,
},
areaServed: ["EU", "US"],
hasOfferCatalog: {
"@type": "OfferCatalog",
name: "Minecraft Hosting Plans",
itemListElement: plans.map((p) => ({
"@type": "Offer",
name: p.name,
price: p.price,
description: `${p.specs}${p.typical}`,
url: `${site.url}/minecraft#deploy`,
category: "Minecraft Hosting",
})),
},
};
const faqLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqItems.map((i) => ({
"@type": "Question",
name: i.q,
acceptedAnswer: { "@type": "Answer", text: i.a },
})),
};
export default function MinecraftPage() {
const hostname = new URL(site.url).hostname;
return (
<main className="relative isolate min-h-screen bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
{/* HERO */}
<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-24 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">
Managed Minecraft Server Hosting Lag-Free & Done-for-You
</span>
</h1>
<p className="mx-auto mt-6 max-w-3xl text-lg sm:text-xl text-neutral-700 dark:text-neutral-300">
Hardened VPS, tuned performance, backups with restore tests, and monitoring. Deploy your server in minutes or let us set it up for you. Need features? We build custom <strong>Paper/Spigot</strong> plugins with tests and docs.
</p>
<div className="mt-8 flex flex-col sm:flex-row gap-3 justify-center">
<a
href="#deploy"
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"
>
Deploy a Server
</a>
<Link
href="/contact?type=minecraft-hosting"
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"
>
Talk to an Engineer
</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">Stable TPS</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">tuned Paper/Velocity stacks</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">Restore-Proven</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">backups with timed drills</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">SLA-backed</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">monitoring & incident notes</p>
</div>
</div>
</div>
</section>
{/* BENEFITS */}
<section className="relative border-y border-neutral-200 dark:border-neutral-800">
<div className="container mx-auto max-w-6xl px-4 py-16">
<h2 className="text-3xl sm:text-5xl font-bold tracking-tight text-center">
Everything you need for smooth gameplay
</h2>
<div className="mt-10 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{[
{ t: "Performance Tuning", d: "Paper, Timings, async I/O, and caching where it counts." },
{ t: "Backups + Restore Tests", d: "Automated backups and scheduled restore drills with notes." },
{ t: "Monitoring & Incidents", d: "Uptime, resource alerts, clear incident notes & follow-up." },
{ t: "Security Basics", d: "Hardened OS, firewall rules, and safe update process." },
].map((b) => (
<article
key={b.t}
className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-6"
>
<h3 className="text-lg font-semibold">{b.t}</h3>
<p className="mt-2 text-sm text-neutral-700 dark:text-neutral-300">{b.d}</p>
</article>
))}
</div>
</div>
</section>
{/* PLANS */}
<section className="relative py-16">
<div className="container mx-auto max-w-6xl px-4">
<h2 className="text-3xl sm:text-5xl font-bold tracking-tight text-center">Hosting Plans</h2>
<p className="mt-4 text-neutral-700 dark:text-neutral-300 text-center max-w-3xl mx-auto">
Choose a plan that fits your community. Player counts are typical, not hard limitstuning and plugins affect performance.
</p>
<div className="mt-10 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{plans.map((p) => (
<div
key={p.id}
className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-6 flex flex-col"
>
<div className="flex items-baseline justify-between">
<h3 className="text-xl font-semibold">{p.name}</h3>
<div className="text-lg font-bold">{p.price}</div>
</div>
<div className="mt-2 text-sm text-neutral-700 dark:text-neutral-300">{p.specs}</div>
<div className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{p.typical}</div>
<ul className="mt-4 space-y-2 text-sm">
{p.features.map((f) => (
<li key={f} className="flex items-start gap-2">
<span className="mt-0.5"></span>
<span>{f}</span>
</li>
))}
</ul>
<a
href={`#deploy?plan=${p.id}`}
className="mt-6 inline-flex items-center justify-center rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 px-4 py-2 text-white font-semibold hover:opacity-90 transition"
>
{p.id === "network" ? "Request Custom Plan" : `Deploy ${p.name}`}
</a>
</div>
))}
</div>
</div>
</section>
{/* PLUGIN DEVELOPMENT */}
<section className="relative py-16 border-y border-neutral-200 dark:border-neutral-800">
<div className="container mx-auto max-w-6xl px-4">
<div className="grid gap-8 lg:grid-cols-2">
<div>
<h2 className="text-3xl sm:text-5xl font-bold tracking-tight">Minecraft Plugin Development</h2>
<p className="mt-4 text-neutral-700 dark:text-neutral-300">
Custom Paper/Spigot plugins built to spectests, config, permissions, and docs included.
Ownership transferred to you after payment.
</p>
<ul className="mt-6 space-y-2 text-sm">
{[
"Feature scoping and milestones",
"Automated test suite & changelog",
"Config, permissions, and admin docs",
"Private repo transfer and handover",
].map((li) => (
<li key={li} className="flex items-start gap-2">
<span className="mt-0.5">🔧</span>
<span>{li}</span>
</li>
))}
</ul>
<div className="mt-6 flex gap-3">
<Link
href="/contact?type=plugin"
className="inline-flex items-center rounded-lg bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 px-5 py-2 font-semibold hover:opacity-90 transition"
>
Request Plugin Quote
</Link>
<Link
href="/contact?type=minecraft-hosting"
className="inline-flex items-center rounded-lg border border-neutral-300 dark:border-neutral-700 px-5 py-2 font-semibold text-neutral-900 dark:text-white hover:bg-neutral-50 dark:hover:bg-neutral-800 transition"
>
Talk About Your Server
</Link>
</div>
</div>
<div 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">Example plugin ideas</div>
<ul className="mt-3 space-y-2 text-sm text-neutral-700 dark:text-neutral-300">
<li> Economy & shops with anti-dupe checks</li>
<li> Claims / protection with region flags</li>
<li> Cross-server chat via Velocity</li>
<li> Mini-games scaffolding & leaderboards</li>
</ul>
<p className="mt-4 text-xs text-neutral-500 dark:text-neutral-400">
(Well confirm feasibility and scope before building.)
</p>
</div>
</div>
</div>
</section>
{/* SELF-SERVICE DEPLOYMENT */}
<section id="deploy" className="relative py-16">
<div className="container mx-auto max-w-3xl px-4">
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight text-center">
Deploy a Minecraft Server (Self-Service)
</h2>
<p className="mt-3 text-center text-neutral-700 dark:text-neutral-300">
Pick a plan, choose edition & version, set a region, and well provision your server and email access details.
</p>
<DeployWizard plans={plans} regions={regions} siteHostname={hostname} />
</div>
</section>
{/* FAQ */}
<section className="relative py-16 bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
<div className="container mx-auto max-w-4xl px-4">
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight text-center">FAQ</h2>
<div className="mt-8 grid gap-6">
{faqItems.map((i) => (
<article
key={i.q}
className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-6"
>
<h3 className="text-lg font-semibold">{i.q}</h3>
<p className="mt-2 text-sm text-neutral-700 dark:text-neutral-300">{i.a}</p>
</article>
))}
</div>
</div>
</section>
<JsonLd data={serviceLd} />
<JsonLd data={faqLd} />
</main>
);
}

View File

@ -8,9 +8,9 @@ import JsonLd from "@/components/JsonLd";
import Link from "next/link"; import Link from "next/link";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "SMB Website Reliability & Deliverability", title: "Managed Hosting & Development — VPS, Websites & Minecraft | Van Hunen IT",
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.", "Managed VPS hosting (managed or owned), Managed Website hosting & care, Website development, and Managed Minecraft hosting & plugins. Self-service provisioning, SLAs, restore-tested backups, expert support.",
}; };
export default function HomePage() { export default function HomePage() {
@ -26,16 +26,20 @@ export default function HomePage() {
const faqItems = [ const faqItems = [
{ {
q: "How fast can you start?", q: "How fast can you start?",
a: "Most sprints start within 23 business days after scope confirmation.", a: "Most projects start within 23 business days after scope confirmation.",
},
{
q: "Can I own the VPS through your platform?",
a: "Yes. Choose Owned mode on the VPS page. We provision into your cloud account via a secure connect flow. You keep full ownership; we still manage it.",
},
{
q: "Do you support self-service deploys?",
a: "Yes. You can self-provision a VPS or deploy a Minecraft server in minutes—both include backups and monitoring by default.",
}, },
{ {
q: "Do you work under NDA?", q: "Do you work under NDA?",
a: "Yes—mutual NDA available on request; we keep credentials least-privilege.", a: "Yes—mutual NDA available on request; we keep credentials least-privilege.",
}, },
{
q: "Can we switch providers later?",
a: "Yes. Everything is documented; you own the accounts and artifacts. Month-to-month plans with a 30-day cancel policy.",
},
{ {
q: "Whats your guarantee?", q: "Whats your guarantee?",
a: "We show proof of outcomes. If the agreed scope isnt met, we make it right or refund the sprint fee.", a: "We show proof of outcomes. If the agreed scope isnt met, we make it right or refund the sprint fee.",
@ -56,6 +60,60 @@ export default function HomePage() {
})), })),
}; };
const heroBullets = [
{
title: "Managed or Owned VPS hosting",
body: "Hardened servers, monitoring, and verified backups—hosted by us or owned in your account.",
href: "/vps",
},
{
title: "Managed Website hosting & care",
body: "Updates, uptime, security checks, and monthly restore-tested backups.",
href: "/services#website-care",
},
{
title: "Website development",
body: "Fast Next.js sites, SEO-ready with deployment included.",
href: "/services#development",
},
{
title: "Minecraft hosting & plugins",
body: "Self-service deploys, tuned performance, backups, and custom features.",
href: "/minecraft",
},
];
const services = [
{
title: "Managed VPS Hosting (Managed or Owned)",
body:
"Secure, fast VPS hosting—server management, monitoring, and restore-tested backups. Provision on our platform or own it in your cloud.",
href: "/vps",
cta: "Provision a VPS →",
},
{
title: "Managed Website Hosting & Care",
body:
"Fully managed website hosting & maintenance—updates, uptime, security checks, and monthly restore tests.",
href: "/services#website-care",
cta: "Start Managed Website Hosting →",
},
{
title: "Website Development (Next.js)",
body:
"Launch a fast, SEO-ready site with modern tech, analytics, and deployment to your managed hosting.",
href: "/services#development",
cta: "Build My Website →",
},
{
title: "Managed Minecraft Server Hosting & Plugins",
body:
"Lag-free, stable Minecraft hosting on a hardened VPS, plus custom Paper/Spigot plugins when you need them.",
href: "/minecraft",
cta: "Deploy a Server →",
},
];
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 --- */}
@ -64,27 +122,56 @@ export default function HomePage() {
<div className="container mx-auto max-w-6xl px-4 py-28 text-center"> <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"> <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"> <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. Managed Hosting & Development Fast, Secure, Done-for-You
</span> </span>
</h1> </h1>
<p className="mx-auto mt-6 max-w-3xl text-lg sm:text-xl text-neutral-700 dark:text-neutral-300"> <p className="mx-auto mt-6 max-w-3xl text-lg sm:text-xl text-neutral-700 dark:text-neutral-300">
SMB website reliability and deliverabilityimplemented with the{" "} Get reliable <strong>Managed or Owned VPS hosting</strong>,{" "}
<span className="font-semibold">Reliability Stack</span>: DMARC-aligned email, Cloudflare edge <strong>Managed Website hosting & care</strong>,{" "}
security & speed, tested backups, and uptime watch. Flat pricing. Before/after proof. <strong>Website development</strong>, and{" "}
<strong>Minecraft server hosting & plugins</strong>. Self-service provisioning with{" "}
restore-tested backups and real SLAsso you can focus on creating and growing.
</p> </p>
<div className="mt-8 flex flex-col sm:flex-row gap-3 justify-center"> {/* Hero bullets */}
<div className="mx-auto mt-8 grid max-w-4xl grid-cols-1 gap-4 sm:grid-cols-2">
{heroBullets.map((b) => (
<Link <Link
href="/free" href={b.href}
key={b.title}
className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-4 text-left hover:shadow-md transition"
>
<div className="text-base font-semibold">{b.title}</div>
<div className="mt-1 text-sm text-neutral-700 dark:text-neutral-300">{b.body}</div>
</Link>
))}
</div>
{/* Primary CTAs */}
<div className="mt-8 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3 justify-center">
<Link
href="/vps#deploy"
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" 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 Provision a VPS
</Link> </Link>
<Link <Link
href="/contact" href="/services#website-care"
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" 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 Manage My Website
</Link>
<Link
href="/services#development"
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"
>
Build My Website
</Link>
<Link
href="/minecraft#deploy"
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"
>
Deploy Minecraft
</Link> </Link>
</div> </div>
@ -92,11 +179,11 @@ export default function HomePage() {
<div className="mt-10 grid grid-cols-1 sm:grid-cols-3 gap-4 max-w-4xl mx-auto"> <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"> <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-2xl font-bold">+38% faster TTFB</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">after Cloudflare tuning</p> <p className="text-sm text-neutral-600 dark:text-neutral-400">after edge & cache tuning</p>
</div> </div>
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-4"> <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-2xl font-bold">DMARC p=reject</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">in 48 hours, spoofing blocked</p> <p className="text-sm text-neutral-600 dark:text-neutral-400">spoofing blocked in 48 hours</p>
</div> </div>
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-4"> <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-2xl font-bold">7m 12s restore</p>
@ -106,45 +193,20 @@ export default function HomePage() {
</div> </div>
</section> </section>
{/* --- RELIABILITY STACK --- */} {/* --- SERVICES --- */}
<section className="relative overflow-hidden border-y border-neutral-200 dark:border-neutral-800"> <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">
The Reliability Stack Hosting & Development Services
</h2> </h2>
<p className="mt-5 text-lg text-neutral-700 dark:text-neutral-300 max-w-3xl mx-auto"> <p className="mt-5 text-lg text-neutral-700 dark:text-neutral-300 max-w-3xl mx-auto">
Four layers that make websites dependable. Implemented in a 13 day sprint, proven with before/after data. Clear outcomes, flat pricing, and real SLAsplus before/after proof on every engagement.
</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-4"> <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{[ {services.map((card) => (
{
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={card.title} key={card.title}
className="group relative overflow-hidden rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-8 shadow-sm hover:shadow-lg transition-all duration-300 backdrop-blur text-left" 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"
@ -158,7 +220,7 @@ export default function HomePage() {
href={card.href} 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 {card.cta}
</Link> </Link>
</article> </article>
))} ))}
@ -175,15 +237,15 @@ export default function HomePage() {
</div> </div>
</section> </section>
{/* --- HOW IT WORKS (3 STEPS) --- */} {/* --- HOW IT WORKS --- */}
<section className="relative py-24"> <section className="relative py-24">
<div className="container mx-auto max-w-6xl px-4"> <div className="container mx-auto max-w-6xl px-4">
<div className="text-center"> <div className="text-center">
<h2 className="text-3xl sm:text-5xl font-bold tracking-tight"> <h2 className="text-3xl sm:text-5xl font-bold tracking-tight">
From audit to outcomes in 72 hours From plan to live in days
</h2> </h2>
<p className="mt-4 text-lg text-neutral-700 dark:text-neutral-300 max-w-2xl mx-auto"> <p className="mt-4 text-lg text-neutral-700 dark:text-neutral-300 max-w-2xl mx-auto">
Diagnose the risks, implement the fixes, and prove the resultsthen choose the right Care plan. Self-service provisioning or white-glove setup. We scope, set up, and verifythen hand you clear reporting and an SLA-backed care plan.
</p> </p>
</div> </div>
@ -191,21 +253,18 @@ export default function HomePage() {
{[ {[
{ {
step: "1", step: "1",
title: "Diagnose", title: "Plan",
body: body: "Technical review and goals—or use the wizard to self-provision.",
"DNS/email/edge scan, backup checks, and uptime review. Clear scope and fixed price before we start.",
}, },
{ {
step: "2", step: "2",
title: "Implement", title: "Set up",
body: body: "Provision VPS/hosting, deploy site or server, configure monitoring and backups.",
"DMARC + Cloudflare + backups + monitors. Least-privilege access, change log, and rollback plan.",
}, },
{ {
step: "3", step: "3",
title: "Prove", title: "Verify",
body: body: "Before/after metrics, restore test timer, and next-step plan you can share internally.",
"Before/after report, restore test timer, incident notes, and next-step plan you can share internally.",
}, },
].map((s) => ( ].map((s) => (
<div <div
@ -221,10 +280,10 @@ export default function HomePage() {
<div className="mt-10 flex justify-center"> <div className="mt-10 flex justify-center">
<Link <Link
href="/contact?sprint=fix" href="/contact?plan=recommendation"
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" 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) Get My Recommendation
</Link> </Link>
</div> </div>
</div> </div>
@ -233,11 +292,10 @@ export default function HomePage() {
{/* --- PRICING --- */} {/* --- 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"> <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} /> <Pricing plans={plans} />
<div className="mt-8 text-center"> <div className="mt-8 text-center">
<p className="text-sm text-neutral-600 dark:text-neutral-400"> <p className="text-sm text-neutral-600 dark:text-neutral-400">
Every plan includes DNS/email monitoring, automated backups with restore verification, uptime & SSL Every plan includes monitoring, automated backups with restore verification, uptime & SSL
watch, and a quarterly health summary. watch, and a quarterly health summary.
</p> </p>
</div> </div>
@ -262,23 +320,23 @@ export default function HomePage() {
<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 website reliable and inbox-ready? Power, Performance & Peace of Mind Managed for You.
</h2> </h2>
<p className="mt-4 text-lg text-blue-100"> <p className="mt-4 text-lg text-blue-100">
Start with a Fix Sprint or choose a Care plan that fits your business. Well prove the results. Provision a VPS, manage your site, or deploy a Minecraft serverbacked by real SLAs and restore-tested backups.
</p> </p>
<div className="mt-8 flex flex-col sm:flex-row gap-3 justify-center"> <div className="mt-8 flex flex-col sm:flex-row gap-3 justify-center">
<Link <Link
href="/free" href="/vps#deploy"
className="inline-flex items-center rounded-lg bg-white text-blue-700 font-semibold px-6 py-3 hover:bg-blue-50 transition" className="inline-flex items-center rounded-lg bg-white text-blue-700 font-semibold px-6 py-3 hover:bg-blue-50 transition"
> >
Start Free Check Provision a VPS
</Link> </Link>
<Link <Link
href="/pricing" href="/minecraft#deploy"
className="inline-flex items-center rounded-lg border border-white/70 text-white font-semibold px-6 py-3 hover:bg-white/10 transition" 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 Deploy Minecraft
</Link> </Link>
</div> </div>
</div> </div>

View File

@ -0,0 +1,358 @@
"use client";
import { useMemo, useState } from "react";
import Link from "next/link";
// ---------- Types ----------
export type CarePlan = {
id: string;
name: string;
bestFor: string;
monthlyPrice: number;
yearlyDiscount: number; // 0.1 = 10%
outcomes: readonly string[];
inclusions: readonly string[];
sla: string;
cta: { label: string; href: string };
popular?: boolean;
contactOnly?: boolean;
};
export type VPSPlan = {
id: "solo" | "team" | "dedicated";
name: string;
price: number | null; // null = custom
specs: string;
bestFor: string;
};
export type MCPlan = {
id: "starter" | "pro" | "network";
name: string;
price: number | null;
specs: string;
typical: string;
features: readonly string[];
};
// ---------- Main Component ----------
export default function PricingConfigurator({
carePlans,
vpsPlans,
minecraftPlans,
}: {
carePlans: readonly CarePlan[];
vpsPlans: readonly VPSPlan[];
minecraftPlans: readonly MCPlan[];
}) {
const [tab, setTab] = useState<"care" | "vps" | "mc">("care");
const [billing, setBilling] = useState<"monthly" | "yearly">("monthly");
const [vpsOwnership, setVpsOwnership] = useState<"managed" | "owned">("managed");
return (
<div className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-6 sm:p-8 shadow-sm">
{/* Tabs */}
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="inline-flex rounded-xl border border-neutral-200 dark:border-neutral-800 p-1">
{[
{ id: "care", label: "Managed Hosting & Care" },
{ id: "vps", label: "VPS (Managed or Owned)" },
{ id: "mc", label: "Minecraft Hosting" },
].map((t) => (
<button
key={t.id}
onClick={() => setTab(t.id as any)}
className={`px-3 py-1.5 text-sm rounded-lg ${
tab === t.id
? "bg-neutral-900 text-white dark:bg-white dark:text-neutral-900"
: ""
}`}
>
{t.label}
</button>
))}
</div>
{/* Billing toggle */}
{tab === "care" && (
<div className="flex items-center gap-2">
<span className="text-xs text-neutral-600 dark:text-neutral-400">Billing:</span>
<div className="inline-flex rounded-xl border border-neutral-200 dark:border-neutral-800 p-1">
{(["monthly", "yearly"] as const).map((b) => (
<button
key={b}
onClick={() => setBilling(b)}
className={`px-3 py-1.5 text-sm rounded-lg ${
billing === b ? "bg-blue-600 text-white" : ""
}`}
>
{b === "monthly" ? "Monthly" : "Yearly (-10%)"}
</button>
))}
</div>
</div>
)}
</div>
{/* Panels */}
<div className="mt-8">
{tab === "care" && <CareGrid plans={carePlans} billing={billing} />}
{tab === "vps" && (
<VPSGrid
plans={vpsPlans}
ownership={vpsOwnership}
onOwnershipChange={setVpsOwnership}
/>
)}
{tab === "mc" && <MinecraftGrid plans={minecraftPlans} />}
</div>
</div>
);
}
// ---------- CARE ----------
function CareGrid({
plans,
billing,
}: {
plans: readonly CarePlan[];
billing: "monthly" | "yearly";
}) {
return (
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{plans.map((p) => {
const price =
billing === "yearly"
? Math.round(p.monthlyPrice * (1 - p.yearlyDiscount))
: p.monthlyPrice;
return (
<div
key={p.id}
className={`relative rounded-2xl border p-6 ${
p.popular
? "border-blue-600 bg-blue-50/40 dark:bg-blue-950/20"
: "border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60"
}`}
>
{p.popular && (
<div className="absolute -top-3 right-4 rounded-full bg-blue-600 text-white text-xs font-semibold px-3 py-1 shadow">
Most Popular
</div>
)}
<div className="flex items-baseline justify-between gap-3">
<h3 className="text-lg font-semibold">{p.name}</h3>
<div className="text-right">
<div className="text-2xl font-extrabold">
{price}
<span className="text-sm font-medium text-neutral-500">/mo</span>
</div>
{billing === "yearly" && (
<div className="text-xs text-neutral-500">billed annually (-10%)</div>
)}
</div>
</div>
<div className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
{p.bestFor}
</div>
<ul className="mt-4 space-y-2 text-sm">
{p.outcomes.map((o) => (
<li key={o} className="flex items-start gap-2">
<span className="mt-0.5">🎯</span>
<span>{o}</span>
</li>
))}
{p.inclusions.map((i) => (
<li key={i} className="flex items-start gap-2">
<span className="mt-0.5"></span>
<span>{i}</span>
</li>
))}
<li className="flex items-start gap-2">
<span className="mt-0.5">🛡</span>
<span>{p.sla}</span>
</li>
</ul>
<div className="mt-6">
<Link
href={p.cta.href}
className={`inline-flex items-center rounded-lg px-4 py-2 font-semibold ${
p.contactOnly
? "border border-neutral-300 dark:border-neutral-700"
: "bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white"
}`}
>
{p.cta.label}
</Link>
</div>
</div>
);
})}
</div>
);
}
// ---------- VPS ----------
function VPSGrid({
plans,
ownership,
onOwnershipChange,
}: {
plans: readonly VPSPlan[];
ownership: "managed" | "owned";
onOwnershipChange: (m: "managed" | "owned") => void;
}) {
return (
<div>
<div className="mb-4 flex items-center justify-between">
<div className="text-sm text-neutral-600 dark:text-neutral-400">
Choose how you want to run your VPS.
</div>
<div className="inline-flex rounded-xl border border-neutral-200 dark:border-neutral-800 p-1">
{(["managed", "owned"] as const).map((m) => (
<button
key={m}
onClick={() => onOwnershipChange(m)}
className={`px-3 py-1.5 text-sm rounded-lg ${
ownership === m ? "bg-blue-600 text-white" : ""
}`}
>
{m === "managed" ? "Managed (default)" : "Owned (your account)"}
</button>
))}
</div>
</div>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{plans.map((p) => (
<div
key={p.id}
className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-6 flex flex-col"
>
<div className="flex items-baseline justify-between">
<h3 className="text-lg font-semibold">{p.name}</h3>
<div className="text-right">
<div className="text-xl font-extrabold">
{p.price ? (
<>
{p.price}
<span className="text-sm font-medium text-neutral-500">/mo</span>
</>
) : (
"Custom"
)}
</div>
</div>
</div>
<div className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">{p.specs}</div>
<div className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{p.bestFor}</div>
<ul className="mt-4 space-y-2 text-sm">
<li className="flex items-start gap-2">
<span className="mt-0.5">🛡</span>
<span>CIS-style hardening, updates & monitoring</span>
</li>
<li className="flex items-start gap-2">
<span className="mt-0.5">💾</span>
<span>Backups with scheduled restore tests</span>
</li>
<li className="flex items-start gap-2">
<span className="mt-0.5">📈</span>
<span>Incident notes & SLA-backed response</span>
</li>
</ul>
<div className="mt-6 flex gap-3">
{p.id === "dedicated" ? (
<Link
href="/contact?type=vps"
className="inline-flex items-center rounded-lg border border-neutral-300 dark:border-neutral-700 px-4 py-2 font-semibold"
>
Request Custom Plan
</Link>
) : (
<Link
href={`/vps#deploy`}
className="inline-flex items-center rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white px-4 py-2 font-semibold"
>
{ownership === "managed" ? "Provision (Managed)" : "Provision (Owned)"}
</Link>
)}
</div>
<p className="mt-3 text-xs text-neutral-600 dark:text-neutral-400">
{ownership === "managed"
? "We host & bill the VPS on our platform. Management included."
: "Provisioned into your cloud account via secure connect. You retain ownership."}
</p>
</div>
))}
</div>
</div>
);
}
// ---------- MINECRAFT ----------
function MinecraftGrid({ plans }: { plans: readonly MCPlan[] }) {
return (
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{plans.map((p) => (
<div
key={p.id}
className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-6 flex flex-col"
>
<div className="flex items-baseline justify-between">
<h3 className="text-lg font-semibold">{p.name}</h3>
<div className="text-xl font-extrabold">
{p.price ? (
<>
{p.price}
<span className="text-sm font-medium text-neutral-500">/mo</span>
</>
) : (
"Custom"
)}
</div>
</div>
<div className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">{p.specs}</div>
<div className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{p.typical}</div>
<ul className="mt-4 space-y-2 text-sm">
{p.features.map((f) => (
<li key={f} className="flex items-start gap-2">
<span className="mt-0.5"></span>
<span>{f}</span>
</li>
))}
</ul>
<div className="mt-6">
{p.id === "network" ? (
<Link
href="/contact?type=minecraft-hosting"
className="inline-flex items-center rounded-lg border border-neutral-300 dark:border-neutral-700 px-4 py-2 font-semibold"
>
Request Custom Plan
</Link>
) : (
<Link
href="/minecraft#deploy"
className="inline-flex items-center rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white px-4 py-2 font-semibold"
>
Deploy Server
</Link>
)}
</div>
<p className="mt-3 text-xs text-neutral-600 dark:text-neutral-400">
Backups with restore tests, tuning & monitoring included. Networks & modpacks may require custom sizing.
</p>
</div>
))}
</div>
);
}

View File

@ -1,45 +1,44 @@
// app/pricing/page.tsx // app/pricing/page.tsx
import type { Metadata } from "next"; import type { Metadata } from "next";
import PricingTable from "@/components/pricing/PricingTable"; import Link from "next/link";
import ComparisonTable from "@/components/pricing/ComparisonTable"; import { site } from "@/lib/site";
import PlanRecommender from "@/components/pricing/PlanRecommender"; import JsonLd from "@/components/JsonLd";
import ROICalculator from "@/components/pricing/ROICalculator"; import PricingConfigurator from "./PricingConfigurator";
import FAQ from "@/components/pricing/FAQ";
import Guarantee from "@/components/pricing/Guarantee";
import { Plan } from "@/components/pricing/types";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Pricing & SLAs — Van Hunen IT", title: "Plans, SLAs & Self-Service — Managed VPS, Websites & Minecraft | Van Hunen IT",
description: description:
"Simple plans with real SLAs. Essential, Growth, Mission-Critical. Month-to-month, 30-day cancel, incident credits.", "Choose Managed Hosting & Care, provision a VPS (managed or owned), or deploy a Minecraft server. SLAs, restore-tested backups, monitoring. Month-to-month.",
}; };
const plans: Plan[] = [ // ---- Server-safe plan data (also used for JSON-LD) ---- //
const carePlans = [
{ {
id: "essential", id: "essential",
name: "Essential Care", name: "Essential Managed Hosting",
bestFor: "Solo & micro businesses", bestFor: "Websites & VPS that need reliable care",
monthlyPrice: 149, monthlyPrice: 149,
yearlyDiscount: 0.1, yearlyDiscount: 0.1,
outcomes: ["Inbox-ready email", "99.9% uptime"], outcomes: ["Reliable care & monitoring", "99.9% uptime target"],
inclusions: [ inclusions: [
"SPF/DKIM/DMARC monitoring", "DNS & email monitoring (SPF/DKIM/DMARC)",
"Automated backups + quarterly restore test", "Automated backups + quarterly restore test",
"Managed updates (WordPress/Next.js)",
"Uptime & SSL watch", "Uptime & SSL watch",
"Incident credits", "Incident credits",
"Business-hours support (8×5)", "Business-hours support (8×5)",
], ],
sla: "Next-business-day first response (8×5)", sla: "Next-business-day first response (8×5)",
ctaLabel: "Start Essential", cta: { label: "Start Essential", href: "/contact?type=website-care" },
popular: false, popular: false,
}, },
{ {
id: "growth", id: "growth",
name: "Growth Care", name: "Growth Managed Hosting",
bestFor: "SMB team sites", bestFor: "Teams & growing sites",
monthlyPrice: 299, monthlyPrice: 299,
yearlyDiscount: 0.1, yearlyDiscount: 0.1,
outcomes: ["99.95% uptime", "Faster TTFB"], outcomes: ["99.95% uptime target", "Faster performance"],
inclusions: [ inclusions: [
"Everything in Essential", "Everything in Essential",
"Cloudflare WAF & bot tuning", "Cloudflare WAF & bot tuning",
@ -47,72 +46,213 @@ const plans: Plan[] = [
"Priority incident handling", "Priority incident handling",
], ],
sla: "4-hour first response (8×5)", sla: "4-hour first response (8×5)",
ctaLabel: "Start Growth", cta: { label: "Start Growth", href: "/contact?type=website-care" },
popular: true, popular: true,
}, },
{ {
id: "mission", id: "mission",
name: "Mission-Critical", name: "Mission-Critical Managed Hosting",
bestFor: "High-traffic & 24/7", bestFor: "High-traffic & 24/7 operations",
monthlyPrice: 649, monthlyPrice: 649,
yearlyDiscount: 0.1, yearlyDiscount: 0.1,
outcomes: ["99.99% uptime", "24/7 on-call"], outcomes: ["99.99% uptime target", "24/7 on-call"],
inclusions: [ inclusions: ["Everything in Growth", "24/7 paging", "Weekly checks", "DR runbook & drills"],
"Everything in Growth",
"24/7 paging",
"Weekly checks",
"DR runbook & drills",
],
sla: "1-hour first response (24/7)", sla: "1-hour first response (24/7)",
ctaLabel: "Talk to us", cta: { label: "Talk to Us", href: "/contact?type=website-care" },
popular: false, popular: false,
contactOnly: true, contactOnly: true,
}, },
]; ] as const;
const vpsPlans = [
{
id: "solo",
name: "Solo VPS",
price: 24, // €/mo
specs: "2 vCPU • 4 GB RAM • 50 GB NVMe",
bestFor: "Small apps, staging, low-traffic services",
},
{
id: "team",
name: "Team VPS",
price: 49,
specs: "4 vCPU • 8 GB RAM • 100 GB NVMe",
bestFor: "Production apps, WordPress/Next.js, APIs",
},
{
id: "dedicated",
name: "Dedicated / Custom",
price: null, // custom
specs: "Dedicated VPS/metal — tailored",
bestFor: "High traffic, networks, or compliance",
},
] as const;
const minecraftPlans = [
{
id: "starter",
name: "Starter",
price: 19,
specs: "2 vCPU • 4 GB RAM • 40 GB SSD",
typical: "Up to ~20 players (optimized)",
features: ["Paper/Spigot or Bedrock", "Daily backups + restore test", "Basic tuning & monitoring"],
},
{
id: "pro",
name: "Pro",
price: 39,
specs: "3 vCPU • 8 GB RAM • 80 GB SSD",
typical: "Up to ~60 players (optimized)",
features: [
"Paper/Velocity or Bedrock + Geyser",
"Backups (2x/day) + restore test",
"Advanced tuning & monitoring",
],
},
{
id: "network",
name: "Network",
price: null,
specs: "Dedicated VPS spec",
typical: "Networks & modpacks",
features: ["Velocity/Bungee multi-server", "Backups (hourly) + restore test", "Custom tuning & SLA"],
},
] as const;
// ---- JSON-LD (Offers for three catalogs) ---- //
const pricingLd = {
"@context": "https://schema.org",
"@type": "OfferCatalog",
name: "Van Hunen IT — Plans & Catalog",
url: `${site.url}/pricing`,
itemListElement: [
{
"@type": "OfferCatalog",
name: "Managed Hosting & Care",
itemListElement: carePlans.map((p) => ({
"@type": "Offer",
name: p.name,
price: `${p.monthlyPrice}/mo`,
priceCurrency: "EUR",
url: `${site.url}/pricing`,
description: `${p.bestFor} — SLA: ${p.sla}`,
})),
},
{
"@type": "OfferCatalog",
name: "VPS (Managed or Owned)",
itemListElement: vpsPlans.map((p) => ({
"@type": "Offer",
name: p.name,
price: p.price ? `${p.price}/mo` : "Custom",
priceCurrency: "EUR",
url: `${site.url}/vps#deploy`,
description: `${p.specs}${p.bestFor}`,
})),
},
{
"@type": "OfferCatalog",
name: "Minecraft Hosting",
itemListElement: minecraftPlans.map((p) => ({
"@type": "Offer",
name: p.name,
price: p.price ? `${p.price}/mo` : "Custom",
priceCurrency: "EUR",
url: `${site.url}/minecraft#deploy`,
description: `${p.specs}${p.typical}`,
})),
},
],
};
export default function PricingPage() { export default function PricingPage() {
return ( return (
<main className="mx-auto max-w-7xl px-6 py-16"> <main className="relative isolate bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
<header className="mx-auto max-w-3xl text-center"> {/* HERO */}
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl"> <header className="container mx-auto max-w-6xl px-6 pt-16 pb-10 text-center">
Simple plans with real SLAs <h1 className="text-4xl sm:text-5xl font-bold tracking-tight">
Plans, SLAs & Self-Service
</h1> </h1>
<p className="mt-4 text-lg text-gray-600"> <p className="mt-4 text-lg text-neutral-700 dark:text-neutral-300">
Pick the care level that matches your traffic and risk. Month-to-month, 30-day Pick **Managed Hosting & Care**, provision a **VPS (managed or owned)**, or **deploy a Minecraft server**.
cancel. Fix Sprint available for urgent issues. Month-to-month, 30-day cancel, restore-tested backups, real SLAs.
</p> </p>
<div className="mt-8 flex flex-col sm:flex-row gap-3 justify-center">
<Link href="/vps#deploy" className="rounded-lg bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 px-5 py-2 font-semibold hover:opacity-90 transition">
Provision a VPS
</Link>
<Link href="/minecraft#deploy" className="rounded-lg border border-neutral-300 dark:border-neutral-700 px-5 py-2 font-semibold text-neutral-900 dark:text-white hover:bg-neutral-50 dark:hover:bg-neutral-800 transition">
Deploy Minecraft
</Link>
</div>
</header> </header>
{/* Engaging element #1: Billing toggle inside table */} {/* NEW PRICING EXPERIENCE */}
<section className="mt-12"> <section className="container mx-auto max-w-6xl px-6 pb-20">
<PricingTable plans={plans} /> <PricingConfigurator carePlans={carePlans} vpsPlans={vpsPlans} minecraftPlans={minecraftPlans} />
<p className="mt-3 text-center text-sm text-gray-500">
Prices exclude VAT. Annual billing saves 10%. <p className="mt-6 text-center text-sm text-neutral-500">
Prices exclude VAT. Annual billing saves 10% on Managed Hosting & Care. VPS and Minecraft prices shown are base infrastructure fees; management is included on our platform. Owned VPS incurs provider charges in your account.
</p> </p>
</section> </section>
{/* Engaging element #2: Plan recommender */} {/* TRUST / GUARANTEE */}
<section className="mt-24"> <section className="border-t border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60">
<PlanRecommender plans={plans} /> <div className="container mx-auto max-w-6xl px-6 py-14 grid gap-6 sm:grid-cols-3">
{[
{ t: "Restore-Tested Backups", s: "We time drills and keep notes." },
{ t: "SLA-Backed Response", s: "Credits per plan. 24/7 available." },
{ t: "Month-to-Month", s: "30-day cancel. No lock-in." },
].map((i) => (
<div key={i.t} className="rounded-xl border border-neutral-200 dark:border-neutral-800 p-5">
<div className="text-base font-semibold">{i.t}</div>
<div className="text-sm text-neutral-600 dark:text-neutral-400">{i.s}</div>
</div>
))}
</div>
</section> </section>
{/* Comparison matrix */} {/* FAQ */}
<section className="mt-24"> <section className="container mx-auto max-w-5xl px-6 py-16">
<ComparisonTable plans={plans} /> <h2 className="text-2xl sm:text-3xl font-bold tracking-tight text-center">FAQ</h2>
<div className="mt-8 grid gap-6">
{[
{
q: "Whats the difference between Managed and Owned VPS?",
a: "Managed = we host & bill the VPS on our platform (backups, monitoring, SLAs included). Owned = we provision into your cloud account via a secure connect; you retain ownership. Both include management.",
},
{
q: "Can I migrate my current site or server?",
a: "Yes. We handle data transfer, DNS cutover, and rollback. We also tune performance and verify backups with a restore drill.",
},
{
q: "Do you support modpacks and networks for Minecraft?",
a: "Yes—Forge/Fabric and Velocity/Bungee networks are supported, sized to resources. Use the Network/Custom option or contact us.",
},
{
q: "How do incident credits work?",
a: "If we miss your SLA, we credit according to your plans policy. Details are included in your agreement.",
},
{
q: "Do you offer NDAs and least-privilege access?",
a: "Yes—mutual NDA available; we only request the credentials needed for the task.",
},
].map((i) => (
<article key={i.q} className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-6">
<h3 className="text-lg font-semibold">{i.q}</h3>
<p className="mt-2 text-sm text-neutral-700 dark:text-neutral-300">{i.a}</p>
</article>
))}
</div>
<div className="mt-10 text-center">
<Link href="/contact?plan=recommendation" className="inline-flex items-center rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white px-6 py-3 text-sm font-semibold hover:opacity-90 transition">
Get My Recommendation
</Link>
</div>
</section> </section>
{/* Engaging element #3: ROI calculator */} <JsonLd data={pricingLd} />
<section className="mt-24">
<ROICalculator plans={plans} />
</section>
<section className="mt-24">
<Guarantee />
</section>
<section className="mt-24">
<FAQ />
</section>
</main> </main>
); );
} }

View File

@ -1,159 +1,445 @@
// app/services/page.tsx // app/services/page.tsx
import { Metadata } from "next"; import type { Metadata } from "next";
import { getAllServices, SERVICE_CATEGORIES, type ServiceCategoryId } from "@/lib/services";
import ServiceCard from "@/components/ServiceCard";
import Link from "next/link"; import Link from "next/link";
import JsonLd from "@/components/JsonLd";
import { site } from "@/lib/site";
export const revalidate = 86400; export const revalidate = 86400;
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Services — Website Reliability for SMBs | Van Hunen IT", title: "Services — Managed Hosting, Websites & Minecraft | Van Hunen IT",
description: description:
"Fixed-scope sprints and care plans for SMB website reliability: email deliverability (DMARC), Cloudflare edge security & speed, tested backups, and uptime watch.", "Managed or Owned VPS hosting, Managed Website hosting & care, Website development, and Minecraft hosting & plugins. Clear outcomes, flat pricing, SLAs, and before/after proof.",
alternates: { canonical: "/services" }, alternates: { canonical: "/services" },
}; };
export default function ServicesPage() { type Card = {
const services = getAllServices(); title: string;
body: string;
href: string;
cta: string;
};
// Emphasize core Reliability first; keep the rest discoverable. type Category = {
const categories: ServiceCategoryId[] = [ id:
"web-performance", | "web-dev"
"infrastructure-devops", | "web-performance"
"dev-platforms", | "minecraft"
"migrations", | "infrastructure-devops"
"web-dev", | "migrations"
"minecraft", | "dev-platforms";
]; anchor: string;
label: string;
lead: string;
cards: Card[];
};
const CATEGORIES: Category[] = [
{
id: "web-dev",
anchor: "web-dev",
label: "Website Development",
lead:
"Fast, SEO-ready websites with modern tooling and clean deployments. We build for reliability first.",
cards: [
{
title: "Next.js Website Build",
body:
"Design system, content structure, analytics, basic SEO, and production deployment. Optimized for Core Web Vitals.",
href: "/contact?type=development",
cta: "Build My Website →",
},
{
title: "Landing Pages & Microsites",
body:
"High-converting pages with performance and tracking baked in. Perfect for campaigns and product launches.",
href: "/contact?type=development",
cta: "Start a Landing Page →",
},
{
title: "Headless CMS Setup",
body:
"Content modeling, editor workflows, preview, and safe publishing. Optional migration from legacy CMS.",
href: "/contact?type=development",
cta: "Discuss CMS →",
},
],
},
{
id: "web-performance",
anchor: "web-performance",
label: "Web Reliability & Performance (incl. Website Hosting)",
lead:
"We make sites dependable: faster TTFB, verified backups, uptime & SSL watch, and incident notes you can share.",
cards: [
{
title: "Managed Website Hosting & Care",
body:
"Updates, uptime, security checks, and monthly restore-tested backups. Real SLAs and incident credits.",
href: "/pricing#care",
cta: "Start Website Care →",
},
{
title: "Cloudflare Edge Hardening",
body:
"WAF & bot rules, cache & HTTP/3 tuning, origin shields, and fewer attacks for faster pages.",
href: "/contact?type=website-care",
cta: "Harden My Edge →",
},
{
title: "Email Deliverability (SPF/DKIM/DMARC)",
body:
"Block spoofing, align auth, and fix lead-loss from junk folders. Monitoring and reports included.",
href: "/contact?type=website-care",
cta: "Fix My Email →",
},
{
title: "Backups + Restore Drills",
body:
"Automated backups plus timed restore tests with notes. Know recovery is proven—not just configured.",
href: "/contact?type=website-care",
cta: "Verify My Backups →",
},
{
title: "Performance & Web Vitals",
body:
"TTFB improvements, image/CDN strategy, and Core Web Vitals uplift with before/after proof.",
href: "/contact?type=website-care",
cta: "Boost My Speed →",
},
],
},
{
id: "minecraft",
anchor: "minecraft",
label: "Minecraft Services",
lead:
"Lag-free servers on a hardened VPS with backups, monitoring, and tuning. Need features? We build custom plugins.",
cards: [
{
title: "Managed Minecraft Hosting",
body:
"Java (Paper/Spigot/Velocity) and Bedrock (Geyser) supported. Backups with restore tests and incident notes.",
href: "/minecraft#deploy",
cta: "Deploy a Server →",
},
{
title: "Minecraft Plugin Development",
body:
"Custom plugins with tests, config, permissions, and docs. Private repo and handover on completion.",
href: "/contact?type=plugin",
cta: "Request Plugin Quote →",
},
{
title: "Migrations & Tuning",
body:
"Move worlds and plugins safely, clean up timings, and stabilize TPS. Optional network (Velocity/Bungee).",
href: "/contact?type=minecraft-hosting",
cta: "Plan My Migration →",
},
],
},
{
id: "infrastructure-devops",
anchor: "infrastructure-devops",
label: "Infrastructure & DevOps",
lead:
"Managed or Owned VPS, monitoring, and incident response—plus safe automation for deploys and updates.",
cards: [
{
title: "Managed VPS Hosting",
body:
"CIS-style hardening, updates, monitoring, and restore-tested backups. SLA-backed response.",
href: "/vps#deploy",
cta: "Provision (Managed) →",
},
{
title: "Owned VPS (Your Account)",
body:
"We provision into your cloud via secure connect. You retain ownership; we still manage and monitor.",
href: "/vps#deploy",
cta: "Provision (Owned) →",
},
{
title: "Monitoring & Incident Notes",
body:
"Uptime, resource alerts, clear incident write-ups, and post-mortems that drive improvements.",
href: "/contact?type=vps",
cta: "Set Up Monitoring →",
},
],
},
{
id: "migrations",
anchor: "migrations",
label: "Migrations & Refreshes",
lead:
"Safer cutovers with rollback plans. Move hosting, upgrade stacks, and modernize without downtime surprises.",
cards: [
{
title: "Hosting & DNS Migration",
body:
"Plan, test, and cut over with a rollback path. Documented steps and after-action report.",
href: "/contact?type=website-care",
cta: "Migrate Hosting →",
},
{
title: "WordPress → Next.js",
body:
"Modern rebuilds with headless options and faster delivery. SEO preservation and redirects included.",
href: "/contact?type=development",
cta: "Discuss Rebuild →",
},
{
title: "Platform Upgrades",
body:
"PHP/Node/OpenSSL updates, TLS/HTTP versions, and dependency hygiene without breaking the app.",
href: "/contact?type=vps",
cta: "Plan My Upgrade →",
},
],
},
{
id: "dev-platforms",
anchor: "dev-platforms",
label: "Developer Platforms & Tooling",
lead:
"Make shipping easier: pipelines, environments, and guardrails that keep production stable.",
cards: [
{
title: "CI/CD & Environments",
body:
"Staging, previews, and blue/green deployment workflows. Safer changes, faster feedback.",
href: "/contact?type=development",
cta: "Improve Deploys →",
},
{
title: "Observability Basics",
body:
"Structured logs, metrics, and health checks that find issues before users do.",
href: "/contact?type=vps",
cta: "Add Observability →",
},
{
title: "Runbooks & Readiness",
body:
"Clear runbooks and checklists for on-call, upgrades, and incident handling.",
href: "/contact?type=website-care",
cta: "Create Runbooks →",
},
],
},
] as const;
// JSON-LD OfferCatalog: top-level sections surfaced for SEO
const servicesLd = {
"@context": "https://schema.org",
"@type": "OfferCatalog",
name: "Van Hunen IT — Service Catalog",
url: `${site.url}/services`,
itemListElement: CATEGORIES.map((cat) => ({
"@type": "OfferCatalog",
name: cat.label,
url: `${site.url}/services#${cat.anchor}`,
itemListElement: cat.cards.map((c) => ({
"@type": "Offer",
name: c.title,
description: c.body,
url: `${site.url}${c.href.startsWith("/") ? c.href : `/${c.href}`}`,
priceCurrency: "EUR",
})),
})),
};
export default function ServicesPage() {
const chips = CATEGORIES.map((c) => ({ id: c.id, label: c.label, anchor: c.anchor }));
return ( return (
<main <main
id="top" 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" 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 */}
<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="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-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">
Services that make websites reliable Services: Managed Hosting, Websites & Minecraft
</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-3xl mx-auto">
Fixed-scope sprints and care plans for{" "} Managed or Owned VPS, Managed Website hosting & care, Website development, and Minecraft hosting & plugins.
<strong>email deliverability</strong>, <strong>Cloudflare edge security &amp; speed</strong>,{" "} Clear outcomes, flat pricing, real SLAsand before/after proof.
<strong>backups with restore tests</strong>, and <strong>uptime watch</strong>. Clear outcomes, flat pricing,
and before/after proof you can keep.
</p> </p>
{/* Category chips */}
<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) => ( {chips.map((c) => (
<a <a
key={id} key={c.id}
href={`#${SERVICE_CATEGORIES[id].anchor}`} href={`#${c.anchor}`}
className="rounded-full border border-neutral-300 dark:border-neutral-700 px-4 py-1.5 text-sm font-medium text-neutral-700 dark:text-neutral-300 hover:border-blue-500 hover:text-blue-600 dark:hover:text-blue-400 transition" className="rounded-full border border-neutral-300 dark:border-neutral-700 px-4 py-1.5 text-sm font-medium text-neutral-700 dark:text-neutral-300 hover:border-blue-500 hover:text-blue-600 dark:hover:text-blue-400 transition"
> >
{SERVICE_CATEGORIES[id].label} {c.label}
</a> </a>
))} ))}
</div> </div>
<div className="mt-8 flex items-center justify-center gap-3"> {/* Quick actions */}
<div className="mt-8 grid grid-cols-1 sm:grid-cols-2 gap-3 max-w-2xl mx-auto">
<Link <Link
href="/pricing" href="/vps#deploy"
className="inline-flex items-center rounded-lg bg-blue-600 px-5 py-2.5 text-white font-semibold hover:bg-blue-500 transition" className="inline-flex items-center justify-center rounded-lg bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 px-5 py-3 font-semibold hover:opacity-90 transition"
> >
See pricing Provision a VPS
</Link> </Link>
<Link <Link
href="/contact" href="/minecraft#deploy"
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" className="inline-flex items-center justify-center rounded-lg border border-neutral-300 dark:border-neutral-700 px-5 py-3 font-semibold text-neutral-900 dark:text-white hover:bg-neutral-50 dark:hover:bg-neutral-800 transition"
> >
Book a 15-min Fit Call Deploy Minecraft
</Link> </Link>
</div> </div>
</div> </div>
</section> </section>
{/* --- Service Categories --- */} {/* FEATURED TILES */}
<div className="container mx-auto max-w-6xl px-4 py-16"> <section className="container mx-auto max-w-6xl px-4 py-12">
{categories.map((id, index) => { <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
const list = services.filter((s) => s.category === id); {[
if (!list.length) return null; {
id: "vps",
title: "Managed VPS (Managed or Owned)",
body: "Hardened & monitored VPS with restore-tested backups.",
href: "/vps#deploy",
cta: "Provision VPS →",
},
{
id: "website-care",
title: "Managed Website Hosting & Care",
body: "Updates, uptime, security checks & monthly restore tests.",
href: "/pricing#care",
cta: "Start Website Care →",
},
{
id: "development",
title: "Website Development (Next.js)",
body: "Fast, SEO-ready sites with analytics & deployment.",
href: "/contact?type=development",
cta: "Build My Website →",
},
{
id: "minecraft",
title: "Minecraft Hosting & Plugins",
body: "Lag-free servers on a hardened VPS + custom plugins.",
href: "/minecraft#deploy",
cta: "Deploy a Server →",
},
].map((card) => (
<article
id={card.id}
key={card.id}
className="group relative overflow-hidden rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-6 shadow-sm hover:shadow-lg transition-all duration-300 backdrop-blur"
>
<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-lg font-semibold tracking-tight text-neutral-900 dark:text-white">
{card.title}
</h3>
<p className="mt-2 text-sm text-neutral-700 dark:text-neutral-300">{card.body}</p>
<Link
href={card.href}
className="mt-4 inline-flex items-center text-sm font-medium text-blue-600 hover:underline"
>
{card.cta}
</Link>
</article>
))}
</div>
</section>
const headingId = `${id}-heading`; {/* SECTIONS */}
const sectionClasses = `relative py-16 scroll-mt-24 ${ <div className="container mx-auto max-w-6xl px-4 py-8">
{CATEGORIES.map((cat, index) => (
<section
key={cat.id}
id={cat.anchor}
className={`relative py-16 scroll-mt-24 rounded-2xl mb-12 shadow-sm ${
index % 2 === 0 index % 2 === 0
? "bg-white dark:bg-neutral-900/50" ? "bg-white dark:bg-neutral-900/50"
: "bg-neutral-50 dark:bg-neutral-900/30" : "bg-neutral-50 dark:bg-neutral-900/30"
} rounded-2xl mb-12 shadow-sm`; }`}
aria-labelledby={`${cat.anchor}-heading`}
return (
<section
key={id}
id={SERVICE_CATEGORIES[id].anchor}
aria-labelledby={headingId}
className={sectionClasses}
> >
<div className="absolute inset-0 -z-10 bg-gradient-to-br from-blue-50/10 via-transparent to-purple-50/10 dark:from-blue-950/10 dark:to-purple-950/10 rounded-2xl" /> <div className="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={headingId} id={`${cat.anchor}-heading`}
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} {cat.label}
</h2> </h2>
<Link <Link
href="#top" 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>
</div> </div>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3"> <p className="max-w-3xl text-neutral-700 dark:text-neutral-300">
{list.map((svc, i) => ( {cat.lead}
<div </p>
key={svc.slug}
className="animate-[fadeInUp_0.6s_ease_forwards]" <div className="mt-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
style={{ animationDelay: `${i * 80}ms` }} {cat.cards.map((card, i) => (
<article
key={`${cat.id}-${i}`}
className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-6 hover:shadow-md transition"
> >
<ServiceCard svc={svc} /> <h3 className="text-lg font-semibold">{card.title}</h3>
</div> <p className="mt-2 text-sm text-neutral-700 dark:text-neutral-300">{card.body}</p>
<Link
href={card.href}
className="mt-4 inline-flex items-center text-sm font-medium text-blue-600 hover:underline"
>
{card.cta}
</Link>
</article>
))} ))}
</div> </div>
</div> </div>
</section> </section>
); ))}
})}
</div> </div>
{/* --- CTA footer --- */} {/* CTA FOOTER */}
<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 improve reliability and deliverability? Power, Performance & Peace of Mind Managed for You.
</h2> </h2>
<p className="mt-4 text-lg text-blue-100"> <p className="mt-4 text-lg text-blue-100">
Tell us about your site. Well recommend the right Fix Sprint or Care plan and show you the expected Provision a VPS, manage your site, or deploy a Minecraft serverbacked by real SLAs and restore-tested backups.
before/after.
</p> </p>
<div className="mt-8 flex items-center justify-center gap-3"> <div className="mt-8 flex items-center justify-center gap-3">
<Link <Link
href="/pricing" 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" className="inline-flex items-center rounded-lg bg-white text-blue-700 font-semibold px-6 py-3 hover:bg-blue-50 transition"
> >
Compare plans Compare Plans
</Link> </Link>
<Link <Link
href="/contact" href="/contact?plan=recommendation"
className="inline-flex items-center rounded-lg border border-white/80 text-white font-semibold px-6 py-3 hover:bg-white/10 transition" 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 Get My Recommendation
</Link> </Link>
</div> </div>
</div> </div>
</section> </section>
<JsonLd data={servicesLd} />
</main> </main>
); );
} }

View File

@ -0,0 +1,410 @@
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { toast } from "sonner";
type PlanId = "solo" | "team" | "dedicated";
type Region = "eu-nl" | "eu-de" | "us-east" | "us-west";
type OSImage = "ubuntu-24-04" | "ubuntu-22-04" | "debian-12";
type OwnershipMode = "managed" | "owned";
type Plan = {
id: PlanId;
name: string;
price: string;
specs: string;
bestFor: string;
features: readonly string[];
};
type RegionOpt = { id: Region; label: string };
type OSOpt = { id: OSImage; label: string };
export default function VPSDeployWizard({
plans,
regions,
osImages,
siteHostname,
}: {
plans: readonly Plan[];
regions: readonly RegionOpt[];
osImages: readonly OSOpt[];
siteHostname: string;
}) {
const router = useRouter();
const [step, setStep] = useState<number>(1);
const [loading, setLoading] = useState<boolean>(false);
const [form, setForm] = useState<{
plan: PlanId | "";
region: Region | "";
osImage: OSImage | "";
hostname: string;
fqdn: string;
sshPublicKey: string;
ownership: OwnershipMode;
provider: "" | "hetzner" | "digitalocean" | "vultr";
email: string;
notes: string;
acceptTerms: boolean;
enableBackups: boolean;
}>({
plan: "",
region: "",
osImage: "ubuntu-24-04",
hostname: "",
fqdn: "",
sshPublicKey: "",
ownership: "managed",
provider: "",
email: "",
notes: "",
acceptTerms: false,
enableBackups: true,
});
const set = <K extends keyof typeof form>(key: K, value: (typeof form)[K]) =>
setForm((f) => ({ ...f, [key]: value }));
// Preselect plan from hash/query if present
if (typeof window !== "undefined" && !form.plan) {
const hash = window.location.hash;
const url = new URL(window.location.href);
const qp = url.searchParams.get("plan") as PlanId | null;
const fromHash = /plan=(solo|team|dedicated)/.exec(hash || "")?.[1] as PlanId | undefined;
const initial = qp || fromHash;
if (initial) set("plan", initial);
}
const canContinue1 = !!form.plan;
const canContinue2 = !!form.region && !!form.osImage;
const canSubmit =
canContinue2 &&
!!form.hostname &&
!!form.email &&
form.acceptTerms &&
(form.ownership === "managed" || (form.ownership === "owned" && !!form.provider));
const handleSubmit = async () => {
if (!canSubmit) return;
setLoading(true);
try {
const res = await fetch("/api/vps/create", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-secret-token": process.env.NEXT_PUBLIC_FORM_SECRET || "",
},
body: JSON.stringify(form),
});
if (!res.ok) throw new Error("Provisioning failed");
toast.success(
form.ownership === "owned"
? "Owned VPS request received! Check your email to securely connect your provider."
: "VPS provisioning started! Check your email for access details."
);
router.push("/vps/success");
} catch (err) {
console.error(err);
toast.error("Something went wrong. Please try again.");
} finally {
setLoading(false);
}
};
return (
<div className="mt-8 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 shadow-sm p-6">
<AnimatePresence mode="wait">
{step === 1 && (
<motion.div
key="vps-step1"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.25 }}
>
<h3 className="text-xl font-semibold text-neutral-900 dark:text-white">1) Choose a Plan</h3>
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
You can change plans later. Dedicated is customcontact us for sizing.
</p>
<div className="mt-5 grid gap-4 sm:grid-cols-3">
{plans.map((p) => (
<button
key={p.id}
onClick={() => set("plan", p.id)}
className={`text-left rounded-xl border p-4 transition ${
form.plan === p.id
? "border-blue-600 bg-blue-50/50 dark:bg-blue-950/30"
: "border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60"
}`}
>
<div className="flex items-baseline justify-between">
<div className="font-semibold">{p.name}</div>
<div className="text-sm font-bold">{p.price}</div>
</div>
<div className="mt-1 text-xs text-neutral-600 dark:text-neutral-400">{p.specs}</div>
<div className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{p.bestFor}</div>
</button>
))}
</div>
<div className="mt-6 flex justify-end">
<button
disabled={!canContinue1}
onClick={() => setStep(2)}
className="rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white font-medium px-5 py-2 disabled:opacity-50"
>
Continue
</button>
</div>
</motion.div>
)}
{step === 2 && (
<motion.div
key="vps-step2"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.25 }}
>
<h3 className="text-xl font-semibold text-neutral-900 dark:text-white">2) Configure</h3>
<div className="mt-4 grid gap-4 sm:grid-cols-2">
{/* Region */}
<div>
<label className="block text-sm font-medium mb-1">Region</label>
<select
value={form.region}
onChange={(e) => set("region", e.target.value as Region)}
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2"
>
<option value="">Select region</option>
{regions.map((r) => (
<option key={r.id} value={r.id}>
{r.label}
</option>
))}
</select>
</div>
{/* OS */}
<div>
<label className="block text-sm font-medium mb-1">Operating System</label>
<select
value={form.osImage}
onChange={(e) => set("osImage", e.target.value as OSImage)}
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2"
>
{osImages.map((o) => (
<option key={o.id} value={o.id}>
{o.label}
</option>
))}
</select>
</div>
{/* Hostname */}
<div>
<label className="block text-sm font-medium mb-1">Hostname</label>
<input
type="text"
value={form.hostname}
onChange={(e) => set("hostname", e.target.value.replace(/[^a-z0-9-.]/gi, "").toLowerCase())}
placeholder="app-1"
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2"
/>
</div>
{/* FQDN */}
<div>
<label className="block text-sm font-medium mb-1">FQDN (optional)</label>
<div className="flex items-center gap-2">
<input
type="text"
value={form.fqdn}
onChange={(e) => set("fqdn", e.target.value.toLowerCase())}
placeholder={`app.${siteHostname}`}
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2"
/>
</div>
<p className="mt-1 text-xs text-neutral-500">
We can map a subdomain like <code>app</code>.{siteHostname}.
</p>
</div>
{/* SSH Public Key */}
<div className="sm:col-span-2">
<label className="block text-sm font-medium mb-1">SSH Public Key (recommended)</label>
<textarea
value={form.sshPublicKey}
onChange={(e) => set("sshPublicKey", e.target.value)}
rows={3}
placeholder="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5.... your_email@example.com"
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2"
/>
<label className="mt-2 inline-flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={form.enableBackups}
onChange={(e) => set("enableBackups", e.target.checked)}
className="h-4 w-4"
/>
Enable backups (recommended)
</label>
</div>
</div>
<div className="mt-6 flex justify-between">
<button
onClick={() => setStep(1)}
className="text-sm text-neutral-600 dark:text-neutral-400 hover:underline"
>
Back
</button>
<button
disabled={!canContinue2}
onClick={() => setStep(3)}
className="rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white font-medium px-5 py-2 disabled:opacity-50"
>
Continue
</button>
</div>
</motion.div>
)}
{step === 3 && (
<motion.div
key="vps-step3"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.25 }}
>
<h3 className="text-xl font-semibold text-neutral-900 dark:text-white">3) Ownership & Provision</h3>
<div className="mt-4 grid gap-4">
{/* Ownership Mode */}
<div>
<label className="block text-sm font-medium mb-1">Management Mode</label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<button
onClick={() => set("ownership", "managed")}
type="button"
className={`rounded-lg border p-3 text-left ${
form.ownership === "managed"
? "border-blue-600 bg-blue-50/50 dark:bg-blue-950/30"
: "border-neutral-300 dark:border-neutral-700"
}`}
>
<div className="font-semibold">Managed (default)</div>
<div className="text-xs text-neutral-600 dark:text-neutral-400">
We host & bill the VPS; you still get access. Backups & monitoring included.
</div>
</button>
<button
onClick={() => set("ownership", "owned")}
type="button"
className={`rounded-lg border p-3 text-left ${
form.ownership === "owned"
? "border-blue-600 bg-blue-50/50 dark:bg-blue-950/30"
: "border-neutral-300 dark:border-neutral-700"
}`}
>
<div className="font-semibold">Owned (your account)</div>
<div className="text-xs text-neutral-600 dark:text-neutral-400">
We provision into your cloud account via a secure connect. You keep full ownership.
</div>
</button>
</div>
</div>
{/* Provider (only for Owned) */}
{form.ownership === "owned" && (
<div>
<label className="block text-sm font-medium mb-1">Cloud Provider</label>
<select
value={form.provider}
onChange={(e) =>
set("provider", e.target.value as "hetzner" | "digitalocean" | "vultr" | "")
}
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2"
>
<option value="">Select provider</option>
<option value="hetzner">Hetzner</option>
<option value="digitalocean">DigitalOcean</option>
<option value="vultr">Vultr</option>
</select>
<p className="mt-1 text-xs text-neutral-500">
After submission, well email a secure link to connect your providerno API keys here.
</p>
</div>
)}
{/* Contact + Notes */}
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium mb-1">Contact Email</label>
<input
type="email"
value={form.email}
onChange={(e) => set("email", e.target.value)}
placeholder="you@example.com"
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Notes (optional)</label>
<input
type="text"
value={form.notes}
onChange={(e) => set("notes", e.target.value)}
placeholder="Apps to deploy, access prefs, etc."
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2"
/>
</div>
</div>
<label className="mt-2 inline-flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={form.acceptTerms}
onChange={(e) => set("acceptTerms", e.target.checked)}
className="h-4 w-4"
/>
I accept the service terms and understand provisioning involves infrastructure costs.
</label>
</div>
<div className="mt-6 flex justify-between items-center">
<button
onClick={() => setStep(2)}
className="text-sm text-neutral-600 dark:text-neutral-400 hover:underline"
>
Back
</button>
<button
onClick={handleSubmit}
disabled={!canSubmit || loading}
className="rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white font-medium px-6 py-2 disabled:opacity-50"
>
{loading ? "Provisioning..." : "Provision VPS"}
</button>
</div>
<p className="mt-3 text-xs text-neutral-600 dark:text-neutral-400">
Well send access details by email. Managed mode includes monitoring and backups. Owned mode will
prompt a secure provider connect after submission.
</p>
</motion.div>
)}
</AnimatePresence>
</div>
);
}

305
web/app/vps/page.tsx Normal file
View File

@ -0,0 +1,305 @@
import type { Metadata } from "next";
import Link from "next/link";
import JsonLd from "@/components/JsonLd";
import { site } from "@/lib/site";
import VPSDeployWizard from "./VPSDeployWizard";
export const metadata: Metadata = {
title: "Managed VPS Hosting — Hardened, Monitored & Restore-Tested | Van Hunen IT",
description:
"Secure, fast VPS hosting with server management, monitoring, and restore-tested backups. Choose managed or own your VPS through our platform.",
};
const plans = [
{
id: "solo",
name: "Solo",
price: "€24/mo",
specs: "2 vCPU • 4 GB RAM • 50 GB NVMe",
bestFor: "Small apps, staging, low-traffic services",
features: [
"CIS-style hardening & firewall",
"Automated updates & monitoring",
"Daily backups + monthly restore test",
],
},
{
id: "team",
name: "Team",
price: "€49/mo",
specs: "4 vCPU • 8 GB RAM • 100 GB NVMe",
bestFor: "Production apps, WordPress/Next.js, APIs",
features: [
"Advanced monitoring & alerts",
"Backups (2x/day) + restore test",
"Incident notes & SLA response",
],
},
{
id: "dedicated",
name: "Dedicated",
price: "Custom",
specs: "Dedicated VPS/metal — tailored",
bestFor: "High traffic, networks, or compliance",
features: [
"Custom sizing & network",
"Hourly backups + drills (configurable)",
"Custom SLA & runbook",
],
},
] as const;
const regions = [
{ id: "eu-nl", label: "EU — Netherlands" },
{ id: "eu-de", label: "EU — Germany" },
{ id: "us-east", label: "US — East" },
{ id: "us-west", label: "US — West" },
] as const;
const osImages = [
{ id: "ubuntu-24-04", label: "Ubuntu 24.04 LTS" },
{ id: "ubuntu-22-04", label: "Ubuntu 22.04 LTS" },
{ id: "debian-12", label: "Debian 12" },
] as const;
const faqItems = [
{
q: "Can I truly own the VPS?",
a: "Yes. Choose “Owned (your account)” during provisioning and well provision into your cloud account after a secure connect flow. You retain full ownership and root access.",
},
{
q: "Whats included in management?",
a: "Hardening, updates, monitoring, backups with restore tests, incident notes, and SLA-backed response depending on your plan.",
},
{
q: "Can you migrate my existing app or site?",
a: "Yes. Well review your stack, plan the cutover, and provide a rollback path. We can also set up staging and blue/green deploys.",
},
{
q: "Do I get root access?",
a: "Yes. For Managed mode we provide least-privilege users by default and root on request. For Owned mode you have full control by default.",
},
{
q: "How are backups verified?",
a: "We schedule restore drills and record timing and notes so you know recovery is proven—not just configured.",
},
];
const serviceLd = {
"@context": "https://schema.org",
"@type": "Service",
serviceType: "Managed VPS Hosting",
provider: {
"@type": "Organization",
name: site.name,
url: site.url,
logo: site.org.logo,
},
areaServed: ["EU", "US"],
hasOfferCatalog: {
"@type": "OfferCatalog",
name: "VPS Plans",
itemListElement: plans.map((p) => ({
"@type": "Offer",
name: p.name,
price: p.price,
description: `${p.specs}${p.bestFor}`,
url: `${site.url}/vps#deploy`,
category: "Managed VPS Hosting",
})),
},
};
const faqLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqItems.map((i) => ({
"@type": "Question",
name: i.q,
acceptedAnswer: { "@type": "Answer", text: i.a },
})),
};
export default function VPSPage() {
const hostname = new URL(site.url).hostname;
return (
<main className="relative isolate min-h-screen bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
{/* HERO */}
<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-24 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">
Managed VPS Hosting Hardened, Monitored, Proven
</span>
</h1>
<p className="mx-auto mt-6 max-w-3xl text-lg sm:text-xl text-neutral-700 dark:text-neutral-300">
Fast, secure VPS hosting with <strong>server management</strong>,{" "}
<strong>monitoring</strong>, and <strong>restore-tested backups</strong>. Prefer ownership?{" "}
Provision an <strong>Owned VPS</strong> directly into your cloud accountstill fully managed.
</p>
<div className="mt-8 flex flex-col sm:flex-row gap-3 justify-center">
<a
href="#deploy"
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"
>
Provision a VPS
</a>
<Link
href="/contact?type=vps"
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"
>
Talk to an Engineer
</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">CIS-Style Hardening</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">SSH, firewall, baseline checks</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">Restore-Proven</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">backup drills with timing</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">SLA-Backed</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">monitoring & incident notes</p>
</div>
</div>
</div>
</section>
{/* BENEFITS */}
<section className="relative border-y border-neutral-200 dark:border-neutral-800">
<div className="container mx-auto max-w-6xl px-4 py-16">
<h2 className="text-3xl sm:text-5xl font-bold tracking-tight text-center">
Everything you need for reliable infrastructure
</h2>
<div className="mt-10 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{[
{ t: "Security & Hardening", d: "SSH config, firewall, updates, baseline audits." },
{ t: "Backups + Restore Tests", d: "Automated backups and scheduled restore drills with notes." },
{ t: "Monitoring & Incidents", d: "Uptime, CPU/RAM/disk alerts, incident notes & follow-up." },
{ t: "Cloudflare Optional", d: "Edge hardening, HTTP/3, cache tuning for faster TTFB." },
].map((b) => (
<article
key={b.t}
className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-6"
>
<h3 className="text-lg font-semibold">{b.t}</h3>
<p className="mt-2 text-sm text-neutral-700 dark:text-neutral-300">{b.d}</p>
</article>
))}
</div>
</div>
</section>
{/* PLANS */}
<section className="relative py-16">
<div className="container mx-auto max-w-6xl px-4">
<h2 className="text-3xl sm:text-5xl font-bold tracking-tight text-center">VPS Plans</h2>
<p className="mt-4 text-neutral-700 dark:text-neutral-300 text-center max-w-3xl mx-auto">
Pick a plan, then choose <em>Managed</em> or <em>Owned</em> provisioning during checkout.
</p>
<div className="mt-10 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{plans.map((p) => (
<div
key={p.id}
className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-6 flex flex-col"
>
<div className="flex items-baseline justify-between">
<h3 className="text-xl font-semibold">{p.name}</h3>
<div className="text-lg font-bold">{p.price}</div>
</div>
<div className="mt-2 text-sm text-neutral-700 dark:text-neutral-300">{p.specs}</div>
<div className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">{p.bestFor}</div>
<ul className="mt-4 space-y-2 text-sm">
{p.features.map((f) => (
<li key={f} className="flex items-start gap-2">
<span className="mt-0.5"></span>
<span>{f}</span>
</li>
))}
</ul>
<a
href={`#deploy?plan=${p.id}`}
className="mt-6 inline-flex items-center justify-center rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 px-4 py-2 text-white font-semibold hover:opacity-90 transition"
>
{p.id === "dedicated" ? "Request Custom Plan" : `Provision ${p.name}`}
</a>
</div>
))}
</div>
</div>
</section>
{/* CTA */}
<section className="relative py-16 border-y border-neutral-200 dark:border-neutral-800">
<div className="container mx-auto max-w-6xl px-4 text-center">
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight">
Prefer a recommendation?
</h2>
<p className="mt-3 text-neutral-700 dark:text-neutral-300">
Tell us about your stack and goalswell size a VPS and outline next steps.
</p>
<div className="mt-6">
<Link
href="/contact?type=vps"
className="inline-flex items-center rounded-lg bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 px-5 py-2 font-semibold hover:opacity-90 transition"
>
Get My Recommendation
</Link>
</div>
</div>
</section>
{/* SELF-SERVICE DEPLOYMENT */}
<section id="deploy" className="relative py-16">
<div className="container mx-auto max-w-3xl px-4">
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight text-center">
Provision a VPS (Managed or Owned)
</h2>
<p className="mt-3 text-center text-neutral-700 dark:text-neutral-300">
Choose a plan, region, and OS. Select management mode: <strong>Managed</strong> (we host & bill) or{" "}
<strong>Owned</strong> (we provision into <em>your</em> cloud account via a secure connect).
</p>
<VPSDeployWizard
plans={plans}
regions={regions}
osImages={osImages}
siteHostname={hostname}
/>
</div>
</section>
{/* FAQ */}
<section className="relative py-16 bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
<div className="container mx-auto max-w-4xl px-4">
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight text-center">FAQ</h2>
<div className="mt-8 grid gap-6">
{faqItems.map((i) => (
<article
key={i.q}
className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-6"
>
<h3 className="text-lg font-semibold">{i.q}</h3>
<p className="mt-2 text-sm text-neutral-700 dark:text-neutral-300">{i.a}</p>
</article>
))}
</div>
</div>
</section>
<JsonLd data={serviceLd} />
<JsonLd data={faqLd} />
</main>
);
}

View File

@ -18,7 +18,7 @@ export default function Footer() {
<ul className="mt-3 space-y-1 text-sm"> <ul className="mt-3 space-y-1 text-sm">
{[ {[
{ href: "/services", label: "Services" }, { href: "/services", label: "Services" },
{ href: "/free", label: "Free tools" }, { href: "/pricing", label: "Pricing" },
{ href: "/contact", label: "Contact" }, { href: "/contact", label: "Contact" },
{ href: "/privacy", label: "Privacy" }, { href: "/privacy", label: "Privacy" },
{ href: "/terms", label: "Terms" }, { href: "/terms", label: "Terms" },

View File

@ -14,6 +14,8 @@ export default function Header() {
{ href: "/services", label: "Services" }, { href: "/services", label: "Services" },
// { href: "/free", label: "Free tools" }, // { href: "/free", label: "Free tools" },
{ href: "/pricing", label: "Pricing" }, { href: "/pricing", label: "Pricing" },
{ href: "/vps", label: "VPS" },
{ href: "/minecraft", label: "Minecraft" },
{ href: "/contact", label: "Contact" } { href: "/contact", label: "Contact" }
].map((item) => ( ].map((item) => (
<Link <Link

View File

@ -2,24 +2,14 @@
import { Plan } from "./types"; import { Plan } from "./types";
import { CheckIcon, MinusIcon } from "./icons"; 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[] }) { export default function ComparisonTable({ plans }: { plans: Plan[] }) {
// Hard-code feature mapping for clarity (keeps copy outcome-driven) // Outcome-focused features mapped to each plan
const matrix: Record<string, (planId: string) => string | boolean> = { const matrix: Record<string, (planId: string) => string | boolean> = {
"Email deliverability monitoring": () => true, "DNS & email monitoring": () => true,
"Automated backups": () => true, "Automated backups": () => true,
"Restore test cadence": (id) => "Restore test cadence": (id) =>
id === "essential" ? "Quarterly" : id === "growth" ? "Quarterly" : "Monthly", id === "essential" ? "Quarterly" : id === "growth" ? "Quarterly" : "Monthly",
"Managed updates (WordPress/Next.js)": () => true,
"Cloudflare WAF & bot tuning": (id) => id !== "essential", "Cloudflare WAF & bot tuning": (id) => id !== "essential",
"Web Vitals reporting": (id) => (id === "growth" || id === "mission" ? "Monthly" : false), "Web Vitals reporting": (id) => (id === "growth" || id === "mission" ? "Monthly" : false),
"On-call paging": (id) => (id === "mission" ? "24/7" : false), "On-call paging": (id) => (id === "mission" ? "24/7" : false),
@ -82,7 +72,7 @@ export default function ComparisonTable({ plans }: { plans: Plan[] }) {
</table> </table>
</div> </div>
<div className="bg-gray-50 px-6 py-3 text-xs text-gray-500"> <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. Uptime target is an SLO over a rolling 30-day period. Credits apply if response SLAs are missed.
</div> </div>
</div> </div>
); );

View File

@ -1,35 +1,99 @@
// components/pricing/FAQ.tsx 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() { export default function FAQ() {
const faqs = [ const items: QA[] = [
{ {
q: "Do I need to sign a long contract?", 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.", a: "No. Plans are month-to-month with a 30-day cancel policy. Youll also receive an exit plan and full documentation for a smooth handover if you ever switch providers.",
},
{
q: "Do you migrate from my current host?",
a: "Yes. We schedule a zero-downtime migration window, pre-configure backups and monitoring, and verify stability after cutover.",
}, },
{ {
q: "What happens if you miss an SLA?", q: "What happens if you miss an SLA?",
a: "We apply incident credits on your next invoice. Credits scale with the breach severity.", a: "You receive incident credits on your next invoice. The credit amount scales with severity and plan level — our uptime and response guarantees are real.",
},
{
q: "Do you host WordPress and Next.js?",
a: "Yes. We manage WordPress and modern Next.js hosting, including updates, uptime monitoring, and restore-tested backups.",
}, },
{ {
q: "Are you GDPR compliant?", 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.", a: "Yes. We sign a DPA, follow least-privilege access, and maintain audit logs. Privacy and cookie templates are available on request.",
}, },
{ {
q: "Can you help outside business hours?", 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.", a: "Mission-Critical includes 24/7 paging. Ad-hoc after-hours support is also available for Essential and Growth clients on request.",
}, },
]; ];
return ( return (
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm"> <section
<h2 className="text-xl font-semibold">Pricing FAQs</h2> aria-labelledby="faq-heading"
<dl className="mt-6 space-y-6"> 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"
{faqs.map((item) => ( >
<div key={item.q}> <div className="container mx-auto max-w-4xl px-4">
<dt className="text-sm font-medium text-gray-900">{item.q}</dt> <h2
<dd className="mt-2 text-sm text-gray-700">{item.a}</dd> id="faq-heading"
className="text-3xl sm:text-4xl font-bold tracking-tight bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent text-center"
>
Frequently Asked Questions
</h2>
<div className="mt-10 space-y-4">
{items.map((x, i) => {
const id = slugify(x.q) || `faq-${i}`;
return (
<details
key={id}
id={id}
className="group rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-5 shadow-sm hover:shadow-md transition"
>
<summary className="cursor-pointer text-lg font-medium text-neutral-900 dark:text-white flex items-center justify-between">
<span>{x.q}</span>
<span
className="ml-4 select-none transition-transform group-open:rotate-180"
aria-hidden="true"
>
</span>
</summary>
<div className="mt-3 text-sm text-neutral-700 dark:text-neutral-300 leading-relaxed">
{x.a}
</div> </div>
))} </details>
</dl> );
})}
</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>
</section>
); );
} }

View File

@ -5,8 +5,8 @@ export default function Guarantee() {
<div className="mx-auto max-w-3xl text-center"> <div className="mx-auto max-w-3xl text-center">
<h2 className="text-xl font-semibold">Risk reversed</h2> <h2 className="text-xl font-semibold">Risk reversed</h2>
<p className="mt-2 text-gray-700"> <p className="mt-2 text-gray-700">
30-day, no-questions cancel. If we dont deliver your agreed outcomes in the first month, 30-day, no-questions cancel. If we dont deliver the agreed outcomes in your first month
well complete the sprint free. (including a successful restore-tested backup), well complete the sprint free.
</p> </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"> <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> <span className="inline-block rounded-full bg-emerald-100 px-2 py-0.5 text-xs">Guarantee</span>

View File

@ -35,7 +35,9 @@ export default function PlanCard({
<span className="text-sm text-gray-500">/mo</span> <span className="text-sm text-gray-500">/mo</span>
</div> </div>
{billing === "yearly" && ( {billing === "yearly" && (
<p className="mt-1 text-xs text-emerald-700">Billed annually (save {Math.round(plan.yearlyDiscount * 100)}%)</p> <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"> <ul className="mt-6 space-y-2 text-sm">

View File

@ -22,7 +22,7 @@ export default function PlanRecommender({ plans }: { plans: Plan[] }) {
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm"> <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> <h2 className="text-xl font-semibold">Not sure where to start?</h2>
<p className="mt-1 text-sm text-gray-600"> <p className="mt-1 text-sm text-gray-600">
Answer three quick questions and well suggest a plan. Answer three quick questions and well suggest a managed hosting plan.
</p> </p>
<div className="mt-6 grid gap-6 md:grid-cols-3"> <div className="mt-6 grid gap-6 md:grid-cols-3">

View File

@ -1,254 +0,0 @@
import type { Metadata } from "next";
import Link from "next/link";
export const metadata: Metadata = {
title: "Free Tools & Audits — Van Hunen IT",
description:
"Run free reliability, DNS, and performance checks or request quick IT audits. Diagnose, optimize, and secure your website in minutes.",
robots: { index: true, follow: true },
alternates: { canonical: "/free" },
};
export default function FreeIndex() {
const tools = [
{
title: "Web Reliability Check",
excerpt:
"Test your websites uptime, SSL, and DNS performance — get an instant reliability grade (AF).",
href: "/free/web-reliability-check",
},
{
title: "DNS Health Analyzer",
excerpt:
"Check your domains DNS setup for missing SPF, DMARC, or CAA records and other critical issues.",
href: "/free/dns-health-analyzer",
},
{
title: "Website Speed Test (Lite)",
excerpt:
"Run a Lighthouse-based performance test and see how your site scores for speed, SEO, and accessibility.",
href: "/free/website-speed-test",
},
{
title: "Server Header Inspector",
excerpt:
"Check your sites security headers like HSTS, CSP, and Referrer-Policy to spot missing protections.",
href: "/free/server-header-inspector",
},
{
title: "Cloudflare Optimization Checker",
excerpt:
"Analyze if your Cloudflare settings are optimized for caching, SSL, and performance.",
href: "/free/cloudflare-checker",
},
{
title: "Uptime Monitor (Free Tier)",
excerpt:
"Set up free uptime monitoring for your website and receive downtime alerts instantly.",
href: "/free/uptime-monitor",
},
{
title: "SSL/TLS Expiry Tracker",
excerpt:
"Track when your SSL certificate expires and receive an automated renewal reminder.",
href: "/free/ssl-expiry-tracker",
},
{
title: "Website Backup Readiness Test",
excerpt:
"Check if your website is protected by regular automated backups and detect missing layers.",
href: "/free/backup-readiness-test",
},
{
title: "Email Deliverability Tester",
excerpt:
"Send a test email and verify SPF, DKIM, and DMARC for maximum inbox reliability.",
href: "/free/email-deliverability-tester",
},
{
title: "Docker Deployment Readiness Tool",
excerpt:
"Scan your public repository for Docker configuration quality and best practices.",
href: "/free/docker-readiness",
},
{
title: "Kubernetes Cost Estimator",
excerpt:
"Estimate infrastructure costs for your Kubernetes or k3s setup based on your resources.",
href: "/free/kubernetes-cost-estimator",
},
{
title: "Minecraft Server Performance Checker",
excerpt:
"Test TPS, latency, and plugin performance of your Minecraft server in seconds.",
href: "/free/minecraft-checker",
},
{
title: "Site Downtime Simulation Demo",
excerpt:
"Simulate downtime to visualize business impact and recovery time objectives.",
href: "/free/downtime-simulator",
},
{
title: "Automation ROI Calculator",
excerpt:
"See how much time and cost you could save by automating repetitive IT or admin tasks.",
href: "/free/automation-roi-calculator",
},
{
title: "VPS Performance Benchmark Tool",
excerpt:
"Run a quick benchmark on your VPS or server to test CPU, I/O, and disk performance.",
href: "/free/vps-benchmark",
},
];
const services = [
{
title: "20-Minute Tech Health Check",
excerpt:
"A quick diagnostic session covering your hosting, DNS, SSL, and uptime setup.",
href: "/free/tech-health-check",
},
{
title: "DNS & Mail Deliverability Audit",
excerpt:
"Get a personalized DNS and email configuration audit to fix SPF, DKIM, and DMARC issues.",
href: "/free/dns-mail-audit",
},
{
title: "Website Speed & Optimization Preview",
excerpt:
"Receive a short performance report and improvement roadmap for your website.",
href: "/free/website-optimization-preview",
},
{
title: "Cloudflare Setup Review",
excerpt:
"Let us analyze your Cloudflare configuration for speed, caching, and SSL performance.",
href: "/free/cloudflare-review",
},
{
title: "Security Headers Checkup",
excerpt:
"Well scan your site headers for vulnerabilities and send a one-page hardening report.",
href: "/free/security-checkup",
},
{
title: "Backup & Recovery Simulation",
excerpt:
"Test your current backup setup and identify gaps in your disaster recovery plan.",
href: "/free/backup-simulation",
},
{
title: "Uptime Reliability Snapshot",
excerpt:
"Well monitor your domain for 24 hours and share an uptime report with insights.",
href: "/free/reliability-snapshot",
},
{
title: "Automation Opportunity Call",
excerpt:
"Tell us your weekly routines — well identify tasks that could be automated instantly.",
href: "/free/automation-call",
},
];
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">
{/* --- Header --- */}
<section className="relative overflow-hidden border-b border-neutral-200 dark:border-neutral-800">
<div className="container mx-auto max-w-5xl px-6 py-20 text-center">
<h1 className="text-4xl sm:text-5xl font-bold tracking-tight bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 bg-clip-text text-transparent">
Free Tools & Quick Audits
</h1>
<p className="mt-4 text-lg text-neutral-700 dark:text-neutral-300 max-w-2xl mx-auto">
Diagnose, benchmark, and optimize your IT setup with a collection of
free utilities and expert mini-audits.
No sign-up required, privacy-friendly, and built by{" "}
<span className="font-semibold">Van Hunen IT</span>.
</p>
</div>
</section>
{/* --- Automated Tools --- */}
<section className="container mx-auto max-w-6xl px-6 py-16">
<h2 className="text-2xl font-bold mb-10 text-neutral-900 dark:text-white text-center">
Automated Tools
</h2>
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
{tools.map((t, i) => (
<article
key={t.href}
className="group relative overflow-hidden 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-all duration-300 backdrop-blur animate-[fadeInUp_0.6s_ease_forwards]"
style={{ animationDelay: `${i * 60}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" />
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
{t.title}
</h3>
<p className="mt-3 text-sm text-neutral-700 dark:text-neutral-300">
{t.excerpt}
</p>
<Link
href={t.href}
className="mt-6 inline-block w-full rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white text-center py-2.5 text-sm font-medium hover:opacity-90 transition"
>
Open Tool
</Link>
</article>
))}
</div>
</section>
{/* --- Free Audits & Services --- */}
<section className="container mx-auto max-w-6xl px-6 py-16 border-t border-neutral-200 dark:border-neutral-800">
<h2 className="text-2xl font-bold mb-10 text-neutral-900 dark:text-white text-center">
Free Audits & Quick Services
</h2>
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
{services.map((s, i) => (
<article
key={s.href}
className="group relative overflow-hidden 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-all duration-300 backdrop-blur animate-[fadeInUp_0.6s_ease_forwards]"
style={{ 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" />
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
{s.title}
</h3>
<p className="mt-3 text-sm text-neutral-700 dark:text-neutral-300">
{s.excerpt}
</p>
<Link
href={s.href}
className="mt-6 inline-block w-full rounded-lg bg-gradient-to-r from-purple-600 via-blue-600 to-purple-600 text-white text-center py-2.5 text-sm font-medium hover:opacity-90 transition"
>
Request
</Link>
</article>
))}
</div>
</section>
{/* --- CTA Banner --- */}
<section className="border-t border-neutral-200 dark:border-neutral-800 bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white py-16 mt-12 text-center">
<div className="container mx-auto max-w-4xl px-6">
<h2 className="text-2xl sm:text-3xl font-bold mb-4">
Not Sure Where to Start?
</h2>
<p className="text-base sm:text-lg text-white/90 mb-6">
Book a free 15-minute consultation and well help you pick the right
checks or services for your setup.
</p>
<Link
href="/contact?topic=consultation"
className="inline-block rounded-lg bg-white text-blue-700 font-semibold py-3 px-6 shadow hover:shadow-md transition"
>
Book Free Call
</Link>
</div>
</section>
</main>
);
}

View File

@ -1,5 +1,5 @@
// lib/services.ts
export type ServiceCategoryId = export type ServiceCategoryId =
| "hosting"
| "infrastructure-devops" | "infrastructure-devops"
| "web-performance" | "web-performance"
| "dev-platforms" | "dev-platforms"
@ -33,35 +33,42 @@ export const SERVICE_CATEGORIES: Record<
ServiceCategoryId, ServiceCategoryId,
{ label: string; anchor: string } { label: string; anchor: string }
> = { > = {
hosting: { label: "Hosting (Managed)", anchor: "hosting" },
"web-dev": { label: "Websites (Development)", anchor: "web-dev" },
minecraft: { label: "Minecraft Services", anchor: "minecraft" },
"infrastructure-devops": { "infrastructure-devops": {
label: "Infrastructure & DevOps", label: "Infrastructure & DevOps",
anchor: "infrastructure-devops", anchor: "infrastructure-devops",
}, },
"web-performance": { "web-performance": {
label: "Website Reliability & Performance", label: "Website Reliability, Performance & Hosting",
anchor: "web-performance", anchor: "web-performance",
}, },
"dev-platforms": { label: "Developer Platforms & Tooling", anchor: "dev-platforms" }, "dev-platforms": {
label: "Developer Platforms & Tooling",
anchor: "dev-platforms",
},
migrations: { label: "Migrations & Refreshes", anchor: "migrations" }, migrations: { label: "Migrations & Refreshes", anchor: "migrations" },
minecraft: { label: "Minecraft Services", anchor: "minecraft" },
"web-dev": { label: "Web Development", anchor: "web-dev" },
}; };
// ------- 23 services ------- // ------- services -------
export const SERVICES: Service[] = [ export const SERVICES: Service[] = [
// --- HOSTING (Managed) ---
{ {
slug: "vps-hardening-care", slug: "vps-hardening-care",
title: "VPS Hardening & Care", title: "Managed VPS Hosting (Hardened & Monitored)",
category: "infrastructure-devops", category: "hosting",
outcome: "Secure, monitored, and backed-up VPS—foundation of the Reliability Stack™.", outcome:
"Secure, fast VPS hosting—server management, monitoring, and restore-tested backups handled for you.",
who: [ who: [
"SMBs running WordPress/Next.js/apps on a VPS", "SMBs running WordPress/Next.js/custom apps on a VPS",
"Teams needing a baseline security and backup posture", "Teams that want managed VPS hosting with proof of outcomes",
], ],
deliverables: [ deliverables: [
"SSH/CIS hardening, firewall (ufw), fail2ban", "SSH/CIS hardening, firewall (ufw), fail2ban",
"Automated updates, audit log, intrusion checks", "Automated updates, audit log, intrusion checks",
"Backups + restore test, uptime & resource monitoring", "Backups + restore test, uptime & resource monitoring",
"Optional Cloudflare edge hardening & HTTP/3",
], ],
timeline: "12 days", timeline: "12 days",
price: "€390 one-off or €89/mo care", price: "€390 one-off or €89/mo care",
@ -80,10 +87,196 @@ export const SERVICES: Service[] = [
}, },
], ],
relatedSlugs: ["cloudflare-edge-hardening", "backup-disaster-recovery-drill"], relatedSlugs: ["cloudflare-edge-hardening", "backup-disaster-recovery-drill"],
metaTitle: "VPS Hardening & Care — Van Hunen IT", metaTitle: "Managed VPS Hosting (Hardened & Monitored) — Van Hunen IT",
metaDescription: metaDescription:
"Secure your VPS with hardening, monitoring, and tested backups. One-off setup or ongoing care.", "Secure, fast VPS hosting with server management, monitoring, and restore-tested backups. Optional Cloudflare hardening.",
}, },
{
slug: "website-care-plan",
title: "Managed Website Hosting & Care",
category: "hosting",
outcome:
"Fully managed website hosting and maintenance—updates, uptime, and monthly restore-tested backups.",
who: ["SMBs wanting stable updates and monitoring", "Teams without in-house ops"],
deliverables: [
"Managed WordPress/Next.js hosting & updates",
"Uptime monitoring and incident handling (per SLA)",
"Backups with monthly restore test",
"Security checks and monthly reporting",
],
timeline: "Monthly",
price: "from €149/mo",
proof: ["Monthly report & restore test evidence", "Incident notes with timestamps"],
faq: [
{ q: "What platforms?", a: "WordPress, Next.js, Node backends; others on request." },
{ q: "SLA?", a: "Incident response windows depend on plan tier." },
],
relatedSlugs: ["backup-disaster-recovery-drill", "core-web-vitals-sprint"],
metaTitle: "Managed Website Hosting & Care — Van Hunen IT",
metaDescription:
"Fully managed website hosting & maintenance with updates, uptime monitoring, and monthly restore-tested backups.",
},
// --- WEBSITES (Development) ---
{
slug: "quick-launch-website",
title: "Website Development (Next.js)",
category: "web-dev",
outcome: "Launch a fast, SEO-ready site with modern tech—clear scope, predictable timeline.",
who: ["SMBs needing a credible web presence fast", "Consultants/creators launching offers"],
deliverables: [
"57 sections, contact forms, OG/Twitter cards",
"Analytics, sitemap, and basic SEO setup",
"Deploy to your managed VPS or hosting",
"Optional headless CMS (Ghost/Strapi)",
],
timeline: "57 days",
price: "€2,450",
proof: ["Lighthouse pass for basics", "Deployed site link & repo handover"],
faq: [
{ q: "Copy & assets?", a: "We provide a brief and templates; you can supply or we refine." },
{ q: "CMS?", a: "Optional—see Headless CMS Setup." },
],
relatedSlugs: ["headless-cms-setup", "core-web-vitals-sprint"],
metaTitle: "Website Development (Next.js) — Van Hunen IT",
metaDescription:
"Launch a fast, SEO-ready site with analytics and deployment to your managed hosting.",
},
{
slug: "headless-cms-setup",
title: "Headless CMS Setup (Ghost/Strapi)",
category: "web-dev",
outcome: "Non-tech content updates without redeploys.",
who: ["Teams wanting easy publishing", "Sites separating content from code"],
deliverables: ["CMS install & roles", "Content model & CI/CD", "Image optimization & docs"],
timeline: "23 days",
price: "€1,190",
proof: ["Editor demo & role permissions", "Publishing pipeline test"],
faq: [
{ q: "Migration from WordPress?", a: "We can import and map key content types." },
{ q: "Auth & SSO?", a: "SSO/OAuth possible depending on CMS chosen." },
],
relatedSlugs: ["quick-launch-website", "website-care-plan"],
metaTitle: "Headless CMS Setup — Van Hunen IT",
metaDescription:
"Install and configure Ghost/Strapi with roles, CI/CD, and image optimization.",
},
{
slug: "security-compliance-baseline",
title: "Security & Compliance Baseline (GDPR-aware)",
category: "web-dev",
outcome: "Reasonable security for small teams.",
who: ["SMBs formalizing access and logging", "Teams preparing for audits"],
deliverables: [
"Password policy, 2FA & least-privilege",
"Audit logging & data retention",
"Incident checklist & drills",
],
timeline: "12 days",
price: "€740",
proof: ["Policy documents & checklists", "Access review and logging tests"],
faq: [
{ q: "Covers DPIA?", a: "We provide input; legal sign-off remains with your DPO/counsel." },
{ q: "Tooling?", a: "We match to your stack—SaaS or self-hosted where appropriate." },
],
relatedSlugs: ["vps-hardening-care", "backup-disaster-recovery-drill"],
metaTitle: "Security & Compliance Baseline — Van Hunen IT",
metaDescription:
"Implement practical security policies, logging, and incident readiness for SMBs.",
},
// --- MINECRAFT ---
{
slug: "minecraft-managed-server",
title: "Managed Minecraft Server Hosting",
category: "minecraft",
outcome:
"Lag-free, stable Minecraft hosting on a hardened VPS—backups, tuning, and monitoring included.",
who: ["Communities, schools, creators", "Small networks needing reliable ops"],
deliverables: [
"VPS sizing/hardening, Paper/Velocity setup",
"Auto-backups + restore test",
"Performance tuning, grief/anti-cheat basics, monitoring",
],
timeline: "Setup 1 day · Ongoing monthly",
price: "Starter €49/mo · Pro €99/mo · Network €199/mo (+ VPS)",
proof: ["TPS baseline & timings report", "Restore test proof & monitoring"],
faq: [
{ q: "Java or Bedrock?", a: "Java by default; Bedrock or Geyser support on request." },
{ q: "Modpacks?", a: "CurseForge/modded supported—resource-dependent." },
],
relatedSlugs: ["minecraft-performance-audit", "minecraft-plugin-development", "minecraft-monetization-pack"],
metaTitle: "Managed Minecraft Server Hosting — Van Hunen IT",
metaDescription:
"Lag-free Minecraft hosting on a hardened VPS with backups, tuning, and monitoring. Java/Bedrock; modpacks supported.",
},
{
slug: "minecraft-performance-audit",
title: "Minecraft Performance & Stability Audit",
category: "minecraft",
outcome: "Higher TPS, fewer crashes.",
who: ["Servers with lag or frequent crashes", "Owners scaling to more players"],
deliverables: ["Profiler run (Spark), timings analysis", "JVM flags & plugin audit", "Before/after TPS report"],
timeline: "1 day",
price: "€390",
proof: ["Timings & Spark screenshots", "Updated config diff & TPS before/after"],
faq: [
{ q: "Supports Bungee/Velocity?", a: "Yes—networked setups supported." },
{ q: "Player cap increase?", a: "We optimize, then size infra appropriately." },
],
relatedSlugs: ["minecraft-managed-server"],
metaTitle: "Minecraft Performance Audit — Van Hunen IT",
metaDescription:
"Fix lag with timings, JVM flags, and plugin optimizations plus TPS reporting.",
},
{
slug: "minecraft-plugin-development",
title: "Minecraft Server Plugin Development",
category: "minecraft",
outcome:
"Custom Paper/Spigot plugins with tests, config, and docs—built for your servers goals.",
who: ["Servers needing unique mechanics", "Creators monetizing custom content"],
deliverables: [
"Spec, plugin build & tests",
"Config & permissions + changelog",
"Private repo transfer; ownership after payment",
],
timeline: "From 37 days",
price: "€85/hr or fixed from €650",
proof: ["Feature demo & test suite run", "Config docs and changelog"],
faq: [
{
q: "Source code ownership?",
a: "You own it after payment unless agreed otherwise (private repo transfer included).",
},
{ q: "API compatibility?", a: "Paper API targeted; cross-version support is scoped case-by-case." },
],
relatedSlugs: ["minecraft-managed-server", "minecraft-monetization-pack"],
metaTitle: "Minecraft Server Plugin Development — Van Hunen IT",
metaDescription:
"Custom Paper/Spigot plugins with tests, config, and docs. Ownership transferred after payment.",
},
{
slug: "minecraft-monetization-pack",
title: "Creator Monetization Pack (Tebex)",
category: "minecraft",
outcome: "Clean store + safe donations.",
who: ["Servers adding a store", "Creators formalizing monetization"],
deliverables: ["Tebex setup & product catalog", "Rank automation & receipts", "Anti-fraud notes & webhooks"],
timeline: "1 day",
price: "€420",
proof: ["Test purchase flow", "Webhook logs to grants"],
faq: [
{ q: "Compliance?", a: "We avoid P2W violations and follow platform rules." },
{ q: "Branding?", a: "Store theme aligned with your site and server style." },
],
relatedSlugs: ["minecraft-plugin-development", "minecraft-managed-server"],
metaTitle: "Minecraft Monetization Pack — Van Hunen IT",
metaDescription:
"Set up Tebex with products, ranks, and safe donation flows.",
},
// --- INFRASTRUCTURE & DEVOPS ---
{ {
slug: "dockerize-deploy", slug: "dockerize-deploy",
title: "Dockerize & Deploy", title: "Dockerize & Deploy",
@ -113,7 +306,11 @@ export const SERVICES: Service[] = [
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: ["Teams needing private images with access control", "Orgs adopting container scanning & retention policies"], who: ["Teams needing private images with access control", "Orgs adopting container scanning & retention policies"],
deliverables: ["GHCR/Harbor registry, RBAC & tokens", "Retention & vulnerability scanning", "CI push/pull integration docs"], deliverables: [
"GHCR/Harbor registry, RBAC & tokens",
"Retention & vulnerability scanning",
"CI push/pull integration docs",
],
timeline: "12 days", timeline: "12 days",
price: "€490", price: "€490",
proof: ["Policy & RBAC screenshots", "Pipeline run showing signed/pushed images"], proof: ["Policy & RBAC screenshots", "Pipeline run showing signed/pushed images"],
@ -193,7 +390,11 @@ export const SERVICES: Service[] = [
category: "infrastructure-devops", category: "infrastructure-devops",
outcome: "Verified restore path—not just backups.", outcome: "Verified restore path—not just backups.",
who: ["Sites that never tested restore", "Teams formalizing RPO/RTO targets"], who: ["Sites that never tested restore", "Teams formalizing RPO/RTO targets"],
deliverables: ["Backup plan (files/db), encryption & rotation", "Restore test with documented steps", "RPO/RTO notes & recommendations"], 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: ["Restore demonstration on staging", "Report with timings and gaps"], proof: ["Restore demonstration on staging", "Report with timings and gaps"],
@ -212,7 +413,11 @@ export const SERVICES: Service[] = [
category: "infrastructure-devops", category: "infrastructure-devops",
outcome: "Lower TTFB, fewer bad bots, safer origins.", outcome: "Lower TTFB, fewer bad bots, safer origins.",
who: ["Sites facing spam/bot abuse or high TTFB", "Teams needing sane edge security fast"], who: ["Sites facing spam/bot abuse or high TTFB", "Teams needing sane edge security fast"],
deliverables: ["WAF & bot tuning, page rules, cache keys", "Origin shielding, HTTP/3, rate limiting", "TTFB and cache-hit improvements"], 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: ["Before/after WebPageTest/TTFB screenshots", "WAF rule set export & notes"], proof: ["Before/after WebPageTest/TTFB screenshots", "WAF rule set export & notes"],
@ -225,13 +430,19 @@ export const SERVICES: Service[] = [
metaDescription: metaDescription:
"Tune WAF, caching, and HTTP/3 to reduce TTFB and block abusive traffic.", "Tune WAF, caching, and HTTP/3 to reduce TTFB and block abusive traffic.",
}, },
// --- WEBSITE RELIABILITY & PERFORMANCE ---
{ {
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 with measurable before/after.", outcome: "CLS/LCP/INP into the green with measurable before/after.",
who: ["Marketing sites and shops with poor CWV", "Next.js/WordPress teams needing a focused fix"], who: ["Marketing sites and shops with poor CWV", "Next.js/WordPress teams needing a focused fix"],
deliverables: ["Image strategy (WebP/next/image), font loading", "Script defers, critical CSS, caching headers", "Before/after CWV report"], deliverables: [
"Image strategy (WebP/next/image), font loading",
"Script defers, critical CSS, caching headers",
"Before/after CWV report",
],
timeline: "23 days", timeline: "23 days",
price: "€820", price: "€820",
proof: ["Lighthouse/CrUX before vs after", "Largest contentful paint assets diff"], proof: ["Lighthouse/CrUX before vs after", "Largest contentful paint assets diff"],
@ -244,29 +455,6 @@ export const SERVICES: Service[] = [
metaDescription: metaDescription:
"Improve LCP/CLS/INP with image, font, and script strategy plus caching.", "Improve LCP/CLS/INP with image, font, and script strategy plus caching.",
}, },
{
slug: "website-care-plan",
title: "Website Care Plan",
category: "web-performance",
outcome: "Updates, uptime watch, and verified restores—the Reliability Stack™ on autopilot.",
who: ["SMBs wanting stable updates and monitoring", "Teams without in-house ops"],
deliverables: [
"Updates, uptime monitoring, backups + monthly restore test",
"Incident credits and priority support",
"Security checks & reporting",
],
timeline: "Monthly",
price: "from €149/mo",
proof: ["Monthly report & restore test evidence", "Incident notes with timestamps"],
faq: [
{ q: "What platforms?", a: "WordPress, Next.js, Node backends; others on request." },
{ q: "SLA?", a: "Incident response windows depend on plan tier." },
],
relatedSlugs: ["backup-disaster-recovery-drill", "core-web-vitals-sprint"],
metaTitle: "Website Care Plan — Van Hunen IT",
metaDescription:
"Monthly updates, uptime, and verified restores for peace of mind.",
},
{ {
slug: "email-deliverability-pack", slug: "email-deliverability-pack",
title: "Secure Contact & Email Deliverability Pack", title: "Secure Contact & Email Deliverability Pack",
@ -290,6 +478,8 @@ export const SERVICES: Service[] = [
metaDescription: metaDescription:
"Fix spam issues with SPF/DKIM/DMARC, verified tests, and safer contact forms.", "Fix spam issues with SPF/DKIM/DMARC, verified tests, and safer contact forms.",
}, },
// --- DEV PLATFORMS & MIGRATIONS ---
{ {
slug: "self-hosted-gitea-sso", slug: "self-hosted-gitea-sso",
title: "Self-Hosted Git (Gitea) with SSO", title: "Self-Hosted Git (Gitea) with SSO",
@ -353,7 +543,11 @@ export const SERVICES: Service[] = [
category: "migrations", category: "migrations",
outcome: "Zero-to-minimal downtime move with rollback.", outcome: "Zero-to-minimal downtime move with rollback.",
who: ["Teams changing hosts or platforms", "Apps consolidating infra"], who: ["Teams changing hosts or platforms", "Apps consolidating infra"],
deliverables: ["Inventory & plan, containerization if needed", "DNS/cutover & rollback plan", "Smoke tests & timed runbook"], deliverables: [
"Inventory & plan, containerization if needed",
"DNS/cutover & rollback plan",
"Smoke tests & timed runbook",
],
timeline: "24 days", timeline: "24 days",
price: "€1,190", price: "€1,190",
proof: ["Cutover timeline & metrics", "Rollback rehearsal log"], proof: ["Cutover timeline & metrics", "Rollback rehearsal log"],
@ -385,143 +579,6 @@ export const SERVICES: Service[] = [
metaDescription: metaDescription:
"Containerize legacy apps with healthchecks, backups, and docs.", "Containerize legacy apps with healthchecks, backups, and docs.",
}, },
{
slug: "minecraft-managed-server",
title: "Managed Minecraft Server",
category: "minecraft",
outcome: "Fast, stable, and safe server on a dedicated VPS.",
who: ["Communities, schools, creators", "Small networks needing reliable ops"],
deliverables: [
"VPS sizing/hardening, Paper/Velocity setup",
"Auto-backups + restore test",
"Performance tuning, grief/anti-cheat basics, monitoring",
],
timeline: "Setup 1 day · Ongoing monthly",
price: "Starter €49/mo · Pro €99/mo · Network €199/mo (+ VPS)",
proof: ["TPS baseline & timings report", "Restore test proof & monitoring"],
faq: [
{ q: "Java or Bedrock?", a: "Java by default; Bedrock or Geyser support on request." },
{ q: "Modpacks?", a: "CurseForge/modded supported—resource-dependent." },
],
relatedSlugs: ["minecraft-performance-audit", "minecraft-plugin-development", "minecraft-monetization-pack"],
metaTitle: "Managed Minecraft Server — Van Hunen IT",
metaDescription:
"Turnkey Minecraft hosting on a hardened VPS with backups and performance tuning.",
},
{
slug: "minecraft-performance-audit",
title: "Minecraft Performance & Stability Audit",
category: "minecraft",
outcome: "Higher TPS, fewer crashes.",
who: ["Servers with lag or frequent crashes", "Owners scaling to more players"],
deliverables: ["Profiler run (Spark), timings analysis", "JVM flags & plugin audit", "Before/after TPS report"],
timeline: "1 day",
price: "€390",
proof: ["Timings & Spark screenshots", "Updated config diff & TPS before/after"],
faq: [
{ q: "Supports Bungee/Velocity?", a: "Yes—networked setups supported." },
{ q: "Player cap increase?", a: "We optimize, then size infra appropriately." },
],
relatedSlugs: ["minecraft-managed-server"],
metaTitle: "Minecraft Performance Audit — Van Hunen IT",
metaDescription:
"Fix lag with timings, JVM flags, and plugin optimizations plus TPS reporting.",
},
{
slug: "minecraft-plugin-development",
title: "Custom Minecraft Plugin Development",
category: "minecraft",
outcome: "Features tailored to your server/community.",
who: ["Servers needing unique mechanics", "Creators monetizing custom content"],
deliverables: ["Spec, plugin build & tests", "Config & permissions", "Maintenance window"],
timeline: "From 37 days",
price: "€85/hr or fixed from €650",
proof: ["Feature demo & test suite run", "Config docs and changelog"],
faq: [
{ q: "Source code ownership?", a: "You own it after payment unless agreed otherwise (private repo transfer included)." },
{ q: "API compatibility?", a: "Paper API targeted; cross-version support is scoped case-by-case." },
],
relatedSlugs: ["minecraft-managed-server", "minecraft-monetization-pack"],
metaTitle: "Minecraft Plugin Development — Van Hunen IT",
metaDescription:
"Build custom Paper/Spigot plugins with tests, configs, and docs.",
},
{
slug: "minecraft-monetization-pack",
title: "Creator Monetization Pack (Tebex)",
category: "minecraft",
outcome: "Clean store + safe donations.",
who: ["Servers adding a store", "Creators formalizing monetization"],
deliverables: ["Tebex setup & product catalog", "Rank automation & receipts", "Anti-fraud notes & webhooks"],
timeline: "1 day",
price: "€420",
proof: ["Test purchase flow", "Webhook logs to grants"],
faq: [
{ q: "Compliance?", a: "We avoid P2W violations and follow platform rules." },
{ q: "Branding?", a: "Store theme aligned with your site and server style." },
],
relatedSlugs: ["minecraft-plugin-development", "minecraft-managed-server"],
metaTitle: "Minecraft Monetization Pack — Van Hunen IT",
metaDescription:
"Set up Tebex with products, ranks, and safe donation flows.",
},
{
slug: "quick-launch-website",
title: "Quick-Launch Website (Next.js)",
category: "web-dev",
outcome: "Fast, SEO-ready site in days.",
who: ["SMBs needing a credible web presence fast", "Consultants/creators launching offers"],
deliverables: ["57 sections, forms, OG/Twitter cards", "Analytics & deploy to your VPS", "Basic SEO & sitemap"],
timeline: "57 days",
price: "€2,450",
proof: ["Lighthouse pass for basics", "Deployed site link & repo handover"],
faq: [
{ q: "Copy & assets?", a: "We provide a brief and templates; you can supply or we refine." },
{ q: "CMS?", a: "Optional—see Headless CMS Setup." },
],
relatedSlugs: ["headless-cms-setup", "core-web-vitals-sprint"],
metaTitle: "Quick-Launch Website (Next.js) — Van Hunen IT",
metaDescription:
"Launch a fast, SEO-ready site in days with forms, analytics, and deployment.",
},
{
slug: "headless-cms-setup",
title: "Headless CMS Setup (Ghost/Strapi)",
category: "web-dev",
outcome: "Non-tech content updates without redeploys.",
who: ["Teams wanting easy publishing", "Sites separating content from code"],
deliverables: ["CMS install & roles", "Content model & CI/CD", "Image optimization & docs"],
timeline: "23 days",
price: "€1,190",
proof: ["Editor demo & role permissions", "Publishing pipeline test"],
faq: [
{ q: "Migration from WordPress?", a: "We can import and map key content types." },
{ q: "Auth & SSO?", a: "SSO/OAuth possible depending on CMS chosen." },
],
relatedSlugs: ["quick-launch-website", "website-care-plan"],
metaTitle: "Headless CMS Setup — Van Hunen IT",
metaDescription:
"Install and configure Ghost/Strapi with roles, CI/CD, and image optimization.",
},
{
slug: "security-compliance-baseline",
title: "Security & Compliance Baseline (GDPR-aware)",
category: "web-dev",
outcome: "Reasonable security for small teams.",
who: ["SMBs formalizing access and logging", "Teams preparing for audits"],
deliverables: ["Password policy, 2FA & least-privilege", "Audit logging & data retention", "Incident checklist & drills"],
timeline: "12 days",
price: "€740",
proof: ["Policy documents & checklists", "Access review and logging tests"],
faq: [
{ q: "Covers DPIA?", a: "We provide input; legal sign-off remains with your DPO/counsel." },
{ q: "Tooling?", a: "We match to your stack—SaaS or self-hosted where appropriate." },
],
relatedSlugs: ["vps-hardening-care", "backup-disaster-recovery-drill"],
metaTitle: "Security & Compliance Baseline — Van Hunen IT",
metaDescription:
"Implement practical security policies, logging, and incident readiness for SMBs.",
},
]; ];
export function getAllServices(): Service[] { export function getAllServices(): Service[] {