Saltar al contenido

Datos estructurados: JSON-LD con set:html

Hablarle al buscador en su propio vocabulario: qué son los datos estructurados y por qué JSON-LD es el formato preferido, cómo inyectar el bloque con la directiva set:html sin que Astro lo escape, y cómo modelar artículos, productos y migas de pan con schema.org derivando todo del frontmatter, sin mentirle nunca a la máquina.

Qué son los datos estructurados y por qué JSON-LD

Los datos estructurados son metadatos que describen el significado de tu contenido usando un vocabulario estándar, schema.org, entendido por todos los buscadores. Existen tres sintaxis para incrustarlos —microdata y RDFa, que se entretejen con el HTML visible, y JSON-LD, que vive aparte en un bloque de script—, y hoy JSON-LD es la recomendada, por una razón de diseño: desacopla el dato semántico del marcado de presentación. No tienes que ensuciar tus etiquetas con atributos itemprop; describes la página en un objeto JSON independiente que puedes generar, versionar y razonar por separado.

Ese objeto se declara en dos campos raíz: @context, que casi siempre es la URL de schema.org, y @type, que nombra qué representas —un Article, un Product, una BreadcrumbList—. A partir de ahí, cada tipo define sus propiedades: un artículo tiene headline, author y datePublished; un producto tiene offers con price y availability. El premio no es un puesto más alto de forma directa —los datos estructurados no son un factor de ranking por sí mismos— sino la elegibilidad para resultados enriquecidos, que suben la tasa de clics porque tu enlace ocupa más espacio y comunica más.

Más allá de los tipos por página, dos esquemas describen el sitio entero y conviene emitirlos una vez desde el layout: Organization, que declara quién publica —nombre, logotipo, perfiles sociales—, y WebSite, que puede incluir un potentialAction de tipo SearchAction para que el buscador ofrezca una caja de búsqueda de tu sitio en los propios resultados. Son la identidad de fondo sobre la que se apoyan los esquemas de cada página, y por eso viven en el marco compartido y no en cada ruta.

set:html: inyectar el bloque sin que Astro lo escape

Aquí aparece un detalle técnico que, ignorado, rompe todo el mecanismo. Si intentas meter el JSON con una expresión normal dentro del script, Astro trata su contenido como texto y escapa los caracteres especiales: las comillas se convierten en entidades HTML y el bloque deja de ser JSON válido, así que el buscador lo descarta. La directiva set:html le dice a Astro que inserte esa cadena tal cual, sin escaparla, que es justo lo que el rastreador necesita para parsearla.

ASTRO
---// src/components/JsonLd.astrointerface Props {  data: Record<string, unknown>;}const { data } = Astro.props;---<script type="application/ld+json" set:html={JSON.stringify(data)} />

El componente es minúsculo pero hace una cosa bien: recibe un objeto, lo serializa con JSON.stringify y lo inyecta crudo. Que set:html sea explícito no es un capricho; es Astro obligándote a declarar que confías en ese contenido, porque insertar HTML sin escapar es justo la puerta por la que entran los ataques de inyección. Aquí es seguro porque el objeto lo construyes tú a partir de datos tipados, no de texto que teclee un usuario; esa distinción es la que debes tener siempre presente al usar la directiva.

Esquemas útiles: Article, Product, BreadcrumbList

El vocabulario de schema.org es enorme, pero tres tipos cubren la mayoría de los casos. Para un artículo de blog, BlogPosting —una especialización de Article— describe el título, el autor, las fechas y la imagen, todo derivado del frontmatter que ya tienes.

ASTRO
---const articuloLd = {  '@context': 'https://schema.org',  '@type': 'BlogPosting',  headline: post.data.title,  description: post.data.description,  datePublished: post.data.fecha.toISOString(),  author: { '@type': 'Person', name: post.data.autor },  image: new URL(`/og${Astro.url.pathname}.png`, Astro.site).href,};---<JsonLd data={articuloLd} />

Para una ficha de producto, Product con un Offer anidado comunica precio, moneda y disponibilidad; si tienes valoraciones visibles en la página, un aggregateRating habilita las estrellas en el resultado.

ASTRO
---const productoLd = {  '@context': 'https://schema.org',  '@type': 'Product',  name: producto.nombre,  image: new URL(producto.imagen, Astro.site).href,  offers: {    '@type': 'Offer',    price: producto.precio,    priceCurrency: 'EUR',    availability: 'https://schema.org/InStock',  },};---

Y la BreadcrumbList describe la ruta de navegación que lleva a la página, para que el buscador la muestre como migas de pan en vez de una URL cruda. Se modela como una lista ordenada de ListItem, cada uno con su posición y su enlace absoluto.

ASTRO
---const migasLd = {  '@context': 'https://schema.org',  '@type': 'BreadcrumbList',  itemListElement: migas.map((m, i) => ({    '@type': 'ListItem',    position: i + 1,    name: m.nombre,    item: new URL(m.ruta, Astro.site).href,  })),};---

En páginas complejas —un artículo con sus migas y su editor— no necesitas tres scripts sueltos: el patrón @graph reúne varias entidades en un solo bloque, enlazadas entre sí por identificador, de modo que el buscador las lee como un grafo coherente y no como fragmentos aislados.

ASTRO
---const grafo = {  '@context': 'https://schema.org',  '@graph': [articuloLd, migasLd, organizacionLd],};---<JsonLd data={grafo} />

Reunirlas en un grafo no es sólo aseo: le permite al buscador entender que este artículo pertenece a esta organización y se alcanza por estas migas, relaciones que se pierden cuando cada esquema viaja por su cuenta sin referirse a los demás.