WP Manifestindependent plugin directory
manifest / seo / wp-schemata

Schemata

Clean, opinionated JSON-LD schema.org emitter for WordPress. Organization + WebSite + BreadcrumbList everywhere; pluggable Event / NewsArticle schemas. MIT.

by renner.dev · github.com/rennerdo30/wp-schemata · website

0stars
0forks

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.zip

Clean, 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

  1. Drop the plugin into wp-content/plugins/schemata/ (or upload the zip).
  2. Activate.
  3. Settings → Schemata → fill in Organization name, logo, and Same-As URLs.
  4. 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 / LocalBusiness schemas yet. Add a custom emitter via schemata/emit_custom if 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 the data filter on whatever schema is already active.
  • No archive-level schema beyond BreadcrumbList. CollectionPage for 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.