Minecraft & VPS added
This commit is contained in:
parent
789678a0c1
commit
95b3ea4383
|
|
@ -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. We’ll 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 you’d like to do — audits, consultations, or support.
|
||||
We’ll guide you through the right steps and get back within one business day.
|
||||
Tell us about your stack or server. We’ll recommend a plan and outline next steps—no 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>
|
||||
|
|
|
|||
385
web/app/minecraft/DeployWizard.tsx
Normal file
385
web/app/minecraft/DeployWizard.tsx
Normal 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 custom—contact 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">
|
||||
We’ll 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">
|
||||
We’ll 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
328
web/app/minecraft/page.tsx
Normal 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. We’ll 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, we’ll 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 limits—tuning 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 spec—tests, 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">
|
||||
(We’ll 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 we’ll 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>
|
||||
);
|
||||
}
|
||||
198
web/app/page.tsx
198
web/app/page.tsx
|
|
@ -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 2–3 business days after scope confirmation.",
|
||||
a: "Most projects start within 2–3 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: "What’s your guarantee?",
|
||||
a: "We show proof of outcomes. If the agreed scope isn’t met, we make it right or refund the sprint fee.",
|
||||
|
|
@ -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 deliverability—implemented with the{" "}
|
||||
<span className="font-semibold">Reliability Stack™</span>: DMARC-aligned email, Cloudflare edge
|
||||
security & speed, tested backups, and uptime watch. Flat pricing. Before/after proof.
|
||||
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 SLAs—so 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 1–3 day sprint, proven with before/after data.
|
||||
Clear outcomes, flat pricing, and real SLAs—plus 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 results—then choose the right Care plan.
|
||||
Self-service provisioning or white-glove setup. We scope, set up, and verify—then 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. We’ll prove the results.
|
||||
Provision a VPS, manage your site, or deploy a Minecraft server—backed 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>
|
||||
|
|
|
|||
358
web/app/pricing/PricingConfigurator.tsx
Normal file
358
web/app/pricing/PricingConfigurator.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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: "What’s 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 plan’s 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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 & 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 SLAs—and 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. We’ll 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 server—backed 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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
410
web/app/vps/VPSDeployWizard.tsx
Normal file
410
web/app/vps/VPSDeployWizard.tsx
Normal 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 custom—contact 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, we’ll email a secure link to connect your provider—no 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">
|
||||
We’ll 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
305
web/app/vps/page.tsx
Normal 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 we’ll provision into your cloud account after a secure connect flow. You retain full ownership and root access.",
|
||||
},
|
||||
{
|
||||
q: "What’s 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. We’ll 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 account—still 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 goals—we’ll 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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. You’ll 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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 don’t deliver your agreed outcomes in the first month,
|
||||
we’ll complete the sprint free.
|
||||
30-day, no-questions cancel. If we don’t deliver the agreed outcomes in your first month
|
||||
(including a successful restore-tested backup), we’ll complete the sprint free.
|
||||
</p>
|
||||
<div className="mt-4 inline-flex items-center gap-3 rounded-xl border border-emerald-200 bg-white px-4 py-2 text-sm font-medium text-emerald-900">
|
||||
<span className="inline-block rounded-full bg-emerald-100 px-2 py-0.5 text-xs">Guarantee</span>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 we’ll suggest a plan.
|
||||
Answer three quick questions and we’ll suggest a managed hosting plan.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 grid gap-6 md:grid-cols-3">
|
||||
|
|
|
|||
|
|
@ -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 website’s uptime, SSL, and DNS performance — get an instant reliability grade (A–F).",
|
||||
href: "/free/web-reliability-check",
|
||||
},
|
||||
{
|
||||
title: "DNS Health Analyzer",
|
||||
excerpt:
|
||||
"Check your domain’s 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 site’s 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:
|
||||
"We’ll 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:
|
||||
"We’ll 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 — we’ll 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 we’ll 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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: "1–2 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: [
|
||||
"5–7 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: "5–7 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: "2–3 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: "1–2 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 server’s 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 3–7 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: "1–2 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: "2–3 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: "2–4 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 3–7 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: ["5–7 sections, forms, OG/Twitter cards", "Analytics & deploy to your VPS", "Basic SEO & sitemap"],
|
||||
timeline: "5–7 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: "2–3 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: "1–2 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[] {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user