{"id":50471984,"url":"https://github.com/marcinzmuda/ligase-schema-markup","last_synced_at":"2026-06-01T11:01:08.414Z","repository":{"id":347835225,"uuid":"1195435728","full_name":"MarcinZmuda/Ligase-Schema-Markup","owner":"MarcinZmuda","description":"Complete schema.org JSON-LD for WordPress blogs. Entity graph, AI search optimization, and schema auditor - all in one plugin.","archived":false,"fork":false,"pushed_at":"2026-05-28T20:15:39.000Z","size":517,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-28T20:17:38.727Z","etag":null,"topics":["schema","seo","seo-optimization","wordpress"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MarcinZmuda.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-29T17:11:53.000Z","updated_at":"2026-05-28T20:15:41.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/MarcinZmuda/Ligase-Schema-Markup","commit_stats":null,"previous_names":["marcinzmuda/ligase-schema-markup-for-blogs","marcinzmuda/ligase-schema-markup"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/MarcinZmuda/Ligase-Schema-Markup","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MarcinZmuda%2FLigase-Schema-Markup","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MarcinZmuda%2FLigase-Schema-Markup/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MarcinZmuda%2FLigase-Schema-Markup/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MarcinZmuda%2FLigase-Schema-Markup/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MarcinZmuda","download_url":"https://codeload.github.com/MarcinZmuda/Ligase-Schema-Markup/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MarcinZmuda%2FLigase-Schema-Markup/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33771629,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["schema","seo","seo-optimization","wordpress"],"created_at":"2026-06-01T11:00:36.365Z","updated_at":"2026-06-01T11:01:08.396Z","avatar_url":"https://github.com/MarcinZmuda.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"assets/images/banner-772x250.png\" alt=\"Ligase — Schema Markup for WordPress\" width=\"772\"\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003eSchema.org JSON-LD for WordPress — entity graph, AI-citation optimization, WooCommerce-aware merchant listings, and a single-source-of-truth field contract.\u003c/strong\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://wordpress.org/\"\u003e\u003cimg src=\"https://img.shields.io/badge/WordPress-6.0%2B-blue?logo=wordpress\" alt=\"WordPress\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.php.net/\"\u003e\u003cimg src=\"https://img.shields.io/badge/PHP-8.0%2B-777BB4?logo=php\u0026logoColor=white\" alt=\"PHP\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.gnu.org/licenses/gpl-2.0.html\"\u003e\u003cimg src=\"https://img.shields.io/badge/License-GPLv2-green.svg\" alt=\"License\"\u003e\u003c/a\u003e\n  \u003cimg src=\"https://img.shields.io/badge/version-2.3.2-orange.svg\" alt=\"Version\"\u003e\n\u003c/p\u003e\n\n---\n\n## What is Ligase?\n\nLigase generates a single consolidated `@graph` of schema.org JSON-LD for every page of a WordPress site — connecting `BlogPosting`/`Product`/`Recipe`/`JobPosting` nodes to a unified `Person` (author) and `Organization` (publisher / OnlineStore) through `@id` references. The graph is rendered server-side in `wp_head` so it's visible to AI crawlers (GPTBot, ClaudeBot, PerplexityBot) that don't execute JavaScript.\n\n**For:** professional bloggers, news publishers, WooCommerce stores, recipe sites, job boards, and forums that want Google rich results and AI-citation visibility without writing schema by hand.\n\n---\n\n## What's new in 2.3.0\n\n- **Three new high-value schema types:** `Recipe` (host-carousel-eligible), `JobPosting` (Google Jobs), `DiscussionForumPosting` (bbPress / Discussions \u0026 Forums SERP).\n- **WooCommerce merchant listings** — `Product` + `Offer` + `MerchantReturnPolicy` (with `returnPolicyCountry` required since March 2025) + `OfferShippingDetails`, plus `ProductGroup` + `hasVariant` for size/color variants and `SalePrice`/`StrikethroughPrice` for SERP strikethrough pricing.\n- **Field-contract system** — every schema field declares its required level, source chain (manual → WC → post → option → ref → NER → derive), and sanitization rule in one declarative file. Auto-fill happens at render time; manual overrides are persisted but auto values never are. Powers the in-editor **readiness panel** that shows which rich results the post qualifies for and why.\n- **OnlineStore mode** — site-level return + shipping policies emitted once on the Organization node; product Offers reference them by `@id` instead of repeating the full policy. Massive payload reduction on big catalogs.\n- **Hardening:** stored XSS in JSON-LD output fixed (literal `\u003c/script\u003e` neutralized by both `str_replace` AND dropping `JSON_UNESCAPED_SLASHES`); capability split between admin and editor AJAX endpoints; logger now safe to leak regardless of webserver (PHP-die prefix + `.php` extension + `web.config`).\n\nFull release notes: [`readme.txt`](readme.txt). Architectural history: [`docs/audit-history/`](docs/audit-history/).\n\n---\n\n## Schema types (v2.3.0)\n\n### Publishing\n| Type | Notes |\n|---|---|\n| `BlogPosting` / `Article` / `NewsArticle` / `TechArticle` / `LiveBlogPosting` | Category → variant resolver; headline ≤ 110; 3 image ratios (1:1, 4:3, 16:9); paywall (`isAccessibleForFree`); dateModified discipline; NewsArticle `citation` + `dateline`. |\n| `Person` + `ProfilePage` | `sameAs` (Wikidata/LinkedIn), `knowsAbout`, `alumniOf`, `hasCredential`, `worksFor` → Organization. |\n| `Organization` / `OnlineStore` | Logo 112×112+ (Google 2025 requirement), `sameAs`, `knowsAbout`, store-level `hasMerchantReturnPolicy` + `shippingDetails`. |\n| `LocalBusiness` | 60 supported subtypes with structured `openingHoursSpecification`. |\n| `WebSite` | `SearchAction`. |\n| `BreadcrumbList` | Full hierarchy with parent pages. |\n\n### E-commerce\n| Type | Notes |\n|---|---|\n| `Product` + `Offer` | WooCommerce auto-detection; sale strikethrough via `priceSpecification`; `priceValidUntil` past-date guard. |\n| `MerchantReturnPolicy` | `returnPolicyCountry` enforced. |\n| `OfferShippingDetails` | `MonetaryAmount` + `DefinedRegion` + `ShippingDeliveryTime`. |\n| `ProductGroup` + `hasVariant` | Per-variant SKU/GTIN/Offer, `variesBy`, `productGroupID`. |\n| `Review` | With `positiveNotes`/`negativeNotes` (pros/cons — editorial reviews only). |\n\n### Editorial \u0026 community\n| Type | Notes |\n|---|---|\n| `Recipe` | Host-carousel-eligible. `HowToStep` auto-conversion. |\n| `JobPosting` | Google Jobs. Auto-expires past `validThrough`. |\n| `DiscussionForumPosting` | bbPress auto-detection. Nests up to 50 `Comment` nodes. |\n| `FAQPage` / `HowTo` / `QAPage` | All three emitted as JSON-LD (FAQ/HowTo rich results are deprecated but Ligase keeps them for AI-citation signals). |\n| `ClaimReview` | Documented as niche — Google deprecated June 12, 2025. Schema still emitted for verified fact-checkers. |\n| `Course`, `SoftwareApplication`, `Event`, `Service`, `VideoObject`, `AudioObject`, `DefinedTerm` | All emitted. |\n\n---\n\n## Field-contract system\n\nA single declarative file (`includes/class-field-contract.php`) defines, per @type:\n\n```php\n'Product' =\u003e [\n    'fields' =\u003e [\n        'name' =\u003e [\n            'level'    =\u003e 'required',\n            'sources'  =\u003e [ 'manual:', 'wc:name', 'post:title' ],\n            'sanitize' =\u003e 'text',\n        ],\n        'offers.price' =\u003e [\n            'level'    =\u003e 'required',\n            'sources'  =\u003e [ 'manual:', 'wc:price' ],\n            'sanitize' =\u003e 'float',\n        ],\n        // …\n    ],\n],\n```\n\n`Ligase_Field_Resolver` walks each contract for a given `(type, post_id)`, tries each source in order, sanitizes, and assembles a nested JSON-LD node. It reports:\n\n- per-field state: `auto` / `manual` / `missing_required` / `missing_optional`\n- overall `eligible` — false when any required field is empty\n- list of `missing_required` keys\n\nThe **eligibility gate** stops Ligase from emitting half-formed merchant listings (e.g. `Product` without `Offer` is downgraded to a snippet rather than fabricated). Manual overrides are persisted in `_ligase_override[Type][key]` post meta and always win over auto sources; clearing the input reverts to auto. Auto values are **never** persisted — they're computed at render time on every cache miss, so stale prices/dates are impossible.\n\nThe in-editor **readiness panel** (sidebar metabox) calls `Ligase_Readiness::for_post()` via AJAX and shows the user exactly which fields are filled, by which source, and which required fields are still missing — so non-technical editors can fix rich-result issues without leaving the editor.\n\n---\n\n## AI Search Readiness Score (0–100)\n\nSite-wide and per-post score measuring how ready content is to be cited by AI engines (Google AI Overviews, ChatGPT, Perplexity). Checks include entity-graph linking (`@id` references), Wikidata `sameAs` density, image dimensions ≥ 1200 px, author completeness (`knowsAbout`, `jobTitle`, `sameAs`), `dateModified` discipline, presence of `about`/`mentions` with Wikidata, NER LLM coverage. Pipeline-aware: the score reads `_ligase_wikidata_suggestions`, `_ligase_ner_api_results`, `_ligase_about_entities` so it grades real AI signals, not vanity field-presence checks.\n\n---\n\n## Schema Auditor\n\nDetects and (optionally) suppresses or supplements competitor SEO plugins' schema:\n\n- Yoast SEO, Rank Math, All in One SEO, SEOPress, The SEO Framework, Slim SEO, The Events Calendar.\n- Three modes: **Scan** (report only), **Supplement** (additive — `apply_supplement()`), **Replace** (full takeover below score threshold + `restore_replacement()` for undo).\n- Type-aware scoring rubrics: separate scoring for Article, Event, Product, LocalBusiness, Recipe, FAQ, HowTo, Video, Organization, Person — Event no longer scored against an Article rubric and force-replaced.\n\n---\n\n## Entity-detection pipeline\n\n```\nLevel 1: WordPress Native       (tags, categories, author)         ~0ms\nLevel 2: Structural             (Wikipedia links, YouTube, blocks)  ~5ms\nLevel 3a: Regex NER             (Polish lemmatization-aware dedup) ~20ms\nLevel 3b: LLM NER (cron)        (OpenAI / Anthropic / Google /\n                                Dandelion — merged with regex)     async\nLevel 4: Wikidata lookup        (locale → en fallback, exact-label\n                                match guard, single-confidence)    async\n```\n\nLLM results take precedence on entity-name conflicts; Wikidata `sameAs` attaches only when the label exactly matches the entity name (avoids wrong-entity linking).\n\n---\n\n## More features\n\n- **Google Search Console** — rich-results dashboard (clicks, impressions, CTR per schema type).\n- **Gutenberg sidebar** — live schema preview + validator.\n- **Import from Yoast / Rank Math / AIOSEO** — handles attachment-ID logos, AIOSEO v3 serialized PHP, v4.5+ nested social URLs.\n- **WPML / Polylang** — `inLanguage` BCP47 + sameAs sync across translations.\n- **Weekly Health Report** — email digest of schema issues.\n- **Speakable schema** — configurable CSS selectors per post.\n- **Auto-repair** — ISO 8601 date fixes, headline truncation, type conversion.\n- **WooCommerce cache invalidation** — hooks into 5 product/stock events; price changes never lag the schema cache.\n- **i18n-ready** — `wp_set_script_translations` + `wp-i18n` for JS strings (translation source needs migration to English for wordpress.org; current source is Polish).\n\n---\n\n## Requirements\n\n- WordPress 6.0+\n- PHP 8.0+\n- No external runtime dependencies (`ext-json`, `ext-mbstring` standard)\n- Optional: WooCommerce 6.0+ for store features\n- Optional: PHP API key for one of OpenAI / Anthropic / Google NLP / Dandelion to enable LLM NER\n\n---\n\n## Installation\n\n### From ZIP\n1. Download the latest release from [Releases](../../releases).\n2. WordPress → **Plugins → Add New → Upload Plugin** → select the zip → **Install Now** → **Activate**.\n\n### Post-installation setup\n1. **Ligase → Settings** → fill organization data (name, logo URL ≥ 112×112 square, email, Wikidata ID).\n2. Add social media links (Wikidata + LinkedIn = strongest entity signals).\n3. Edit author profiles — `jobTitle`, `knowsAbout`, `sameAs`.\n4. (Stores) Settings → Store → return + shipping country, return days, shipping rates.\n5. Check **Ligase → Dashboard** for AI Search Readiness Score.\n\n---\n\n## Developer API\n\n### Filters\n\n```php\n// Modify the full @graph\nadd_filter( 'ligase_schema_graph', function ( array $graph ): array {\n    $graph[] = [ '@type' =\u003e 'Event', 'name' =\u003e 'My Event' ];\n    return $graph;\n} );\n\n// Modify a single type node\nadd_filter( 'ligase_blogposting', function ( array $schema, int $post_id ): array {\n    $schema['speakable'] = [\n        '@type'       =\u003e 'SpeakableSpecification',\n        'cssSelector' =\u003e [ '.entry-summary' ],\n    ];\n    return $schema;\n}, 10, 2 );\n\n// Extend the field contract from a theme/plugin\nadd_filter( 'ligase_field_contract', function ( array $contract, string $type ): array {\n    if ( $type === 'Product' ) {\n        $contract['fields']['offers.priceValidUntil']['level'] = 'required';\n    }\n    return $contract;\n}, 10, 2 );\n\n// Which schema types appear in the readiness panel for this post\nadd_filter( 'ligase_readiness_panel_types', function ( array $types, int $post_id ): array {\n    if ( has_category( 'news', $post_id ) ) {\n        $types[] = 'NewsArticle';\n    }\n    return $types;\n}, 10, 2 );\n```\n\nAvailable type filters: `ligase_blogposting`, `ligase_person`, `ligase_organization`, `ligase_website`, `ligase_breadcrumb`, `ligase_product`, `ligase_productgroup`, `ligase_recipe`, `ligase_jobposting`, `ligase_discussionforumposting`.\n\n### WP-CLI helper\n\n```bash\n# Print readiness report for a post\nwp eval 'var_export( ligase_readiness( 123 ) );'\n```\n\n---\n\n## Testing \u0026 quality gates\n\n```bash\ncomposer install\ncomposer test            # PHPUnit\ncomposer phpcs           # WordPress Coding Standards\ncomposer phpstan         # PHPStan level 5\n```\n\nGitHub Actions runs all of the above on every push, plus a smoke test that boots WordPress, activates the plugin, creates a post, and verifies that `\u003cscript type=\"application/ld+json\"\u003e` appears in the raw HTML *and* that `\u003c/script\u003e` inside the JSON is properly escaped (regression guard for the XSS fix).\n\n---\n\n## License\n\n[GNU General Public License v2.0](LICENSE) or later.\n\n## Author\n\nBuilt by **[Marcin Żmuda](https://marcinzmuda.com)** · [Report a bug](../../issues) · [Releases](../../releases)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarcinzmuda%2Fligase-schema-markup","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarcinzmuda%2Fligase-schema-markup","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarcinzmuda%2Fligase-schema-markup/lists"}