Schemata
Clean, opinionated JSON-LD schema.org emitter for WordPress. Organization + WebSite + BreadcrumbList everywhere; pluggable Event / NewsArticle schemas. MIT.
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/rennerdo30/wp-schemata/archive/refs/heads/main.zipClean, opinionated JSON-LD schema.org emitter for WordPress.
Yoast bundles schema with 30+ unrelated features. Schemata is just schema, done right.
Schemata auto-emits the schemas every site needs (Organization, WebSite, BreadcrumbList) and ships pluggable per-post-type schemas (Event, NewsArticle, Article) with sensible defaults — no SEO plugin, no ACF dependency, no upsell prompts.
Why Schemata
| Yoast | RankMath | Schemata | |
|---|---|---|---|
| Plugin scope | SEO + schema + redirects + breadcrumbs + readability + … | SEO + schema + analytics + AI + redirects + … | Schema only |
Free Event schema |
No | Yes (often invalid) | Yes (validated) |
| Themeable via filters | Limited | Limited | Every payload |
| Required-field validator | No | No | Yes (logs in WP_DEBUG) |
| Dependencies | Yoast\WP\Free, etc. |
RankMath core, etc. | None |
| LOC | 100k+ | 60k+ | < 900 |
Quick start
- Drop the plugin into
wp-content/plugins/schemata/(or upload the zip). - Activate.
- Settings → Schemata → fill in Organization name, logo, and Same-As URLs.
- Done — JSON-LD ships on every front-end page.
What's auto-emitted
| Schema | When |
|---|---|
Organization |
every front-end page |
WebSite |
every front-end page (with searchbox) |
BreadcrumbList |
archives + singles (toggle in settings) |
Event |
is_singular($event_post_types) |
NewsArticle |
is_singular($news_post_types) |
Article |
is_singular('post') fallback |
The post-type → schema-class map is configurable in Settings → Schemata, or at runtime via the schemata/post_type_schema filter (see below).
Customizing the payloads
Every schema runs through two filters before emit. Filters carry the (array $data, ?int $post_id) signature; $post_id is null on site-wide schemas (Organization, WebSite).
// Append a custom field to the Event schema for a singular event:
add_filter('schemata/event/data', function (array $data, ?int $post_id): array {
$data['offers'] = [
'@type' => 'Offer',
'url' => get_field('buy_link', $post_id),
'price' => (string) get_field('ticket_price', $post_id),
'priceCurrency' => 'JPY',
'availability' => 'https://schema.org/InStock',
];
return $data;
}, 10, 2);
// Inject sameAs URLs from theme Customizer mods on Organization:
add_filter('schemata/organization/data', function (array $data): array {
$data['sameAs'] = array_filter([
get_theme_mod('social_twitter'),
get_theme_mod('social_facebook'),
get_theme_mod('social_instagram'),
]);
return $data;
});
// Suppress the WebSite searchbox on a specific theme:
add_filter('schemata/website/should_emit', '__return_false');
Filter slugs
Per-schema:
schemata/organization/{data,should_emit}schemata/website/{data,should_emit}schemata/breadcrumb_list/{data,should_emit}schemata/event/{data,should_emit}schemata/news_article/{data,should_emit}schemata/article/{data,should_emit}
Cross-cutting:
schemata/post_type_schema— override the post-type → schema-class map at runtime. Receives the schema slug ('event','news_article','article',''for none) and the post type; return the slug to use.schemata/emit_custom— fires for each emitted schema with the slug + post ID. Useful for logging or appending side-channel JSON-LD.
Validator
Schemata\Schema\Validator::missing($payload) returns the list of missing required fields per @type. The emitter calls Validator::validate() on every payload before output. In WP_DEBUG mode any missing field is logged to error_log so theme authors can spot bad data without the page breaking.
// Build a payload, then verify before sending it somewhere else:
$missing = \Schemata\Schema\Validator::missing($data);
if ($missing !== []) {
error_log('Bad event data: missing ' . implode(', ', $missing));
}
Required-field rules live in src/Schema/Validator.php.
Defaults
Schemata reads sensible fallbacks if Settings is empty:
- Organization name →
get_bloginfo('name') - Organization URL →
home_url() - Organization logo →
get_site_icon_url()→ custom-logo theme mod - Same-As URLs → empty
- Post-type map →
post → NewsArticle,event → Event,news → NewsArticle,page → none
Known limitations
- No
Product/Recipe/LocalBusinessschemas yet. Add a custom emitter viaschemata/emit_customif you need them, or open an issue. - One schema per post type. A post can only resolve to one of
Event/NewsArticle/Article. To emit multiple types per page, add the extra payload via thedatafilter on whatever schema is already active. - No archive-level schema beyond
BreadcrumbList.CollectionPagefor category archives is a possible future addition. - Validator is required-fields only. It catches missing fields, not invalid types or malformed URLs. Use Google's Rich Results Test for full structural validation.
Tests
composer install
composer test
# or, no composer:
php tests/ValidatorTest.php
License
MIT — Copyright 2026 Renner.
See LICENSE.