Como insertar datos estructurados JSON-LD desde PHP en WordPress

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

  1. functions.php del tema hijo (rápido para pruebas).
  2. Un pequeño plugin a medida (recomendado para portabilidad).
  3. 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

  1. Usa Google Rich Results Test para comprobar resultados enriquecidos.
  2. Usa Schema Markup Validator para comprobar conformidad con schema.org.
  3. 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 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *