/ โ† JSON-LD Fixes / JSON-LD in WordPress
In this guide
  1. Where JSON-LD comes from on a WordPress site
  2. How WordPress breaks JSON-LD
  3. Tracing a block to its source
  4. Building JSON-LD safely in PHP
  5. Changing what the SEO plugin outputs
  6. The fix, step by step
  7. WooCommerce, block themes and page builders
  8. Keeping WordPress JSON-LD healthy
  9. Questions and answers

Where JSON-LD comes from on a WordPress site

Before fixing anything, it helps to know the usual suspects. Each one has its own way of being changed, and editing the wrong layer is the main reason fixes do not stick.

SourceWhat it usually outputsHow to change it
SEO plugin (Yoast SEO, Rank Math, All in One SEO, SEOPress and others)A graph with Organization or Person, WebSite, WebPage, BreadcrumbList and ArticlePlugin settings, per-post schema options, and the plugin's developer filters
WooCommerceProduct with offers and ratings on product pages, plus some shop-level dataWooCommerce filters and actions in a small custom plugin
ThemeVaries: sometimes Article or Organization, sometimes old Microdata in the templatesTheme options, or a child theme template override
Page builderMarkup from specific widgets such as FAQ, review or business info modulesThe widget's own settings
Recipe, events, reviews and booking pluginsRecipe, Event, Review, LocalBusinessThat plugin's settings; some offer filters
Header and footer snippet pluginsHand-written blocks, often site-wideEdit the snippet in the plugin's screen
Custom HTML blocks in postsHand-written blocks on single pagesEdit the post in the block editor
Google Tag ManagerBlocks injected by script after loadThe tag in the Tag Manager container

If you only remember one thing: never fix a plugin's output by editing the plugin's own files. Plugin updates overwrite those files. Changes belong in settings, in a child theme, or in a small site-specific plugin that uses the filters the plugin provides.

How WordPress breaks JSON-LD

Some faults are common to every platform, such as a trailing comma in a hand-written block. Others come from the way WordPress stores and processes content, and they catch out people who know JSON well:

Tracing a block to its source

WordPress sites rarely label their markup clearly, but there are reliable clues:

  1. Class names on the script tag. Yoast SEO's graph has usually been output with a class such as yoast-schema-graph, and Rank Math's with a class containing rank-math-schema. Other plugins often add their own class or an id.
  2. HTML comments. Many SEO plugins wrap their head output in comments naming the plugin. The block between those comments belongs to it.
  3. Position. Blocks in the head usually come from plugins hooked to wp_head; WooCommerce has output its product data near the end of the page through wp_footer; blocks in the middle of the content come from the post itself or a page builder widget.
  4. Query Monitor. This free plugin lists the functions attached to each hook, including wp_head and wp_footer, with the plugin or theme each belongs to. It narrows the search quickly on a busy site.
  5. Elimination on staging. On a staging copy, deactivate plugins one at a time and reload the page. When a block disappears, you have its source. Do this on staging, never on the live shop.
๐Ÿ’ก Search the database for hand-written blocks with WP-CLI: wp db search "application/ld+json" lists the tables and rows that contain it, which finds forgotten Custom HTML blocks and snippet plugin entries in seconds.

Building JSON-LD safely in PHP

Whenever your own code outputs JSON-LD, build a PHP array and let WordPress encode it. wp_json_encode() handles quotes, line breaks and non-ASCII characters correctly, so no product name can break the block. The example below adds a small block to a custom post type called course, and is intended for a site-specific plugin or a child theme's functions.php.

add_action( 'wp_head', function () {
    if ( ! is_singular( 'course' ) ) {
        return;
    }
    $post_id = get_queried_object_id();
    $name    = html_entity_decode( get_the_title( $post_id ), ENT_QUOTES, 'UTF-8' );
    $summary = wp_strip_all_tags( get_the_excerpt( $post_id ) );

    $data = array(
        '@context'    => 'https://schema.org',
        '@type'       => 'Course',
        '@id'         => get_permalink( $post_id ) . '#course',
        'name'        => $name,
        'description' => $summary,
        'provider'    => array( '@id' => home_url( '/#organization' ) ),
    );

    $flags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG;
    echo '<script type="application/ld+json">' . wp_json_encode( $data, $flags ) . '</script>' . PHP_EOL;
} );

Three details matter. html_entity_decode() turns the entities from the title filter back into real characters. wp_strip_all_tags() removes HTML from the excerpt. JSON_HEX_TAG encodes angle brackets, which keeps a stray </script> inside a value from ending the block early; include it whenever you use JSON_UNESCAPED_SLASHES, which otherwise removes that protection. The provider reference assumes your SEO plugin defines the organisation with that identifier, so check the live output and match it exactly.

