---
title: "Adding Schema in WordPress Without Duplicating What Yoast Already Writes"
url: https://hostmy.blog/schema-plugin-wordpress/
date: 2026-09-13
modified: 2026-09-03
lang: en
author: "Aditya Sharma"
description: "Find out what schema your SEO plugin already emits, then add only the AI-era types it misses. With a runnable script to inspect any page."
categories:
  - "RankReady"
image: https://hostmy.blog/wp-content/uploads/2026/09/hmb-card-1861-1024x538.jpg
word_count: 1449
---

# Adding Schema in WordPress Without Duplicating What Yoast Already Writes

A site audited here in June was emitting four JSON-LD blocks on every post. Two of them declared an `Article`. Those two disagreed about who the author was, because one pulled the WordPress user and the other pulled a custom field nobody had maintained since 2023.

That is the normal failure when you install a second schema plugin. Nothing errors. Nothing warns you. You just start publishing two contradictory claims about the same page and hope a parser picks the right one.

So the sequence below is inspect first, then add only what is missing. Schema is one job among several on [the WordPress side of AI search work](/ai-search-optimization-wordpress/), and most sites need far less new schema than they think, and far more consistency than they have.

## Find out what you already emit before adding anything

Rank Math, Yoast, AIOSEO and SEOPress all write structured data by default, and that overlap is behind [the redundancy question Rank Math users raise](/rank-math-alternative/). Not the same set, and not in the same shape, so you cannot assume from the plugin name.

Start with a count:

`curl -s https://yoursite.com/your-post/ | grep -c 'application/ld+json'`

One block is healthy. Two is common and usually fine if they describe different things. Four is the case above.

Then read what those blocks actually declare. Save the page and run this:

`curl -s https://yoursite.com/your-post/ -o /tmp/page.html

python3 - <<'PY'
import re, json
html = open('/tmp/page.html').read()
blocks = re.findall(r'<script[^>]*application/ld\+json[^>]*>(.*?)</script>', html, re.S)
print(len(blocks), 'JSON-LD block(s) found')
for i, raw in enumerate(blocks, 1):
try:
data = json.loads(raw)
except json.JSONDecodeError as err:
print(i, 'INVALID JSON:', err)
continue
nodes = data.get('@graph', [data]) if isinstance(data, dict) else data
types = [n.get('@type') for n in nodes if isinstance(n, dict)]
print(i, types)
PY`

The output is a list of types per block. That list is your real starting point, and it is usually different from what the plugin's settings screen implies.

If you see `INVALID JSON` on any block, stop and fix that first. A broken block is worse than a missing one, because some parsers discard the whole page's structured data when one script fails.

## Duplicate types are the actual problem, not missing ones

Here is what a healthy WordPress post looks like in practice.

| Type | Who usually writes it | Should you add it again |
| ---- | --------------------- | ----------------------- |
| `Article` or `BlogPosting` | Yoast, Rank Math, AIOSEO, SEOPress | No |
| `BreadcrumbList` | Your SEO plugin | No |
| `WebSite` and `Organization` | Your SEO plugin, sitewide | No |
| `Person` | Sometimes, often thin | Extend rather than replace |
| `FAQPage` | Only if you built the FAQ block | Yes, if you have real questions |
| `speakable` | Almost never | Yes |
| `HowTo` | Rarely, and only where genuine | Yes, where genuine |

One URL, one Article node
One URL, one Article node
One plugin emitting schema
Two plugins emitting schema

Article nodes on the URL
1
2

author
one Person
two, naming different authors

mainEntityOfPage
one WebPage
two, competing

What a parser does
reads one entity
picks one, or neither

Two plugins each emitting a correct Article block produce an incorrect page. The parser has no rule for choosing between them.

The right mental model is a single connected graph per page, not a pile of scripts. Your SEO plugin already publishes an `@graph` with `@id` values that reference each other. New schema should either join that graph or describe something the graph does not cover.

Two independent `Article` nodes on one URL is the failure state, and a second general SEO plugin is the usual cause, which is the practical half of [the case for a narrow AI layer on top of Yoast](/yoast-alternative/). A separate `FAQPage` block alongside an existing `Article` graph is fine, because they describe different things.

## Turn duplicate detection into one command

