SEO: apex canonical on project pages, add BreadcrumbList + richer Service schema, image schema

This commit is contained in:
Zac Gaetano 2026-05-04 00:01:22 -04:00
parent f4b58f789e
commit fa6cd08f9f

View file

@ -4,6 +4,8 @@ import Image from "next/image";
import { ArrowLeft, ArrowRight, ExternalLink } from "lucide-react";
import { projects, getProjectBySlug } from "@/data/projects";
const SITE_URL = "https://wilddragon.net";
export function generateStaticParams() {
return projects.map((project) => ({
slug: project.slug,
@ -15,8 +17,11 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
const project = getProjectBySlug(slug);
if (!project) return { title: "Project Not Found" };
const url = `${SITE_URL}/projects/${slug}`;
const ogImage = project.thumbnail.startsWith("http") ? project.thumbnail : `${SITE_URL}${project.thumbnail}`;
return {
title: `${project.client} | Zachary Gaetano — Broadcast Systems Integration`,
title: `${project.client} ${project.category} | Broadcast Facility Case Study`,
description: project.summary,
keywords: [
...project.technologies,
@ -27,24 +32,25 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
"Wild Dragon",
"Washington DC",
"broadcast facility design",
"case study",
],
alternates: {
canonical: `https://www.wilddragon.net/projects/${slug}`,
canonical: url,
},
openGraph: {
title: `${project.client} | Zachary Gaetano`,
title: `${project.client}${project.category} | Zachary Gaetano`,
description: project.summary,
url: `https://www.wilddragon.net/projects/${slug}`,
url,
siteName: "Wild Dragon",
type: "website",
type: "article",
locale: "en_US",
images: [{ url: project.thumbnail, width: 1200, height: 630, alt: `${project.client}${project.category}` }],
images: [{ url: ogImage, width: 1200, height: 630, alt: `${project.client}${project.category} broadcast facility` }],
},
twitter: {
card: "summary_large_image",
title: `${project.client} | Zachary Gaetano`,
title: `${project.client} ${project.category}`,
description: project.summary,
images: [project.thumbnail],
images: [ogImage],
},
};
}
@ -57,14 +63,45 @@ export default async function ProjectPage({ params }: { params: Promise<{ slug:
notFound();
}
const url = `${SITE_URL}/projects/${slug}`;
const imageUrl = project.thumbnail.startsWith("http") ? project.thumbnail : `${SITE_URL}${project.thumbnail}`;
const structuredData = {
"@context": "https://schema.org",
"@type": "Service",
name: project.title,
description: project.summary,
serviceType: project.category,
provider: { "@id": "https://www.wilddragon.net/#organization" },
areaServed: "United States",
"@graph": [
{
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "Home", item: `${SITE_URL}/` },
{ "@type": "ListItem", position: 2, name: "Projects", item: `${SITE_URL}/#projects` },
{ "@type": "ListItem", position: 3, name: project.client, item: url },
],
},
{
"@type": "Service",
"@id": `${url}#service`,
name: project.title,
description: project.summary,
serviceType: project.category,
category: project.category,
provider: { "@id": `${SITE_URL}/#organization` },
areaServed: "United States",
image: imageUrl,
url,
},
{
"@type": "CreativeWork",
"@id": `${url}#case-study`,
name: `${project.client}${project.category}`,
headline: project.title,
description: project.summary,
about: project.technologies,
author: { "@id": `${SITE_URL}/#person` },
publisher: { "@id": `${SITE_URL}/#organization` },
image: imageUrl,
url,
},
],
};
const currentIndex = projects.findIndex((p) => p.slug === project.slug);
@ -76,13 +113,13 @@ export default async function ProjectPage({ params }: { params: Promise<{ slug:
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
{/* Header bar */}
<nav className="fixed top-0 left-0 right-0 z-50 bg-white/95 backdrop-blur-lg shadow-sm py-3.5">
<nav className="fixed top-0 left-0 right-0 z-50 bg-white/95 backdrop-blur-lg shadow-sm py-3.5" aria-label="Project navigation">
<div className="max-w-6xl mx-auto px-6 flex items-center justify-between">
<Link href="/#projects" className="flex items-center gap-2 text-[13px] text-muted hover:text-primary transition-colors duration-200 group">
<ArrowLeft size={15} className="group-hover:-translate-x-0.5 transition-transform duration-200" />
Back to Projects
</Link>
<Link href="/" className="font-mono text-[10px] tracking-[0.2em] uppercase text-muted hover:text-primary transition-colors duration-200">
<Link href="/" className="font-mono text-[10px] tracking-[0.2em] uppercase text-muted hover:text-primary transition-colors duration-200" aria-label="Wild Dragon — home">
Wild Dragon
</Link>
</div>
@ -185,11 +222,11 @@ export default async function ProjectPage({ params }: { params: Promise<{ slug:
</section>
{/* Prev / Next navigation */}
<section className="border-t border-neutral-200">
<section className="border-t border-neutral-200" aria-label="Project pagination">
<div className="max-w-6xl mx-auto px-6">
<div className="grid grid-cols-1 md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-neutral-200">
{prevProject ? (
<Link href={`/projects/${prevProject.slug}`} className="group flex items-center gap-4 py-10 pr-8 hover:bg-neutral-50 transition-colors duration-200">
<Link href={`/projects/${prevProject.slug}`} className="group flex items-center gap-4 py-10 pr-8 hover:bg-neutral-50 transition-colors duration-200" rel="prev">
<ArrowLeft size={18} className="text-neutral-300 group-hover:text-accent group-hover:-translate-x-1 transition-all duration-200 shrink-0" />
<div>
<p className="font-mono text-[10px] tracking-wider uppercase text-muted mb-1">Previous</p>
@ -199,7 +236,7 @@ export default async function ProjectPage({ params }: { params: Promise<{ slug:
) : <div className="py-10" />}
{nextProject ? (
<Link href={`/projects/${nextProject.slug}`} className="group flex items-center justify-end gap-4 py-10 pl-8 hover:bg-neutral-50 transition-colors duration-200">
<Link href={`/projects/${nextProject.slug}`} className="group flex items-center justify-end gap-4 py-10 pl-8 hover:bg-neutral-50 transition-colors duration-200" rel="next">
<div className="text-right">
<p className="font-mono text-[10px] tracking-wider uppercase text-muted mb-1">Next</p>
<p className="text-sm font-medium text-primary group-hover:text-accent transition-colors duration-200">{nextProject.client}</p>