Security: GET /export now capability-filters every row it returns, using the same capability the matching write path enforces. Term rows require the edit_term meta capability on each term, so taxonomies registered with their own capabilities are respected. CPT archive rows require manage_options, matching the archive write path, because those values live in the SEO plugin’s site-wide settings rather than on an object. Post rows are checked with edit_post per post instead of relying on the edit_others_posts pre-filter, which covers only the core post capabilities and not custom post types that declare their own capability_type. Rows the caller cannot manage are omitted, so /export no longer returns SEO metadata for objects the caller could not open in wp-admin.
Note: a caller who could previously export taxonomy terms or CPT archive settings without holding the matching capability will now receive fewer rows. Administrators, and Editors acting within their own capabilities, see no change. row_count still reports the number of rows actually emitted.
1.8.0
Security: every bulk write now enforces a per-object capability check in addition to the route-level edit_posts gate. POST /bulk-alts checks edit_post on each resolved attachment, term updates check the edit_term meta capability on each term, and CPT archive updates now require manage_options (they change the SEO plugin’s site-wide settings, the same bar the global scopes already had). Authors and Contributors can no longer touch objects they could not edit in wp-admin.
Changed: writes into Yoast SEO settings now go through Yoast’s own public API instead of updating its stored options directly. Term meta is saved with WPSEO_Taxonomy_Meta::set_values() (merged with the term’s current values so untouched fields are preserved) and archive/global titles with WPSEO_Options::set(), which routes each key to the right option group, validates it, and clears Yoast’s caches. Rank Math ships no public setter for its title settings, so those writes still read-modify-write the option array Rank Math reads back, through a single documented helper.
Internal: the plugin version constant is now a plain define kept in sync with the plugin header, replacing the runtime header parse.
1.7.0
Changed: GET /export now returns JSON in the shape { “filename”, “row_count”, “csv” } instead of streaming raw CSV bytes. The csv field holds the complete CSV content; save it to a file to open in a spreadsheet. The plugin no longer produces any direct output, so every response flows through the REST API serializer. Query parameters and the CSV column shape are unchanged, and /import accepts the same CSV as before.
Upgraders: scripts that saved the old raw response body to a file should now read the csv field of the JSON response. The bundled MCP server 1.7.0 handles both shapes automatically.
1.6.1
Fix: image alt updates now resolve far more image URLs to their media-library attachment. Previously the resolver only matched the exact stored file URL, so it missed the most common front-end forms and returned “not in media library” for images that were genuinely in the library. It now also resolves: size crops (foo-1024×683.jpg), the “-scaled” big-image original WordPress generates since 5.3 (whether the page references the scaled file or the plain original), cache-busting query strings, and images served from a CDN or alternate host that keep the /wp-content/uploads/ path. A safe filename fallback resolves year/month folder drift only when exactly one attachment owns that filename, so it never guesses between duplicates. Genuinely external or page-builder-hardcoded images (including most inline SVG icons) still report a clear “not an attachment” hint, since their alt lives in the builder, not the media library.
1.6.0
New: custom JSON-LD schema per post or page. Store a block of structured data against any post and it is rendered inside the active SEO plugin’s schema graph at output time (Yoast via the wpseo_schema_graph filter, Rank Math via rank_math/json_ld). The plugin emits no markup itself, so there is no extra output-escaping surface, and the nodes join the existing @graph instead of creating a second competing block (no duplicate-schema conflict).
Write with the schema field on /bulk or /import, e.g. { id: 12, meta: { schema: “{…JSON-LD…}”, schema_mode: “add” } }. JSON is validated on write; invalid JSON is rejected with invalid_json_schema.
schema_mode: add (default) merges your nodes into the graph; replace makes your nodes the whole graph for that page.
Accepts a full document ({ “@context”: …, “@graph”: […] }), a bare list of nodes, or a single node object. Per-node @context is dropped so the SEO plugin sets one top-level context.
New GET /schema?id=123 reads back the stored schema and mode for a post.
/status now reports supports_schema: true.
1.5.1
Internal — renamed all internal PHP function and define prefixes from seo_meta_bridge_* / SEO_META_BRIDGE_* to bulkseme_* / BULKSEME_* so the prefix derives directly from the plugin slug. No public REST surface changes — endpoints still live at /wp-json/seo-meta-bridge/v1/*. No migration required.
Internal — removed if (!function_exists(…)) guards around the plugin’s own helper functions per WordPress.org plugin guidelines. Unique prefixes prevent collisions; the guards were unnecessary and risked loading a same-named function from another plugin instead of ours.
Code clarity — added comments next to update_option(‘wpseo_titles’, …) and update_option(‘rank-math-options-titles’, …) documenting that those option keys belong to Yoast SEO and Rank Math respectively, not this plugin. We integrate WITH those plugins by reading and writing the exact keys they use; a custom-prefixed key would store data nowhere those plugins look.
1.5.0
New — bulk update image alt text. New endpoint POST /seo-meta-bridge/v1/bulk-alts accepts { items: [{ image_url, new_alt }, …] } (max 200/req) and updates _wp_attachment_image_alt on the matching media-library attachment. Resolves URLs via attachment_url_to_postid() with two fallbacks: strips -WIDTHxHEIGHT.ext size suffix (so a -300×200.jpg thumb still maps to its parent attachment), then strips any query string. Each row reports status: ok | unchanged | skipped | error plus attachment_id, matched_via, and previous_alt so callers can audit changes. Skipped rows include a hint when the URL doesn’t resolve to a media-library item (typical for CDN-hosted or page-builder-hardcoded images). /status now reports supports_alts: true.
1.4.2
Security — /export now filters by author for low-privilege roles. Authors and Contributors (anyone with edit_posts but not edit_others_posts) calling /export previously received titles and SEO meta for every post and draft on the site, including drafts owned by other users. The endpoint now scopes the underlying WP_Query to the calling user’s own authored posts when they lack edit_others_posts, matching the behaviour of the wp-admin Posts list. No change for Editors or Administrators — they continue to see all posts as before. Recommended upgrade for any site with multi-author setups.
1.4.1
Bug fix — term meta updates now invalidate Yoast’s Indexable cache. Previously, writing to wpseo_taxonomy_meta via /bulk (kind=term) updated the option correctly and /export read the new value back, but the FRONT-END kept rendering the old meta description because Yoast 14+ caches rendered SEO meta in the yoast_indexable table and our update didn’t fire the hooks Yoast’s Indexable_Term_Watcher listens on. After updating the term option, the plugin now also fires do_action(‘wpseo_save_taxonomy_meta’, $term_id, $taxonomy) and the standard do_action(‘edited_term’, …), and as a final safety net deletes the term’s row in the Yoast indexable repository so Yoast rebuilds it on the next request. No-op when Yoast isn’t installed (Rank Math path was unaffected and unchanged).
1.4.0
CPT archive page support — write SEO meta to custom-post-type archive pages (e.g. /challenges/, /news/) for any CPT registered with has_archive=true. Yoast: stored in wpseo_titles option (title-ptarchive-{ptype}, metadesc-ptarchive-{ptype}); Rank Math: stored in rank-math-options-titles (pt_{ptype}_archive_title, pt_{ptype}_archive_description).
Global SEO scopes — write meta for author_archive, date_archive, search, p404, home (latest-posts mode) via a single registry. New scopes can be added in one place without touching dispatch code.
/status reports supports_archives, supports_globals, archive_fields, and global_scopes (alias map per active scope).
/export?include_archives=1 appends one synthetic row per CPT-with-archive (id=0, kind=cpt_archive). Backwards compatible — pre-1.4 consumers ignore unknown kind values.
/import reads cpt_archive and global rows from CSV for round-trip edits.
Permission: cpt_archive requires the post type’s edit_posts cap; global scopes require manage_options (admin-only — they affect site-wide SEO settings).
1.3.0
Taxonomy term archives are now editable — categories, tags, and any custom taxonomy archive (e.g. WooCommerce product_cat, product_tag, theme-registered taxonomies). Previously the plugin only handled posts, pages and CPTs; term archive SEO meta had to be edited in wp-admin one term at a time.
/status now reports supports_terms: true and a term_fields alias map so clients can detect the capability.
/export?include_terms=1 appends term archive rows to the CSV. New trailing kind and taxonomy columns flag term rows; post rows have kind=post with an empty taxonomy. Filter to specific taxonomies with ?taxonomy=category,product_cat. Backwards compatible — v1.2.x clients that ignore the trailing columns see the same column shape.
/export?post_type=any now correctly returns all public post types (previously needed an explicit comma list of CPTs).
/bulk accepts term updates via { id: <term_id>, kind: “term”, taxonomy: “category”, meta: {…} }. Existing post-update payloads are unchanged.
/import reads kind and taxonomy columns from CSV uploads so a mixed posts+terms export round-trips cleanly.
Yoast term meta is stored in the wpseo_taxonomy_meta option (read-modify-write per call). Rank Math term meta uses wp_termmeta. Per-taxonomy edit_terms capability is enforced on every write.
1.2.6
/export now includes title_chars and desc_chars helper columns by default — character counts for the SEO title and description so you can spot over-limit cells at a glance when editing in Excel/LibreOffice/Google Sheets. Pass ?lengths=0 to get the original column shape. /import ignores these columns, so exports round-trip unchanged. Counts are static at export time.
MCP export_csv tool gains an include_lengths boolean (default true) that maps to the new query param.
1.2.5
/export now writes a UTF-8 BOM and CRLF line endings so curly quotes, em dashes and other non-ASCII characters open correctly in Excel, LibreOffice and Google Sheets instead of appearing as mojibake (e.g. ’ rendering as ’).
/import now strips a leading UTF-8 BOM before parsing, so re-uploading an exported CSV doesn’t silently drop the id column on round-trip.
1.2.4
/import now treats empty CSV cells as “do not touch this field” instead of overwriting existing values with empty strings. Lets you upload a partially-filled CSV (e.g. only the description column populated) without wiping titles, OG fields, etc.
1.2.3
/bulk now accepts friendly field aliases (title, description, focus_kw, …) in addition to raw Yoast/Rank Math meta keys, so CSV columns from /export round-trip through /bulk without manual remapping.
/status now reports the actual plugin version dynamically instead of a hardcoded string.
1.2.2
Updated author metadata
1.2.1
Replaced raw fopen/fclose with WP_Filesystem (CSV import) and string-builder CSV (export) per Plugin Check
Bumped tested-up-to to 6.9
Trimmed short description to <=150 chars
1.2.0
Initial public release
REST endpoints: /status, /bulk, /export, /import
Standard /wp/v2/posts/{id} route registers Yoast and Rank Math meta keys
Bundled Node.js MCP server companion
Auto-detection of active SEO plugin (Yoast / Rank Math)
Per-post permission checks; allowlisted meta keys; URL field sanitisation