seo: dynamic sitemap + robots + per-page metadata + JSON-LD
Brings the site up to enterprise SEO standards. Google now gets a complete machine-readable map of the content, with multilingual hreflang tags, structured data for the knowledge panel, and rich Open Graph cards on LinkedIn / WhatsApp / Twitter. NEW - src/app/sitemap.ts: dynamic sitemap.xml from Prisma. Emits 5 locales x every active application + every active news article, with hreflang alternates linking each translation. Hourly revalidation. - src/app/robots.ts: robots.txt blocks /hq-command/, /api/, /parts (B2B auth-gated), points crawlers at the sitemap. - src/lib/seo.ts: helpers for canonical URLs, hreflang alternates, and JSON-LD schemas (Organization, WebSite, Article, Product, BreadcrumbList). - src/components/seo/JsonLd.tsx: server component that emits one application/ld+json script tag per page. PER-PAGE generateMetadata - Home: localized titles + descriptions in EN/IT/VEC/ES/DE - News hub: title built from translations, hreflang tags - News article: title/description from DB, OG image = cover, type=article, publishedTime + modifiedTime for date freshness signals - Applications: title/description from DB, type=product, hero image - Heritage: localized title/description JSON-LD STRUCTURED DATA - Site-wide (in root layout): Organization (with HQ address, founder, contact, social profiles) + WebSite — drives Google knowledge panel - Article pages: Article schema with publisher/datePublished/dateModified — required for Google News / Discover eligibility - Application pages: Product schema (FLUX brand, RF Industrial category) + BreadcrumbList — drives rich-snippet breadcrumb in search results NOTES - Open Graph metadataBase set from NEXT_PUBLIC_APP_URL so absolute URLs for OG images are correct (LinkedIn previews require absolute paths) - All pages have canonical URLs to prevent duplicate-content penalties - /parts already has noindex meta (B2B portal) — also blocked in robots - No DB schema changes. Pure additions to /src/lib and /src/app.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// ISR: revalidates every 60s + on-demand via revalidatePath after CMS uploads.
|
||||
export const revalidate = 60;
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
@@ -8,8 +9,14 @@ import { prisma } from "@/lib/prisma";
|
||||
import { notFound } from "next/navigation";
|
||||
import ApplicationClient from "./ApplicationClient";
|
||||
|
||||
// 🔥 IMPORTAMOS LA TUBERÍA MÁGICA
|
||||
import { getLocalizedData } from "@/lib/i18nHelper";
|
||||
import {
|
||||
buildPageMetadata,
|
||||
productSchema,
|
||||
breadcrumbSchema,
|
||||
baseUrl,
|
||||
} from "@/lib/seo";
|
||||
import JsonLd from "@/components/seo/JsonLd";
|
||||
|
||||
// --- FUNCIÓN ORIGINAL PARA LEER IMÁGENES LOCALES ---
|
||||
function getApplicationImages(slug: string) {
|
||||
@@ -29,6 +36,39 @@ function getApplicationImages(slug: string) {
|
||||
return { heroImage, blueprints, machines };
|
||||
}
|
||||
|
||||
// ── Per-page metadata (Open Graph, Twitter, hreflang, canonical) ───────────
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string; locale: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { slug, locale } = await params;
|
||||
|
||||
try {
|
||||
const raw = await prisma.application.findUnique({ where: { slug } });
|
||||
if (!raw) {
|
||||
return {
|
||||
title: "Application not found | FLUX",
|
||||
robots: { index: false, follow: false },
|
||||
};
|
||||
}
|
||||
|
||||
const data = getLocalizedData(raw, locale);
|
||||
const heroImage = getApplicationImages(slug).heroImage;
|
||||
|
||||
return buildPageMetadata({
|
||||
locale,
|
||||
pathWithoutLocale: `applications/${slug}`,
|
||||
title: `${data.title} — RF Industrial Solutions | FLUX`,
|
||||
description: data.shortDescription || data.subtitle,
|
||||
ogImageUrl: heroImage || undefined,
|
||||
type: "product",
|
||||
});
|
||||
} catch {
|
||||
return { title: "FLUX | Energy, Directed." };
|
||||
}
|
||||
}
|
||||
|
||||
// GENERACIÓN DE RUTAS ESTÁTICAS DESDE LA BD
|
||||
export async function generateStaticParams() {
|
||||
// In production Docker build, DB is not available.
|
||||
@@ -85,6 +125,26 @@ export default async function ApplicationPage({ params }: { params: Promise<{ sl
|
||||
// 3. Leemos las imágenes de la carpeta original
|
||||
const images = getApplicationImages(slug);
|
||||
|
||||
// Pasamos TODO al componente cliente interactivo (que ya viene traducido)
|
||||
return <ApplicationClient data={data} realCases={realCases} images={images} />;
|
||||
const url = `${baseUrl()}/${locale}/applications/${slug}`;
|
||||
const jsonLd = [
|
||||
productSchema({
|
||||
name: data.title,
|
||||
description: data.shortDescription || data.subtitle,
|
||||
imageUrl: images.heroImage || undefined,
|
||||
category: data.category,
|
||||
url,
|
||||
}),
|
||||
breadcrumbSchema([
|
||||
{ name: "Home", url: `${baseUrl()}/${locale}` },
|
||||
{ name: "Applications", url: `${baseUrl()}/${locale}#applications-deep` },
|
||||
{ name: data.title, url },
|
||||
]),
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<JsonLd data={jsonLd} />
|
||||
<ApplicationClient data={data} realCases={realCases} images={images} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user