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 🙂 |
