Fixing JSON-LD Errors in WordPress
On a typical WordPress site the JSON-LD comes from several places at once: an SEO plugin, WooCommerce, the theme, a page builder, a reviews or events plugin, and perhaps a block someone pasted into a post years ago.
When the JSON Checker reports a parse error, an invalid type or conflicting blocks, the hard part is often working out which of those produced the faulty code and how to change it without the fix vanishing at the next update. This guide covers tracing each block to its source, the WordPress behaviours that break JSON-LD, and safe ways to correct or extend plugin output.
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.
| Source | What it usually outputs | How 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 Article | Plugin settings, per-post schema options, and the plugin's developer filters |
| WooCommerce | Product with offers and ratings on product pages, plus some shop-level data | WooCommerce filters and actions in a small custom plugin |
| Theme | Varies: sometimes Article or Organization, sometimes old Microdata in the templates | Theme options, or a child theme template override |
| Page builder | Markup from specific widgets such as FAQ, review or business info modules | The widget's own settings |
| Recipe, events, reviews and booking plugins | Recipe, Event, Review, LocalBusiness | That plugin's settings; some offer filters |
| Header and footer snippet plugins | Hand-written blocks, often site-wide | Edit the snippet in the plugin's screen |
| Custom HTML blocks in posts | Hand-written blocks on single pages | Edit the post in the block editor |
| Google Tag Manager | Blocks injected by script after load | The 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:
- Script tags removed on save. WordPress filters post content for users who lack the
unfiltered_htmlcapability. On a standard single site only Administrators and Editors have it; on multisite only Super Admins do. When an Author or an Editor on a multisite network saves a post containing a Custom HTML block with a script tag, the script is stripped and the JSON is left as visible text or lost. - Titles containing HTML entities.
get_the_title()runs the title through WordPress's typography filter, which turns straight quotes and apostrophes into curly ones and may return them as HTML entities such as’. A template that drops that into JSON-LD publishes the entity literally, so a name reads "Tom’s Bakery". - Strings glued together by hand. Templates that build JSON by concatenating strings in PHP break the first time a product name contains a double quote, such as a 6" planter, or a description contains a line break.
- Escaping for the wrong context. Running values through
esc_attr()oresc_html()turns quotes into". That is correct for HTML attributes and wrong inside a JSON-LD script, where entities are not decoded. - Shortcodes and blocks inside values. A description taken from post content can include
unrendered shortcodes, block comments such as
<!-- wp:paragraph -->or HTML tags. None of those belong in a text value. - Empty custom fields. Fields from Advanced Custom Fields or similar plugins that
have never been filled in can produce empty strings,
nullorfalse, giving values such as"price": ""or a date offalse. - Optimisation plugins. Minification, script combining and delayed script loading
features are designed for JavaScript. Most leave JSON-LD alone, but if a block breaks only when optimisation
is on, exclude
application/ld+jsonscripts from those features.
Tracing a block to its source
WordPress sites rarely label their markup clearly, but there are reliable clues:
- 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 containingrank-math-schema. Other plugins often add their own class or an id. - HTML comments. Many SEO plugins wrap their head output in comments naming the plugin. The block between those comments belongs to it.
- 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 throughwp_footer; blocks in the middle of the content come from the post itself or a page builder widget. - Query Monitor. This free plugin lists the functions attached to each hook, including
wp_headandwp_footer, with the plugin or theme each belongs to. It narrows the search quickly on a busy site. - 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.
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.
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
- Keep custom schema code in one site-specific plugin. It survives theme changes, can be version-controlled, and gives you one place to look.
- Test updates on staging first. SEO plugin and WooCommerce updates occasionally change schema output. Run the JSON Checker on staging after updating and before going live.
- Check new plugins for markup. Reviews, events, recipe and booking plugins often add structured data by default. Look at their settings the day you install them.
- Keep JSON-LD out of the content editor. Hand-written blocks in posts are invisible to anyone reviewing the theme and are easily damaged by role restrictions.
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.
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 ’?
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.
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 โ