Why "how to write correct JSON-LD" is the wrong question
People searching for "how to write JSON-LD" usually end up on a property reference, a copy-pasted example, and a validator that returns a positive result. The problem is that the green light certifies grammar, not meaning. And meaning is exactly what Google and AI engines read.
The key difference is therefore what Syntax (the JSON is valid), Semantics (the router connects the pages) and Ontology (the Knowledge Graph understands what we sell) each tell us.
The relationships declared in Schema.org are assertions about how the site is built.
When they are wrong — when a hasPart declares a containment that does not exist,
when a breadcrumb lives inside an Article instead of the WebPage, when
an @id points to a node that was never defined — no automatic syntax-validation tool
flags it. Yet the graph that machines build contains those errors.
→ Automated validators
→ Human audit
→ Effects on the Knowledge Graph
Where this method comes from
The semantic-audit methodology did not come from a theoretical hypothesis. It formed by accumulation: one error at a time, a template that generated the same problem across hundreds of pages, a fix that revealed the next pattern. There was no initial phase in which someone "designed" this architecture: there was a path of discovery, and this document is the formalisation of what worked.
From that experience came a precise conviction: validators check grammar, not
meaning. A hasPart pointing to an external page passes validation; a
breadcrumb inside an Article passes validation; an @id pointing to a
non-existent node passes validation. The green light says "syntax OK", not "coherent ontology".
When the number of pages exceeds the capacity to check them one by one, errors do not disappear — they change shape, becoming systemic patterns instead of isolated typos. Architecture, not the single page, is what must be designed with care.
What a Schema.org audit analyses
The audit does not stop at checking whether the code is valid. It analyses the graph as a whole and verifies that it holds up logically. Here are the areas it checks.
1. Type coherence
Is every @type declared only once in the graph, without contradictions? Is the same @id never classified in one place as WebPage and in another as WebApplication? This is the Ontological Chimera: the validator does not flag it, but the AI reading the graph stalls in front of two incompatible types on the same node.
2. @id resolvability
Does every referenced @id actually exist somewhere — in the same @graph or on a reachable page? This analysis hunts down Dangling Nodes: edges pointing to nothing. On one page it is a detail; on 250 pages it is a pattern that weakens the entire graph.
3. Property appropriateness
Is containment declared with hasPart only when it is real? Do external thematic links use significantLink? Is the breadcrumb in the WebPage or has it crept inside the Article? These are all choices that pass the validator but change the meaning of the graph.
4. Data parity
Does every value in the JSON-LD (rating, price, claim) also appear in the DOM visible to the user? Hidden markup is a violation of Google's guidelines and can lead to a manual action for misleading markup.
5. Container / content separation
Do navigation loops (menu, related, hub) point only to containers (#webpage) or also to final content (#article, #app)? A loop that forces the WebPage type onto a software ID is an error invisible to the validator but lethal to the graph.
6. Fragment naming convention
Does every page follow a rigid convention for fragments (#webpage, #article, #breadcrumb, #organization)? An effective audit checks that @id values are consistent across the site's pages, without surprises generated by different templates.
7. Diagnostics of resolution conflicts
When an error seems to make no sense — because two structurally identical pages produce different outcomes — the cause lies in reference resolution, not in the page. The same @id appearing in multiple properties with different ranges, an array with mixed elements (typed and untyped), an unrecognised custom fragment: these are all mechanisms that generate downstream errors, apparently unrelated to the real cause. The Diagnostics chapter covers the method to isolate them.
The operating rules: how to write JSON-LD that holds the graph together
Nineteen patterns that recur in production, with examples of correct and incorrect code. Every rule exists because a real case generated it: a template that got it wrong on 250 pages, an audit that revealed a Dangling Node, a validator that gave a green light to an ontologically broken relationship.
1. Containment vs linking: hasPart vs significantLink
hasPart indicates literal composition: X physically contains Y.
significantLink indicates editorial relevance: Y is one of the most important
links on the page, but it is not contained in X.
- Use
hasPart: CollectionPage → its child pages, Book → Chapter, Course → Lesson, Article → FAQPage. - Use
significantLink: WebPage → related external pages, Article → cited sources, Hub → thematic siblings.
Operating test: translate the relationship into a sentence ("X physically
contains Y"). If it sounds forced, the correct property is significantLink.
2. The syntactic trap of significantLink
significantLink accepts strings only. Not Reference Nodes, not objects.
If you pass {"@id": "..."} the Schema.org validator returns a blocking error.
// ❌ Validator error
"significantLink": [{ "@id": "https://site.com/page/#article" }]
// ✅ Correct
"significantLink": ["https://site.com/page/#article"]
Critical: always include the final fragment (#article,
#app, #webpage) in the string. The syntax stays valid and the AI
knows exactly which node you are pointing to, avoiding Dangling Nodes.
3. The breadcrumb: where it really belongs
The breadcrumb belongs to the WebPage, not to the Article. The
reason is ontological: the breadcrumb describes a URL path, and URLs belong to pages, not to
content. Many sites put it inside the Article and Google still shows the path in the SERP —
hence the false perception that it is fine. At scale it becomes a systemic pattern.
// ✅ Correct pattern
{
"@type": "WebPage",
"@id": "/page/#webpage",
"breadcrumb": { "@id": "/page/#breadcrumb" },
"mainEntity": { "@id": "/page/#article" }
}
Architectural rule: assign the breadcrumb at the page-template level, not the
entity level. In WordPress → single.php; in React → page layout, not content
component.
4. Reference Node vs Stub Node
When you reference a node defined elsewhere, use only @id. Adding
@type without the essential properties (name,
publisher, etc.) creates a partial entity, which at scale generates ambiguity
about which is the authoritative version of the entity.
// ❌ STUB — creates a partial entity
"hasPart": [
{ "@type": "WebPage", "@id": "/p1/#webpage", "url": "/p1/" }
]
// ✅ REFERENCE NODE — flattened graph
"hasPart": [
{ "@id": "https://site.com/p1/#webpage" }
]
5. ItemList: when to generate it and when not to
ItemList is a type, not a property. It should only be generated when the page is
structurally a list: hubs, category indexes, rankings.
- Yes: hub pages, categories, rankings.
- No: single articles, products, tools. And never for FAQ sections — those use
FAQPagewith a question/answer structure.
Every ListItem must have a readable name. A bare URL is a weak signal.
6. Mixed data formats: handle both
A property can be populated in two valid forms. The generator must recognise both.
// Format A
["/page-a/", "/page-b/"]
// Format B
[{ "id": "/page-a/", "type": "..." }]
If the code expects only one format, it works until someone — maybe months later, maybe a different collaborator — writes the list in the other. The markup silently stops being valid.
7. ID conflicts: the sameAs bridge
If you inject a global site-wide Organization and a richer
Organization on the "About Us" page with a different @id, Google may
treat them as two distinct entities. Do not modify the global template: link the two entities
with sameAs.
{
"@type": "Organization",
"@id": "https://site.com/#org-detail",
"sameAs": "https://site.com/#org"
}
8. The Ontological Chimera
An ID declared as WebApplication in one part of the graph and as
WebPage in another is a logical contradiction. The validator stays silent, the AI
stalls in front of two incompatible types on the same node. The most frequent cause: a
navigation loop that forces @type: "WebPage" onto every element, even when the
ID belongs elsewhere to software.
Rule: navigation loops (menu, related, hub) must point only to containers
(#webpage), never to final content (#article, #app).
9. Target typing and rich snippets
@type: "WebApplication" is semantically correct but Google requires the
SoftwareApplication class plus operatingSystem for the rich result.
A single type is not enough. Use multiple inheritance.
"@type": ["SoftwareApplication", "WebApplication"],
"operatingSystem": "Web Browser"
Same pattern when a range constraint blocks a semantically valid link:
// teaches expects Text or DefinedTerm
"@type": ["WebApplication", "DefinedTerm"],
"@id": "/software/#app",
"name": "Advanced SEO Protocol"
10. Container ≠ content
A page that hosts a tool is not the tool. Separate the entities with mainEntity.
// ❌
"@type": ["WebPage", "WebApplication"]
// ✅
{
"@type": "WebPage",
"@id": "/tool/#webpage",
"mainEntity": {
"@type": "WebApplication",
"@id": "/tool/#app",
"name": "SEO Calculator"
}
}
Anti-pattern: applying WebPage or Article
indiscriminately in a shared header. Fix it with semantic routing — the type depends on the
data, not just the layout.
11. about vs mentions
about→ main topic (1–2 entities max).mentions→ secondary references.
Too many about values dilute the signal: the page competes for the wrong queries.
12. Data parity
Every value in the JSON-LD must be visible in the rendered DOM. If you declare
aggregateRating: 4.8 and the DOM has no badge showing "4.8", you are violating
Google's guidelines. Risk: manual action for misleading markup.
- If you declare
price: 0, the page must say "Free" or "0€". - If you declare
ratingValue: 4.8, there must be a visible badge showing "4.8". - Purely graphical icons must be marked
aria-hidden="true".
13. Fragment naming convention
Adopt a rigid convention and check it periodically. Example:
#webpage→ page container#article→ article entity#app→ application entity#breadcrumb→ breadcrumb entity#organization→ organization
Validators do not open URLs: they do not verify that the @id actually exists on
the other side. A Fragment Audit — extracting all @id values used in navigation
loops and verifying that they point to containers — is the only way to catch them.
14. hasPart + ItemList: the index trap
Do not put an ItemList inside hasPart. ItemList is
classified as Intangible (an abstract data structure), not as
CreativeWork. A book has chapters, not "a list of data".
// ❌
"hasPart": { "@type": "ItemList", "itemListElement": [...] }
// ✅
"hasPart": [
{ "@id": "https://site.com/chapter-1/#webpage" },
{ "@id": "https://site.com/chapter-2/#webpage" }
]
15. The bidirectional CollectionPage pattern
On a true Topic Cluster — a Pillar Page that organises a series of deep dives — bidirectionality is valuable:
- The CollectionPage (parent) declares
hasParttowards the children. - Every child page declares
isPartOftowards the parent. - Both with pure Reference Nodes (absolute
@id, no@type).
16. Global identifiers: citing third parties
If you cite third-party software (isBasedOn: SoftwareApplication), Google
activates the commercial rich-snippet filters and floods you with warnings for missing
offers and operatingSystem. The lazy solution is to downgrade to
Thing — which lobotomises the graph. The correct solution is to point to a global
IRI.
"isBasedOn": {
"@type": "SoftwareApplication",
"@id": "https://www.wikidata.org/wiki/Q161676",
"name": "WordPress"
}
The bot recognises an external citation, not a local distribution: the commercial filters are
disarmed, and the @type is preserved.
17. Cross-property @id conflict
When the same @id appears in multiple properties with different
ranges within the same graph, and is not defined locally with an explicit
@type, the validator resolves it only once — at the first
occurrence — and propagates the resulting type to all the others. If the resolved type is weak
(Thing), the downgrade can invalidate properties that require stricter ranges such
as CreativeWork.
// ❌ Conflict: same @id in hasPart (requires CreativeWork)
// and in isRelatedTo (free range), with no local @type
{
"@graph": [
{
"@type": "CollectionPage",
"hasPart": [
{ "@id": "https://site.com/page/#article" }
]
},
{
"@type": "Service",
"isRelatedTo": [
{ "@id": "https://site.com/page/#article" }
]
}
]
}
// ✅ Solution: differentiate the fragments
// hasPart points to the content (#article)
// isRelatedTo points to the container (#webpage)
{
"@graph": [
{
"@type": "CollectionPage",
"hasPart": [
{ "@id": "https://site.com/page/#article" }
]
},
{
"@type": "Service",
"isRelatedTo": [
{ "@id": "https://site.com/page/#webpage" }
]
}
]
}
The diagnostic signal is not the error message, which appears downstream at a
seemingly unrelated point. It is the presence of the same @id in multiple
properties. When you find that pattern, look there for the cause before looking
elsewhere.
18. Fragment auto-inference: the fragile dependency
Pure Reference Nodes ({"@id": "..."}) work only if the validator
can resolve the @id. There are three levels of resolution, in priority order:
- Local definition: the
@idexists in the@graphof the current page as an object with an explicit@type. Resolution guaranteed. - Fragment auto-inference: if the fragment is in a whitelist of known conventions (
#webpage→WebPage,#article→Article,#faq→FAQPage,#breadcrumb→BreadcrumbList), the validator infers the type. - Downgrade to
Thing: if the fragment is custom and there is no local definition, the type isThing. This can invalidate properties with stricter ranges.
Practical consequence: pure Reference Nodes work "by luck" when they use
standard fragments. One custom fragment anywhere in the graph and the mechanism breaks
silently. An effective audit verifies that every referenced @id is either
whitelisted or explicitly defined in the local graph.
19. Index as a view, not a container
A page that lists other pages does not contain them. They are two ontologically different relationships:
- Containment (
hasPart): the container is physically made of its parts. A book contains chapters. - Listing (
significantLink): the page refers to external resources without containing them. An index lists the chapters of a book.
The temptation to use hasPart on an index is strong ("the index contains the
chapters"). It is ontologically wrong: the chapters are part of the book,
not of the index that lists them.
// ❌ Index with hasPart towards the chapters
{
"@type": "CollectionPage",
"@id": "/index/#webpage",
"hasPart": [
{ "@id": "/chapter-1/#webpage" },
{ "@id": "/chapter-2/#webpage" }
]
}
// ✅ Index with significantLink (strings) towards the chapters
{
"@type": "CollectionPage",
"@id": "/index/#webpage",
"significantLink": [
"https://site.com/chapter-1/#webpage",
"https://site.com/chapter-2/#webpage"
]
}
Containment stays on the real container (the book, the work, the collection). The index is a view that exposes the list, not a physical container.
Consequence for narrative patterns: in branching content (interactive
path, step-by-step tutorial, decision tree), each node belongs to the previous node with
isPartOf, and the subsequent nodes are declared with significantLink.
The index remains the overview view with significantLink towards all nodes. No
hasPart.
Diagnostics: when two identical structures give different outcomes
There is a moment in every audit when two apparently identical pages get opposite results from the validator. One passes, the other does not. The code looks the same. The instinctive reaction is to think of a validator bug, a cache, a temporary problem.
It is none of those. The reason is that validators resolve references contextually, not in isolation: it depends on what is in the rest of the graph, not just on what you write at the specific point.
The principle: resolve once, propagate
When a validator encounters a reference to an @id, it resolves it only
once — at the first occurrence — and uses the result for all subsequent
occurrences. The decision is not re-evaluated for each property.
If the first occurrence is in a context that does not allow the type to be determined
(because the @id is not defined locally and the fragment is not recognisable),
the validator classifies it as Thing. That Thing is then propagated
to the other properties that reference the same @id.
If one of those properties requires a more specific range (hasPart requires
CreativeWork, for example), the Thing is not compatible and the
validator reports the error on the wrong property — apparently unrelated to
the real cause.
The suspicious pattern: same @id in multiple properties
The most important diagnostic signal is not the error message, it is the presence of
the same @id in more than one property within the same graph.
When you find this pattern, check two things:
- Is the
@iddefined locally with an explicit@type? If yes, no problem: resolution is guaranteed and there is no downgrade. - Do the properties using it have compatible ranges? If one is
hasPart(restricted range) and the other is a generic property, the stricter range requires a stronger type thanThing.
If the @id is not defined locally and the properties have incompatible ranges,
you have found the cause. Differentiate the fragments: one property points
to the content (#article), the other to the container (#webpage).
They are two different entities by nature, and the conflict is resolved without touching the
article's type.
Internal inconsistency within arrays
A related and subtle case: an array (hasPart, mentions,
isRelatedTo) in which some elements have an explicit @type
and others do not. The validator looks for internal coherence, and in its absence:
- It may downgrade the typed elements to the weakest type among the others
- It may report errors on the untyped elements even though the problem is uniformity
- The error message may not name the element that is actually problematic
Operating rule: in an array, elements must be all typed or
all without @type. Never mixed. A mixed array is an array that
hides problems.
URL parent vs semantic parent
Two different concepts that are often confused, and that validators do not distinguish.
The URL parent is physical: it is obtained by cutting the last segment of the URL. Every route has one, whether or not it exists as a public page.
The semantic parent is the one declared in isPartOf. It does
not always coincide with the URL parent, and it must not.
Typical examples of intentional divergence:
- A service page may be a structural child of a mini-hub and thematically related (via
about) to an encyclopaedic page - A volume may belong to its series, not to the index page of the volumes
- A tool may be a child of a tools centre rather than of its URL category
- A top-level page may refer to the home page, even though its "natural" URL parent would be a prefix that is not a public page
The diagnostic test: compare the physical parent URL and the declared
isPartOf. Divergences are not automatically errors. They are choices.
But they must be checked one by one and classified into two categories: deliberate
choices and likely errors.
Four-step verification method
Extract the JSON-LD of the problematic page. Identify all entities with an explicit @type and all @id references without a local definition.
Build a map @id → [properties using it]. Identify the @id values that appear in more than one property.
For every conflicting @id: is it defined locally with an @type? If not, is the fragment in the whitelist of known conventions? If neither, differentiate the fragments across properties, or add an explicit definition.
For every @id referenced but not defined locally, verify that the fragment is in the whitelist. If it is not, add an explicit definition or change the fragment.
A practical case (generic)
Two pages with a virtually identical structure:
- Both children of the same parent page
- Both referenced in the parent's
hasPart - Both with their own entity of type
TechArticle - Both without
@typein the reference
One passes the validator, the other does not. No apparent difference in the code of the two pages.
Diagnosis: the failing page is also referenced by another
property — for example the isRelatedTo of a Service — with
the same @id. The validator resolved that @id at the first
occurrence (in isRelatedTo) and classified it as Thing because it
was not defined locally, then propagated the Thing to all other occurrences —
including the one in hasPart, which requires a CreativeWork range
and rejects Thing.
Solution: differentiate the two references.
hasPart towards the article (#article),
isRelatedTo towards the page (#webpage). The two entities are
different by nature — content vs container — and the conflict is resolved
without touching the article's type.
Lesson: when a reference is ambiguous, the validator does not flag the
ambiguity: it flags the error downstream, at a point that may seem
unrelated. The useful signal is the presence of the same @id in multiple
properties. That is the pattern to look for first when an error "makes no sense".
Symptom → rule map
For those arriving with an already-identified problem, this table serves as a quick index.
| Symptom | Rule |
|---|---|
| Green validator, missing rich snippet | §9 Target typing |
| "Element without a name" on a nested entity | §4 Reference Node |
| Breadcrumb in the SERP but incoherent graph | §3 WebPage |
Dangling node, orphan @id | §1, §13 |
| Two organizations in the Knowledge Graph | §7 sameAs |
| FAQs treated as a link list | §5 scope |
| Wrong type forced by a loop | §8 Chimera |
| Huge JSON-LD slowing the page down | §4 flattened graph |
ItemList on a single product | §5 scope |
Validator error on significantLink | §2 string only |
| Two identical pages with different validator outcomes | §17, Diagnostics |
| Error on an apparently unrelated property | Diagnostics, Step 2 |
Thing error on a property with a restricted range | §17 |
Index with hasPart towards its entries | §19 |
| Reference Node that works on one page but not on another | §18 |
When in doubt
Translate the JSON-LD triple into a simple sentence (Subject → Verb → Object). If the sentence sounds forced, the property is wrong — regardless of what the validator says.
"This Article physically contains a Breadcrumb" → forced → the breadcrumb belongs to the WebPage.
"This WebPage contains this other external WebPage" → forced → use significantLink.
"This WebPage is a WebApplication" → wrong → use mainEntity.
"This index physically contains its chapters" → forced → the index lists, the book contains.
Final rule: every rule above exists because a validator said "OK" while the Knowledge Graph said "no". Trust the graph, not the green tick.
The operating checklist
Concrete questions that a Semantic Audit asks of every page. It is not a test to tick off once: it is a grid to reapply when a content type is added or a rule is automated.
On the single document
- Is the page's
@idunique and consistent? - Does the breadcrumb live in the
WebPageor in theArticle? - Are containment properties (
hasPart) used only when containment is real? - Do thematic outbound links use
significantLink(string) orhasPart(object)? - Does every referenced
@idexist somewhere in the graph? - Is the declared type coherent with the reality of the page?
- Does the same
@idappear in multiple properties with different ranges? If yes, you have a potential resolution conflict. - Are relational arrays all typed or all untyped? If mixed, you have a potential internal-coherence problem.
On the entire site
- Does every page use the same fragment convention?
- Do navigation loops point only to containers (
#webpage)? - Are there
@idvalues declared as two different types on different pages? - Have Dangling Nodes towards external URLs been eliminated?
- Do master entities live at the root of the
@graph, with references using only@id? - Are exceptions (a page hosting a tool, a course teaching a concept) handled with
mainEntityor polymorphism? - Does every custom fragment (not in the conventions whitelist) have an explicit definition in the local graph?
- Does every index use
significantLink(nothasPart) to list its entries?
What an audit cannot do
To be useful, a method must also declare its limits.
- It does not judge editorial strategy. It verifies that the relationships are coherent with what the site declares itself to be. It does not decide whether the site should have a different structure.
- It does not guarantee the rich snippet. Google has specific requirements for rich results that go beyond the Schema.org vocabulary. An audit can tell you whether the markup is ontologically correct, not whether Google will decide to show the star rating.
- It does not replace human testing. The linguistic test — translating the JSON-LD into a sentence and checking whether it sounds forced — remains a human judgement that no tool can fully automate.
- It does not see the future of Schema.org. The vocabulary evolves. An audit is a snapshot of the current state of the specifications and Google guidelines. It must be repeated when something changes.
- It does not predict how validators will evolve. Fragment auto-inference, for example, is not official documentation: it is an observed behaviour of the most widespread validators. It can change without notice. An audit based on it remains reliable, but must be rechecked periodically.
Who it is for (and who it is not)
It is genuinely useful for editorial platforms with dozens or hundreds of related pages, e-commerce sites with a structured catalogue, topic-cluster architectures, sites with automated templates that generate markup at scale, and projects that must be read by AI.
It is overkill for brochure sites with 5–10 static pages, projects managed by standard plugins (Yoast, RankMath) that already cover Organization, WebSite, WebPage, and single-topic blogs with only one content type. On those projects, a full audit is a disproportionate investment: the plugin is enough, and the ontological part becomes a theoretical exercise with no practical impact.
Further reading: theory & practice
This page presents the audit method. The two linked guides explore it from two complementary angles on the IMWEB mother site.
📘 The theory — Advanced Structured Data The reference guide on the vocabulary:isPartOf, hasPart, significantLink, ItemList, about, mentions. Explains the ontological meaning of each property, the rules of polymorphism and target typing, data parity, and the case study on false positives.
Go to the theoretical guide →
📗 The practice — Semantic Architecture at Scale
The practical follow-up: what happens when theoretically correct patterns are replicated by a template across hundreds of pages. Six real "cracks" encountered in production, six rendering-engine traps, a 4-step debugging framework (with the Fragment Audit), and a data-separation architecture.
Go to the practical guide →
The two guides are complementary and cross-reference each other. Theory without practice remains an intellectual exercise; practice without theory becomes a checklist of rules with no "why".
FAQ
Is following the Schema.org specifications enough to write correct JSON-LD?
Following the specifications is necessary but not sufficient. The specifications tell you which types exist, which properties they accept, and which ranges are valid. They do not tell you whether the relationship you are declaring makes sense for your site. A hasPart pointing to an external page is syntactically valid but ontologically questionable. The validator passes; the graph does not.
Does a Schema.org audit replace Google's Rich Results Test?
No, it complements it. The Rich Results Test checks compatibility with Google rich snippets: it tells you whether the markup is valid according to Search Console standards and whether a rich result will be shown. The semantic audit checks a different level: that the declared relationships are coherent, that the nodes are resolvable, that the graph holds. You can have a perfect rich result and an incoherent graph — and vice versa.
What are Dangling Nodes and why should an audit hunt them down?
A Dangling Node is an @id reference pointing to an entity that does not exist in the graph. In graph theory, an edge pointing to nothing reduces the coherence of the dataset. In practice, when an AI system builds the Knowledge Graph of your site, it finds a reference to an entity it cannot resolve and has to guess or ignore it. Repeated across hundreds of pages, this weakens the clarity of the graph.
Why do two pages with identical structure get different validator outcomes?
Because the validator resolves @id references contextually, not in isolation. If the same @id appears in multiple properties with different ranges and is not defined locally, the validator resolves it only once — at the first occurrence — and propagates the resulting type. If the type is weak (Thing), the downgrade can invalidate properties that require stricter ranges such as CreativeWork. The symptom appears downstream, in a point that seems unrelated.
How often should a Semantic Audit be redone?
Every time one of these three things changes: the Schema.org specifications, Google's guidelines on rich results, or the structure of the site. In practice: a full audit every time a new content type is added, a template that generates markup is modified, or a new section is added to the site. It is not a lifetime certificate — it is a snapshot of the state of the graph, and it must be renewed.
Do I need an audit if I already use an SEO plugin that generates JSON-LD?
It depends. A standard plugin generates correct markup for common cases: Organization, WebSite, WebPage, Article, breadcrumb. It covers the syntax for most pages of a normal site. But it does not know the specific structure of the site: it does not know which pages are hubs, which are single pieces of content, or how they connect to each other. On a small brochure site, the plugin is enough. On a topic cluster project or a catalogue, you need an audit on top of the plugin.