it/web/app/services/[slug]/page.tsx
2025-10-25 20:37:00 +02:00

140 lines
5.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// app/services/[slug]/page.tsx
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import JsonLd from "@/components/JsonLd";
import CtaPanel from "@/components/CtaPanel";
import { ServiceBullets } from "@/components/ServiceBullets";
import { getAllServices, getServiceBySlug } from "@/lib/services";
import { site } from "@/lib/site"; // assumes lib/site.ts exports { site: { url, name, ... } }
export const revalidate = 86400;
type Params = { slug: string };
export async function generateStaticParams() {
return getAllServices().map((s) => ({ slug: s.slug }));
}
export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {
const svc = getServiceBySlug(params.slug);
if (!svc) return {};
return {
title: svc.metaTitle,
description: svc.metaDescription,
alternates: { canonical: `/services/${svc.slug}` },
openGraph: {
title: svc.metaTitle,
description: svc.metaDescription,
url: `${site.url}/services/${svc.slug}`,
type: "article",
images: [{ url: "/og.png", width: 1200, height: 630, alt: "Van Hunen IT" }],
},
twitter: {
card: "summary_large_image",
title: svc.metaTitle,
description: svc.metaDescription,
images: ["/og.png"],
},
};
}
export default function ServicePage({ params }: { params: Params }) {
const svc = getServiceBySlug(params.slug);
if (!svc) notFound();
const breadcrumbLd = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", position: 1, name: "Services", item: `${site.url}/services` },
{ "@type": "ListItem", position: 2, name: svc.title, item: `${site.url}/services/${svc.slug}` },
],
};
const svcLd = {
"@context": "https://schema.org",
"@type": "Service",
name: svc.title,
description: svc.metaDescription || svc.outcome,
provider: { "@type": "Organization", name: site.name, url: site.url, logo: `${site.url}/og.png` },
areaServed: "EU",
offers: { "@type": "Offer", price: svc.price.replace(/[^\d]/g, "") || "0", priceCurrency: "EUR", availability: "https://schema.org/InStock" },
};
const related = (svc.relatedSlugs || [])
.map((slug) => getAllServices().find((s) => s.slug === slug))
.filter(Boolean) as NonNullable<ReturnType<typeof getServiceBySlug>>[];
return (
<main className="container mx-auto max-w-3xl px-4 py-10">
<JsonLd data={breadcrumbLd} />
<JsonLd data={svcLd} />
<header aria-labelledby="service-title">
<p className="text-sm text-neutral-500">
<Link className="hover:underline" href="/services"> Services</Link>
</p>
<h1 id="service-title" className="mt-2 text-3xl font-bold tracking-tight">{svc.title}</h1>
<p className="mt-2 text-neutral-700 dark:text-neutral-300">{svc.outcome}</p>
</header>
<section aria-labelledby="who-heading" className="mt-8">
<h2 id="who-heading" className="text-xl font-semibold tracking-tight">Who its for</h2>
<ul className="mt-3 list-disc pl-6 space-y-1 text-neutral-800 dark:text-neutral-200">
{svc.who.map((w, i) => <li key={i}>{w}</li>)}
</ul>
</section>
{svc.outcomes && <ServiceBullets title="Outcomes" items={svc.outcomes} />}
<ServiceBullets title="Deliverables" items={svc.deliverables} />
<section aria-labelledby="timeline-price-heading" className="mt-8 grid gap-4 sm:grid-cols-2">
<div>
<h3 id="timeline-price-heading" className="text-xl font-semibold tracking-tight">Timeline</h3>
<p className="mt-2">{svc.timeline}</p>
</div>
<div>
<h3 className="text-xl font-semibold tracking-tight">Price</h3>
<p className="mt-2">{svc.price}</p>
</div>
</section>
<ServiceBullets title="Proof we deliver" items={svc.proof} />
{svc.faq?.length ? (
<section aria-labelledby="faq-heading" className="mt-8">
<h2 id="faq-heading" className="text-xl font-semibold tracking-tight">FAQ</h2>
<div className="mt-4 space-y-4">
{svc.faq.map((f, i) => (
<details key={i} className="rounded-md border p-4">
<summary className="cursor-pointer font-medium">{f.q}</summary>
<p className="mt-2 text-neutral-700 dark:text-neutral-300">{f.a}</p>
</details>
))}
</div>
</section>
) : null}
{related.length ? (
<section aria-labelledby="related-heading" className="mt-10">
<h2 id="related-heading" className="text-xl font-semibold tracking-tight">Related services</h2>
<ul className="mt-3 space-y-2">
{related.map((r) => (
<li key={r.slug}>
<Link href={`/services/${r.slug}`} className="hover:underline">{r.title}</Link>
</li>
))}
</ul>
</section>
) : null}
<CtaPanel
title="Ready to start?"
body="Book a free 20-minute check. Well confirm scope, risks, and the fastest path to proof."
href={`/contact?topic=${encodeURIComponent(svc.slug)}`}
/>
</main>
);
}