โš ๏ธ This outputs a separate block alongside the SEO plugin's graph. That is fine for a type the plugin does not produce. For types the plugin already outputs, extend its graph instead, as described next, or you will create competing descriptions.

Changing what the SEO plugin outputs

Most problems with SEO plugin markup should be fixed in the plugin's settings first: the organisation name and logo, whether the site represents a person or an organisation, the default article type, and per-post schema options. When settings are not enough, the major plugins provide filters. Plugin APIs change between versions, so confirm the names against the plugin's current developer documentation before relying on them.

Yoast SEO

Yoast outputs one @graph built from separate pieces. It has offered a filter for each piece, named after it, and one for the whole graph. This adds a property to the Article piece:

add_filter( 'wpseo_schema_article', function ( $data ) {
    if ( is_singular( 'post' ) ) {
        $data['articleSection'] = 'Garden projects';
    }
    return $data;
} );

Yoast has also provided a filter, wpseo_json_ld_output, that turns the whole graph off. Use it only if another tool will supply everything instead, because removing the graph also removes the organisation, website and breadcrumb data other markup may be pointing at.

Rank Math

Rank Math passes its whole schema array through the rank_math/json_ld filter, keyed by entity. Hooking it at a late priority lets you adjust or remove entries after Rank Math has built them:

add_filter( 'rank_math/json_ld', function ( $data, $jsonld ) {
    if ( isset( $data['publisher'] ) && is_array( $data['publisher'] ) ) {
        $data['publisher']['name'] = 'Kestrel Garden Supplies';
    }
    return $data;
}, 99, 2 );

Print the array to a log on staging before changing it, as the keys depend on your settings and the page type. The same principle applies to All in One SEO, SEOPress and others: find the documented filter, change the data it passes, and return it.

The fix, step by step

Step 1

Capture the live output as a visitor sees it

Log out, or use a private window, and load one page of each type: home, post, page, product, product category and any custom post types. Run each through the JSON Checker and save the results. Then open developer tools and search the Elements panel for application/ld+json, because blocks added by Tag Manager or a reviews plugin after load do not appear in view source. Note every finding against the block that produced it.

Step 2

Trace each block to the plugin or theme that wrote it

Use the class name on the script tag, the comments around it and its position in the page to identify the source of each block. Where that is unclear, install Query Monitor and look at the functions attached to wp_head and wp_footer, or deactivate plugins one at a time on a staging copy. Run wp db search for hand-written blocks stored in posts or snippet plugins. Write the source next to each finding.

Step 3

Repair parse errors in the code that builds the block

For blocks your theme or custom code produces, replace any string concatenation with a PHP array passed to wp_json_encode(), and decode title entities and strip tags from any text taken from content. For hand-written blocks in snippets or Custom HTML, correct the JSON and test it in a validator before saving. For plugin blocks, check the plugin's settings for the bad value, and report reproducible faults to the plugin's support forum.

Step 4

Stop the editor and content filters altering the code

Move hand-written JSON-LD out of post content wherever possible and into a snippet plugin or a child theme, where content filters and user role restrictions do not apply. If a block must stay in a post, have an Administrator add it, since users without unfiltered_html lose script tags on save. Where an optimisation plugin rewrites scripts, add application/ld+json to its exclusions, then compare the output with optimisation switched on and off.

Step 5

Extend the SEO plugin graph instead of adding blocks

When a type your SEO plugin already outputs needs more properties or a correction, change it through the plugin's settings or its documented filter rather than pasting a second block. For Yoast that means the piece filters such as wpseo_schema_article; for Rank Math, the rank_math/json_ld filter. Put the code in a small site-specific plugin, so it survives theme changes, and reuse the plugin's own identifiers for anything you reference.

Step 6

Switch off duplicate WooCommerce or theme output

If both WooCommerce and an SEO or reviews plugin output a Product for the same item, decide which one owns it and turn the other off. Many SEO plugins have a WooCommerce setting or add-on for this; otherwise remove WooCommerce's output with a documented action. For themes that still print Microdata or their own JSON-LD, look for a theme option, or override the template in a child theme and remove the markup there.

Step 7

Purge every cache and retest

Clear the page cache plugin, any server or host cache, the CDN, and the optimisation plugin's combined files, then reload each page logged out and run the JSON Checker again. Each block should now parse, each type should be valid, and each entity should be described once. Record which filters and settings you changed, and recheck after WordPress, theme, SEO plugin and WooCommerce updates, which are when schema output most often changes.

WooCommerce, block themes and page builders

WooCommerce product data

