Datos estructurados en Next.js, generados desde un solo grafo
Casi todo el JSON-LD se publica como cuatro bloques inconexos que redeclaran la organización. Un grafo conectado con referencias @id estables es menos código, y es el que un rastreador puede resolver.
Los datos estructurados son la parte del SEO técnico con mayor distancia entre
lo que los equipos publican y lo que la especificación premia. La
implementación habitual es un componente por tipo de página, cada uno emitiendo
su propio <script type="application/ld+json">, y cada uno redeclarando la
organización, el sitio y el logotipo.
Valida. También le dice a un rastreador que tu sitio contiene cuatro organizaciones sin relación que casualmente comparten nombre.
Un grafo, no cuatro bloques
La clave @graph existe para esto. Contiene una lista de nodos, cada uno con
un @id estable, y los nodos se refieren entre sí por ese @id en lugar de
repetir su contenido.
{
"@context": "https://schema.org",
"@graph": [
{ "@type": "Organization", "@id": "https://example.com/#org", "name": "Example" },
{ "@type": "WebSite", "@id": "https://example.com/#site", "publisher": { "@id": "https://example.com/#org" } },
{ "@type": "WebPage", "@id": "https://example.com/blog/post#page", "isPartOf": { "@id": "https://example.com/#site" } },
{ "@type": "Article", "@id": "https://example.com/blog/post#article", "mainEntityOfPage": { "@id": "https://example.com/blog/post#page" } }
]
}Cuatro nodos, una organización, y cada relación dicha una sola vez. Los valores
de @id son el mecanismo entero: son lo que permite a un rastreador fusionar
el nodo que ve aquí con el mismo nodo en cualquier otra página.
Dos reglas los hacen funcionar:
- URLs absolutas, siempre.
#orgpor sí solo no es un identificador, es un fragmento que significa algo distinto en cada página. - Estables para siempre. Un
@idque cambia entre despliegues -porque se construyó desde un slug que alguien editó, o desde el id de una fila- tira a la basura todo lo que el rastreador había acumulado sobre él.
Generarlo en vez de escribirlo
El JSON-LD escrito a mano se desincroniza porque nada lo obliga a coincidir con
la página. El título cambia en el export metadata y el headline de los datos
estructurados conserva el viejo, porque son dos cadenas en dos ficheros.
La solución es un helper tipado, llamado desde donde ya se conoce la respuesta:
// lib/jsonld.ts
export function articleNode({ url, title, description, published, modified }: ArticleInput) {
return {
'@type': 'Article',
'@id': `${url}#article`,
headline: title,
description,
datePublished: published,
dateModified: modified ?? published,
mainEntityOfPage: { '@id': `${url}#page` },
publisher: { '@id': `${SITE}#org` },
};
}// app/blog/[slug]/page.tsx
<JsonLd data={graph(
webPageNode({ url, title: doc.title, description: doc.description }),
articleNode({ url, title: doc.title, description: doc.description,
published: doc.date, modified: doc.updated }),
breadcrumbNode(trail),
)} />Ahora los datos estructurados no pueden contradecir a la página, porque leen las mismas variables que la página renderiza. Es el mismo principio que canónicos y sitemaps que no pueden desincronizarse: una sola fuente de verdad, generada en los dos extremos.
Dónde va dentro de la respuesta
En el HTML renderizado en servidor. Un <script> inyectado por un componente de
cliente después de la hidratación es marcado que un rastreador puede ejecutar o
no, y no hay razón para apostar: el dato se conoce en el servidor.
export function JsonLd({ data }: { data: object }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
);
}dangerouslySetInnerHTML es lo correcto aquí y no un atajo: React escapa los
hijos de texto, lo que corrompe el JSON. Lo que no debes hacer es interpolar
texto controlado por el usuario sin escapar <: un comentario que contenga
</script> cerraría la etiqueta y ejecutaría lo que venga después.
Los tipos que valen la pena y los que son teatro
Los datos estructurados se ganan su sitio cuando mapean a un resultado enriquecido o resuelven una entidad. Más allá de eso son marcado que nadie lee.
Valen la pena:
OrganizationyWebSite, una vez, referenciados desde todas partes.BreadcrumbList, que aparece directamente en los resultados.Articleen artículos, yProductcon datos reales deOfferen productos.FAQPage, pero solo donde las preguntas estén visibles en la página. Marcar preguntas que no se renderizan es una violación de las directrices, no un truco.LocalBusinesso un subtipo donde haya una dirección real. Ojo: los subtipos exigenaddress- unProfessionalServicesin ella es inválido, y es una forma frecuente de publicar un grafo roto creyendo que es más rico.
No valen gran cosa:
Reviewescrita por ti sobre ti. El marcado de reseñas propias lleva años sin ser elegible para resultados enriquecidos.HowToyRecipeen páginas que no son ninguna de las dos.speakable,SiteNavigationElementy el resto de la cola larga que ninguna superficie consume.
Comprobarlo
Dos herramientas, y responden preguntas distintas. La Prueba de resultados
enriquecidos te dice si una página es elegible para un tipo de resultado
concreto. El Schema Markup Validator te dice si tu grafo está bien formado: es
el que detecta una referencia @id colgando, cosa que la primera ignora tan
tranquila.
Ejecuta las dos sobre el HTML renderizado de la página desplegada, no sobre un fragmento pegado. El fragmento es lo que escribiste; el HTML renderizado es lo que publicaste, y en sitios con una capa de caché delante no son fiablemente lo mismo.
