Contents
Introducción
En este tutorial detallado aprenderás a insertar datos estructurados en formato JSON-LD desde PHP en un sitio WordPress. Cubriremos desde conceptos básicos hasta implementaciones prácticas y seguras: dónde colocar el código, cómo generar JSON-LD dinámico con datos del post, uso de ACF/WooCommerce, recomendaciones de escape, cache y validación con las herramientas de Google. Todos los ejemplos son directamente reutilizables en functions.php o en un plugin ligero.
Por qué usar JSON-LD y por qué inyectarlo desde PHP
- JSON-LD es el formato recomendado por Google para datos estructurados porque es fácil de integrar y separar del HTML.
- Generarlo desde PHP permite usar datos dinámicos del entorno WordPress (post, meta, taxonomías, WooCommerce, ACF…) sin depender de plugins de terceros.
- Control total: se evita duplicidad con plugins que ya inyecten schemas y se puede versionar/automatizar en el tema o en un plugin propio.
Buenas prácticas antes de empezar
- Usar wp_json_encode para serializar arrays PHP a JSON y evitar problemas de codificación.
- Marcar el JSON-LD dentro de un ltscript type=application/ld jsongt al imprimirlo (mostraremos cómo dentro de los ejemplos).
- Comprobar si ya existe JSON-LD inyectado por otros plugins (Yoast, RankMath) para evitar duplicidad.
- No confiar en meta fields no saneados: sanitizar y castear antes de incluirlos en el esquema.
- Minimizar el output en páginas que no necesitan schema (usar condicionales como is_singular(), is_front_page(), is_product(), etc.).
Dónde colocar el código
- functions.php del tema hijo (rápido para pruebas).
- Un pequeño plugin a medida (recomendado para portabilidad).
- Un mu-plugin para instalaciones multi-site cuando se quiere globalidad.
Ejemplo 1 — Schema básico de Website (global)
Inserta un schema tipo Website en todas las páginas. Este ejemplo va en functions.php o en tu plugin.
lt?php function mi_schema_website() { data = array( @context => https://schema.org, @type => WebSite, url => home_url(), name => get_bloginfo(name), description => get_bloginfo(description), // Opcional: búsqueda interna potentialAction => array( @type => SearchAction, target => home_url(/?s={search_term_string}), query-input => required name=search_term_string ) ) // Usar wp_json_encode para manejar correctamente la codificación json = wp_json_encode( data, JSON_UNESCAPED_SLASHES JSON_UNESCAPED_UNICODE JSON_PRETTY_PRINT ) echo ltscript type=application/ld jsongt . json . lt/scriptgtn } add_action(wp_head, mi_schema_website, 1) ?gt
Ejemplo 2 — Article schema para posts individuales
Genera schema Article solo en entradas individuales (single posts). Usa datos dinámicos: título, autor, fechas, imagen destacada y excerpt.
lt?php function mi_schema_article() { if ( ! is_singular(post) ) { return } global post // Autor author_id = post->post_author author_name = get_the_author_meta(display_name, author_id) author_url = get_author_posts_url(author_id) // Imagen destacada (si existe) image = if ( has_post_thumbnail(post) ) { img_id = get_post_thumbnail_id(post) img_src = wp_get_attachment_image_src(img_id, full) if ( img_src ) { image = img_src[0] } } data = array( @context => https://schema.org, @type => Article, mainEntityOfPage => array( @type => WebPage, @id => get_permalink(post) ), headline => get_the_title(post), image => image ? array(image) : array(), datePublished => get_the_date(c, post), dateModified => get_the_modified_date(c, post), author => array( @type => Person, name => author_name, url => author_url ), publisher => array( @type => Organization, name => get_bloginfo(name), // Añade logo si tienes uno en el customizer o una opción ), description => get_the_excerpt(post) ) json = wp_json_encode( data, JSON_UNESCAPED_SLASHES JSON_UNESCAPED_UNICODE JSON_PRETTY_PRINT ) echo ltscript type=application/ld jsongt . json . lt/scriptgtn } add_action(wp_head, mi_schema_article, 10) ?gt
Ejemplo 3 — FAQ Schema usando ACF (repeater)
Si usas Advanced Custom Fields con un repeater que guarda preguntas y respuestas en los meta faq_questions, faq_answer, puedes crear un FAQPage.
lt?php function mi_schema_faq_acf() { if ( ! is_singular() ) { return } global post if ( ! function_exists(have_rows) ) { return // ACF no está activo } if ( ! have_rows(faqs, post->ID) ) { return // No hay preguntas } main = array( @context => https://schema.org, @type => FAQPage, mainEntity => array() ) while ( have_rows(faqs, post->ID) ) { the_row() q = get_sub_field(question) a = get_sub_field(answer) if ( q a ) { main[mainEntity][] = array( @type => Question, name => wp_strip_all_tags( q ), acceptedAnswer => array( @type => Answer, text => wp_strip_all_tags( a ) ) ) } } if ( empty(main[mainEntity]) ) { return } json = wp_json_encode( main, JSON_UNESCAPED_SLASHES JSON_UNESCAPED_UNICODE JSON_PRETTY_PRINT ) echo ltscript type=application/ld jsongt . json . lt/scriptgtn } add_action(wp_head, mi_schema_faq_acf, 20) ?gt
Ejemplo 4 — Product schema con WooCommerce
Ejemplo para plantillas de producto. Usa wc_get_product() para obtener precio, disponibilidad, SKU e imagenes.
lt?php function mi_schema_product_woo() { if ( ! function_exists(is_product) ! is_product() ) { return } global product if ( ! product ) { product = wc_get_product( get_the_ID() ) } if ( ! product ) { return } images = array() gallery_ids = product->get_gallery_image_ids() if ( product->get_image_id() ) { images[] = wp_get_attachment_url( product->get_image_id() ) } foreach ( gallery_ids as gid ) { images[] = wp_get_attachment_url( gid ) } data = array( @context => https://schema.org/, @type => Product, name => product->get_name(), image => images, description => wp_strip_all_tags( product->get_description() ), sku => product->get_sku(), offers => array( @type => Offer, url => get_permalink( product->get_id() ), priceCurrency => get_woocommerce_currency(), price => product->get_price(), availability => product->is_in_stock() ? https://schema.org/InStock : https://schema.org/OutOfStock, itemCondition => https://schema.org/NewCondition ) ) json = wp_json_encode( data, JSON_UNESCAPED_SLASHES JSON_UNESCAPED_UNICODE JSON_PRETTY_PRINT ) echo ltscript type=application/ld jsongt . json . lt/scriptgtn } add_action(wp_head, mi_schema_product_woo, 20) ?gt
Ejemplo 5 — BreadcrumbList dinámico
Genera breadcrumbs en JSON-LD para ayudar a Google a entender la jerarquía de la página. Asegúrate de que no esté duplicado por otros plugins.
lt?php function mi_schema_breadcrumbs() { if ( is_front_page() ) { return } items = array() position = 1 // Home items[] = array( @type => ListItem, position => position , name => get_bloginfo(name), item => home_url() ) if ( is_singular() ) { post = get_queried_object() // Añadir categorías (si existen) cats = get_the_category( post->ID ) if ( ! empty(cats) ) { cat = cats[0] ancestors = get_ancestors(cat->term_id, category) ancestors = array_reverse(ancestors) foreach ( ancestors as ancestor_id ) { term = get_term(ancestor_id, category) if ( term ) { items[] = array( @type => ListItem, position => position , name => term->name, item => get_category_link(term) ) } } items[] = array( @type => ListItem, position => position , name => cat->name, item => get_category_link(cat) ) } // Finalmente el propio post items[] = array( @type => ListItem, position => position , name => get_the_title(post), item => get_permalink(post) ) } elseif ( is_category() ) { cat = get_queried_object() items[] = array( @type => ListItem, position => position , name => cat->name, item => get_category_link(cat) ) } elseif ( is_page() ) { post = get_queried_object() anc = get_post_ancestors(post) anc = array_reverse(anc) foreach ( anc as anc_id ) { items[] = array( @type => ListItem, position => position , name => get_the_title(anc_id), item => get_permalink(anc_id) ) } items[] = array( @type => ListItem, position => position , name => get_the_title(post), item => get_permalink(post) ) } if ( count(items) < 2 ) { return } schema = array( @context => https://schema.org, @type => BreadcrumbList, itemListElement => items ) json = wp_json_encode( schema, JSON_UNESCAPED_SLASHES JSON_UNESCAPED_UNICODE JSON_PRETTY_PRINT ) echo ltscript type=application/ld jsongt . json . lt/scriptgtn } add_action(wp_head, mi_schema_breadcrumbs, 5) ?gt
Seguridad y validación de datos
- Sanitiza siempre campos de usuario o meta: wp_strip_all_tags(), esc_url_raw(), floatval para precios, intval para IDs.
- Usa wp_json_encode o json_encode con las banderas JSON_UNESCAPED_SLASHES JSON_UNESCAPED_UNICODE para evitar barras escapadas y problemas con UTF-8.
- Evita inyectar campos vacíos: algunos tipos en schema.org esperan arrays no vacíos (por ejemplo image para Article).
- Si generas URLs, conviértelas con esc_url_raw() antes de incluirlas en el array si se obtienen desde meta.
Rendimiento y cache
- Si el JSON-LD es costoso de generar (consultas complejas), almacénalo en transients con set_transient() y actualiza el cache cuando cambien los datos relevantes.
- Coloca la salida con prioridad baja/alta según necesites que otros plugins lo reemplacen p. ej. 5 para breadcrumbs, 20 para schemas específicos.
- Evita recalcular imágenes y tamaños repetidamente: obtén URLs una vez y cachea si procede.
Compatibilidad con plugins SEO
Plugins como Yoast o RankMath ya inyectan JSON-LD. Antes de añadir tu propio schema:
- Verifica si el plugin ya produce ese tipo de schema (y desactiva duplicados).
- Si necesitas complementarlo, hazlo con condicionales (por ejemplo solo en tipos específicos o añadiendo campos que el plugin no cubre).
Validación
- Usa Google Rich Results Test para comprobar resultados enriquecidos.
- Usa Schema Markup Validator para comprobar conformidad con schema.org.
- Inspecciona el HTML final en el navegador o con curl para verificar que el script JSON-LD se imprimió correctamente.
Tabla rápida: Tipos comunes y cuándo usarlos
Tipo | Uso típico |
---|---|
WebSite | Información general del sitio y búsqueda interna |
Article / BlogPosting | Entradas de blog y artículos |
Product | Tiendas y fichas de producto (WooCommerce) |
FAQPage | Páginas con preguntas frecuentes |
Event | Eventos con fecha, localización y entradas |
LocalBusiness / Organization | Datos de empresa física o contacto corporativo |
BreadcrumbList | Jerarquía de navegación |
Errores comunes y cómo solucionarlos
- Duplicidad de schemas: revisa plugins SEO y desactiva la generación similar o usa condicionales para evitar duplicados.
- JSON inválido: usar json_encode/wq_json_encode con las banderas adecuadas y evitar concatenaciones manuales que rompan comillas.
- Campos esperados vacíos: asegúrate de no enviar arrays vacíos en propiedades que esperan valores o elimina claves vacías antes de serializar.
- Problemas de codificación: usa funciones nativas de WP (wp_json_encode) para evitar caracteres UTF-8 mal representados.
Ejemplo: función utilitaria para limpiar arrays y quitar claves vacías
lt?php function mi_schema_clean_array( arr ) { foreach ( arr as key => value ) { if ( is_array(value) ) { arr[key] = mi_schema_clean_array( value ) if ( empty(arr[key]) ) { unset( arr[key] ) } } else { if ( value === value === null ) { unset( arr[key] ) } } } return arr } ?gt
Checklist antes de publicar
- Verifica que no haya duplicados de schema en la página.
- Valida el JSON-LD con las herramientas de Google y schema.org.
- Revisa el rendimiento y activa transients si la generación es cara.
- Sanea toda la data recibida desde usuarios/meta fields.
- Mantén versionado el código en un plugin o tema hijo para auditoría y despliegues.
Conclusión
Insertar JSON-LD desde PHP en WordPress te da control total sobre los datos estructurados que presenta tu sitio. Usando las APIs de WordPress (get_post, WP_Query, wc_get_product, funciones de ACF, etc.), combinadas con wp_json_encode y buenas prácticas de escape, puedes generar schemas válidos, dinámicos y optimizados. Aplica condicionales para evitar sobrecarga y realiza validaciones periódicas con las herramientas oficiales.
Recursos útiles
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |