Schema Patcher for Yoast SEO
A WordPress plugin that patches Yoast SEO's schema graph by queuing schema "patches" from PHP or ACF-authored JSON and applying them deterministically.
by KNI · github.com/kni-labs/schema-patcher-for-yoast-seo · website
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/kni-labs/schema-patcher-for-yoast-seo/archive/refs/heads/main.zipReadme
Schema Patcher for Yoast SEO
A WordPress plugin that patches Yoast SEO's schema graph by queuing schema "patches" from PHP or ACF-authored JSON and applying them deterministically.
Requirements
- WordPress 5.9+
- PHP 7.4+
- Yoast SEO (required)
- Advanced Custom Fields Pro (required)
Compatibility
Supported:
- WordPress 5.9+
- PHP 7.4 - 8.4
- Yoast SEO 19.0+
- ACF Pro 5.9+
Tested on:
- WordPress 5.9, 6.0, 6.4
- PHP 7.4, 8.0, 8.1, 8.4
- Yoast SEO 19.7, 21.0
- ACF Pro 6.0
Installation
- Upload the plugin folder to
/wp-content/plugins/schema-patcher-for-yoast-seo/ - Activate the plugin through the 'Plugins' menu in WordPress
- Ensure both Yoast SEO and ACF Pro are installed and activated
Verification
After installation, verify the plugin works:
-
Check dependencies: Visit Plugins page.
- Expected: No error notices and plugin is active.
-
Test PHP API: Add to theme's functions.php.
add_action('wp_head', function() { patch_yoast_schema(['@type' => 'Organization', 'name' => 'Test']); });- Expected: No PHP errors in debug.log.
-
Test ACF UI: Edit any Page.
- Expected: "Schema Patcher for Yoast SEO" field group appears.
-
Check schema output: View page source and search for
application/ld+json.- Expected: JSON-LD block present (may need Yoast configured first).
Usage
CRITICAL: Timing Requirements
The patch_yoast_schema() function MUST be called BEFORE the wp_head() hook fires.
Schema output happens during wp_head() (which runs inside get_header() in template files). Any patches queued after this point will be silently ignored.
✅ CORRECT Usage Patterns
In functions.php or plugin files:
// Option 1: template_redirect hook (recommended for conditional logic)
add_action('template_redirect', 'my_schema_patches');
function my_schema_patches() {
if (is_singular('casestudy')) {
patch_yoast_schema([/* ... */]);
}
}
// Option 2: wp hook (earlier, still has post data)
add_action('wp', 'my_schema_patches');
function my_schema_patches() {
patch_yoast_schema([/* ... */]);
}
// Option 3: init/wp_loaded (for global patches)
add_action('wp_loaded', 'my_global_schema');
function my_global_schema() {
patch_yoast_schema([/* ... */]);
}
In template files:
<?php
// MUST come BEFORE get_header()
patch_yoast_schema([/* ... */]);
get_header(); // wp_head() fires here
?>
❌ INCORRECT Usage (Will Fail)
<?php
get_header(); // wp_head() already fired
// TOO LATE - patch will be ignored and error logged
patch_yoast_schema([/* ... */]);
get_footer();
?>
// TOO LATE - these hooks fire after wp_head
add_action('wp_footer', function() {
patch_yoast_schema([/* ... */]); // ❌ Will fail
});
add_action('loop_start', function() {
patch_yoast_schema([/* ... */]); // ❌ Will fail
});
Debugging Timing Issues
If patches aren't appearing in your schema output:
- Check browser console or WordPress debug log for timing warnings.
- Enable WP_DEBUG to see detailed error messages.
- Verify hook timing:
init→wp_loaded→template_redirect→wp→wp_head(patches must be queued before this). - In templates, ensure
patch_yoast_schema()appears beforeget_header().
Via PHP (Programmatic)
Use the patch_yoast_schema() function anywhere in your theme or plugin (before wp_head):
// Simple patch with JSON string
patch_yoast_schema('{"@type": "Organization", "name": "My Company"}');
// Patch with PHP array (simple format)
patch_yoast_schema([
'@type' => 'WebPage',
'description' => 'Custom description'
]);
// Patch with PHP array (JSON-LD format with @graph wrapper - also supported)
patch_yoast_schema([
'@context' => 'https://schema.org',
'@graph' => [
[
'@type' => 'Article',
'headline' => 'My Article',
'description' => 'Article description'
],
[
'@type' => 'Dataset',
'name' => 'My Data'
]
]
]);
// Targeted patch (merge into existing node)
patch_yoast_schema([
'@target' => 'Organization',
'address' => [
'@type' => 'PostalAddress',
'streetAddress' => '123 Main St'
]
]);
// Graph upsert (by @id)
patch_yoast_schema([
'@id' => 'https://example.com/#custom-software',
'@type' => 'SoftwareApplication',
'name' => 'My App'
]);
// With options
patch_yoast_schema($json, [
'order' => 100, // Run early
'merge' => [
'offers.price' => 'fill', // Only set if null
'@type' => 'union' // Merge as unique array
],
'remove' => [
'internalProperty', // Remove this key
'offers[].invalidKey' // Remove from all offer items
],
'debug' => true // Log to error_log
]);
Via ACF (Admin UI)
- Edit any Page in WordPress.
- Scroll to the "Schema Patcher for Yoast SEO" field group.
- Click "Add Patch".
- Configure the patch:
- Enabled: Toggle to enable/disable.
- Attach To: Target (
WebPage,Organization#primaryor full URL). - Order: Queue priority (lower = earlier).
- JSON Patch: Your schema JSON.
- Merge Overrides: Path-specific merge strategies.
- Remove Paths: Paths to remove before merging.
Features
Input Formats
- JSON string: Auto-parsed, supports objects, arrays and JSON-LD
@graphwrappers. - PHP array: Native associative or indexed arrays with optional
@contextand@graphwrappers. - UTF-8 BOM: Automatically stripped from JSON strings.
- @context: Automatically removed (not needed in Yoast graph).
- @graph wrapper: Automatically extracted from both JSON and PHP array formats.
Targeting
- By Type:
WebPageorOrganization. - By Type + Fragment:
Organization#primary(creates or updates@idfragment). - By URL:
https://example.com/page/#softwareapp(exact@idmatch). - Graph Upsert: Nodes with
@idbut no@targetare upserted to@graph.
Merge Strategies
Default behaviors:
@type: Union (unique values).- Scalars: Overwrite.
- Objects: Deep merge.
- Arrays: Append unique (dedupe by
@idor JSON).
Override strategies:
overwrite: Replace value.fill: Set only if target is null/missing.union: Merge as unique array.append_unique: Append with deduplication.deep_merge: Recursive object merge.
Path Syntax
- Dot notation:
address.streetAddress. - Array wildcard:
offers[].priceorhasPart[].offers[].priceCurrency. - Remove paths: Delete keys at paths.
Development Setup
Prerequisites:
- MySQL must be running - Tests require database connection
- For Local by Flywheel: start your Local site before running tests
- For MAMP/XAMPP/native MySQL: ensure MySQL service is running
- Verify:
mysql -u root -p -e "SHOW DATABASES;"
For plugin development, run the setup script to install dependencies and configure tests:
cd wp-content/plugins/schema-patcher-for-yoast-seo
bash local-setup.sh
This script will:
- Install Composer dependencies (PHPUnit, PHPStan, testing tools)
- Install WordPress test suite to
/tmp/wordpress-tests-lib/ - Auto-detect your database configuration from
wp-config.php - Verify database connection (fails early with helpful error if MySQL not running)
- Create test database (e.g.,
local_testif your DB islocal) - Run all tests to verify setup
Database Auto-Detection:
- Setup script reads
DB_NAME,DB_USER,DB_PASSWORD,DB_HOSTfrom your WordPresswp-config.php - For Local by Flywheel: automatically finds MySQL socket path
- For standard MySQL: uses
localhostor detected socket - Test database name:
{DB_NAME}_test(e.g.,local→local_test) - If tests fail with database errors: ensure MySQL is running and accessible
Test Commands:
composer test # Run static analysis + all tests
composer test:unit # Run unit tests only
composer test:wp # Run integration tests only
composer analyze # Run PHPStan static analysis
Fresh Install Test: To verify setup works from scratch:
bash test-fresh-install.sh
This removes vendor/ and composer.lock, then runs local-setup.sh to simulate a new developer cloning the repo.
Disabling the Plugin
Set environment variable or constant:
define('PATCH_YOAST_SCHEMA_DISABLED', true);
Or in .env:
PATCH_YOAST_SCHEMA_DISABLED=true
Development
Quick Start for New Developers
One command to set up everything:
cd wp-content/plugins/schema-patcher-for-yoast-seo
./local-setup.sh
That's it! The script will:
- ✅ Install Composer dependencies
- ✅ Set up WordPress test suite
- ✅ Run all tests to verify setup
- ✅ Show you're ready to develop
Output: ✅ Setup Complete! You're ready to develop.
Available Test Commands
composer test # All tests
composer test:unit # Unit tests only
composer test:wp # Integration tests only
composer analyze # PHPStan static analysis
Note: The install script may show warnings about database connection with Local by Flywheel. This is expected - the database is already created, and tests will work. If tests fail with database errors, see the troubleshooting section below.
Test Structure
tests/unit/- Pure PHP unit tests (ingest, merge, path resolution)tests/integration/- WordPress integration tests (plugin, ACF, Yoast)tests/bootstrap.php- Test bootstraptests/wp-test-stubs.php- Type stubs for IDE (Intelephense) - provides type definitions for WordPress test suite classes/functions that exist in/tmp/wordpress-tests-lib
Static Analysis
PHPStan is configured to analyze the plugin code (level 5) with WordPress stubs. It ignores test files since they depend on the dynamically-loaded WordPress test suite. The stub file provides IDE type hints without affecting runtime behavior.
Troubleshooting
Test Failures
Error: "Error establishing a database connection" or "Cannot connect to MySQL server"
- Cause: MySQL is not running.
- Solution:
- For Local by Flywheel: Open Local app and start your site
- For MAMP: Start MySQL service in MAMP control panel
- For native MySQL:
brew services start mysqlormysql.server start
- Verify:
mysql -u root -p -e "SHOW DATABASES;"
Error: "Unknown database 'local_test'" or similar test database missing
-
Cause: Test database wasn't created automatically.
-
Solution: Create it manually:
mysql -u root -p -e "CREATE DATABASE local_test;"Replace
local_testwith{your_db_name}_testfrom wp-config.php
Error: "No such file or directory" for MySQL socket
- Cause: Socket path changed or Local site not running.
- Solution:
- Ensure Local site is running
- Delete test config and re-run setup:
rm /tmp/wordpress-tests-lib/wp-tests-config.php && bash local-setup.sh
Plugin Not Loading
Error: "Function patch_yoast_schema() already exists"
- Another plugin or theme defines this function.
- Plugin automatically disables itself.
Error: "Yoast SEO plugin is required"
- Install and activate Yoast SEO.
Error: "Advanced Custom Fields Pro is required"
- Install and activate ACF Pro.
Patches Not Applying
-
Check if patch is enabled in ACF.
-
Verify JSON is valid - save triggers validation.
-
Check target resolution - enable debug mode:
patch_yoast_schema($json, ['debug' => true]); -
View error_log for debug messages.
ACF Fields Not Showing
- Ensure you're editing a Page (not Post).
- Plugin extends to Pages by default.
- To add to other post types, use ACF location rules filter.
Architecture
Classes
Schema_Patcher_Yoast_Plugin- Dependency checks and initialization.Schema_Patcher_Yoast_Ingest- Input normalization (JSON/array).Schema_Patcher_Yoast_Queue- Patch queue management.Schema_Patcher_Yoast_Merge- Merge engine (strategies and paths).Schema_Patcher_Yoast_Target_Resolver- Target resolution.Schema_Patcher_Yoast_Integration- Yoast filter hook.Schema_Patcher_Yoast_ACF- ACF fields, validation and ingestion.
Hooks
plugins_loadedpriority 1 - Plugin initialization.acf/init- Field group registration.wppriority 1 - Runtime ACF ingestion.wpseo_schema_graphpriority 9999 - Apply patches to Yoast graph.
License
GPL v2 or later
Support
For issues, questions, or contributions, visit the project repository.