feat(seo): LocalBusiness + CollectionPage structured data schemas

- Add localBusinessSchema() with geo coords, phone, opening hours for
  Google Local Pack and Knowledge Panel visibility
- Add collectionPageSchema() with ItemList for article listing pages
- Inject LocalBusiness alongside Organization+WebSite in root layout
- Inject CollectionPage in /news hub page with article items

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 18:06:12 -05:00
parent 8d80cbbc27
commit 7ec99734c5
3 changed files with 85 additions and 2 deletions
+5 -1
View File
@@ -20,7 +20,7 @@ export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale })); return routing.locales.map((locale) => ({ locale }));
} }
import { getBranding, getSocialLinks } from '@/lib/siteSettings'; 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'; import JsonLd from '@/components/seo/JsonLd';
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
@@ -154,6 +154,10 @@ export default async function RootLayout({
logoUrl: branding.logoUrl, logoUrl: branding.logoUrl,
sameAs: sameAs.length ? sameAs : undefined, sameAs: sameAs.length ? sameAs : undefined,
}), }),
localBusinessSchema({
logoUrl: branding.logoUrl,
sameAs: sameAs.length ? sameAs : undefined,
}),
websiteSchema(), websiteSchema(),
]} ]}
/> />
+16 -1
View File
@@ -7,7 +7,8 @@ import BreathingField from "@/components/visuals/BreathingField";
import { getLocalizedData } from "@/lib/i18nHelper"; import { getLocalizedData } from "@/lib/i18nHelper";
import { getTranslations, setRequestLocale } from "next-intl/server"; 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. // ISR: revalidate every 60s — see /[locale]/page.tsx for the full rationale.
export const revalidate = 60; 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 heroArticle = articles.length > 0 ? articles[0] : null;
const gridArticles = articles.length > 1 ? articles.slice(1) : []; 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 ( return (
<main className="relative min-h-screen pt-32 pb-24"> <main className="relative min-h-screen pt-32 pb-24">
{collectionSchema && <JsonLd data={collectionSchema} />}
<BreathingField /> <BreathingField />
<div className="relative z-10 max-w-7xl mx-auto px-6"> <div className="relative z-10 max-w-7xl mx-auto px-6">
+64
View File
@@ -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() { export function websiteSchema() {
const base = baseUrl(); const base = baseUrl();
return { return {