WooCommerce builds Product structured data from the product's own fields: price, stock status, SKU, reviews and images. It has provided a woocommerce_structured_data_product filter for changing the Product array, which is the right place to add a GTIN or brand held in a custom field. To remove WooCommerce's output entirely because another tool owns products, a commonly used approach is to unhook its output method from the footer:

add_action( 'init', function () {
    if ( function_exists( 'WC' ) && isset( WC()->structured_data ) ) {
        remove_action( 'wp_footer', array( WC()->structured_data, 'output_structured_data' ), 10 );
    }
} );

Test on staging and confirm the Product block has gone and the replacement is present, because removing product markup without a replacement loses product results. If products are variable, check what the markup says for the default variant against what the page shows on first load.

Block themes and the Site Editor

With a block theme, templates live in the Site Editor rather than PHP files. A Custom HTML block placed in a template part, such as the header, appears on every page that uses it. That is a tidy place for a site-wide block, and an easy place to forget one. Check template parts as well as posts when hunting for a stray block.

Page builders

Builders such as Elementor or Divi store content in their own format, and widgets like FAQ, star rating or business details may output markup of their own. If a builder widget produces a type your SEO plugin also outputs, switch off the widget's schema option. Hand-written JSON-LD in a builder's HTML widget is subject to the same user role rules as the block editor.

Keeping WordPress JSON-LD healthy

More on reading findings is in the JSON-LD Schema Guide. If your markup comes from an AI generator, see AI schema in WordPress, and for identifier mismatches between plugins, duplicate or missing @id.

Questions and answers

1. Why does my JSON-LD show up as text on the page?

Usually because the script tag around it was removed when the post was saved. WordPress strips script tags from content saved by users who do not have the unfiltered_html capability, which on a normal site means anyone below Editor, and on multisite anyone who is not a Super Admin. The JSON is left behind as ordinary text. Move the block into a snippet plugin or child theme, or have an Administrator add it on a single site.

2. Do I need a separate schema plugin if I already use Yoast SEO or Rank Math?

Usually not. The major SEO plugins already output a connected graph covering the organisation, website, web pages, breadcrumbs and articles, and many handle products through a WooCommerce integration. Adding a second schema plugin on top is one of the most common causes of competing blocks. Use a separate tool only for types your SEO plugin does not cover, and check its settings so it does not also output the types your SEO plugin already provides.

3. Can I edit the schema that my SEO plugin creates?

Yes, within limits. Start with the plugin settings, which control things like the organisation name, logo and default article type, and with the per-post schema options. For changes the settings do not allow, the major plugins provide filters that let custom code adjust the data before it is printed, such as the Yoast piece filters and Rank Math json_ld filter. Put that code in a site-specific plugin rather than editing the SEO plugin files.

4. Why do apostrophes in my JSON-LD look like &#8217;?

WordPress passes titles through a typography filter that converts straight apostrophes and quotes into curly ones, and they can come out as numeric HTML entities. Inside a JSON-LD script, entities are not decoded, so the entity code appears in the value. Custom code should decode the title with html_entity_decode before adding it to the data array, or read the raw title field. The block remains valid JSON, but the name is wrong.

5. WooCommerce and my SEO plugin both output Product schema. Which should I keep?

Keep whichever gives the most complete and accurate product data for your shop, then switch the other off. Many SEO plugins offer a WooCommerce integration that replaces the built-in output, and when enabled it usually handles the switch for you. If not, remove WooCommerce output with its documented hooks in a small custom plugin. Whatever you choose, make sure only one Product block remains per product page and that its price matches the page.

6. Where should custom JSON-LD code go in WordPress?

In a small site-specific plugin or, failing that, in a child theme. Both survive updates to the parent theme and to other plugins, and a site-specific plugin also survives a change of theme. Avoid editing the parent theme functions file, which is overwritten when the theme updates, and avoid pasting code into plugin files. Snippet manager plugins are a reasonable middle ground for small sites, provided you keep a record of what each snippet does.

7. My structured data changed after a plugin update. What should I do?

Run the JSON Checker on a page from each template and compare the findings with your last results. Updates can change identifier formats, add new entities, or switch a feature on by default, and any custom code that referenced the old output may now be pointing at nothing. Check the plugin changelog for schema changes, adjust your filters or settings to match, purge caches, and test again. Testing updates on staging first avoids surprises on the live site.

Check your WordPress JSON-LD

The JSON Checker parses every JSON-LD block on your page, validates the syntax, checks each type against schema.org, verifies required fields, detects conflicting types and tests the result against Google rich result criteria.

Open JSON-LD Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All JSON-LD Fixes  ·  Fixing Conflicting Schema Blocks  ·  AI Schema in WordPress  ·  JSON-LD Schema Guide  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.