Bryan Hamilton / projects / jsonld-schema-audit

bryanhamilton.info / open source / jsonld-schema-audit

jsonld-schema-audit Hundreds of warnings to zero.

The structured data behind Google's rich results breaks silently. This zero-dependency PHP tool took The DJ Calendar from hundreds of Rich Results warnings to zero across ~1,000 pages, and it runs in CI so broken schema can never ship again.

  • One PHP file
  • Fails CI on regressions
  • Audits itself
  • MIT license
In brief: jsonld-schema-audit is a zero-dependency PHP command-line tool that checks HTML JSON-LD for missing required schema.org properties, unresolved internal @id links, and parse errors. Its exit codes make it suitable for continuous-integration checks.

Run an audit

Paste HTML or JSON-LD, or pick an example

Try:

Output

Schema Audit CI—

Why it exists

On a large site, the most common structured-data failure is silent. An event says its venue is #venue and its performer is #group, pointing at them by @id, but the venue or performer node was never actually included in the page. To a person the page looks perfect. To Google, the event's location doesn't exist.

Google's Rich Results Test shows the symptom ("references missing @id") one page at a time. Nothing tells you the root cause across a thousand pages, and nothing stops the same mistake from coming back next week. This tool does both.

Three checks in one pass

1. Required properties
Each schema.org type has properties it can't do without: a MusicEvent needs a performer, a Question needs an answer. Every node is checked against its type.
2. @id resolution
Every internal #fragment reference must point to a node that actually exists in the same graph. "Mentioned" isn't enough: it has to be defined.
3. Parse errors
A JSON-LD block with a stray comma is invisible to Google. It's reported as an error instead of being silently skipped.
The exit code
0 means clean, 1 means errors. That one number is what lets it guard a CI pipeline.

Why the @id check takes two passes

A reference can appear before the node it points to: an event near the top of the graph can point at a venue defined at the bottom. Checking references in a single top-to-bottom pass would report false errors. So the checker first walks the whole graph and indexes every node that's actually defined with an @id, then walks it again checking every reference against that index.

// Pass 1: index every node that is defined (has an @id AND content of its own)
foreach (allNodes($graph) as $node) {
    if (isset($node['@id']) && count($node) > 1) $defined[$node['@id']] = true;
}
// Pass 2: every bare {"@id": ...} reference must point at something defined
foreach (allReferences($graph) as [$type, $prop, $ref]) {
    if (str_contains($ref, '#') && !isset($defined[$ref])) {
        $errors[] = "[$type] property \"$prop\" references unresolved @id \"$ref\"";
    }
}

Simplified sketch. The real implementation is one file, audit.php on GitHub. The demo above is a JavaScript version of the same three checks, using a representative set of required properties per type.

Usage

php audit.php path/to/html/            # audit a directory (*.html, *.php)
php audit.php page.html                # audit one file
php audit.php ./ --report=out.txt      # write errors to a file
$ php audit.php tests/fixtures/broken.html
=== SCHEMA AUDIT: 2 error(s) across 1 block(s) ===

tests/fixtures/broken.html  [JSON-LD block 1]
  [MusicEvent] missing required property "performer" on https://example.com/event/2#event
  [MusicEvent] property "@id" references unresolved @id "https://example.com/venue/ghost#venue"

$ php audit.php tests/fixtures/good.html
=== SCHEMA AUDIT: clean. 0 errors across 1 file(s). ===

From the repo's README. PHP 7.4+, no extensions, no Composer: one file you can drop anywhere.

A tool that tests itself

The repo audits itself on every push, and the CI checks both directions: the good fixture must pass and the broken fixture must fail. Most test suites only prove the happy path. This one also proves the tool still catches the exact failure it was built for. If it ever stops catching it, CI goes red.

The same discipline on this site

Every page on bryanhamilton.info carries structured data: a profile page, your Person and the projects you've written, the FAQ, and a breadcrumb trail and software entry on each of these project pages. Every reference between them resolves. Press bryanhamilton.info homepage or This page above to audit them yourself.

Frequently asked questions

What does jsonld-schema-audit check?

The PHP CLI checks required properties for supported schema.org types, unresolved internal @id fragment references, and JSON-LD blocks that cannot be parsed.

How does it check internal @id references?

It first indexes nodes that define an @id, then checks references against that index so a reference only passes when its target node is actually defined in the graph.

Can jsonld-schema-audit run in continuous integration?

Yes. A clean audit exits with code 0 and findings exit with code 1, allowing CI to fail when structured-data regressions are detected.

Does this tool replace Google’s Rich Results Test?

No. It complements page-level testing by auditing structural issues such as missing required properties, unresolved graph references, and malformed JSON-LD; it does not guarantee search eligibility or rankings.

Technical SEO that holds

Bryan Hamilton is a full-stack web engineer with 25+ years in PHP and JavaScript, available immediately for remote, hybrid or on-site roles in the NYC metro area.