Allá por 2021, un cliente de viajes le entregó a Seahawk un brief de migración que me hizo el corazón un poco pesado. Noventa y uno mil páginas de destinos y hoteles. Cada una necesitaba schema markup válido, específico y probado -- no el tipo genérico WebPage de talla única que la mayoría de plugins ponen sin pensarlo. El cliente ya había probado dos plugins de WordPress de "schema automático". Ambos habían producido JSON-LD técnicamente válido que era también, en todo sentido que importa, inútil -- nombres genéricos, sin entidades anidadas, precios faltantes, agregados de reseñas apuntando a lo incorrecto. La Prueba de Resultados Enriquecidos de Google estaba cortésmente confundida.tested schema markup -- not the lazy one-size-fits-all WebPage type that most plugins slap on and call it a day. The client had already tried two "automatic schema" WordPress plugins. Both had produced technically valid JSON-LD that was also, in every meaningful sense, useless -- generic names, no nested entities, prices missing, review aggregates pointing at the wrong thing. Google's Rich Results Test was politely confused.
Punto clave: El esquema para 91,000 páginas es un problema de arquitectura, no de plugin: genéralo desde la capa de datos en tiempo de compilación y valídalo en el pipeline.Schema for 91,000 pages is an architecture problem, not a plugin problem: generate it from the data layer at build time and validate it in the pipeline.
Ese proyecto me enseñó más sobre schema a escala que los ocho años anteriores combinados. Entonces aquí está lo que realmente sé.
---
Por Qué "Solo Instala un Plugin" Se Quiebra a Escala
Mira, no estoy aquí para criticar a Yoast o Rank Math. Para un sitio de 40 páginas genuinamente están bien. Pero en algún punto alrededor de la marca de 500 páginas, el schema generado por plugins comienza a ceder bajo sus propias suposiciones.
El problema central es que los plugins están construidos alrededor de plantillas de página, no modelos de datos. Leen el título del post, quizá uno o dos campos personalizados, y construyen un blob de schema. Cuando tu sitio tiene 91,000 páginas en seis tipos de contenido -- hoteles, destinos, tours, reseñas, FAQs y perfiles de autor -- una sola configuración de plugin no puede expresar esa variedad sin un trabajo de override manual enorme. Y si estás haciendo overrides manuales a esa escala, ya perdiste.page templates, not data models. They read the post title, maybe a custom field or two, and construct a schema blob. When your site has 91,000 pages across six content types -- hotels, destinations, tours, reviews, FAQs, and author profiles -- a single plugin configuration cannot express that variety without enormous manual override work. And if you're doing manual overrides at that scale, you've already lost.
Aquí está lo importante: el markup de schema es fundamentalmente un problema de transformación de datos. Tienes datos estructurados en una base de datos; necesitas que se expresen como JSON-LD en una etiqueta <script>. Eso es todo. En el momento en que lo planteas así, la arquitectura correcta se vuelve mucho más clara.<script>tag. That's it. The moment you frame it that way, the right architecture becomes much clearer.
Los Tres Modos de Fallo Que Sigo Viendo
- Blobs de schema estáticos codificados en plantillas. Está bien hasta que el nombre del producto cambia, entonces tienes 12,000 páginas mintiendo a Google. hardcoded in templates. Fine until the product name changes, then you've got 12,000 pages lying to Google.
- Configuraciones de plugin que no pueden manejar lógica condicional -- como mostrar aggregateRating solo cuando hay reseñas de verdad, o diferente @type por categoría de post. that can't handle conditional logic -- like only showing
aggregateRatingwhen there are actually reviews, or different@typeper post category. - Archivos generados en batch, subidos una sola vez y nunca actualizados. He auditado sitios donde el schema tenía dieciocho meses de antigüedad. Los precios estaban mal. Las fechas de eventos ya habían pasado. uploaded once and never updated. I've audited sites where the schema was eighteen months stale. The prices were wrong. The event dates had passed.
---
Cómo JSON-LD Realmente Funciona a Escala
Antes de entrar en herramientas: un anclaje rápido. JSON-LD -- JSON for Linked Data -- es el formato de schema preferido de Google precisamente porque vive en un bloque <script>, separado de tu HTML. Eso significa que puedes generarlo en el lado del servidor, inyectarlo limpiamente, y actualizarlo sin tocar el markup. Esa separación es todo cuando estás lidiando con decenas de miles de páginas.JSON-LD -- JSON for Linked Data -- is Google's preferred schema format precisely because it lives in a<script>block, separate from your HTML. That means you can generate it server-side, inject it cleanly, and update it without touching markup. That separation is everything when you're dealing with tens of thousands of pages.
El vocabulario de Schema.org es vasto. La mayoría de la gente usa cerca del 1% de él. A escala necesitas ir más profundo -- Hotel, TouristDestination, LocalBusiness, Review, AggregateRating, objetos Offer anidados, BreadcrumbList. Cada tipo tiene propiedades requeridas y recomendadas, e la interpretación de Google de "recomendado" es básicamente "requerido si quieres el resultado enriquecido".Schema.org vocabulary is vast. Most people use about 1% of it. At scale you need to go deeper -- Hotel,TouristDestination,LocalBusiness,Review,AggregateRating, nested Offer objects,BreadcrumbList. Each type has required and recommended properties, and Google's interpretation of "recommended" is basically "required if you want the rich result."
La regla fundamental con la que trabajo: un @type primario por página, con tipos anidados según sea necesario. No apiles cinco valores @type esperando que uno funcione. Elige el tipo más específico que se ajuste, luego anida tipos de apoyo dentro de él.one primary `@type` per page, with nested types as needed.Don't stack five@type values hoping one sticks. Pick the most specific type that fits, then nest supporting types inside it.
---
La arquitectura que realmente usamos
Para el cliente de viajes, terminamos con un sistema de tres capas. No es elegante en el sentido de un diagrama de pizarra, pero funcionó.
Capa 1: Clases de esquema a nivel de plantilla (PHP)
Cada tipo de contenido obtuvo su propia clase PHP responsable de construir su array de schema. HotelSchemaBuilder, DestinationSchemaBuilder, TourSchemaBuilder -- ya te haces la idea. Cada clase extraía de campos personalizados de ACF Pro, datos de WooCommerce donde aplicaba, y algunos valores computados (como calcular aggregateRating desde un sistema de reseñas basado en CPT).HotelSchemaBuilder,DestinationSchemaBuilder,TourSchemaBuilder -- you get the idea. Each class pulled from ACF Pro custom fields, WooCommerce data where applicable, and a few computed values (like calculating aggregateRating from a CPT-based review system).
El resultado de cada clase era un array PHP simple. Sin JSON aún. Solo datos.
Esto importa porque significa que puedes hacer unit tests de la lógica de datos separada de la serialización. Desearía haberlo hecho desde el primer día en este proyecto. No lo hice. Eso nos costó alrededor de dos días de debugging en staging cuando ratingValue retornaba un string en lugar de un float y el validador de Google silenciosamente ignoraba todo el bloque aggregateRating.ratingValue was returning a string instead of a float and Google's validator was silently ignoring the whole aggregateRating block.
Capa 2: Un gestor de esquema centralizado
Una única clase SchemaManager, enganchada en wp_head, era responsable de:SchemaManager class, hooked into wp_head, was responsible for:
- Determinar qué clase de constructor invocar en función de la plantilla/tipo de publicación actual
- Fusionar entidades a nivel de sitio (el gráfico Organization, WebSite con SearchAction, BreadcrumbList)
Organizationgraph,WebSitewithSearchAction,BreadcrumbList) - Codificar el array final como JSON con JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE - Envolverlo en una etiqueta <script type="application/ld+json"> y enviarlo
<script type="application/ld+json">tag and echoing it
La lógica de breadcrumb fue la parte más complicada. Los destinos tenían una jerarquía de tres niveles: Región → País → Ciudad. Hacer que BreadcrumbList reflejara eso dinámicamente, sin hardcodear nada, significaba recorrer los antecesores del post en tiempo de renderizado. Lento, si no tienes cuidado. Cacheamos los arrays de breadcrumb por ID de post en un transient con TTL de 24 horas. Eso redujo la sobrecarga a algo negligible.BreadcrumbList to reflect that dynamically, without hardcoding anything, meant traversing post ancestors at render time. Slow, if you're not careful. We cached the breadcrumb arrays per post ID in a transient with a 24-hour TTL. That brought the overhead down to negligible.
Capa 3: Validación y Monitoreo
Generar schema es el primer paso. Saber cuándo se rompe es el segundo, y la mayoría de equipos lo saltan completamente.
Configuramos una propiedad de Google Search Console y observamos el reporte de Resultados Enriquecidos semanalmente. Pero eso es reactivo -- GSC te cuenta sobre errores después de que Google ha rastreado la página. Para verificaciones proactivas, ejecutamos SchemaApp en un rastreo de las 2,000 páginas principales mensualmente. Expone errores a nivel de propiedad que el reporte de GSC oscurece.after Google has crawled the page. For proactive checks, we ran SchemaApp on a crawl of the top 2,000 pages monthly. It surfaces property-level errors that the GSC report obscures.
Además: Google's Rich Results Test tiene una API. Escribimos un pequeño script que golpearía la API con una muestra aleatoria de 50 URLs cada noche y registraría cualquier falla de validación. Seguro barato.Google's Rich Results Test has an API. We wrote a small script that would hit the API with a random sample of 50 URLs nightly and log any validation failures. Cheap insurance.
---
Manejo de datos dinámicos sin sacrificar el rendimiento
Aquí es donde la mayoría de implementaciones a escala se caen. Schema que referencia datos en vivo -- precios, disponibilidad, conteos de reseñas -- tiene que mantenerse fresco. Pero regenerar JSON-LD en cada carga de página para 91,000 páginas no es gratis.
Mi enfoque, que he refinado en alrededor de una docena de sitios grandes desde entonces:
Cache agresivamente, invalida inteligentemente.
Para las páginas de hoteles, el blob de schema se almacenaba como post meta -- una cadena JSON-LD serializada -- y se regeneraba solo cuando:
- El post mismo era actualizado
- Se enviaba una nueva reseña para ese post
- El campo personalizado de precio cambió (enganchamos esto en la acción ACF save_post)
save_postaction for this)
Todo lo demás servía la cadena cacheada. Increíblemente rápido. Y como los hooks de invalidación eran específicos, el schema se mantenía preciso.
Algo que hice mal inicialmente: cacheé la etiqueta <script> completa, incluyendo los elementos de apertura y cierre. Luego necesitamos cambiar la URL @context para un tipo de contenido. Tuvimos que limpiar cada entrada de cache. Ahora cacheo solo la cadena JSON y la envuelvo en tiempo de renderizado. Cinco minutos de código extra, ahorré una hora de confusión.<script>tag, including the opening and closing elements. Then we needed to change the@context URL for one content type. Had to bust every cache entry. Now I cache only the JSON string and wrap it at render time. Five minutes of extra code, saved an hour of head-scratching.
¿Qué hay con los precios en tiempo real?
Para precios de tours que cambiaban múltiples veces al día, tomamos un enfoque diferente. El schema base se cacheaba, pero el bloque Offer se generaba fresco en tiempo de solicitud y se fusionaba antes de la serialización. Sí, agregó una pequeña sobrecarga por solicitud. Pero era una consulta de base de datos por carga de página, no doce. Un compromiso aceptable.Offer block was generated fresh at request time and merged in before serialisation. Yes, it added a small overhead per request. But it was one database query per page load, not twelve. Acceptable trade-off.
---
Escalabilidad a múltiples sitios: El enfoque Seahawk
Seahawk ha construido más de 12,000 sitios, y la implementación de schema aparece en una cantidad significativa de ellos. El cliente de viajes fue un caso extremo. Pero los mismos principios arquitectónicos aplican si estás trabajando con 91,000 páginas o 4,000.
Lo que he definido como patrón reutilizable es un pequeño plugin interno de WordPress -- lo llamamos seahawk-schema-core -- que proporciona el scaffolding del manager/builder sin ninguna lógica específica del tipo de contenido. Los proyectos de clientes lo extienden con sus propias clases builder. Sin dependencias de plugins para la lógica central del schema. Sin riesgo de que una actualización de un plugin de terceros destruya toda la presencia de rich results del sitio.seahawk-schema-core -- that provides the manager/builder scaffolding without any content-type-specific logic. Client projects extend it with their own builder classes. No plugin dependencies for the core schema logic. No risk of a third-party plugin update blowing up a site's entire rich results presence.
Ese último punto es más real de lo que la gente admite. He visto actualizaciones de Rank Math romper silenciosamente overrides de schema personalizados. No porque Rank Math sea malo -- no lo es -- sino porque cuando estás personalizando el output al nivel que requiere un sitio grande, estás operando fuera de lo que el plugin fue diseñado para manejar. Sé dueño del código, sé dueño del perfil de riesgo.
---
Pruebas a esta escala: Una checklist práctica
No puedes probar manualmente 91,000 URLs. Así que pruebas de forma inteligente.
- Muestra por tipo de template. Elige 10 URLs por tipo de contenido. Prueba esas. Si el builder es correcto para una página de hotel, es correcto para las 3,000 páginas de hoteles (a menos que haya datos malos -- más sobre eso abajo).Pick 10 URLs per content type. Test those. If the builder is correct for one hotel page, it's correct for all 3,000 hotel pages (unless there's bad data -- more on that below).
- Prueba casos extremos específicamente. Páginas sin reseñas. Páginas con campos personalizados incompletos. Páginas con caracteres especiales en títulos (&,", caracteres acentuados). La serialización JSON se come muchos de estos, pero no todos.Pages with no reviews. Pages with incomplete custom fields. Pages with special characters in titles (
&,", accented characters). JSON serialisation eats a lot of these, but not all of them. - Ejecuta un rastreo completo de datos estructurados con Screaming Frog. El Screaming Frog SEO Spider tiene un modo de extracción de datos estructurados que extrae y valida JSON-LD desde cada URL que rastrea. Exporta los errores, agrúpalos por tipo de plantilla, corrige en la fuente.The Screaming Frog SEO Spider has a structured data extraction mode that'll pull and validate JSON-LD from every URL it crawls. Export the errors, group by template type, fix at the source.
- Monitorea la pestaña Enhancements en GSC. Establece una alerta de umbral -- si los elementos válidos caen más del 5% semana a semana, algo se rompió. Actúa dentro de 48 horas.Set a threshold alert -- if valid items drop by more than 5% week-over-week, something broke. Act within 48 hours.
- Verifica manualmente después de cada deployment. Incluso si el código de schema no cambió. Las migraciones de base de datos, las actualizaciones de plugins, los cambios de tema -- cualquiera de ellos puede introducir problemas de datos upstream que corrompan el output del schema.Even if the schema code didn't change. Database migrations, plugin updates, theme changes -- any of them can introduce upstream data issues that corrupt schema output.
Los Datos Malos Son el Asesino Silencioso
El sitio de viajes tenía un equipo de contenido de doce personas en tres países. Algunas páginas de destino tenían HTML malformado en el campo de descripción -- pegado probablemente desde Word. Cuando ese campo alimentaba la propiedad de descripción del schema, el JSON era técnicamente válido pero la descripción incluía entidades y etiquetas <span> sueltas. Google ignoró la propiedad. Agregamos un paso de sanitización en cada clase builder que elimina etiquetas y decodifica entidades HTML antes de que el valor llegue al array del schema. Lo solucionamos permanentemente.description field -- pasted from Word, presumably. When that field fed into the schema description property, the JSON was technically valid but the description included entities and stray<span>tags. Google ignored the property. We added a sanitisation step in every builder class that strips tags and decodes HTML entities before the value hits the schema array. Solved it permanently.
---
El Entity Graph: No Lo Ignores
Una cosa que separa el trabajo de schema mediocre del genuinamente buen SEO técnico es el entity graph -- específicamente, las entidades sitewide Organization y WebSite que deberían aparecer en cada página y vincular todo junto.Organization and WebSite entities that should appear on every page and link everything together.
La mayoría de los sitios tienen estas, pero mal. Nombre, URL, quizá un logo. El tipo Organization completo soporta enlaces sameAs a tu entrada de Wikidata, perfiles sociales, y otras fuentes autoritativas. Ese entrecruzamiento es cómo Google construye confianza en que tu entidad Organization en su Knowledge Graph es la misma entidad que aparece en tu schema de página.Organization type supports sameAs links to your Wikidata entry, social profiles, and other authoritative sources. That cross-linking is how Google builds confidence that your Organization entity in its Knowledge Graph is the same entity appearing in your page schema.
Para el cliente de viajes, construimos el bloque Organization con:Organization block with:
sameAs apuntando a su perfil de Crunchbase, página de LinkedIn, y un stub de Wikipedia que teníanpointing to their Crunchbase profile, LinkedIn page, and a Wikipedia stub they hadcontactPoint con información estructurada de teléfono y departamentowith structured phone and department infofoundingDate y numberOfEmployees (rango aproximado -- de todos modos es información pública)andnumberOfEmployees(rough range -- this is public info anyway)
¿Movió rankings de la noche a la mañana? No. El schema casi nunca lo hace aislado. Pero es infraestructura. Lo construyes una vez, correctamente, y se compone con el tiempo.
---
FAQ
¿Cuánto tiempo tarda implementar schema a esta escala?
Para el sitio de viajes de 91,000 páginas, la implementación completa -- arquitectura, clases builder, capa de caching, testing, configuración de monitoreo en GSC -- tomó alrededor de seis semanas con dos desarrolladores. Eso suena como mucho. Pero la mitad de ese tiempo fue auditar la calidad de datos existente, no escribir código de schema. Si tus datos son limpios, puedes moverte más rápido.
¿Debo usar un plugin o construir algo personalizado para sitios grandes?
Para cualquier cosa por debajo de algunos cientos de páginas, un plugin funciona perfectamente. El módulo de schema de Rank Math es sólido y el bloque de schema personalizado te da flexibilidad razonable. Por encima de algunos miles de páginas con múltiples tipos de contenido distintos, voy personalizado siempre. El control vale la pena el costo de desarrollo.
¿Cuál es el error de schema más común a escala?
Falta aggregateRating cuando existen reseñas -- o incluirlo cuando no las hay. Google es estricto con esto. Si tu schema afirma un aggregateRating de 4.7 de 843 reseñas y un usuario llega a la página sin ver ninguna reseña, eso es una acción manual esperando suceder. La lógica condicional en tus clases builder es innegociable.aggregateRating when reviews exist -- or including it when they don't. Google is strict about this. If your schema claims an aggregateRating of 4.7 from 843 reviews and a user lands on the page and sees no reviews, that's a manual action waiting to happen. Conditional logic in your builder classes is non-negotiable.
¿El schema mejora directamente los rankings?
¿Directamente? Probablemente no mucho para la mayoría de tipos de búsqueda. Lo que hace es desbloquear rich results -- calificaciones con estrellas, desplegables de FAQ, fragmentos de reseñas, breadcrumbs en el SERP -- y esas características mejoran las tasas de clics de manera medible. El cliente de viajes vio un aumento de CTR del 22% en páginas de hoteles dentro de cuatro meses de implementación completa. Eso se traduce en señales de engagement, que sí afectan los rankings. Entonces: indirectamente, sí. Sustancialmente.
¿Qué herramientas usas realmente día a día para trabajo de schema?
Screaming Frog para auditoría a nivel de crawl. Google's Rich Results Test para verificaciones puntuales. Schema Markup Validator en validator.schema.org para validación a nivel de propiedades. Y honestamente, la documentación de Schema.org en sí -- tengo la página del tipo Hotel y un puñado de otras marcadas y las consulto constantemente. No se necesita ninguna herramienta de suscripción fancy.validator.schema.org for property-level validation. And honestly, the Schema.org documentation itself -- I have the Hotel type page and a handful of others bookmarked and I refer to them constantly. No fancy subscription tool needed.
---
Schema a escala es uno de esos problemas que parece un problema de plugin hasta que estás dentro y te das cuenta de que en realidad es un problema de arquitectura de software disfrazado de SEO. Obtén el modelo de datos correcto. Almacena en caché inteligentemente. Valida sin cesar. El markup en sí es casi la parte fácil.
