SEO: apex canonical on project pages, add BreadcrumbList + richer Service schema, image schema
This commit is contained in:
parent
f4b58f789e
commit
fa6cd08f9f
1 changed files with 56 additions and 19 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue