Compare commits
4 Commits
6b9a94490b
...
c45a5be99e
| Author | SHA1 | Date | |
|---|---|---|---|
| c45a5be99e | |||
| cb7458cded | |||
| 7ec99734c5 | |||
| 8d80cbbc27 |
@@ -184,6 +184,13 @@
|
||||
"page": "Seite",
|
||||
"of": "von"
|
||||
},
|
||||
"ArticlePage": {
|
||||
"backToNewsHub": "Zurück zum News Hub",
|
||||
"backToNews": "Zurück zu Nachrichten",
|
||||
"mediaGallery": "Mediengalerie",
|
||||
"joinLinkedIn": "Diskussion auf LinkedIn beitreten",
|
||||
"internalRelease": "Interne Unternehmensmitteilung"
|
||||
},
|
||||
"AuthModal": {
|
||||
"b2bPortal": "B2B-Portal",
|
||||
"signIn": "Anmelden",
|
||||
|
||||
@@ -184,6 +184,13 @@
|
||||
"page": "Page",
|
||||
"of": "of"
|
||||
},
|
||||
"ArticlePage": {
|
||||
"backToNewsHub": "Back to News Hub",
|
||||
"backToNews": "Back to News",
|
||||
"mediaGallery": "Media Gallery",
|
||||
"joinLinkedIn": "Join the conversation on LinkedIn",
|
||||
"internalRelease": "Internal Corporate Release"
|
||||
},
|
||||
"AuthModal": {
|
||||
"b2bPortal": "B2B Portal",
|
||||
"signIn": "Sign In",
|
||||
|
||||
@@ -184,6 +184,13 @@
|
||||
"page": "Página",
|
||||
"of": "de"
|
||||
},
|
||||
"ArticlePage": {
|
||||
"backToNewsHub": "Volver al News Hub",
|
||||
"backToNews": "Volver a Noticias",
|
||||
"mediaGallery": "Galería de Medios",
|
||||
"joinLinkedIn": "Únete a la conversación en LinkedIn",
|
||||
"internalRelease": "Comunicado Corporativo Interno"
|
||||
},
|
||||
"AuthModal": {
|
||||
"b2bPortal": "Portal B2B",
|
||||
"signIn": "Iniciar Sesión",
|
||||
|
||||
@@ -184,6 +184,13 @@
|
||||
"page": "Pagina",
|
||||
"of": "di"
|
||||
},
|
||||
"ArticlePage": {
|
||||
"backToNewsHub": "Torna al News Hub",
|
||||
"backToNews": "Torna alle Notizie",
|
||||
"mediaGallery": "Galleria Media",
|
||||
"joinLinkedIn": "Partecipa alla conversazione su LinkedIn",
|
||||
"internalRelease": "Comunicato Aziendale Interno"
|
||||
},
|
||||
"AuthModal": {
|
||||
"b2bPortal": "Portale B2B",
|
||||
"signIn": "Accedi",
|
||||
|
||||
@@ -184,6 +184,13 @@
|
||||
"page": "Pagina",
|
||||
"of": "de"
|
||||
},
|
||||
"ArticlePage": {
|
||||
"backToNewsHub": "Torna al News Hub",
|
||||
"backToNews": "Torna a łe Notissie",
|
||||
"mediaGallery": "Gałeria Media",
|
||||
"joinLinkedIn": "Parteçipa a ła conversassion su LinkedIn",
|
||||
"internalRelease": "Comunicato Aziendałe Interno"
|
||||
},
|
||||
"AuthModal": {
|
||||
"b2bPortal": "Portal par ditte",
|
||||
"signIn": "Entra chive",
|
||||
|
||||
@@ -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
|
||||
@@ -904,7 +906,7 @@ function ExpandedCaseStudy({ node }: { node: any }) {
|
||||
const fullImgSrc = `/cases/${nodeSlug}/${img}`;
|
||||
return (
|
||||
<div key={`g-${idx}`} className="relative w-full aspect-video rounded-2xl md:rounded-3xl overflow-hidden border border-black/10 dark:border-white/10 shadow-lg group bg-white">
|
||||
<Image src={fullImgSrc} alt="Installation" fill className="object-cover group-hover:scale-105 transition-transform duration-700" />
|
||||
<Image src={fullImgSrc} alt="Installation" fill sizes="(max-width: 768px) 100vw, 50vw" className="object-cover group-hover:scale-105 transition-transform duration-700" />
|
||||
<button
|
||||
onClick={() => openLightbox(fullImgSrc)}
|
||||
className="absolute inset-0 w-full h-full bg-black/0 hover:bg-black/30 active:bg-black/40 transition-all flex items-center justify-center opacity-0 group-hover:opacity-100 backdrop-blur-[0px] hover:backdrop-blur-sm touch-manipulation"
|
||||
@@ -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);
|
||||
@@ -1045,7 +1047,7 @@ export default function ApplicationClient({ data, realCases, images }: { data: a
|
||||
|
||||
<section className="relative w-full h-[50vh] md:h-[70vh] flex items-end justify-start overflow-hidden">
|
||||
{heroImage ? (
|
||||
<Image src={heroImage} alt={data.title} fill className="object-cover object-center scale-105 animate-slow-zoom" priority />
|
||||
<Image src={heroImage} alt={data.title} fill sizes="100vw" className="object-cover object-center scale-105 animate-slow-zoom" priority />
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#0066CC]/20 to-black/80" />
|
||||
)}
|
||||
@@ -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>
|
||||
@@ -1149,7 +1152,7 @@ export default function ApplicationClient({ data, realCases, images }: { data: a
|
||||
<div className="flex items-center gap-5 flex-1">
|
||||
<div className="w-16 h-16 md:w-20 md:h-20 rounded-2xl md:rounded-3xl bg-black/5 dark:bg-black/50 border border-black/10 dark:border-white/5 flex items-center justify-center shrink-0 overflow-hidden relative shadow-inner">
|
||||
{node.mediaFileName ? (
|
||||
<Image src={`/cases/${nodeToSlug(node.title)}/${node.mediaFileName}`} alt={node.title} fill className="object-cover opacity-90 group-hover:scale-110 transition-transform duration-700" />
|
||||
<Image src={`/cases/${nodeToSlug(node.title)}/${node.mediaFileName}`} alt={node.title} fill sizes="100px" className="object-cover opacity-90 group-hover:scale-110 transition-transform duration-700" />
|
||||
) : (
|
||||
<Cpu size={28} className="text-[#0066CC]/50 dark:text-[#00F0FF]/50" />
|
||||
)}
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ export default async function HeritagePage({ params }: { params: Promise<{ local
|
||||
|
||||
{sec.type === 'image' && sec.mediaUrl && (
|
||||
<div className="relative w-full h-80 md:h-[500px] rounded-3xl overflow-hidden border border-black/10 dark:border-white/10 shadow-2xl">
|
||||
<Image src={`/heritage/${sec.mediaUrl}`} alt={sec.title || "Heritage Image"} fill className="object-cover grayscale hover:grayscale-0 transition-all duration-1000" />
|
||||
<Image src={`/heritage/${sec.mediaUrl}`} alt={sec.title || "Heritage Image"} fill sizes="100vw" className="object-cover grayscale hover:grayscale-0 transition-all duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({ locale }));
|
||||
}
|
||||
import { getBranding, getSocialLinks } from '@/lib/siteSettings';
|
||||
import { organizationSchema, websiteSchema } from '@/lib/seo';
|
||||
import { organizationSchema, websiteSchema, localBusinessSchema } from '@/lib/seo';
|
||||
import JsonLd from '@/components/seo/JsonLd';
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
@@ -154,6 +154,10 @@ export default async function RootLayout({
|
||||
logoUrl: branding.logoUrl,
|
||||
sameAs: sameAs.length ? sameAs : undefined,
|
||||
}),
|
||||
localBusinessSchema({
|
||||
logoUrl: branding.logoUrl,
|
||||
sameAs: sameAs.length ? sameAs : undefined,
|
||||
}),
|
||||
websiteSchema(),
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -8,8 +8,9 @@ import { prisma } from "@/lib/prisma";
|
||||
import { ArrowLeft, Calendar, Tag, Linkedin } from "lucide-react";
|
||||
import BreathingField from "@/components/visuals/BreathingField";
|
||||
|
||||
import { setRequestLocale } from "next-intl/server";
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
import { getLocalizedData } from "@/lib/i18nHelper";
|
||||
import Breadcrumbs from "@/components/seo/Breadcrumbs";
|
||||
import {
|
||||
buildPageMetadata,
|
||||
articleSchema,
|
||||
@@ -227,6 +228,7 @@ export default async function ArticlePage({ params }: { params: Promise<{ slug:
|
||||
const resolvedParams = await params;
|
||||
const { slug, locale } = resolvedParams;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations({ locale, namespace: "ArticlePage" });
|
||||
|
||||
let rawArticle: any = null;
|
||||
try {
|
||||
@@ -259,6 +261,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 +278,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);
|
||||
@@ -288,20 +294,23 @@ export default async function ArticlePage({ params }: { params: Promise<{ slug:
|
||||
<div className="fixed top-24 left-6 z-50 hidden md:block">
|
||||
{/* EL BOTÓN DE VOLVER AHORA RESPETA EL IDIOMA */}
|
||||
<Link href={`/${locale}/news`} className="inline-flex items-center gap-2 text-sm font-medium text-[#86868B] hover:text-[#0066CC] dark:hover:text-[#4DA6FF] transition-colors py-2 px-4 bg-white/50 dark:bg-black/50 backdrop-blur-md rounded-full group shadow-sm border border-black/5 dark:border-white/10">
|
||||
<ArrowLeft size={16} className="group-hover:-translate-x-1 transition-transform" /> Back to News Hub
|
||||
<ArrowLeft size={16} className="group-hover:-translate-x-1 transition-transform" /> {t("backToNewsHub")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<section className="relative w-full h-[50vh] md:h-[70vh] flex items-end justify-center overflow-hidden bg-[#1D1D1F]">
|
||||
{article.coverImage && (
|
||||
<Image src={`/news/${slug}/${article.coverImage}`} alt={article.title} fill className="object-cover object-center opacity-60" priority />
|
||||
<Image src={`/news/${slug}/${article.coverImage}`} alt={article.title} fill sizes="100vw" className="object-cover object-center opacity-60" priority />
|
||||
)}
|
||||
<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>
|
||||
<span 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' })}</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>
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-6xl font-medium text-[#1D1D1F] dark:text-[#F5F5F7] tracking-tight mb-6 leading-tight">
|
||||
{article.title}
|
||||
@@ -312,7 +321,7 @@ export default async function ArticlePage({ params }: { params: Promise<{ slug:
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="relative z-10 max-w-3xl mx-auto px-6 mt-12 md:mt-20">
|
||||
<article className="relative z-10 max-w-3xl mx-auto px-6 mt-12 md:mt-20">
|
||||
|
||||
{/* 🔥 EL MARKDOWN TRADUCIDO AL VUELO 🔥 */}
|
||||
<div className="max-w-none mb-16">
|
||||
@@ -321,11 +330,11 @@ export default async function ArticlePage({ params }: { params: Promise<{ slug:
|
||||
|
||||
{gallery.length > 0 && (
|
||||
<div className="mt-16 pt-16 border-t border-black/5 dark:border-white/5">
|
||||
<h3 className="text-xl font-medium mb-8 text-[#1D1D1F] dark:text-white">Media Gallery</h3>
|
||||
<h3 className="text-xl font-medium mb-8 text-[#1D1D1F] dark:text-white">{t("mediaGallery")}</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{gallery.map((imgSrc: string, idx: number) => (
|
||||
<div key={idx} className={`relative rounded-3xl overflow-hidden bg-[#1D1D1F] border border-black/10 dark:border-white/10 ${idx === 0 && gallery.length % 2 !== 0 ? 'sm:col-span-2 h-64 md:h-96' : 'h-48 md:h-64'}`}>
|
||||
<Image src={`/news/${slug}/gallery/${imgSrc}`} alt={`Gallery image ${idx + 1}`} fill className="object-cover hover:scale-105 transition-transform duration-700" />
|
||||
<Image src={`/news/${slug}/gallery/${imgSrc}`} alt={`Gallery image ${idx + 1}`} fill sizes="(max-width: 640px) 100vw, 50vw" className="object-cover hover:scale-105 transition-transform duration-700" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -334,7 +343,7 @@ export default async function ArticlePage({ params }: { params: Promise<{ slug:
|
||||
|
||||
<div className="mt-16 pt-8 border-t border-black/10 dark:border-white/10 flex justify-between items-center">
|
||||
<Link href={`/${locale}/news`} className="text-[#0066CC] dark:text-[#4DA6FF] font-medium flex items-center gap-2 hover:underline md:hidden">
|
||||
<ArrowLeft size={16} /> Back to News
|
||||
<ArrowLeft size={16} /> {t("backToNews")}
|
||||
</Link>
|
||||
|
||||
{article.linkedinUrl ? (
|
||||
@@ -344,15 +353,15 @@ export default async function ArticlePage({ params }: { params: Promise<{ slug:
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-sm font-semibold text-white bg-[#0A66C2] px-6 py-3 rounded-full hover:bg-[#004182] transition-all shadow-lg shadow-[#0A66C2]/20 ml-auto md:ml-0"
|
||||
>
|
||||
<Linkedin size={16} /> Join the conversation on LinkedIn
|
||||
<Linkedin size={16} /> {t("joinLinkedIn")}
|
||||
</a>
|
||||
) : (
|
||||
<div className="text-xs text-[#86868B] italic hidden md:block">
|
||||
Internal Corporate Release
|
||||
{t("internalRelease")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import BreathingField from "@/components/visuals/BreathingField";
|
||||
|
||||
import { getLocalizedData } from "@/lib/i18nHelper";
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
import { buildPageMetadata } from "@/lib/seo";
|
||||
import { buildPageMetadata, collectionPageSchema, baseUrl } from "@/lib/seo";
|
||||
import JsonLd from "@/components/seo/JsonLd";
|
||||
|
||||
// ISR: revalidate every 60s — see /[locale]/page.tsx for the full rationale.
|
||||
export const revalidate = 60;
|
||||
@@ -49,8 +50,22 @@ export default async function NewsHub({ params }: { params: Promise<{ locale: st
|
||||
const heroArticle = articles.length > 0 ? articles[0] : null;
|
||||
const gridArticles = articles.length > 1 ? articles.slice(1) : [];
|
||||
|
||||
const collectionSchema = articles.length > 0
|
||||
? collectionPageSchema({
|
||||
name: `${t("title1")} ${t("title2")} — FLUX`,
|
||||
description: t("description"),
|
||||
url: `${baseUrl()}/${locale}/news`,
|
||||
items: articles.map((a: any, idx: number) => ({
|
||||
name: a.title,
|
||||
url: `${baseUrl()}/${locale}/news/${a.slug}`,
|
||||
position: idx + 1,
|
||||
})),
|
||||
})
|
||||
: null;
|
||||
|
||||
return (
|
||||
<main className="relative min-h-screen pt-32 pb-24">
|
||||
{collectionSchema && <JsonLd data={collectionSchema} />}
|
||||
<BreathingField />
|
||||
|
||||
<div className="relative z-10 max-w-7xl mx-auto px-6">
|
||||
@@ -76,10 +91,11 @@ export default async function NewsHub({ params }: { params: Promise<{ locale: st
|
||||
|
||||
{/* HERO ARTICLE */}
|
||||
{heroArticle && (
|
||||
<article>
|
||||
<Link href={`/${locale}/news/${heroArticle.slug}`} className="group relative w-full rounded-[2rem] overflow-hidden bg-white/60 dark:bg-[#1D1D1F]/40 backdrop-blur-2xl border border-black/5 dark:border-white/10 flex flex-col md:flex-row shadow-lg hover:shadow-xl transition-all duration-500 hover:-translate-y-1">
|
||||
<div className="w-full md:w-1/2 h-56 md:h-[400px] relative overflow-hidden bg-[#1D1D1F]">
|
||||
{heroArticle.coverImage ? (
|
||||
<Image src={`/news/${heroArticle.slug}/${heroArticle.coverImage}`} alt={heroArticle.title} fill className="object-cover transition-transform duration-700 group-hover:scale-105" />
|
||||
<Image src={`/news/${heroArticle.slug}/${heroArticle.coverImage}`} alt={heroArticle.title} fill sizes="(max-width: 768px) 100vw, 50vw" className="object-cover transition-transform duration-700 group-hover:scale-105" />
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#0066CC]/20 to-[#0A0A0C] flex items-center justify-center"><Newspaper size={64} className="text-white/10" /></div>
|
||||
)}
|
||||
@@ -87,7 +103,7 @@ export default async function NewsHub({ params }: { params: Promise<{ locale: st
|
||||
<div className="w-full md:w-1/2 p-6 md:p-10 flex flex-col justify-center">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<span className="text-[9px] font-bold uppercase tracking-widest text-[#0066CC] dark:text-[#4DA6FF] bg-[#0066CC]/10 dark:bg-[#4DA6FF]/10 px-2.5 py-1 rounded-full">{heroArticle.category}</span>
|
||||
<span className="text-[10px] font-medium text-[#86868B] flex items-center gap-1"><Calendar size={12}/> {new Date(heroArticle.publishedAt).toLocaleDateString(locale === 'en' ? 'en-US' : locale, { month: 'short', day: 'numeric', year: 'numeric' })}</span>
|
||||
<time dateTime={new Date(heroArticle.publishedAt).toISOString()} className="text-[10px] font-medium text-[#86868B] flex items-center gap-1"><Calendar size={12}/> {new Date(heroArticle.publishedAt).toLocaleDateString(locale === 'en' ? 'en-US' : locale, { month: 'short', day: 'numeric', year: 'numeric' })}</time>
|
||||
</div>
|
||||
<h2 className="text-2xl md:text-3xl font-medium text-[#1D1D1F] dark:text-white mb-3 leading-tight group-hover:text-[#0066CC] dark:group-hover:text-[#4DA6FF] transition-colors">{heroArticle.title}</h2>
|
||||
<p className="text-sm text-[#86868B] dark:text-[#A1A1A6] leading-relaxed mb-6 line-clamp-3">{heroArticle.excerpt}</p>
|
||||
@@ -96,16 +112,18 @@ export default async function NewsHub({ params }: { params: Promise<{ locale: st
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</article>
|
||||
)}
|
||||
|
||||
{/* GRID COLUMNAS */}
|
||||
{gridArticles.length > 0 && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6 mt-4">
|
||||
{gridArticles.map((article) => (
|
||||
<Link key={article.id} href={`/${locale}/news/${article.slug}`} className="group flex flex-col bg-white/60 dark:bg-[#1D1D1F]/40 backdrop-blur-md border border-black/5 dark:border-white/10 rounded-3xl overflow-hidden shadow-sm hover:shadow-md transition-all duration-500 hover:-translate-y-1">
|
||||
<article key={article.id} className="flex flex-col">
|
||||
<Link href={`/${locale}/news/${article.slug}`} className="group flex flex-col flex-1 bg-white/60 dark:bg-[#1D1D1F]/40 backdrop-blur-md border border-black/5 dark:border-white/10 rounded-3xl overflow-hidden shadow-sm hover:shadow-md transition-all duration-500 hover:-translate-y-1">
|
||||
<div className="w-full h-40 relative overflow-hidden bg-[#1D1D1F]">
|
||||
{article.coverImage ? (
|
||||
<Image src={`/news/${article.slug}/${article.coverImage}`} alt={article.title} fill className="object-cover transition-transform duration-700 group-hover:scale-105" />
|
||||
<Image src={`/news/${article.slug}/${article.coverImage}`} alt={article.title} fill sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw" className="object-cover transition-transform duration-700 group-hover:scale-105" />
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#0066CC]/10 to-[#0A0A0C]" />
|
||||
)}
|
||||
@@ -113,7 +131,7 @@ export default async function NewsHub({ params }: { params: Promise<{ locale: st
|
||||
<div className="p-5 flex flex-col flex-1">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<span className="text-[8px] font-bold uppercase tracking-widest text-[#0066CC] dark:text-[#4DA6FF]">{article.category}</span>
|
||||
<span className="text-[9px] text-[#86868B]">{new Date(article.publishedAt).toLocaleDateString(locale === 'en' ? 'en-US' : locale, { month: 'short', day: 'numeric' })}</span>
|
||||
<time dateTime={new Date(article.publishedAt).toISOString()} className="text-[9px] text-[#86868B]">{new Date(article.publishedAt).toLocaleDateString(locale === 'en' ? 'en-US' : locale, { month: 'short', day: 'numeric' })}</time>
|
||||
</div>
|
||||
<h3 className="text-base font-medium text-[#1D1D1F] dark:text-white mb-2 leading-snug group-hover:text-[#0066CC] dark:group-hover:text-[#4DA6FF] transition-colors line-clamp-2">{article.title}</h3>
|
||||
<p className="text-xs text-[#86868B] dark:text-[#A1A1A6] line-clamp-2 mb-4">{article.excerpt}</p>
|
||||
@@ -122,6 +140,7 @@ export default async function NewsHub({ params }: { params: Promise<{ locale: st
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -114,6 +114,70 @@ export function organizationSchema(opts?: { logoUrl?: string; sameAs?: string[]
|
||||
};
|
||||
}
|
||||
|
||||
export function localBusinessSchema(opts?: { logoUrl?: string; sameAs?: string[] }) {
|
||||
const base = baseUrl();
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "LocalBusiness",
|
||||
"@id": `${base}/#local-business`,
|
||||
name: "FLUX Srl",
|
||||
legalName: "FLUX Srl",
|
||||
url: base,
|
||||
logo: opts?.logoUrl ? absoluteUrl(opts.logoUrl) : `${base}/flux-logo.png`,
|
||||
description:
|
||||
"Manufacturer of solid-state Radio Frequency (RF), Microwave, and Infrared industrial equipment since 1978.",
|
||||
foundingDate: "1978",
|
||||
founder: { "@type": "Person", name: "Patrizio Grando" },
|
||||
address: {
|
||||
"@type": "PostalAddress",
|
||||
streetAddress: "Via Benedetto Marcello 32",
|
||||
addressLocality: "Romano d'Ezzelino",
|
||||
addressRegion: "Vicenza",
|
||||
postalCode: "36060",
|
||||
addressCountry: "IT",
|
||||
},
|
||||
geo: {
|
||||
"@type": "GeoCoordinates",
|
||||
latitude: 45.7836,
|
||||
longitude: 11.7677,
|
||||
},
|
||||
telephone: "+39 0424 287 492",
|
||||
email: "info@rf-flux.com",
|
||||
openingHoursSpecification: {
|
||||
"@type": "OpeningHoursSpecification",
|
||||
dayOfWeek: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
|
||||
opens: "08:00",
|
||||
closes: "17:00",
|
||||
},
|
||||
areaServed: { "@type": "Place", name: "Worldwide" },
|
||||
...(opts?.sameAs ? { sameAs: opts.sameAs } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function collectionPageSchema(opts: {
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
items: { name: string; url: string; position: number }[];
|
||||
}) {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "CollectionPage",
|
||||
name: opts.name,
|
||||
description: opts.description,
|
||||
url: opts.url,
|
||||
mainEntity: {
|
||||
"@type": "ItemList",
|
||||
itemListElement: opts.items.map((item) => ({
|
||||
"@type": "ListItem",
|
||||
position: item.position,
|
||||
url: item.url,
|
||||
name: item.name,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function websiteSchema() {
|
||||
const base = baseUrl();
|
||||
return {
|
||||
|
||||
+6
-2
@@ -37,7 +37,9 @@ export async function proxy(request: NextRequest) {
|
||||
if (isPublicHQRoute) {
|
||||
return NextResponse.redirect(new URL('/hq-command/dashboard', request.url));
|
||||
}
|
||||
return NextResponse.next(); // Pasa al CMS tranquilamente
|
||||
const authedRes = NextResponse.next();
|
||||
authedRes.headers.set('X-Robots-Tag', 'noindex, nofollow');
|
||||
return authedRes;
|
||||
} catch (error) {
|
||||
// Pase falso o expirado
|
||||
if (!isPublicHQRoute) {
|
||||
@@ -45,7 +47,9 @@ export async function proxy(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return NextResponse.next(); // Pasa a login/setup si no hay cookie
|
||||
const hqRes = NextResponse.next();
|
||||
hqRes.headers.set('X-Robots-Tag', 'noindex, nofollow');
|
||||
return hqRes;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user