El marcado semántico bajo el estándar Schema.org actúa como el puente fundamental entre el código HTML no estructurado y los algoritmos de inteligencia artificial y grafos de conocimiento de los buscadores (Google Knowledge Graph). Los datos estructurados permiten a los rastreadores identificar con precisión entidades de negocio, autores, ofertas de catálogo, rutas de navegación y contenido editorial, originando resultados visuales enriquecidos (Rich Results).
Históricamente, en el ecosistema de React estos esquemas se implementaban mediante bibliotecas de cliente (react-helmet, next-seo) o manipulación imperativa del DOM en хуках useEffect. Este patrón acarrea tres defectos arquitectónicos graves:
JSON.stringify() sin escape de caracteres HTML reservados permite a atacantes inyectar código JavaScript ejecutable mediante el cierre anticipado de la etiqueta </script>.<script type="application/ld+json"> aisladas en diferentes componentes. Como consecuencia, el buscador percibe fragmentos dispersos en lugar de un grafo de conocimiento coherente que vincule producto, vendedor, marca y sitio web.La arquitectura React Server Components (RSC) en Next.js 15 erradica estas deficiencias mediante el paradigma Zero Client-Side JS. La formulación del grafo semántico Schema.org se procesa exclusivamente en el entorno aislado del servidor utilizando la biblioteca de tipado estricto schema-dts.
El navegador del cliente recibe únicamente un bloque HTML estático y serializado que no exige código JavaScript para su hidratación ni impone trabajo al hilo principal.
En lugar de dispersar múltiples bloques JSON-LD independientes, el estándar semántico actual prescribe unificar las entidades en un grafo integrado mediante la propiedad @graph.
Cada nodo del grafo posee un identificador global unívoco (@id), formulado como una URI canónica acompañada de un ancla de fragmento (hash). Los nodos se referencian entre sí a través de estos identificadores, formando un grafo dirigido (Directed Graph) que modela la ontología de la aplicación.
+-----------------------------------------------------------------------------------------+
| GRAFO SEMÁNTICO @graph |
+-----------------------------------------------------------------------------------------+
[Organization] (https://agenticlab.dev/#organization)
▲
│ publisher / brand
│
[WebSite] (https://agenticlab.dev/#website)
▲
│ isPartOf
│
[WebPage] (https://agenticlab.dev/products/server-v5#webpage)
│
├─────────────────────────────┬─────────────────────────────┐
│ breadcrumb │ mainEntity │ primaryImageOfPage
▼ ▼ ▼
[BreadcrumbList] [Product] [ImageObject]
(#breadcrumb) (#product) (#main-image)
│
├─────────────────────────────┐
│ offers │ aggregateRating
▼ ▼
[AggregateOffer] [AggregateRating]
(#offers) (#rating)Conceptos esenciales de la arquitectura de grafos semánticos:
Product + AggregateOffer + MerchantReturnPolicy + ShippingDetails para activar Rich Snippets y sincronizar con Google Merchant Center.TechArticle con perfiles de autor Person (con referencias sameAs hacia GitHub, LinkedIn y ORCID) y la entidad Organization para validar los criterios E-E-A-T.Organization con perfiles sociales oficiales, identificación fiscal (taxID), soporte técnico y geolocalización.Course, temarios, recursos multimedia VideoObject y bloques interactivos FAQPage para carruseles educativos en Google.La inserción de JSON-LD mediante dangerouslySetInnerHTML en React conlleva riesgos de Cross-Site Scripting (XSS). Si un texto introducido por usuarios contiene </script><script>alert(1)</script>, el analizador HTML del navegador cerrará prematuramente el bloque de datos y ejecutará el script malicioso.
Para contrarrestar esta vulnerabilidad, los caracteres reservados <, >, & se sustituyen por sus secuencias de escape seguras en Unicode \u003c, \u003e, \u0026.
import type { Graph, Thing, WithContext } from 'schema-dts';
interface JsonLdGraphProps {
/** Nodos pertenecientes al grafo semántico de Schema.org */
nodes: Thing[];
}
/**
* Sanitiza la cadena JSON reemplazando caracteres HTML peligrosos por escapes Unicode.
* Previene ataques XSS por cierre prematuro de la etiqueta </script>.
*/
function sanitizeJsonLd(data: WithContext<Graph>): string {
return JSON.stringify(data)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026');
}
/**
* React Server Component para la inyección segura de Schema.org con cero coste en cliente.
* Zero Client-Side JS: ¡Añade 0 bytes al bundle de JavaScript del navegador!
*/
export function JsonLdGraph({ nodes }: JsonLdGraphProps) {
// Construcción del grafo raíz de Linked Data
const graph: WithContext<Graph> = {
'@context': 'https://schema.org',
'@graph': nodes,
};
const safeJson = sanitizeJsonLd(graph);
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: safeJson }}
/>
);
}Implementamos un módulo para generar entidades globales con verificación estricta de tipos:
import type {
Organization,
WebSite,
BreadcrumbList,
Thing,
} from 'schema-dts';
const SITE_ORIGIN = 'https://agenticlab.dev';
export const ORGANIZATION_ID = `${SITE_ORIGIN}/#organization`;
export const WEBSITE_ID = `${SITE_ORIGIN}/#website`;
/**
* Genera el nodo raíz de Organización (Organization)
*/
export function createOrganizationEntity(): Organization {
return {
'@type': 'Organization',
'@id': ORGANIZATION_ID,
name: 'Fullstack Agentic Lab Academy',
url: SITE_ORIGIN,
logo: {
'@type': 'ImageObject',
'@id': `${SITE_ORIGIN}/#logo`,
url: `${SITE_ORIGIN}/assets/logo.png`,
caption: 'Logotipo de Fullstack Agentic Lab',
},
sameAs: [
'https://github.com/agenticlab-dev',
'https://twitter.com/agenticlab_dev',
'https://linkedin.com/company/agenticlab',
],
contactPoint: {
'@type': 'ContactPoint',
contactType: 'technical support',
email: '[email protected]',
availableLanguage: ['Spanish', 'English', 'Ukrainian'],
},
};
}
/**
* Genera el nodo de Sitio Web (WebSite) enlazado a la Organización
*/
export function createWebSiteEntity(): WebSite {
return {
'@type': 'WebSite',
'@id': WEBSITE_ID,
url: SITE_ORIGIN,
name: 'Fullstack Agentic Lab',
publisher: {
'@id': ORGANIZATION_ID,
},
potentialAction: {
'@type': 'SearchAction',
target: `${SITE_ORIGIN}/search?q={search_term_string}`,
'query-input': 'required name=search_term_string',
},
};
}
interface BreadcrumbItemInput {
name: string;
path: string;
}
/**
* Genera el nodo de migas de pan (BreadcrumbList)
*/
export function createBreadcrumbsEntity(items: BreadcrumbItemInput[]): BreadcrumbList {
return {
'@type': 'BreadcrumbList',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: `${SITE_ORIGIN}${item.path}`,
})),
};
}Ejemplo completo de un componente de servidor para la ficha de producto que integra todas las entidades en @graph:
import { notFound } from 'next/navigation';
import type { Metadata } from 'next';
import type { Product, AggregateOffer, AggregateRating } from 'schema-dts';
import { JsonLdGraph } from '@/components/seo/JsonLdGraph';
import {
createOrganizationEntity,
createWebSiteEntity,
createBreadcrumbsEntity,
ORGANIZATION_ID,
} from '@/lib/seo/schema-factory';
interface ProductPageProps {
params: Promise<{ slug: string }>;
}
interface ProductData {
id: string;
slug: string;
name: string;
description: string;
sku: string;
gtin13: string;
currency: string;
price: number;
ratingValue: number;
reviewCount: number;
imageUrl: string;
}
async function fetchProductData(slug: string): Promise<ProductData | null> {
if (slug !== 'enterprise-server-license') return null;
return {
id: 'prod_98412',
slug: 'enterprise-server-license',
name: 'Licencia Fullstack Agentic Lab Enterprise Core Server',
description: 'Runtime de alto rendimiento para servidores con soporte para Next.js 15 y PPR.',
sku: 'AG-SRV-2026',
gtin13: '4601234567890',
currency: 'EUR',
price: 1850,
ratingValue: 4.95,
reviewCount: 48,
imageUrl: 'https://agenticlab.dev/images/products/server.png',
};
}
export async function generateMetadata({ params }: ProductPageProps): Promise<Metadata> {
const { slug } = await params;
const product = await fetchProductData(slug);
if (!product) return { title: 'Producto no encontrado' };
return {
title: `${product.name} — Fullstack Agentic Lab`,
description: product.description,
alternates: {
canonical: `https://agenticlab.dev/products/${product.slug}`,
},
};
}
export default async function ProductDetailsPage({ params }: ProductPageProps) {
const { slug } = await params;
const product = await fetchProductData(slug);
if (!product) {
notFound();
}
const pageCanonicalUrl = `https://agenticlab.dev/products/${product.slug}`;
const productId = `${pageCanonicalUrl}#product`;
// 1. Nodo Producto (Product)
const productEntity: Product = {
'@type': 'Product',
'@id': productId,
name: product.name,
description: product.description,
sku: product.sku,
gtin13: product.gtin13,
image: product.imageUrl,
brand: {
'@id': ORGANIZATION_ID, // Vínculo con la Organización global
},
offers: {
'@type': 'AggregateOffer',
priceCurrency: product.currency,
lowPrice: product.price,
highPrice: product.price * 1.2,
offerCount: 1,
offers: [
{
'@type': 'Offer',
url: pageCanonicalUrl,
price: product.price,
priceCurrency: product.currency,
availability: 'https://schema.org/InStock',
itemCondition: 'https://schema.org/NewCondition',
seller: {
'@id': ORGANIZATION_ID,
},
},
],
} as AggregateOffer,
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: product.ratingValue,
reviewCount: product.reviewCount,
bestRating: 5,
worstRating: 1,
} as AggregateRating,
};
// 2. Nodo Migas de Pan (BreadcrumbList)
const breadcrumbsEntity = createBreadcrumbsEntity([
{ name: 'Inicio', path: '/' },
{ name: 'Catálogo', path: '/products' },
{ name: product.name, path: `/products/${product.slug}` },
]);
// 3. Unificación en una ontología enlazada
const schemaNodes = [
createOrganizationEntity(),
createWebSiteEntity(),
breadcrumbsEntity,
productEntity,
];
return (
<>
{/* Grafo semántico inyectado mediante RSC con Cero JS en el cliente */}
<JsonLdGraph nodes={schemaNodes} />
<main className="max-w-5xl mx-auto px-4 py-12 space-y-6">
<h1 className="text-3xl font-extrabold text-slate-100">{product.name}</h1>
<p className="text-lg text-slate-300">{product.description}</p>
<div className="text-2xl font-bold text-accent-cyan">
{product.price.toLocaleString('es-ES')} {product.currency}
</div>
</main>
</>
);
}La comprobación de la integridad del grafo semántico se realiza en dos fases:
En el pipeline de CI/CD, aseguramos la integridad mediante pruebas unitarias automatizadas:
import { describe, it, expect } from 'vitest';
import type { Graph, Product } from 'schema-dts';
describe('Schema.org Graph Integrity', () => {
it('should generate valid interconnected @graph with deterministic @id URIs', () => {
const rawJsonLd = `{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
"@id": "https://agenticlab.dev/#organization",
"name": "Fullstack Agentic Lab"
},
{
"@type": "Product",
"@id": "https://agenticlab.dev/products/server#product",
"name": "Enterprise Server",
"brand": { "@id": "https://agenticlab.dev/#organization" }
}
]
}`;
const parsed = JSON.parse(rawJsonLd) as Graph;
const nodes = parsed['@graph'];
expect(nodes).toBeDefined();
expect(nodes).toHaveLength(2);
const product = nodes?.find((n) => n['@type'] === 'Product') as Product;
expect(product).toBeDefined();
expect(product.brand).toEqual({ '@id': 'https://agenticlab.dev/#organization' });
});
});Si un campo de texto dinámico procedente del usuario contiene </script>, el analizador del navegador interpreta que el bloque de datos ha terminado y ejecuta el texto siguiente como JavaScript activo.
// ❌ ANTIPATRÓN: ¡Vulnerabilidad crítica de XSS!
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>Solución: Escale siempre los caracteres <, >, & a secuencias de escape Unicode mediante sanitizeJsonLd.
Insertar etiquetas <script type="application/ld+json"> separadas en cabecera, pie y cuerpo de página. El rastreador interpreta islas aisladas de datos. Si los nombres o enlaces discrepan levemente, Google invalida la coherencia semántica y suprime los Rich Results.
Solución: Agrupe todas las entidades en un único arreglo @graph con URIs unívocas en @id.
Definir la generación de Schema.org en componentes de cliente obliga a Next.js a empaquetar voluminosos diccionarios de tipos en el bundle de JavaScript. El navegador gasta recursos de CPU serializando el script y los rastreadores de primera fase (sin ejecución JS) pierden los metadatos.
Solución: Renderice JsonLdGraph exclusivamente en React Server Components (RSC).
Declarar en JSON-LD precios rebajados o valoraciones de 5 estrellas que no figuran en la interfaz visible. Google califica esta práctica como manipulación fraudulenta, imponiendo una sanción manual (Structured Data Manual Action Penalty) que revoca la presencia en Rich Results de todo el dominio.
Solución: Alimente las entidades de Schema.org con los mismos datos procedentes del servidor que se emplean para pintar el JSX.
El Server Component recupera datos fuertemente tipados desde la base de datos o microservicio.
Se crean los objetos de Schema.org con validación estricta de tipos de TypeScript sin recurrir a any.
Los nodos (Organization, WebSite, Breadcrumbs, Product) se enlazan mediante referencias deterministas @id.
La cadena JSON se desinfecta contra XSS (< por \u003c) y se inyecta en una etiqueta estática de script.
Pruebas automatizadas analizan el HTML resultante para asegurar la validez antes de integrar cambios en main.