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() {
const router = useRouter();
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 [loading, setLoading] = useState(false);
@ -30,29 +56,46 @@ function RequestForm() {
domain: "",
company: "",
message: "",
hosting: "",
hosting: "", // reused for Minecraft edition (Java/Bedrock/etc.)
concern: "",
});
type RequestType =
| "audit"
| "consultation"
| "support"
| "tool"
| "partnership";
const requestTypes: { id: RequestType; label: string; desc: string; icon: string }[] = [
{
id: "audit",
label: "Free Audit",
desc: "Request a free website, DNS or performance check.",
icon: "🧠",
id: "recommendation",
label: "Get My Recommendation",
desc: "Tell us about your stack/server. Well propose the right plan and next steps.",
icon: "🧭",
},
{
id: "consultation",
label: "Consultation / Quote",
desc: "Discuss a project, hosting setup, or optimization.",
icon: "⚙️",
id: "vps",
label: "Managed VPS Hosting",
desc: "Hardened, monitored VPS with restore-tested backups.",
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",
@ -60,12 +103,6 @@ function RequestForm() {
desc: "Report an issue or request hands-on help.",
icon: "🛠️",
},
{
id: "tool",
label: "Tool Follow-Up",
desc: "Continue from one of our free tools or reports.",
icon: "📊",
},
{
id: "partnership",
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 (
<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">
<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">
Start a Request
Talk to an Engineer
</h1>
<p className="mt-4 text-neutral-700 dark:text-neutral-300 text-lg">
Choose what youd like to do audits, consultations, or support.
Well guide you through the right steps and get back within one business day.
Tell us about your stack or server. Well recommend a plan and outline next stepsno lock-in.
</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>
</section>
{/* --- FORM --- */}
<section className="container mx-auto max-w-3xl px-6 py-12">
<AnimatePresence mode="wait">
{step === 1 && (
@ -132,7 +210,7 @@ function RequestForm() {
transition={{ duration: 0.3 }}
>
<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>
<div className="grid gap-6 sm:grid-cols-2">
{requestTypes.map((t) => (
@ -200,41 +278,66 @@ function RequestForm() {
/>
</div>
{(form.type === "audit" || form.type === "consultation") && (
<>
<div>
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
Website or Domain
</label>
<input
type="text"
name="domain"
placeholder="example.com"
value={form.domain}
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"
/>
</div>
{showDomainInput && (
<div>
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
Website or Domain
</label>
<input
type="text"
name="domain"
placeholder="example.com"
value={form.domain}
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"
/>
</div>
)}
<div>
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
Main Concern
</label>
<select
name="concern"
value={form.concern}
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="speed">Speed / Performance</option>
<option value="security">Security</option>
<option value="dns">DNS / Email Issues</option>
<option value="hosting">Hosting Migration</option>
<option value="other">Other</option>
</select>
</div>
</>
{/* Concern selector for scoping */}
{(form.type !== "" && form.type !== "partnership") && (
<div>
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
Main Concern
</label>
<select
name="concern"
value={form.concern}
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="speed">Speed / Performance</option>
<option value="security">Security</option>
<option value="dns">DNS / Email Issues</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>
</select>
</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>
@ -246,7 +349,15 @@ function RequestForm() {
rows={4}
value={form.message}
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"
/>
</div>
@ -265,9 +376,14 @@ function RequestForm() {
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"
>
{loading ? "Submitting..." : "Submit Request"}
{loading ? "Submitting..." : submitLabel}
</button>
</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>
)}
</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";
export const metadata: Metadata = {
title: "SMB Website Reliability & Deliverability",
title: "Managed Hosting & Development — VPS, Websites & Minecraft | Van Hunen IT",
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() {
@ -26,16 +26,20 @@ export default function HomePage() {
const faqItems = [
{
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?",
a: "Yes—mutual NDA available on request; we keep credentials least-privilege.",
},
{
q: "Can we switch providers later?",
a: "Yes. Everything is documented; you own the accounts and artifacts. Month-to-month plans with a 30-day cancel policy.",
},
{
q: "Whats your guarantee?",
a: "We show proof of outcomes. If the agreed scope isnt met, we make it right or refund the sprint fee.",
@ -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 (
<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 --- */}
@ -64,27 +122,56 @@ export default function HomePage() {
<div className="container mx-auto max-w-6xl px-4 py-28 text-center">
<h1 className="text-4xl sm:text-6xl font-extrabold tracking-tight">
<span className="bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 bg-clip-text text-transparent">
Fixes in days. Uptime for months.
Managed Hosting & Development Fast, Secure, 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">
SMB website reliability and deliverabilityimplemented with the{" "}
<span className="font-semibold">Reliability Stack</span>: DMARC-aligned email, Cloudflare edge
security & speed, tested backups, and uptime watch. Flat pricing. Before/after proof.
Get reliable <strong>Managed or Owned VPS hosting</strong>,{" "}
<strong>Managed Website hosting & care</strong>,{" "}
<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>
<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
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="/free"
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"
>
Start a Free Reliability Check
Provision a VPS
</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"
>
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>
</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="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-4">
<p className="text-2xl font-bold">+38% faster TTFB</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">after Cloudflare tuning</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">after edge & cache tuning</p>
</div>
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-4">
<p className="text-2xl font-bold">DMARC p=reject</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">in 48 hours, spoofing blocked</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">spoofing blocked in 48 hours</p>
</div>
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-4">
<p className="text-2xl font-bold">7m 12s restore</p>
@ -106,45 +193,20 @@ export default function HomePage() {
</div>
</section>
{/* --- RELIABILITY STACK --- */}
{/* --- SERVICES --- */}
<section className="relative overflow-hidden border-y border-neutral-200 dark:border-neutral-800">
<div className="container mx-auto max-w-6xl px-4 py-20 text-center">
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 bg-clip-text text-transparent">
The Reliability Stack
Hosting & Development Services
</h2>
<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>
</div>
<div className="container mx-auto max-w-6xl px-4 pb-20">
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{[
{
title: "Email Deliverability",
body:
"Inbox-ready email with SPF/DKIM/DMARC alignment, monitoring, and reports. Stop spoofing and missing leads.",
href: "/services#email-deliverability",
},
{
title: "Cloudflare Edge",
body:
"WAF & bot mitigation, HTTP/3, cache tuning, and origin shields for fewer attacks and faster TTFB.",
href: "/services#cloudflare",
},
{
title: "Backups & Restore",
body:
"Automated backups with scheduled restore tests and a recovery runbook. Know you can recover, fast.",
href: "/services#backups",
},
{
title: "Uptime & Incidents",
body:
"Monitors and SSL watch with clear incident notes and post-mortems. Reduce surprises and MTTR.",
href: "/services#uptime",
},
].map((card) => (
{services.map((card) => (
<article
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"
@ -158,7 +220,7 @@ export default function HomePage() {
href={card.href}
className="mt-5 inline-flex items-center text-sm font-medium text-blue-600 hover:underline"
>
Explore
{card.cta}
</Link>
</article>
))}
@ -175,15 +237,15 @@ export default function HomePage() {
</div>
</section>
{/* --- HOW IT WORKS (3 STEPS) --- */}
{/* --- HOW IT WORKS --- */}
<section className="relative py-24">
<div className="container mx-auto max-w-6xl px-4">
<div className="text-center">
<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>
<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>
</div>
@ -191,21 +253,18 @@ export default function HomePage() {
{[
{
step: "1",
title: "Diagnose",
body:
"DNS/email/edge scan, backup checks, and uptime review. Clear scope and fixed price before we start.",
title: "Plan",
body: "Technical review and goals—or use the wizard to self-provision.",
},
{
step: "2",
title: "Implement",
body:
"DMARC + Cloudflare + backups + monitors. Least-privilege access, change log, and rollback plan.",
title: "Set up",
body: "Provision VPS/hosting, deploy site or server, configure monitoring and backups.",
},
{
step: "3",
title: "Prove",
body:
"Before/after report, restore test timer, incident notes, and next-step plan you can share internally.",
title: "Verify",
body: "Before/after metrics, restore test timer, and next-step plan you can share internally.",
},
].map((s) => (
<div
@ -221,10 +280,10 @@ export default function HomePage() {
<div className="mt-10 flex justify-center">
<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"
>
Book a Fix Sprint (490)
Get My Recommendation
</Link>
</div>
</div>
@ -233,11 +292,10 @@ export default function HomePage() {
{/* --- PRICING --- */}
<section className="relative py-24 bg-gradient-to-br from-blue-50/40 via-white to-purple-50/40 dark:from-blue-950/10 dark:to-purple-950/10">
<div className="container mx-auto max-w-6xl px-4">
<Pricing plans={plans} />
<div className="mt-8 text-center">
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Every plan includes DNS/email monitoring, automated backups with restore verification, uptime & SSL
Every plan includes monitoring, automated backups with restore verification, uptime & SSL
watch, and a quarterly health summary.
</p>
</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">
<div className="container mx-auto max-w-4xl px-6">
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight">
Ready to make your website reliable and inbox-ready?
Power, Performance & Peace of Mind Managed for You.
</h2>
<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>
<div className="mt-8 flex flex-col sm:flex-row gap-3 justify-center">
<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"
>
Start Free Check
Provision a VPS
</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"
>
See Pricing & SLA
Deploy Minecraft
</Link>
</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
import type { Metadata } from "next";
import PricingTable from "@/components/pricing/PricingTable";
import ComparisonTable from "@/components/pricing/ComparisonTable";
import PlanRecommender from "@/components/pricing/PlanRecommender";
import ROICalculator from "@/components/pricing/ROICalculator";
import FAQ from "@/components/pricing/FAQ";
import Guarantee from "@/components/pricing/Guarantee";
import { Plan } from "@/components/pricing/types";
import Link from "next/link";
import { site } from "@/lib/site";
import JsonLd from "@/components/JsonLd";
import PricingConfigurator from "./PricingConfigurator";
export const metadata: Metadata = {
title: "Pricing & SLAs — Van Hunen IT",
title: "Plans, SLAs & Self-Service — Managed VPS, Websites & Minecraft | Van Hunen IT",
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",
name: "Essential Care",
bestFor: "Solo & micro businesses",
name: "Essential Managed Hosting",
bestFor: "Websites & VPS that need reliable care",
monthlyPrice: 149,
yearlyDiscount: 0.1,
outcomes: ["Inbox-ready email", "99.9% uptime"],
outcomes: ["Reliable care & monitoring", "99.9% uptime target"],
inclusions: [
"SPF/DKIM/DMARC monitoring",
"DNS & email monitoring (SPF/DKIM/DMARC)",
"Automated backups + quarterly restore test",
"Managed updates (WordPress/Next.js)",
"Uptime & SSL watch",
"Incident credits",
"Business-hours support (8×5)",
],
sla: "Next-business-day first response (8×5)",
ctaLabel: "Start Essential",
cta: { label: "Start Essential", href: "/contact?type=website-care" },
popular: false,
},
{
id: "growth",
name: "Growth Care",
bestFor: "SMB team sites",
name: "Growth Managed Hosting",
bestFor: "Teams & growing sites",
monthlyPrice: 299,
yearlyDiscount: 0.1,
outcomes: ["99.95% uptime", "Faster TTFB"],
outcomes: ["99.95% uptime target", "Faster performance"],
inclusions: [
"Everything in Essential",
"Cloudflare WAF & bot tuning",
@ -47,72 +46,213 @@ const plans: Plan[] = [
"Priority incident handling",
],
sla: "4-hour first response (8×5)",
ctaLabel: "Start Growth",
cta: { label: "Start Growth", href: "/contact?type=website-care" },
popular: true,
},
{
id: "mission",
name: "Mission-Critical",
bestFor: "High-traffic & 24/7",
name: "Mission-Critical Managed Hosting",
bestFor: "High-traffic & 24/7 operations",
monthlyPrice: 649,
yearlyDiscount: 0.1,
outcomes: ["99.99% uptime", "24/7 on-call"],
inclusions: [
"Everything in Growth",
"24/7 paging",
"Weekly checks",
"DR runbook & drills",
],
outcomes: ["99.99% uptime target", "24/7 on-call"],
inclusions: ["Everything in Growth", "24/7 paging", "Weekly checks", "DR runbook & drills"],
sla: "1-hour first response (24/7)",
ctaLabel: "Talk to us",
cta: { label: "Talk to Us", href: "/contact?type=website-care" },
popular: false,
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() {
return (
<main className="mx-auto max-w-7xl px-6 py-16">
<header className="mx-auto max-w-3xl text-center">
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl">
Simple plans with real SLAs
<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">
{/* HERO */}
<header className="container mx-auto max-w-6xl px-6 pt-16 pb-10 text-center">
<h1 className="text-4xl sm:text-5xl font-bold tracking-tight">
Plans, SLAs & Self-Service
</h1>
<p className="mt-4 text-lg text-gray-600">
Pick the care level that matches your traffic and risk. Month-to-month, 30-day
cancel. Fix Sprint available for urgent issues.
<p className="mt-4 text-lg text-neutral-700 dark:text-neutral-300">
Pick **Managed Hosting & Care**, provision a **VPS (managed or owned)**, or **deploy a Minecraft server**.
Month-to-month, 30-day cancel, restore-tested backups, real SLAs.
</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>
{/* Engaging element #1: Billing toggle inside table */}
<section className="mt-12">
<PricingTable plans={plans} />
<p className="mt-3 text-center text-sm text-gray-500">
Prices exclude VAT. Annual billing saves 10%.
{/* NEW PRICING EXPERIENCE */}
<section className="container mx-auto max-w-6xl px-6 pb-20">
<PricingConfigurator carePlans={carePlans} vpsPlans={vpsPlans} minecraftPlans={minecraftPlans} />
<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>
</section>
{/* Engaging element #2: Plan recommender */}
<section className="mt-24">
<PlanRecommender plans={plans} />
{/* TRUST / GUARANTEE */}
<section className="border-t border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60">
<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>
{/* Comparison matrix */}
<section className="mt-24">
<ComparisonTable plans={plans} />
{/* FAQ */}
<section className="container mx-auto max-w-5xl px-6 py-16">
<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>
{/* Engaging element #3: ROI calculator */}
<section className="mt-24">
<ROICalculator plans={plans} />
</section>
<section className="mt-24">
<Guarantee />
</section>
<section className="mt-24">
<FAQ />
</section>
<JsonLd data={pricingLd} />
</main>
);
}

View File

@ -1,159 +1,445 @@
// app/services/page.tsx
import { Metadata } from "next";
import { getAllServices, SERVICE_CATEGORIES, type ServiceCategoryId } from "@/lib/services";
import ServiceCard from "@/components/ServiceCard";
import type { Metadata } from "next";
import Link from "next/link";
import JsonLd from "@/components/JsonLd";
import { site } from "@/lib/site";
export const revalidate = 86400;
export const metadata: Metadata = {
title: "Services — Website Reliability for SMBs | Van Hunen IT",
title: "Services — Managed Hosting, Websites & Minecraft | Van Hunen IT",
description:
"Fixed-scope sprints and care plans for SMB website reliability: email deliverability (DMARC), Cloudflare edge security & speed, tested backups, and uptime watch.",
"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" },
};
export default function ServicesPage() {
const services = getAllServices();
type Card = {
title: string;
body: string;
href: string;
cta: string;
};
// Emphasize core Reliability first; keep the rest discoverable.
const categories: ServiceCategoryId[] = [
"web-performance",
"infrastructure-devops",
"dev-platforms",
"migrations",
"web-dev",
"minecraft",
];
type Category = {
id:
| "web-dev"
| "web-performance"
| "minecraft"
| "infrastructure-devops"
| "migrations"
| "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 (
<main
id="top"
className="relative isolate min-h-screen bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950"
>
{/* --- Hero Header --- */}
{/* HERO */}
<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">
<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>
<p className="mt-5 text-lg text-neutral-700 dark:text-neutral-300 max-w-2xl mx-auto">
Fixed-scope sprints and care plans for{" "}
<strong>email deliverability</strong>, <strong>Cloudflare edge security &amp; speed</strong>,{" "}
<strong>backups with restore tests</strong>, and <strong>uptime watch</strong>. Clear outcomes, flat pricing,
and before/after proof you can keep.
<p className="mt-5 text-lg text-neutral-700 dark:text-neutral-300 max-w-3xl mx-auto">
Managed or Owned VPS, Managed Website hosting & care, Website development, and Minecraft hosting & plugins.
Clear outcomes, flat pricing, real SLAsand before/after proof.
</p>
{/* Category chips */}
<div className="mt-8 flex flex-wrap justify-center gap-3">
{categories.map((id) => (
{chips.map((c) => (
<a
key={id}
href={`#${SERVICE_CATEGORIES[id].anchor}`}
key={c.id}
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"
>
{SERVICE_CATEGORIES[id].label}
{c.label}
</a>
))}
</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
href="/pricing"
className="inline-flex items-center rounded-lg bg-blue-600 px-5 py-2.5 text-white font-semibold hover:bg-blue-500 transition"
href="/vps#deploy"
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
href="/contact"
className="inline-flex items-center rounded-lg border border-neutral-300 dark:border-neutral-700 px-5 py-2.5 font-semibold text-neutral-800 dark:text-neutral-200 hover:bg-neutral-50 dark:hover:bg-neutral-900/40 transition"
href="/minecraft#deploy"
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>
</div>
</div>
</section>
{/* --- Service Categories --- */}
<div className="container mx-auto max-w-6xl px-4 py-16">
{categories.map((id, index) => {
const list = services.filter((s) => s.category === id);
if (!list.length) return null;
const headingId = `${id}-heading`;
const sectionClasses = `relative py-16 scroll-mt-24 ${
index % 2 === 0
? "bg-white dark:bg-neutral-900/50"
: "bg-neutral-50 dark:bg-neutral-900/30"
} rounded-2xl mb-12 shadow-sm`;
return (
<section
key={id}
id={SERVICE_CATEGORIES[id].anchor}
aria-labelledby={headingId}
className={sectionClasses}
{/* FEATURED TILES */}
<section className="container mx-auto max-w-6xl px-4 py-12">
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{[
{
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 -z-10 bg-gradient-to-br from-blue-50/10 via-transparent to-purple-50/10 dark:from-blue-950/10 dark:to-purple-950/10 rounded-2xl" />
<div className="px-4 sm:px-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
<h2
id={headingId}
className="text-2xl sm:text-3xl font-bold tracking-tight bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"
>
{SERVICE_CATEGORIES[id].label}
</h2>
<Link
href="#top"
className="mt-3 sm:mt-0 inline-flex items-center text-sm font-medium text-blue-600 hover:underline dark:text-blue-400"
aria-label="Jump to top"
>
Jump to top
</Link>
</div>
<div className="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>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{list.map((svc, i) => (
<div
key={svc.slug}
className="animate-[fadeInUp_0.6s_ease_forwards]"
style={{ animationDelay: `${i * 80}ms` }}
>
<ServiceCard svc={svc} />
</div>
))}
</div>
{/* SECTIONS */}
<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
? "bg-white dark:bg-neutral-900/50"
: "bg-neutral-50 dark:bg-neutral-900/30"
}`}
aria-labelledby={`${cat.anchor}-heading`}
>
<div className="absolute inset-0 -z-10 bg-gradient-to-br from-blue-50/10 via-transparent to-purple-50/10 dark:from-blue-950/10 dark:to-purple-950/10 rounded-2xl" />
<div className="px-4 sm:px-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
<h2
id={`${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"
>
{cat.label}
</h2>
<Link
href="#top"
className="mt-3 sm:mt-0 inline-flex items-center text-sm font-medium text-blue-600 hover:underline dark:text-blue-400"
>
Jump to top
</Link>
</div>
</section>
);
})}
<p className="max-w-3xl text-neutral-700 dark:text-neutral-300">
{cat.lead}
</p>
<div className="mt-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{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"
>
<h3 className="text-lg font-semibold">{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>
</div>
</section>
))}
</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">
<div className="container mx-auto max-w-4xl px-6">
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight">
Ready to improve reliability and deliverability?
Power, Performance & Peace of Mind Managed for You.
</h2>
<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
before/after.
Provision a VPS, manage your site, or deploy a Minecraft serverbacked by real SLAs and restore-tested backups.
</p>
<div className="mt-8 flex items-center justify-center gap-3">
<Link
href="/pricing"
className="inline-flex items-center rounded-lg bg-white text-blue-700 font-semibold px-6 py-3 hover:bg-blue-50 transition"
>
Compare plans
Compare Plans
</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"
>
Contact us
Get My Recommendation
</Link>
</div>
</div>
</section>
<JsonLd data={servicesLd} />
</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">
{[
{ href: "/services", label: "Services" },
{ href: "/free", label: "Free tools" },
{ href: "/pricing", label: "Pricing" },
{ href: "/contact", label: "Contact" },
{ href: "/privacy", label: "Privacy" },
{ href: "/terms", label: "Terms" },

View File

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

View File

@ -2,24 +2,14 @@
import { Plan } from "./types";
import { CheckIcon, MinusIcon } from "./icons";
const rows: { label: string; keys: Array<keyof Plan | string> }[] = [
{ label: "Email deliverability monitoring", keys: [] },
{ label: "Automated backups", keys: [] },
{ label: "Restore test cadence", keys: [] },
{ label: "Cloudflare WAF & bot tuning", keys: [] },
{ label: "Web Vitals reporting", keys: [] },
{ label: "On-call paging", keys: [] },
{ label: "First response SLA", keys: [] },
{ label: "Uptime target", keys: [] },
];
export default function ComparisonTable({ plans }: { plans: Plan[] }) {
// Hard-code feature mapping for clarity (keeps copy outcome-driven)
// Outcome-focused features mapped to each plan
const matrix: Record<string, (planId: string) => string | boolean> = {
"Email deliverability monitoring": () => true,
"DNS & email monitoring": () => true,
"Automated backups": () => true,
"Restore test cadence": (id) =>
id === "essential" ? "Quarterly" : id === "growth" ? "Quarterly" : "Monthly",
"Managed updates (WordPress/Next.js)": () => true,
"Cloudflare WAF & bot tuning": (id) => id !== "essential",
"Web Vitals reporting": (id) => (id === "growth" || id === "mission" ? "Monthly" : false),
"On-call paging": (id) => (id === "mission" ? "24/7" : false),
@ -82,7 +72,7 @@ export default function ComparisonTable({ plans }: { plans: Plan[] }) {
</table>
</div>
<div className="bg-gray-50 px-6 py-3 text-xs text-gray-500">
Uptime target refers to target SLO over a rolling 30-day period. Credits apply if response SLAs are missed.
Uptime target is an SLO over a rolling 30-day period. Credits apply if response SLAs are missed.
</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() {
const faqs = [
const items: QA[] = [
{
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?",
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?",
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?",
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 (
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<h2 className="text-xl font-semibold">Pricing FAQs</h2>
<dl className="mt-6 space-y-6">
{faqs.map((item) => (
<div key={item.q}>
<dt className="text-sm font-medium text-gray-900">{item.q}</dt>
<dd className="mt-2 text-sm text-gray-700">{item.a}</dd>
</div>
))}
</dl>
</div>
<section
aria-labelledby="faq-heading"
className="relative py-24 bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950"
>
<div className="container mx-auto max-w-4xl px-4">
<h2
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>
</details>
);
})}
</div>
<p className="mt-10 text-center text-sm text-neutral-600 dark:text-neutral-400">
Still have questions?{" "}
<a
href="/contact"
className="font-medium text-blue-600 hover:underline"
>
Contact us
</a>{" "}
or{" "}
<a
href="/pricing"
className="font-medium text-blue-600 hover:underline"
>
see Pricing & SLA
</a>
.
</p>
</div>
</section>
);
}

View File

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

View File

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

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">
<h2 className="text-xl font-semibold">Not sure where to start?</h2>
<p className="mt-1 text-sm text-gray-600">
Answer three quick questions and well suggest a plan.
Answer three quick questions and well suggest a managed hosting plan.
</p>
<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 =
| "hosting"
| "infrastructure-devops"
| "web-performance"
| "dev-platforms"
@ -33,35 +33,42 @@ export const SERVICE_CATEGORIES: Record<
ServiceCategoryId,
{ 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": {
label: "Infrastructure & DevOps",
anchor: "infrastructure-devops",
},
"web-performance": {
label: "Website Reliability & Performance",
label: "Website Reliability, Performance & Hosting",
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" },
minecraft: { label: "Minecraft Services", anchor: "minecraft" },
"web-dev": { label: "Web Development", anchor: "web-dev" },
};
// ------- 23 services -------
// ------- services -------
export const SERVICES: Service[] = [
// --- HOSTING (Managed) ---
{
slug: "vps-hardening-care",
title: "VPS Hardening & Care",
category: "infrastructure-devops",
outcome: "Secure, monitored, and backed-up VPS—foundation of the Reliability Stack™.",
title: "Managed VPS Hosting (Hardened & Monitored)",
category: "hosting",
outcome:
"Secure, fast VPS hosting—server management, monitoring, and restore-tested backups handled for you.",
who: [
"SMBs running WordPress/Next.js/apps on a VPS",
"Teams needing a baseline security and backup posture",
"SMBs running WordPress/Next.js/custom apps on a VPS",
"Teams that want managed VPS hosting with proof of outcomes",
],
deliverables: [
"SSH/CIS hardening, firewall (ufw), fail2ban",
"Automated updates, audit log, intrusion checks",
"Backups + restore test, uptime & resource monitoring",
"Optional Cloudflare edge hardening & HTTP/3",
],
timeline: "12 days",
price: "€390 one-off or €89/mo care",
@ -80,10 +87,196 @@ export const SERVICES: Service[] = [
},
],
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:
"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",
title: "Dockerize & Deploy",
@ -113,7 +306,11 @@ export const SERVICES: Service[] = [
category: "infrastructure-devops",
outcome: "Secure image storage and CI/CD-friendly workflows.",
who: ["Teams needing private images with access control", "Orgs adopting container scanning & retention policies"],
deliverables: ["GHCR/Harbor registry, RBAC & tokens", "Retention & vulnerability scanning", "CI push/pull integration docs"],
deliverables: [
"GHCR/Harbor registry, RBAC & tokens",
"Retention & vulnerability scanning",
"CI push/pull integration docs",
],
timeline: "12 days",
price: "€490",
proof: ["Policy & RBAC screenshots", "Pipeline run showing signed/pushed images"],
@ -193,7 +390,11 @@ export const SERVICES: Service[] = [
category: "infrastructure-devops",
outcome: "Verified restore path—not just backups.",
who: ["Sites that never tested restore", "Teams formalizing RPO/RTO targets"],
deliverables: ["Backup plan (files/db), encryption & rotation", "Restore test with documented steps", "RPO/RTO notes & recommendations"],
deliverables: [
"Backup plan (files/db), encryption & rotation",
"Restore test with documented steps",
"RPO/RTO notes & recommendations",
],
timeline: "1 day",
price: "€490",
proof: ["Restore demonstration on staging", "Report with timings and gaps"],
@ -212,7 +413,11 @@ export const SERVICES: Service[] = [
category: "infrastructure-devops",
outcome: "Lower TTFB, fewer bad bots, safer origins.",
who: ["Sites facing spam/bot abuse or high TTFB", "Teams needing sane edge security fast"],
deliverables: ["WAF & bot tuning, page rules, cache keys", "Origin shielding, HTTP/3, rate limiting", "TTFB and cache-hit improvements"],
deliverables: [
"WAF & bot tuning, page rules, cache keys",
"Origin shielding, HTTP/3, rate limiting",
"TTFB and cache-hit improvements",
],
timeline: "1 day",
price: "€420",
proof: ["Before/after WebPageTest/TTFB screenshots", "WAF rule set export & notes"],
@ -225,13 +430,19 @@ export const SERVICES: Service[] = [
metaDescription:
"Tune WAF, caching, and HTTP/3 to reduce TTFB and block abusive traffic.",
},
// --- WEBSITE RELIABILITY & PERFORMANCE ---
{
slug: "core-web-vitals-sprint",
title: "Core Web Vitals Sprint",
category: "web-performance",
outcome: "CLS/LCP/INP into the green with measurable before/after.",
who: ["Marketing sites and shops with poor CWV", "Next.js/WordPress teams needing a focused fix"],
deliverables: ["Image strategy (WebP/next/image), font loading", "Script defers, critical CSS, caching headers", "Before/after CWV report"],
deliverables: [
"Image strategy (WebP/next/image), font loading",
"Script defers, critical CSS, caching headers",
"Before/after CWV report",
],
timeline: "23 days",
price: "€820",
proof: ["Lighthouse/CrUX before vs after", "Largest contentful paint assets diff"],
@ -244,29 +455,6 @@ export const SERVICES: Service[] = [
metaDescription:
"Improve LCP/CLS/INP with image, font, and script strategy plus caching.",
},
{
slug: "website-care-plan",
title: "Website Care Plan",
category: "web-performance",
outcome: "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",
title: "Secure Contact & Email Deliverability Pack",
@ -290,6 +478,8 @@ export const SERVICES: Service[] = [
metaDescription:
"Fix spam issues with SPF/DKIM/DMARC, verified tests, and safer contact forms.",
},
// --- DEV PLATFORMS & MIGRATIONS ---
{
slug: "self-hosted-gitea-sso",
title: "Self-Hosted Git (Gitea) with SSO",
@ -353,7 +543,11 @@ export const SERVICES: Service[] = [
category: "migrations",
outcome: "Zero-to-minimal downtime move with rollback.",
who: ["Teams changing hosts or platforms", "Apps consolidating infra"],
deliverables: ["Inventory & plan, containerization if needed", "DNS/cutover & rollback plan", "Smoke tests & timed runbook"],
deliverables: [
"Inventory & plan, containerization if needed",
"DNS/cutover & rollback plan",
"Smoke tests & timed runbook",
],
timeline: "24 days",
price: "€1,190",
proof: ["Cutover timeline & metrics", "Rollback rehearsal log"],
@ -385,143 +579,6 @@ export const SERVICES: Service[] = [
metaDescription:
"Containerize legacy apps with healthchecks, backups, and docs.",
},
{
slug: "minecraft-managed-server",
title: "Managed Minecraft Server",
category: "minecraft",
outcome: "Fast, stable, and safe server on a dedicated VPS.",
who: ["Communities, schools, creators", "Small networks needing reliable ops"],
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[] {