Headless SEO Kit
WordPress companion plugin for BeyondSEO / RankingCoach. Renders SEO tags, exposes meta via REST for AI/MCP workflows, syncs to BeyondSEO's internal table, handles postmeta-driven 301 slug redirects.
by Columbia Cloudworks LLC · github.com/columbia-cloudworks-llc/headless-seo-kit · 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/columbia-cloudworks-llc/headless-seo-kit/archive/refs/heads/main.zipA WordPress companion plugin for BeyondSEO / RankingCoach that does the four things BeyondSEO doesn't:
- Renders SEO tags. BeyondSEO stores SEO data in postmeta but doesn't reliably emit
<meta>tags on the front end (especially on IONOS Managed WordPress). This plugin reads the samerankingcoach_*postmeta keys and emits proper title overrides, meta description, canonical, Open Graph, Twitter Card, and JSON-LDArticleschema. - Exposes the keys via REST. The
rankingcoach_*keys are registered withshow_in_rest=true, so headless CMS workflows, the WP REST API, and AI agents using MCP (e.g. Easy MCP AI) can read and write them programmatically. - Syncs back to BeyondSEO's internal table. BeyondSEO's React admin UI reads from its own Doctrine-backed table (
{prefix}rankingcoach_metatags), not postmeta. When anything other than the BeyondSEO UI writes postmeta, the admin UI goes stale. This plugin mirrors writes into that internal table so the UI reflects reality. - Handles postmeta-driven 301 redirects. Each post can declare its prior slugs via a single postmeta key. Old URLs 301 to the current permalink. No options page, no database table, no manual config — write the meta key and it just works.
Built and field-tested on IONOS Managed WordPress with BeyondSEO v1.2.0, WordPress 6.9, PHP 8.1.
Why this exists
BeyondSEO (sold by IONOS as part of "RankingCoach SEO" on their Managed WordPress plans) ships with three structural gaps:
- It stores SEO metadata in
rankingcoach_*postmeta keys but its front-end emitter is unreliable. Posts edited via the BeyondSEO UI sometimes render the tags; posts edited any other way usually don't. - Its postmeta keys are not registered with WordPress's REST API. Any tool that authors content through
wp_update_post_meta()or the REST API can write the meta but can't see it back. - Its internal admin UI reads from a parallel Doctrine table, not from postmeta. Direct postmeta writes desync the UI.
If you're running an AI-assisted publishing pipeline against a WordPress site that has BeyondSEO bundled with the hosting plan, you can't uninstall BeyondSEO (it's on a managed host) and you can't make it work properly out of the box. This plugin closes the four gaps without fighting BeyondSEO — it reads the same data, fills in the missing rendering, and mirrors writes back so BeyondSEO's UI keeps showing the current state.
What this plugin doesn't do
- It doesn't replace BeyondSEO. If BeyondSEO emits a tag on its own, this plugin doesn't suppress it (unless you enable strict mode). The two coexist.
- It doesn't score content. No keyword analysis, no readability checker, no recommendations panel. That's BeyondSEO's job. This plugin just makes sure the data BeyondSEO already has gets rendered and synced.
- It doesn't manage redirects through a UI. Redirects are postmeta-driven. Set the
hsk_old_slugsmeta key on a post via the WP admin meta editor, the REST API, WP-CLI, or any MCP client. There's no separate redirect manager screen. - It doesn't optimize images. Image SEO is out of scope.
Installation
- Download the latest release ZIP from the releases page (or build one yourself:
zip -r headless-seo-kit.zip headless-seo-kit/). - WordPress admin → Plugins → Add New → Upload Plugin → select the ZIP → Install Now → Activate.
- Optional: Settings → Headless SEO Kit. Set your Twitter handle, default OG image, and publisher name.
- If you're migrating from
cab-agent-kit, your existingcab_agent_old_slugspostmeta values keep working — no migration required.
Requirements: WordPress 5.6+, PHP 7.4+. No external dependencies. Works alongside BeyondSEO but doesn't require it (the BeyondSEO internal-table sync becomes a silent no-op if the table isn't present).
Postmeta schema
All keys live on the post post type and are registered with show_in_rest=true.
| Meta key | Type | Purpose |
|---|---|---|
rankingcoach_seo_title |
string | Overrides <title> and OG title. Recommended ≤60 chars. |
rankingcoach_seo_description |
string | Meta description + OG description fallback. Recommended 150–160 chars. |
rankingcoach_seo_keywords |
string | Comma-separated keywords. If empty, falls back to primary + secondary. |
rankingcoach_primary_keyword |
string | Primary keyword. Used if seo_keywords is empty. |
rankingcoach_secondary_keywords |
string | Comma-separated. Used if seo_keywords is empty. |
rankingcoach_social_title |
string | OG / Twitter title. Falls back to seo_title. |
rankingcoach_social_description |
string | OG / Twitter description. Falls back to seo_description. |
rankingcoach_schema_type |
string | Schema.org @type. Defaults to BlogPosting. |
rankingcoach_schema_cache |
string | (Read-only here.) If present and valid JSON, emitted verbatim as JSON-LD. Set by BeyondSEO. |
hsk_old_slugs |
string | Comma-separated prior slugs (no leading/trailing /). 301 to current permalink. |
cab_agent_old_slugs |
string | Legacy alias. Read for back-compat with prior cab-agent-kit deploys. Write to hsk_old_slugs on new posts. |
Example: setting via REST
curl -X POST https://example.com/wp-json/wp/v2/posts/123 \
-u user:application_password \
-H 'Content-Type: application/json' \
-d '{
"meta": {
"rankingcoach_seo_title": "Headless SEO Kit: Make BeyondSEO Actually Render",
"rankingcoach_seo_description": "A WordPress plugin that fills the four gaps BeyondSEO leaves on IONOS Managed WordPress: rendering, REST visibility, internal-table sync, and slug redirects.",
"rankingcoach_primary_keyword": "headless seo",
"hsk_old_slugs": "old-slug-1,old-slug-2"
}
}'
Example: setting via WP-CLI
wp post meta update 123 rankingcoach_seo_title "New Title"
wp post meta update 123 hsk_old_slugs "old-url,older-url"
How rendering works
On every singular post or page request:
- WordPress core's
rel_canonicalis suppressed (prevents duplicate canonical tags). - Headless SEO Kit emits its own block at
wp_headpriority 0, bracketed by/comments. - Tags emitted:
<meta name="description">,<meta name="keywords">,<link rel="canonical">, full Open Graph article set, Twitter Card summary_large_image, and JSON-LD schema. pre_get_document_titleandwp_titleare filtered so the<title>element reflectsrankingcoach_seo_titleif set.
Strict mode (optional, off by default)
If a third-party plugin starts emitting conflicting rankingcoach_* or beyondseo_* tags into <head> and you can't disable the offender, enable strict mode (Settings → Headless SEO Kit → Strict mode). An output buffer wraps wp_head and strips matching lines from everything outside this plugin's own block. Leave it off unless you have a confirmed conflict.
How the BeyondSEO sync works
BeyondSEO has two storage layers:
- Postmeta (
rankingcoach_*keys) — what the front-end emitter reads. - Doctrine table (
{prefix}rankingcoach_metatags) — what the React admin UI reads.
The UI writes both. The front-end reads postmeta. Anyone else (REST API, WP-CLI, MCP, headless CMS) writing postmeta directly desyncs the UI.
This plugin hooks added_post_meta and updated_post_meta. When one of the five user-facing keys (rankingcoach_seo_title, _seo_description, _seo_keywords, _social_title, _social_description) is written, the corresponding (post_id, type) row is upserted into the internal table. The internal-table unique_key algorithm is reverse-engineered from BeyondSEO's WPWebPageMetaTag::uniqueKey() source:
strtolower($post_id . '_' . $type . '_' . md5($post_id . $type))
Sync is idempotent — same content = no-op. If the internal table doesn't exist (BeyondSEO not installed), the sync silently no-ops.
How redirects work
Set the hsk_old_slugs postmeta on a post to a comma-separated list of prior slugs:
old-url-1,older-url,really-old-name
On every front-end request, the plugin:
- Builds (and caches for 12 hours) a
slug → post_idmap from postmeta. - Trims the request path to its slug-relative form.
- If the slug matches an entry, 301-redirects to the post's current permalink.
The cache is busted automatically on save_post, trashed_post, untrashed_post, deleted_post, and any hsk_old_slugs / cab_agent_old_slugs meta write. Self-redirects are blocked. Admin, REST, AJAX, cron, and preview requests are skipped.
Hooks reference
The plugin doesn't currently expose its own filters or actions — it just hooks WordPress core. If you want to extend it (e.g. add og:image:width, suppress JSON-LD on certain post types), the cleanest approach right now is to fork or to hook wp_head at priority 0 yourself and call remove_action( 'wp_head', 'hsk_render_head', 0 ); from your own plugin.
Filters / actions are on the roadmap. PRs welcome.
Compatibility notes
- IONOS Managed WordPress: tested as the primary target. Works on shared and dedicated plans.
- BeyondSEO: tested with v1.2.0. The internal-table schema is reverse-engineered, not documented by BeyondSEO. If they change the table structure, this plugin's sync becomes a no-op (it checks for table existence) but rendering still works.
- WordPress core canonical: suppressed only on singular publish posts. Archives, search, etc. are unaffected.
- Other SEO plugins: this plugin assumes it's the sole emitter of
<meta name="description">, OG, Twitter Card, and JSON-LD for singular posts. If you're running Yoast / Rank Math / All in One SEO, deactivate them or accept duplicate tags.
License
GPL-2.0-or-later. See LICENSE.
Author
Columbia Cloudworks LLC — Nicholas E. King
Built originally to power the AI-assisted publishing pipeline behind The CAB Call.
Contributing
Issues and PRs welcome at github.com/Columbia-Cloudworks-LLC/headless-seo-kit.