Inject JSON-LD Structured Data Dynamically with JavaScript in WordPress

JSON-LD (JavaScript Object Notation for Linked Data) is Google’s preferred format for structured data markup, allowing search engines to understand the type and relationships of page content — articles, products, recipes, FAQ sections, and breadcrumbs — without parsing the visible HTML. WordPress does not output JSON-LD structured data by default, relying instead on plugins like Yoast SEO or Rank Math, but generating it directly in PHP or JavaScript gives full control over the schema and avoids plugin overhead. Adding JSON-LD via a <script type="application/ld+json"> tag in wp_head is the most reliable approach because the data is available immediately when the page is parsed, before any JavaScript executes. For dynamically rendered pages — Gutenberg block-based front pages or React-powered WooCommerce storefronts — injecting JSON-LD via JavaScript after the page load is a valid alternative, as long as the script runs synchronously before the page is fully loaded (not deferred). The Article schema benefits WordPress blog posts by enabling rich results including the headline, author, publication date, and article image in Google Search — all fields available directly from post meta. BreadcrumbList schema maps the category hierarchy of a post or page to a navigational breadcrumb display in search result snippets, increasing click-through rate for queries that show the full URL path. FAQPage schema marks up a post section that contains question-and-answer pairs, allowing Google to display the answers directly in the search result without the user clicking through to the page. The Open Graph meta tags post covers the social sharing metadata that complements JSON-LD structured data — both types of metadata should be added to every WordPress post. The sitemap extension post ensures the pages that carry JSON-LD are discoverable by crawlers — structured data has no effect on pages that are not indexed. Validate every schema variation at search.google.com/test/rich-results after deployment — missing required fields or incorrect property types cause Google to ignore the markup without surfacing an error in Search Console.

Problem: WordPress posts and WooCommerce product pages lack machine-readable structured data by default, so search engines cannot generate rich results — article bylines, breadcrumbs, product ratings, and FAQ dropdowns — that increase click-through rates.

Solution: Inject JSON-LD structured data into WordPress pages by outputting a <script type="application/ld+json"> block via wp_head for static pages, or by constructing and appending the script tag from JavaScript for dynamically rendered block-editor content.

// wp-content/themes/mytheme/js/json-ld.js
// Enqueue with wp_enqueue_script (not deferred) so Google sees it early.

(function() {
    var body = document.body;
    if (!body) return;

    // Read PHP-rendered data attributes from  (set via wp_body_open or body_class)
    var postType    = body.dataset.postType   || '';
    var postTitle   = body.dataset.postTitle  || document.title;
    var postDate    = body.dataset.postDate   || '';
    var postModDate = body.dataset.postMod    || '';
    var authorName  = body.dataset.author     || '';
    var siteUrl     = body.dataset.siteUrl    || location.origin;
    var thumbnailUrl= body.dataset.thumbnail  || '';

    if (postType !== 'post' && postType !== 'page') return;

    // Build Article schema
    var schema = {
        '@context': 'https://schema.org',
        '@type':    'Article',
        'headline': postTitle,
        'author':   { '@type': 'Person', 'name': authorName },
        'datePublished': postDate,
        'dateModified':  postModDate,
        'publisher': {
            '@type': 'Organization',
            'name':  document.title.split('|').pop().trim(),
            'url':   siteUrl,
        },
    };
    if (thumbnailUrl) schema['image'] = thumbnailUrl;

    // BreadcrumbList from Yoast / AIOSEO breadcrumb nav (if present)
    var crumbs = document.querySelectorAll('.breadcrumb-item, .breadcrumbs span[typeof="v:Breadcrumb"]');
    if (crumbs.length > 1) {
        schema['breadcrumb'] = {
            '@type': 'BreadcrumbList',
            'itemListElement': Array.from(crumbs).map(function(el, i) {
                var a = el.querySelector('a');
                return {
                    '@type':    'ListItem',
                    'position': i + 1,
                    'name':     el.textContent.trim(),
                    'item':     a ? a.href : location.href,
                };
            }),
        };
    }

    var script = document.createElement('script');
    script.type        = 'application/ld+json';
    script.textContent = JSON.stringify(schema);
    document.head.appendChild(script);
}());

NOTE: Data attributes on <body> are populated in PHP via add_filter('body_class', ...) — use esc_attr(get_the_date('c')) for ISO 8601 dates and esc_attr(get_the_title()) for the headline to prevent XSS through injected attribute values.