Once you know the shape, you can check the whole site rather than one post. Pull your sitemap and sample it:

`curl -s https://yoursite.com/post-sitemap.xml \
| grep -o '<loc>[^<]*</loc>' \
| sed 's/<[^>]*>//g' \
| head -25 \
| while read -r url; do
n=$(curl -s "$url" | grep -c '"@type": *"\(Article\|BlogPosting\)"')
printf '%s article nodes %s\n' "$n" "$url"
done`

Any line starting with `2` or higher is a page publishing contradictory claims about itself. That output has found more real problems for me than any schema testing tool, because testing tools validate one URL at a time and the failures are always systemic.

## The four types that matter for AI readers

Schema for rich results and schema for machine readers overlap, but the priorities differ, and [the markup that holds up when a machine lifts it](/schema-markup-guide-ai/) is a narrower set than the full vocabulary. Four types carry most of the weight.

**Article** establishes what the page is, who wrote it, when it was published and when it was last modified. That last field matters more than people assume. Stale pages are roughly three times more likely to lose citations they already had, and `dateModified` is how a machine knows the difference between a maintained page and an abandoned one.

**FAQPage** attaches question and answer pairs to the page as data rather than prose. This is the one people abuse, and there is a real cost, covered below.

**speakable** marks which sentences on the page are the clean, quotable ones. It is a property rather than a type, applied with a CSS selector or an XPath, and it is the closest thing structured data has to saying "quote this part".

**Person** gives the author an identity that resolves. A `Person` node with a `sameAs` array pointing at real profiles is meaningfully different from a string with a name in it.

The full breakdown of what each one is for, with the markup, sits in [Article, FAQPage, Speakable and Person](/structured-data-wordpress/). This post is about not breaking things while you add them.

## Cap your FAQ entries, do not maximise them

The instinct with FAQ schema is to add twenty questions because more data must be better. The evidence points the other way.

Sequential heading order, H2 to H3 to H4 with no skipped levels, correlates with a 2.8x lift. FAQ blocks generate headings, so a page with twenty FAQ entries is paying that cost twenty times over.

Four to six real questions is the working range on the sites here. Real means someone actually asked it. Padding an FAQ block with questions invented to fill the block is a measurable cost with no offsetting benefit.

## Match the schema to the visible page or remove it

One rule holds across every type. If the markup claims something the page does not visibly say, take the markup out.

FAQ schema on a page with no visible FAQ is the most common version. A `Person` node crediting an author whose name appears nowhere on the page is the second. Both are trivially detectable and both damage the thing you were trying to build, which is a page a machine can trust.

The check is manual and takes a minute. Read the JSON, then read the page, then ask whether a person could verify every claim in the JSON by looking at the page.

## The five minute version on WordPress

Doing this by hand means filter callbacks into your SEO plugin's graph, per post fields for the FAQ entries, and a `speakable` selector that survives your theme's markup changing. All of it lives in `functions.php` until the day a theme update quietly moves the class name.

That is the work [RankReady](https://wordpress.org/plugins/rankready-ai-llm-seo/) takes over. It is a WordPress AI SEO plugin, and the design point is that it runs alongside Rank Math, Yoast, AIOSEO or SEOPress rather than competing with them. Your SEO plugin keeps writing `Article`, `BreadcrumbList` and `Organization`. RankReady adds the AI-era layer on top and stays out of the nodes your SEO plugin already owns. Setup is about five minutes.

What this delivers is a page that is cleanly parseable and internally consistent. Whether a model then cites you is a separate question that no plugin gets to answer, and any vendor telling you otherwise is selling something.

## Verify after every change, not once at the start

Run the count command again after you add anything. Then run Google's Rich Results Test on the same URL, because a page can be valid JSON and still describe a type incorrectly. If you would rather not depend on that tool, the same validation runs locally with [curl and a short Python script](/check-schema-without-google/).

The failure mode worth watching for is a theme or plugin update reintroducing a duplicate block months later. The sitemap loop above takes seconds and is worth putting on a quarterly reminder.

Once your schema is consistent, the layer that usually matters more is whether the crawlers can reach the page at all, and that is the first dependency in [the layered version of this check](/five-layer-ai-seo-audit/).

How many JSON-LD blocks does your highest traffic post actually emit right now, and do they agree with each other?