feat(seo): visual breadcrumb navigation on article + application pages

- Create Breadcrumbs.tsx server component — semantic <nav> + <ol>/<li>
  with aria-current, ChevronRight separators, Apple-clean styling
- Add breadcrumbs to news article hero overlay (reuses JSON-LD crumbs)
- Add breadcrumbs to application detail hero (passed as prop to client
  component)
- Refactor breadcrumb data into shared array for JSON-LD + visual nav

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 18:10:49 -05:00
parent 7ec99734c5
commit cb7458cded
4 changed files with 83 additions and 16 deletions
@@ -9,6 +9,8 @@ import Script from "next/script";
import { ArrowLeft, CheckCircle2, Zap, LayoutDashboard, Cpu, PencilRuler, Factory, MapPin, ChevronDown, Play, FileText, Box, Loader2, Maximize, X, ChevronLeft, ChevronRight } from "lucide-react";
import BreathingField from "@/components/visuals/BreathingField";
import AutoPlayVideo from "@/components/AutoPlayVideo";
import Breadcrumbs from "@/components/seo/Breadcrumbs";
import type { BreadcrumbItem } from "@/components/seo/Breadcrumbs";
// 🔥 EL TRUCO DEFINITIVO PARA TYPESCRIPT Y WEB COMPONENTS 🔥
// Al asignar el string a una variable con 'as any', TypeScript deja de
@@ -999,7 +1001,7 @@ function ExpandedCaseStudy({ node }: { node: any }) {
}
// --- COMPONENTE PRINCIPAL ---
export default function ApplicationClient({ data, realCases, images }: { data: any, realCases: any[], images: any }) {
export default function ApplicationClient({ data, realCases, images, breadcrumbs }: { data: any, realCases: any[], images: any, breadcrumbs?: BreadcrumbItem[] }) {
const [expandedCase, setExpandedCase] = useState<string | null>(null);
const [mainLightboxOpen, setMainLightboxOpen] = useState(false);
@@ -1053,6 +1055,7 @@ export default function ApplicationClient({ data, realCases, images }: { data: a
<div className="relative z-10 max-w-4xl mx-auto px-6 pb-12 md:pb-16 w-full">
<header>
{breadcrumbs && <Breadcrumbs items={breadcrumbs} />}
<div className="inline-flex items-center gap-2 text-[#0066CC] dark:text-[#00F0FF] mb-3 bg-white/80 dark:bg-black/80 backdrop-blur-sm px-3 py-1.5 rounded-full border border-black/5 dark:border-white/10">
<LayoutDashboard size={14} />
<span className="text-[10px] md:text-xs font-semibold uppercase tracking-widest">{data.category}</span>
+14 -10
View File
@@ -158,24 +158,28 @@ export default async function ApplicationPage({ params }: { params: Promise<{ sl
const images = getApplicationImages(slug);
// 4. JSON-LD structured data — wrapped to never break the render.
const appTitle = data?.title || "FLUX Application";
const appUrl = `${baseUrl()}/${locale}/applications/${slug}`;
const crumbs = [
{ name: "Home", url: `/${locale}` },
{ name: "Applications", url: `/${locale}#applications-deep` },
{ name: appTitle, url: `/${locale}/applications/${slug}` },
];
let jsonLd: object[] = [];
try {
const url = `${baseUrl()}/${locale}/applications/${slug}`;
const title = data?.title || "FLUX Application";
const description = data?.shortDescription || data?.subtitle || "";
jsonLd = [
productSchema({
name: title,
name: appTitle,
description,
imageUrl: images.heroImage || undefined,
category: data?.category || "RF Industrial",
url,
url: appUrl,
}),
breadcrumbSchema([
{ name: "Home", url: `${baseUrl()}/${locale}` },
{ name: "Applications", url: `${baseUrl()}/${locale}#applications-deep` },
{ name: title, url },
]),
breadcrumbSchema(
crumbs.map((c) => ({ name: c.name, url: `${baseUrl()}${c.url}` }))
),
];
} catch (error) {
console.error(`[applications/${slug}] JSON-LD build failed:`, error);
@@ -184,7 +188,7 @@ export default async function ApplicationPage({ params }: { params: Promise<{ sl
return (
<>
{jsonLd.length > 0 && <JsonLd data={jsonLd} />}
<ApplicationClient data={data} realCases={realCases} images={images} />
<ApplicationClient data={data} realCases={realCases} images={images} breadcrumbs={crumbs} />
</>
);
}
+13 -5
View File
@@ -10,6 +10,7 @@ import BreathingField from "@/components/visuals/BreathingField";
import { setRequestLocale } from "next-intl/server";
import { getLocalizedData } from "@/lib/i18nHelper";
import Breadcrumbs from "@/components/seo/Breadcrumbs";
import {
buildPageMetadata,
articleSchema,
@@ -259,6 +260,12 @@ export default async function ArticlePage({ params }: { params: Promise<{ slug:
const articleUrl = `${baseUrl()}/${locale}/news/${slug}`;
const cover = article.coverImage ? `/news/${slug}/${article.coverImage}` : undefined;
const crumbs = [
{ name: "Home", url: `/${locale}` },
{ name: "News", url: `/${locale}/news` },
{ name: article?.title || "Article", url: `/${locale}/news/${slug}` },
];
let jsonLd: object[] = [];
try {
jsonLd = [
@@ -270,11 +277,9 @@ export default async function ArticlePage({ params }: { params: Promise<{ slug:
publishedAt: article?.publishedAt || new Date(),
updatedAt: article?.updatedAt || new Date(),
}),
breadcrumbSchema([
{ name: "Home", url: `${baseUrl()}/${locale}` },
{ name: "News", url: `${baseUrl()}/${locale}/news` },
{ name: article?.title || "Article", url: articleUrl },
]),
breadcrumbSchema(
crumbs.map((c) => ({ name: c.name, url: `${baseUrl()}${c.url}` }))
),
];
} catch (error) {
console.error(`[news/${slug}] JSON-LD build failed:`, error);
@@ -299,6 +304,9 @@ export default async function ArticlePage({ params }: { params: Promise<{ slug:
<div className="absolute inset-0 bg-gradient-to-t from-white via-white/80 dark:from-[#0A0A0C] dark:via-[#0A0A0C]/80 to-transparent" />
<div className="relative z-10 max-w-4xl w-full mx-auto px-6 pb-12 md:pb-20 text-center">
<div className="flex justify-center mb-4">
<Breadcrumbs items={crumbs} />
</div>
<div className="flex flex-wrap items-center justify-center gap-4 text-xs font-semibold uppercase tracking-widest text-[#0066CC] dark:text-[#4DA6FF] mb-6">
<span className="flex items-center gap-1.5 bg-[#0066CC]/10 dark:bg-[#4DA6FF]/10 px-3 py-1.5 rounded-full"><Tag size={14}/> {article.category}</span>
<time dateTime={new Date(article.publishedAt).toISOString()} className="flex items-center gap-1.5 text-[#86868B]"><Calendar size={14}/> {new Date(article.publishedAt).toLocaleDateString(locale === 'en' ? 'en-US' : locale, { month: 'long', day: 'numeric', year: 'numeric' })}</time>
+52
View File
@@ -0,0 +1,52 @@
// src/components/seo/Breadcrumbs.tsx
// ─────────────────────────────────────────────────────────────────────────────
// Visible breadcrumb navigation trail — complements the JSON-LD BreadcrumbList
// already rendered by individual pages.
//
// Design: Apple-clean, muted, small text — blends with the hero overlays
// on article and application detail pages.
// ─────────────────────────────────────────────────────────────────────────────
import Link from "next/link";
import { ChevronRight } from "lucide-react";
export interface BreadcrumbItem {
name: string;
url: string;
}
export default function Breadcrumbs({ items }: { items: BreadcrumbItem[] }) {
if (items.length < 2) return null;
return (
<nav aria-label="Breadcrumb" className="mb-4">
<ol className="flex items-center gap-1 flex-wrap text-xs">
{items.map((item, idx) => {
const isLast = idx === items.length - 1;
return (
<li key={item.url} className="flex items-center gap-1">
{idx > 0 && (
<ChevronRight size={11} className="text-[#86868B]/40 shrink-0" />
)}
{isLast ? (
<span
aria-current="page"
className="text-[#1D1D1F] dark:text-white/90 font-medium truncate max-w-[220px]"
>
{item.name}
</span>
) : (
<Link
href={item.url}
className="text-[#86868B] hover:text-[#0066CC] dark:hover:text-[#4DA6FF] transition-colors"
>
{item.name}
</Link>
)}
</li>
);
})}
</ol>
</nav>
);
}