Implement Structured Data Schema Markup for WordPress Articles and Products

Structured data in JSON-LD format added to WordPress pages helps search engines understand content type, authorship, breadcrumbs, ratings, and product availability, enabling rich results such as star ratings, price ranges, and FAQ accordions in Google search. The preferred injection method is a wp_head hook that outputs a <script type="application/ld+json"> block — using JSON-LD instead of Microdata or RDFa keeps markup clean and decoupled from HTML structure, and Google explicitly recommends it. For a blog post the minimum valid Article schema requires @context, @type (use BlogPosting for informal blog posts), headline (max 110 characters), datePublished and dateModified in ISO-8601 format, author as a Person or Organization node with a name and optionally a url, and image as an ImageObject with url, width, and height. For WooCommerce products the Product schema adds offers as an Offer node with price, priceCurrency, availability mapped from WooCommerce stock status (https://schema.org/InStock or OutOfStock), and url. Breadcrumb schema (BreadcrumbList) is output on every page using the category hierarchy from get_the_category() and the post permalink, which enables breadcrumb display in search results independently of the page content schema. The schema array is built in PHP and encoded with wp_json_encode() — never concatenate raw PHP variables into a JSON string, as unescaped quotes or angle brackets will produce invalid JSON and cause Google Search Console to report parse errors. Duplicate schema blocks from multiple plugins conflict; audit with the Rich Results Test tool and consolidate all JSON-LD output into a single hook callback. The custom post type post shows how schema can be extended for custom @type values that match the content model of the CPT.

Problem: WordPress blog posts and WooCommerce products appear as plain blue links in Google search because no structured data is present, missing the star ratings, price, and breadcrumb rich results that increase click-through rate.

Solution: Hook into wp_head to output a JSON-LD <script> block containing BlogPosting schema for posts and Product + BreadcrumbList schema for WooCommerce product pages, built with wp_json_encode() from WordPress data functions.

add_action('wp_head', function() {
    if (!is_singular()) return;

    $schemas = [];

    // ── BlogPosting schema for posts/pages ───────────────────────────
    if (is_single() && !is_woocommerce()) {
        $post      = get_queried_object();
        $author    = get_userdata($post->post_author);
        $img_id    = get_post_thumbnail_id($post);
        $img_src   = $img_id ? wp_get_attachment_image_src($img_id, 'full') : null;

        $schemas[] = array_filter([
            '@context'      => 'https://schema.org',
            '@type'         => 'BlogPosting',
            'headline'      => mb_substr(get_the_title($post), 0, 110),
            'datePublished' => get_the_date('c', $post),
            'dateModified'  => get_the_modified_date('c', $post),
            'author'        => [
                '@type' => 'Person',
                'name'  => $author ? $author->display_name : get_bloginfo('name'),
                'url'   => $author ? get_author_posts_url($author->ID) : null,
            ],
            'publisher' => [
                '@type' => 'Organization',
                'name'  => get_bloginfo('name'),
                'url'   => home_url('/'),
            ],
            'image' => $img_src ? [
                '@type'  => 'ImageObject',
                'url'    => esc_url($img_src[0]),
                'width'  => (int)$img_src[1],
                'height' => (int)$img_src[2],
            ] : null,
            'url' => get_permalink($post),
        ]);
    }

    // ── Product schema for WooCommerce single products ────────────────
    if (function_exists('is_product') && is_product()) {
        $product      = wc_get_product(get_the_ID());
        $availability = $product->is_in_stock()
            ? 'https://schema.org/InStock'
            : 'https://schema.org/OutOfStock';

        $schemas[] = [
            '@context'    => 'https://schema.org',
            '@type'       => 'Product',
            'name'        => get_the_title(),
            'description' => wp_strip_all_tags(get_the_excerpt()),
            'sku'         => $product->get_sku(),
            'offers'      => [
                '@type'         => 'Offer',
                'url'           => get_permalink(),
                'priceCurrency' => get_woocommerce_currency(),
                'price'         => $product->get_price(),
                'availability'  => $availability,
                'seller'        => ['@type' => 'Organization', 'name' => get_bloginfo('name')],
            ],
        ];
    }

    // ── BreadcrumbList for all singular pages ─────────────────────────
    $items   = [['@type' => 'ListItem', 'position' => 1, 'name' => 'Home', 'item' => home_url('/')]];
    $cats    = get_the_category();
    if ($cats) {
        $items[] = ['@type' => 'ListItem', 'position' => 2,
                    'name'  => esc_html($cats[0]->name),
                    'item'  => esc_url(get_category_link($cats[0]->term_id))];
    }
    $items[] = ['@type' => 'ListItem', 'position' => count($items) + 1,
                'name'  => get_the_title(), 'item' => get_permalink()];

    $schemas[] = ['@context' => 'https://schema.org', '@type' => 'BreadcrumbList', 'itemListElement' => $items];

    foreach ($schemas as $schema) {
        echo '' . PHP_EOL;
    }
});

NOTE: Validate every schema variant with Google Rich Results Test after deployment — a missing required field (e.g., datePublished or author on BlogPosting) silently disqualifies the page from rich results without generating a crawl error. Check Google Search Console under Enhancements for ongoing coverage reports.