{"id":306360,"date":"2026-08-18T00:15:48","date_gmt":"2026-08-18T00:15:48","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/bulk-seo-meta-editor-for-ai-agents\/"},"modified":"2026-10-26T04:12:10","modified_gmt":"2026-10-26T04:12:10","slug":"bulk-seo-meta-editor-for-ai-agents","status":"publish","type":"plugin","link":"https:\/\/ko.wordpress.org\/plugins\/bulk-seo-meta-editor-for-ai-agents\/","author":23490474,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"2.3.2","stable_tag":"2.3.2","tested":"7.1.3","requires":"5.7","requires_php":"7.4","requires_plugins":null,"header_name":"Bulk AI CLI Connector: Edit Pages, SEO and Schema with Claude, ChatGPT and MCP Agents","header_author":"Puneet Singh","header_description":"Bulk-update Yoast SEO or Rank Math meta tags via REST API. Designed for AI agents (Claude, ChatGPT, Perplexity) and automation scripts. Auto-detects the active SEO plugin. Supports posts, pages, custom post types, taxonomy term archives (categories, tags, custom taxonomies), and CPT archive pages. Includes CSV import\/export and a bundled MCP server for one-command Claude Code \/ Claude Desktop integration.","assets_banners_color":"2d6c8f","last_updated":"2026-10-26 04:12:10","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/github.com\/puneetindersingh\/bulk-ai-cli-connector","header_author_uri":"https:\/\/github.com\/puneetindersingh","rating":0,"author_block_rating":0,"active_installs":60,"downloads":1262,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.8.1":{"tag":"1.8.1","author":"puneetindersingh","date":"2026-08-21 06:09:40","revision":3658239},"1.8.2":{"tag":"1.8.2","author":"puneetindersingh","date":"2026-09-02 05:50:34","revision":3677228},"1.9.0":{"tag":"1.9.0","author":"puneetindersingh","date":"2026-09-30 01:56:31","revision":3720139},"1.9.1":{"tag":"1.9.1","author":"puneetindersingh","date":"2026-10-12 06:17:46","revision":3740589},"2.0.0":{"tag":"2.0.0","author":"puneetindersingh","date":"2026-10-12 23:37:25","revision":3742623},"2.0.1":{"tag":"2.0.1","author":"puneetindersingh","date":"2026-10-13 02:35:39","revision":3742787},"2.0.2":{"tag":"2.0.2","author":"puneetindersingh","date":"2026-10-13 05:34:10","revision":3742947},"2.1.0":{"tag":"2.1.0","author":"puneetindersingh","date":"2026-10-20 06:21:13","revision":3755980},"2.2.0":{"tag":"2.2.0","author":"puneetindersingh","date":"2026-10-23 04:22:17","revision":3760903},"2.3.0":{"tag":"2.3.0","author":"puneetindersingh","date":"2026-10-23 05:34:02","revision":3760961},"2.3.1":{"tag":"2.3.1","author":"puneetindersingh","date":"2026-10-23 06:00:22","revision":3760989},"2.3.2":{"tag":"2.3.2","author":"puneetindersingh","date":"2026-10-26 04:12:10","revision":3765509}},"upgrade_notice":{"2.3.2":"<p>The live SEO panel no longer asks for prices or product markup on pages that are not WooCommerce products, and shows fewer busywork warnings.<\/p>","2.3.1":"<p>Fixes POST \/update reporting &quot;up to date&quot; when the last update check was stored with a time in the future.<\/p>","2.3.0":"<p>Adds a live SEO panel on the site and in the editors, and a history of SEO changes per post. Nothing changes in what your pages print.<\/p>","2.2.0":"<p>Adds Elementor dropdown and switch settings and inserting an element by copy, both checked against Elementor&#039;s own permissions and capped for accounts without unfiltered_html.<\/p>","2.1.0":"<p>Adds rows to ACF repeaters and flexible content, and ACF fields on terms. If you filter <code>bulkseme_user_can_edit_with_builder<\/code> to keep a role out of ACF, add the same rule to the new <code>bulkseme_user_can_edit_term_acf<\/code>.<\/p>","2.0.2":"<p>Security release. Fixes a CSV import flaw that let an Author&#039;s post title change SEO fields when an administrator re-imported an export, and holds stored schema back on password-protected posts. Recommended for every site.<\/p>","2.0.1":"<p>Security hardening for \/clone. Recommended for every site on 2.0.0.<\/p>","2.0.0":"<p>Adds page content editing (Elementor, Beaver Builder, SiteOrigin, ACF, block and classic pages) with undo, redirects, page copies and API keys. Security: records without an edit screen, such as privacy requests, can no longer be renamed through \/bulk.<\/p>","1.9.1":"<p>Security release. SEO fields can no longer be read through the core REST route without edit rights, the SEO plugin&#039;s own role rules apply to every write, and CSV exports are safe to open in a spreadsheet. Reading meta from wp\/v2 now needs ?context=edit.<\/p>","1.9.0":"<p>Adds schema on taxonomy terms, schema output without an SEO plugin graph, post_title via \/bulk, POST \/sitemap\/flush, and removes Product schema nodes that have no offers, review or rating (filter bulkseme_drop_invalid_product_schema to keep them). Now requires WordPress 5.7.<\/p>","1.8.2":"<p>Security fix. Closes a stored XSS in the JSON-LD schema field on Rank Math sites. Updating is recommended for anyone using the schema feature.<\/p>","1.8.1":"<p>Permission fix: GET \/export now filters its rows by capability, so terms need edit_term, CPT archive settings need manage_options, and posts are checked with edit_post per post. Automation that exported terms or archive settings on a low-privilege account will get fewer rows.<\/p>","1.8.0":"<p>Permission hardening: per-object capability checks on every bulk write, and CPT archive updates now require an administrator (manage_options). Update automation credentials if they relied on editor-level access to archive settings.<\/p>","1.7.0":"<p>GET \/export now returns JSON with a csv field instead of raw CSV bytes. Update any script that saved the raw response body; the bundled MCP server 1.7.0 handles both shapes.<\/p>","1.5.1":"<p>Internal cleanup per WordPress.org review feedback. No public API changes; safe to upgrade.<\/p>","1.5.0":"<p>New endpoint to bulk-update image alt text on media-library attachments. No breaking changes.<\/p>","1.2.0":"<p>Initial release.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3742785,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3742785,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3742785,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3742785,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.8.1","1.8.2","1.9.0","1.9.1","2.0.0","2.0.1","2.0.2","2.1.0","2.2.0","2.3.0","2.3.1","2.3.2"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[216196,229563,76538,242115,2227],"plugin_category":[55],"plugin_contributors":[276180],"plugin_business_model":[],"class_list":["post-306360","plugin","type-plugin","status-publish","hentry","plugin_tags-chatgpt","plugin_tags-claude","plugin_tags-elementor","plugin_tags-mcp","plugin_tags-yoast","plugin_category-seo-and-marketing","plugin_contributors-puneetindersingh","plugin_committers-puneetindersingh"],"banners":{"banner":"https:\/\/ps.w.org\/bulk-seo-meta-editor-for-ai-agents\/assets\/banner-772x250.png?rev=3742785","banner_2x":"https:\/\/ps.w.org\/bulk-seo-meta-editor-for-ai-agents\/assets\/banner-1544x500.png?rev=3742785","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/bulk-seo-meta-editor-for-ai-agents\/assets\/icon-128x128.png?rev=3742785","icon_2x":"https:\/\/ps.w.org\/bulk-seo-meta-editor-for-ai-agents\/assets\/icon-256x256.png?rev=3742785","generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p>Bulk AI CLI Connector connects AI agents and command-line tools to a WordPress site. Claude, Claude Code, ChatGPT, an MCP client or a plain script can read and rewrite SEO titles, meta descriptions, JSON-LD schema, redirects, image alt text and the visible copy of a page, in bulk, over an authenticated REST API. Every page content edit is backed up first and can be undone.<\/p>\n\n<p>There is no AI inside the plugin and it sends nothing anywhere: it makes no requests of its own to any service and needs no AI account. It gives the agent you already use a safe way into the site. Each call signs in as a WordPress user and can only do what that user could do in wp-admin.<\/p>\n\n<h4>See what is live, where you work<\/h4>\n\n<p>New in 2.3: an SEO item in the admin bar on every page you can edit. It opens a panel over the page with what the page really sends to search engines (title, description, robots, canonical, social tags, headings, every JSON-LD block), read from the page itself, so it is true whichever plugin printed it. Problems are marked, the title and description can be changed in place, custom schema can be added, and a history lists every SEO change on the page with who made it and how: in the editor, through an API key, or from the panel. The block editor and the classic editor get the same live view beside the post.<\/p>\n\n<h4>What you can do with it<\/h4>\n\n<ul>\n<li><strong>Connect Claude or ChatGPT to WordPress<\/strong>: Claude Code and other command-line agents call the REST routes directly. Claude Desktop connects through the bundled MCP server. Anything else that can send an authenticated HTTP request works the same way, including n8n, Zapier and Make.<\/li>\n<li><strong>Bulk edit Yoast SEO and Rank Math meta<\/strong>: SEO titles, meta descriptions, focus keywords, canonical URLs, robots directives, Open Graph and Twitter fields on up to 100 posts, pages, products or term archives in one call. Works on the free editions.<\/li>\n<li><strong>Edit Elementor and Beaver Builder pages with an AI agent<\/strong>: read the headings, paragraphs, button labels and list items of a page as a list, change only the ones you name, and leave the layout exactly as it was. SiteOrigin Page Builder, Advanced Custom Fields text fields, Gutenberg, the classic editor, Divi and WPBakery pages are covered too.<\/li>\n<li><strong>Add JSON-LD schema<\/strong>: FAQPage, HowTo, Service or any other type, per post or per category, merged into the Yoast or Rank Math schema graph.<\/li>\n<li><strong>Add redirects<\/strong>: 301, 302, 307 and 410 rules, saved into Rank Math's Redirections where that is in use.<\/li>\n<li><strong>Bulk update image alt text<\/strong>: up to 200 media library images per call, matched by image URL.<\/li>\n<li><strong>Work in a spreadsheet<\/strong>: export every SEO title and description as CSV, edit in Excel or Google Sheets, import it back.<\/li>\n<li><strong>Undo<\/strong>: every page content write stores the previous version first. Dry runs show what would change and write nothing.<\/li>\n<\/ul>\n\n<h4>Works with<\/h4>\n\n<ul>\n<li><strong>SEO plugins<\/strong>: Yoast SEO and Rank Math, free and paid editions. The active one is detected automatically<\/li>\n<li><strong>Page builders and editors<\/strong>: Elementor (including Elementor 4 atomic widgets), Beaver Builder, SiteOrigin Page Builder, Advanced Custom Fields, Gutenberg, the classic editor, Divi, WPBakery<\/li>\n<li><strong>WooCommerce<\/strong>: products, product categories and product tags, plus return policy and shipping details on Product schema<\/li>\n<li><strong>Agents and tools<\/strong>: Claude Code, Claude Desktop, ChatGPT, MCP clients, n8n, Zapier, Make, cURL, Python and Node scripts, headless front ends<\/li>\n<\/ul>\n\n<h4>Why it exists<\/h4>\n\n<p>WordPress does not expose Yoast SEO or Rank Math fields over the REST API, and page builders keep a page in post meta where the core REST API cannot reach it. Editing a meta title or an Elementor heading programmatically normally means WP-CLI with server access, a custom plugin, or a premium tier. This plugin adds a clean, authenticated REST surface instead.<\/p>\n\n<p>It auto-detects which SEO plugin is active and exposes plugin-neutral field aliases (<code>title<\/code>, <code>description<\/code>, <code>focus_kw<\/code>, ...) so you don't need to memorise Yoast vs Rank Math meta keys.<\/p>\n\n<p>This plugin was called Bulk SEO Meta Editor for AI Agents up to version 1.9.1. Same plugin, same address, same REST routes, new name: it now edits page content as well as SEO fields.<\/p>\n\n<h4>New in 2.0: page copy, redirects and API keys<\/h4>\n\n<p>Version 2.0 goes past SEO fields. The same REST API can now read and change the visible text of a page, wherever the site keeps it:<\/p>\n\n<ul>\n<li><strong>Page builders<\/strong>: Elementor (including Elementor 4 atomic widgets), Beaver Builder, SiteOrigin Page Builder, and text fields in Advanced Custom Fields. These keep a page in post meta, where the core REST API cannot reach it. The plugin lists every piece of text as a node and changes only the ones you name. Layout, styling and widgets it does not know are written back exactly as they were read. In Advanced Custom Fields it can also add a row to a repeater or flexible content field (an FAQ item, a product tab), and edit the ACF fields of a category or any other taxonomy term.<\/li>\n<li><strong>Gutenberg, classic editor, Divi and WPBakery<\/strong>: read and replace the post content, or run a literal find and replace across it.<\/li>\n<li><strong>Term descriptions<\/strong>: category and product category introductions keep their headings, lists and tables.<\/li>\n<li><strong>Undo for every edit<\/strong>: each write stores the previous version first. <code>GET \/content\/backups<\/code> lists what can be restored and <code>POST \/content\/revert<\/code> puts it back. The newest ten are kept per page.<\/li>\n<li><strong>Dry run and conflict check<\/strong>: <code>dry_run<\/code> shows what would change and writes nothing. <code>base_hash<\/code> refuses a write when the page changed since it was read.<\/li>\n<li><strong>Redirects<\/strong>: <code>POST \/redirect<\/code> adds a 301, 302, 307 or 410 rule. On a site that uses Rank Math's Redirections the rule goes into Rank Math itself. Elsewhere the plugin serves it.<\/li>\n<li><strong>Copy a page<\/strong>: <code>POST \/clone<\/code> copies a page with its template, design meta and featured image into a new draft.<\/li>\n<li><strong>Merchant listing fields<\/strong>: fills in return policy, shipping details and brand on WooCommerce Product schema from one stored policy.<\/li>\n<li><strong>API keys<\/strong>: for sites where a security plugin or the host blocks application passwords. Made on <strong>Tools &gt; Agent API Keys<\/strong>, shown once, stored only as a hash.<\/li>\n<\/ul>\n\n<h4>What's included<\/h4>\n\n<ul>\n<li><strong>REST endpoints<\/strong>: read and write SEO meta on any post, page or custom post type via the standard <code>\/wp\/v2\/posts\/{id}<\/code> route (reads need <code>?context=edit<\/code>) or the namespaced helpers<\/li>\n<li><strong>Taxonomy term archives<\/strong>: edit SEO meta on category\/tag\/custom-taxonomy archive pages too, not just posts (Yoast and Rank Math both supported)<\/li>\n<li><strong>Bulk update<\/strong>: apply changes to up to 100 posts or terms in a single call<\/li>\n<li><strong>Bulk image alt text<\/strong>: update <code>_wp_attachment_image_alt<\/code> on up to 200 media-library attachments per call by image URL. Resolves CDN paths and <code>-WIDTHxHEIGHT<\/code> thumbnail suffixes back to the parent attachment automatically (v1.5.0+)<\/li>\n<li><strong>CSV import \/ export<\/strong>: round-trip your SEO meta through Excel or Google Sheets, posts and terms in one file<\/li>\n<li><strong>MCP server<\/strong>: bundled Node.js companion (<code>bulk-seo-meta-editor-mcp<\/code> on npm) so Claude Desktop and Claude Code can drive the plugin natively<\/li>\n<li><strong>Auto-detection<\/strong>: works with Yoast SEO or Rank Math, picks the active one automatically<\/li>\n<li><strong>Per-object permission checks<\/strong>: Contributors and Authors can only edit posts, media and terms they could edit in wp-admin; site-wide archive and global settings require an administrator<\/li>\n<li><strong>Allowlist enforcement<\/strong>: only meta keys belonging to the active SEO plugin are accepted; arbitrary postmeta writes are rejected<\/li>\n<li><strong>Custom JSON-LD schema<\/strong> (v1.6.0+): store structured data (FAQPage, HowTo, Service, or any type) per post, and since 1.9.0 per category, tag or custom taxonomy term; it is injected into Yoast's or Rank Math's existing schema graph rather than as a duplicate block. When no SEO plugin schema graph is printed on the page (no Yoast or Rank Math, or their schema output switched off), the plugin prints the stored schema as its own JSON-LD block (v1.9.0+)<\/li>\n<li><strong>Post titles<\/strong> (v1.9.0+): send <code>post_title<\/code> in a <code>\/bulk<\/code> item to rename a post; the slug is never touched<\/li>\n<li><strong>Sitemap refresh<\/strong> (v1.9.0+): <code>POST \/sitemap\/flush<\/code> clears the Yoast or Rank Math sitemap cache after REST edits<\/li>\n<li><strong>Invalid Product schema guard<\/strong> (v1.9.0+): drops <code>Product<\/code> nodes that carry no <code>offers<\/code>, <code>review<\/code> or <code>aggregateRating<\/code>, the ones Search Console reports as invalid on quote-only WooCommerce catalogues. Disable with <code>add_filter('bulkseme_drop_invalid_product_schema', '__return_false')<\/code><\/li>\n<li><strong>Page content<\/strong> (v2.0.0+): read and edit the text of Elementor, Beaver Builder, SiteOrigin, ACF, Gutenberg, classic, Divi and WPBakery pages and of term descriptions, with a backup before every write. From v2.1.0: add rows to ACF repeaters and flexible content, and edit ACF fields on terms. From v2.2.0: Elementor dropdowns and switches, and adding a widget by copying an existing one<\/li>\n<li><strong>Redirects, page copies and merchant listing fields<\/strong> (v2.0.0+): see \"New in 2.0\" above<\/li>\n<li><strong>API keys<\/strong> (v2.0.0+): a second way to sign in, next to application passwords<\/li>\n<\/ul>\n\n<h4>REST endpoints<\/h4>\n\n<p>All endpoints live under <code>\/wp-json\/seo-meta-bridge\/v1\/<\/code> (or <code>?rest_route=\/seo-meta-bridge\/v1\/...<\/code> on plain-permalink installs):<\/p>\n\n<ul>\n<li><code>GET \/status<\/code>: detect the active SEO plugin and list available fields<\/li>\n<li><code>POST \/bulk<\/code>: update SEO meta on up to 100 posts\/terms in one call<\/li>\n<li><code>POST \/bulk-alts<\/code>: update image alt text on up to 200 media-library attachments by URL<\/li>\n<li><code>GET \/export<\/code>: export all posts' SEO meta as CSV, returned in the JSON response body<\/li>\n<li><code>POST \/import<\/code>: apply updates from a CSV upload or JSON rows array<\/li>\n<li><code>GET \/schema<\/code> (v1.6.0+): read back the custom JSON-LD schema stored on a post; add <code>kind=term&amp;taxonomy=&lt;slug&gt;<\/code> for a term (v1.9.0+)<\/li>\n<li><code>POST \/sitemap\/flush<\/code> (v1.9.0+): clear the Yoast or Rank Math sitemap cache so the next sitemap request rebuilds from live data<\/li>\n<li><code>GET \/content?id=N<\/code> (v2.0.0+): which builder owns the page and its text as nodes; add <code>kind=term&amp;taxonomy=&lt;slug&gt;<\/code> for a term description<\/li>\n<li><code>POST \/content<\/code> (v2.0.0+): change nodes with <code>updates: [{el, setting, value}]<\/code>, or send <code>post_content<\/code> for a page that keeps its text there<\/li>\n<li><code>POST \/content\/replace<\/code> (v2.0.0+): literal find and replace across the page's text<\/li>\n<li><code>GET \/content\/backups<\/code> and <code>POST \/content\/revert<\/code> (v2.0.0+): list and restore earlier versions<\/li>\n<li><code>POST \/clone<\/code> (v2.0.0+): copy a page or post into a new draft<\/li>\n<li><code>GET<\/code>, <code>POST<\/code>, <code>DELETE \/redirect<\/code> (v2.0.0+): list, add or remove redirect rules<\/li>\n<li><code>GET<\/code>, <code>POST<\/code>, <code>DELETE \/merchant-policy<\/code> (v2.0.0+): the return policy and shipping details added to WooCommerce Product schema<\/li>\n<li><code>POST \/register-key<\/code> (v2.0.0+): store the hash of an API key from your own signed-in browser session<\/li>\n<li><code>GET<\/code>, <code>POST \/update<\/code> (v2.0.2+): see which version is installed and which one WordPress.org offers, and install the offered version without opening wp-admin<\/li>\n<\/ul>\n\n<p>The standard WordPress REST route also works: <code>POST \/wp\/v2\/posts\/{id}<\/code> with a <code>meta<\/code> payload containing Yoast or Rank Math keys.<\/p>\n\n<h4>Field aliases<\/h4>\n\n\n\n\n  Alias\n  Yoast meta key\n  Rank Math meta key\n\n\n\n\n  title\n  _yoast_wpseo_title\n  rank_math_title\n\n\n  description\n  _yoast_wpseo_metadesc\n  rank_math_description\n\n\n  focus_kw\n  _yoast_wpseo_focuskw\n  rank_math_focus_keyword\n\n\n  canonical\n  _yoast_wpseo_canonical\n  rank_math_canonical_url\n\n\n  og_title\n  _yoast_wpseo_opengraph-title\n  rank_math_facebook_title\n\n\n  og_image\n  _yoast_wpseo_opengraph-image\n  rank_math_facebook_image\n\n\n  tw_title\n  _yoast_wpseo_twitter-title\n  rank_math_twitter_title\n\n\n\n\n<p>Plus robots, OG\/Twitter description fields, and Rank Math primary-taxonomy keys.<\/p>\n\n<h4>MCP server for Claude Desktop and Claude Code<\/h4>\n\n<p>A Node.js MCP server is bundled at https:\/\/github.com\/puneetindersingh\/bulk-ai-cli-connector\/tree\/main\/mcp-server and published to npm as <code>bulk-seo-meta-editor-mcp<\/code>. Add to Claude Desktop \/ Claude Code with one command and Claude can read, edit, bulk-update, and CSV-roundtrip SEO meta on any of your sites.<\/p>\n\n<p>The MCP server has ten tools, for SEO meta and schema. Page content, redirects and the other routes added in 2.0 are REST routes: an agent that can send HTTP requests, Claude Code for example, calls them directly.<\/p>\n\n<h4>Security<\/h4>\n\n<ul>\n<li>Application Passwords (Basic Auth), HTTPS required in production. API keys (v2.0.0+) work over the REST API only, over HTTPS only, on this plugin's routes only unless made with the wider scope, and are stored as SHA-256 hashes<\/li>\n<li>Per-post <code>edit_post<\/code> capability check on every read and write, <code>edit_term<\/code> for terms, <code>manage_options<\/code> for redirects and site-wide settings<\/li>\n<li>The SEO plugin's and the page builder's own rules for who may edit are followed as well: Yoast and Rank Math role settings, Elementor's excluded roles, Beaver Builder's Builder Access setting<\/li>\n<li>Page copy is always cleaned with the post content allowlist (<code>wp_kses_post<\/code>), whoever sends it. Scripts and event handlers are never stored<\/li>\n<li>Meta keys are allowlisted to the active plugin's known fields<\/li>\n<li>URL-shaped fields (canonical, og_image, twitter_image, link URLs, redirect destinations) sanitised through <code>esc_url_raw<\/code><\/li>\n<li>One admin screen, <strong>Tools &gt; Agent API Keys<\/strong>, and only administrators can open it<\/li>\n<\/ul>\n\n<h4>Trademarks<\/h4>\n\n<p>Yoast SEO is a trademark of Yoast BV. Rank Math is a trademark of RankMath. Claude is a trademark of Anthropic PBC. ChatGPT is a trademark of OpenAI OpCo. WordPress is a trademark of the WordPress Foundation. All other product names, logos, and brands are property of their respective owners. This plugin is an independent integration and is not affiliated with, endorsed by, or sponsored by any of them. Trademarks are referenced only to describe interoperability with the listed products.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Install from the WordPress.org plugin directory: <strong>Plugins \u2192 Add New<\/strong>, search for <strong>Bulk AI CLI Connector<\/strong>. Or with WP-CLI: <code>wp plugin install bulk-seo-meta-editor-for-ai-agents --activate<\/code>. Or upload <code>bulk-seo-meta-editor-for-ai-agents.zip<\/code> via <strong>Plugins \u2192 Add New \u2192 Upload Plugin<\/strong><\/li>\n<li>Activate. The REST API is live as soon as the plugin is active<\/li>\n<li>Set up an Application Password: <strong>Users \u2192 Your Profile \u2192 Application Passwords \u2192 Add New<\/strong>. If your site blocks application passwords, make an API key instead under <strong>Tools \u2192 Agent API Keys<\/strong><\/li>\n<li>Test: <code>curl -u 'username:app pass' https:\/\/yoursite.com\/wp-json\/seo-meta-bridge\/v1\/status<\/code>, or with a key: <code>curl -H 'X-Bulkseme-Key: bsme_...' https:\/\/yoursite.com\/wp-json\/seo-meta-bridge\/v1\/status<\/code><\/li>\n<\/ol>\n\n<p>For the optional MCP server, install via npm: <code>npm install -g bulk-seo-meta-editor-mcp<\/code> and configure Claude Desktop \/ Claude Code with <code>WP_BASE_URL<\/code>, <code>WP_USER<\/code>, and <code>WP_APP_PASS<\/code> environment variables.<\/p>\n\n<!--section=faq-->\n<dl>\n<dt id=\"how%20do%20i%20connect%20claude%20or%20chatgpt%20to%20wordpress%3F\"><h3>How do I connect Claude or ChatGPT to WordPress?<\/h3><\/dt>\n<dd><p>Install and activate the plugin, then create an Application Password under <strong>Users \u2192 Your Profile<\/strong>, or an API key under <strong>Tools \u2192 Agent API Keys<\/strong> if your host blocks application passwords. Give the site address and that credential to your agent. Claude Code and other command-line agents call the REST routes under <code>\/wp-json\/seo-meta-bridge\/v1\/<\/code> directly. Claude Desktop uses the bundled MCP server. ChatGPT and other tools work wherever they can send an authenticated HTTPS request. Start with <code>GET \/status<\/code>, which reports the active SEO plugin and the fields available.<\/p><\/dd>\n<dt id=\"is%20this%20an%20mcp%20server%20for%20wordpress%3F\"><h3>Is this an MCP server for WordPress?<\/h3><\/dt>\n<dd><p>It is a REST API plugin with an MCP server bundled alongside it. The MCP server (<code>bulk-seo-meta-editor-mcp<\/code> on npm) runs on your own computer and gives Claude Desktop and Claude Code ten tools for SEO meta and schema. Page content, redirects, page copies and API keys are REST routes, which an agent calls over HTTPS without MCP.<\/p><\/dd>\n<dt id=\"do%20i%20need%20wp-cli%2C%20ssh%20or%20server%20access%3F\"><h3>Do I need WP-CLI, SSH or server access?<\/h3><\/dt>\n<dd><p>No. \"CLI\" in the name refers to the command-line agents and scripts on your own computer. Everything goes over HTTPS to the WordPress REST API, so it works on shared hosting and managed hosts where you have no shell.<\/p><\/dd>\n<dt id=\"does%20the%20plugin%20send%20my%20content%20to%20an%20ai%20service%3F\"><h3>Does the plugin send my content to an AI service?<\/h3><\/dt>\n<dd><p>No. The plugin contains no AI and makes no requests of its own to any service. It only answers REST calls from a signed-in WordPress user. (The one exception is not about content: when asked through <code>\/update<\/code>, it has WordPress check WordPress.org for a new version of this plugin and, if told to install it, download it from there.) What your agent does with the text it reads is between you and that agent.<\/p><\/dd>\n<dt id=\"how%20do%20i%20bulk%20edit%20meta%20descriptions%20in%20wordpress%3F\"><h3>How do I bulk edit meta descriptions in WordPress?<\/h3><\/dt>\n<dd><p>Activate the plugin, create an Application Password under <strong>Users \u2192 Your Profile<\/strong>, then send one <code>POST<\/code> to <code>\/wp-json\/seo-meta-bridge\/v1\/bulk<\/code> with up to 100 items. Each item is a post ID plus the fields to change, for example <code>{\"id\":123,\"meta\":{\"description\":\"...\"}}<\/code>. If you would rather work in a spreadsheet, <code>GET \/export<\/code> returns every row as CSV (with live character counts) and <code>POST \/import<\/code> writes the edited file back.<\/p><\/dd>\n<dt id=\"can%20i%20bulk%20edit%20yoast%20seo%20titles%20without%20yoast%20premium%3F\"><h3>Can I bulk edit Yoast SEO titles without Yoast Premium?<\/h3><\/dt>\n<dd><p>Yes. The plugin writes the same postmeta keys Yoast Free reads from, so bulk title and description edits work on the free edition. Rank Math Free works the same way.<\/p><\/dd>\n<dt id=\"does%20it%20work%20with%20rank%20math%20as%20well%20as%20yoast%3F\"><h3>Does it work with Rank Math as well as Yoast?<\/h3><\/dt>\n<dd><p>Both. It detects whichever plugin is active and maps plugin-neutral aliases (<code>title<\/code>, <code>description<\/code>, <code>focus_kw<\/code>) onto the right meta keys for that plugin, so the same request body works on either site.<\/p><\/dd>\n<dt id=\"can%20claude%20or%20chatgpt%20edit%20my%20wordpress%20seo%20meta%3F\"><h3>Can Claude or ChatGPT edit my WordPress SEO meta?<\/h3><\/dt>\n<dd><p>Yes, that is what it was built for. The bundled MCP server (<code>bulk-seo-meta-editor-mcp<\/code> on npm) gives Claude Code and Claude Desktop ten tools: status, read, write, bulk update, list posts, list terms, CSV export, CSV import, and schema get and set. Any other agent or automation tool can call the REST endpoints directly over HTTP Basic Auth.<\/p><\/dd>\n<dt id=\"how%20do%20i%20bulk%20update%20image%20alt%20text%3F\"><h3>How do I bulk update image alt text?<\/h3><\/dt>\n<dd><p>POST \/bulk-alts accepts up to 200 <code>{ image_url, new_alt }<\/code> pairs per call and writes <code>_wp_attachment_image_alt<\/code> on the matching media-library attachment. CDN paths and <code>-WIDTHxHEIGHT<\/code> thumbnail suffixes resolve back to the parent attachment automatically.<\/p><\/dd>\n<dt id=\"can%20i%20add%20custom%20json-ld%20schema%20to%20a%20post%3F\"><h3>Can I add custom JSON-LD schema to a post?<\/h3><\/dt>\n<dd><p>Yes, since 1.6.0. Schema is stored per post and injected through Yoast's <code>wpseo_schema_graph<\/code> filter or Rank Math's <code>rank_math\/json_ld<\/code> filter, so your nodes merge into the graph those plugins already output rather than appearing as a duplicate second block. Add mode appends, replace mode swaps the graph.<\/p><\/dd>\n<dt id=\"can%20i%20add%20schema%20to%20a%20category%20or%20product%20category%20archive%3F\"><h3>Can I add schema to a category or product category archive?<\/h3><\/dt>\n<dd><p>Yes, since 1.9.0. Send a <code>\/bulk<\/code> item with <code>kind: \"term\"<\/code>, the <code>taxonomy<\/code> slug and a <code>schema<\/code> field, the same way you send term titles and descriptions. The schema renders on the first page of that term's archive only, which is the page that shows the term description. Read it back with <code>GET \/schema?id=&lt;term_id&gt;&amp;kind=term&amp;taxonomy=&lt;slug&gt;<\/code>. Writing needs the <code>edit_term<\/code> capability on that term.<\/p><\/dd>\n<dt id=\"does%20the%20schema%20work%20without%20yoast%20or%20rank%20math%3F\"><h3>Does the schema work without Yoast or Rank Math?<\/h3><\/dt>\n<dd><p>Yes, since 1.9.0. If no SEO plugin schema graph is printed on the page, whether because neither plugin is installed or because its schema output has been switched off, the plugin prints the stored schema as its own <code>&lt;script type=\"application\/ld+json\"&gt;<\/code> block. Meta title and description editing still needs Yoast or Rank Math, because those fields belong to them.<\/p><\/dd>\n<dt id=\"why%20did%20the%20product%20schema%20disappear%20from%20some%20woocommerce%20product%20pages%3F\"><h3>Why did the Product schema disappear from some WooCommerce product pages?<\/h3><\/dt>\n<dd><p>Since 1.9.0 the plugin removes <code>Product<\/code> nodes that carry none of <code>offers<\/code>, <code>review<\/code> or <code>aggregateRating<\/code> from the Yoast and Rank Math schema graphs. Google requires at least one of them, so a quote-only product (no price) can never earn a product rich result and only produces an \"invalid\" entry in Search Console. Products with a price are untouched, and the node comes back on its own once a price is added. To keep those nodes anyway: <code>add_filter('bulkseme_drop_invalid_product_schema', '__return_false');<\/code>.<\/p><\/dd>\n<dt id=\"can%20i%20rename%20a%20post%20through%20the%20api%3F\"><h3>Can I rename a post through the API?<\/h3><\/dt>\n<dd><p>Yes, since 1.9.0. Send <code>post_title<\/code> in a <code>\/bulk<\/code> item. The value is sanitised as plain text, the slug and URL never change, and the caller needs <code>edit_post<\/code> on that post. <code>\/import<\/code> ignores the <code>post_title<\/code> column, so a CSV round trip cannot rename anything.<\/p><\/dd>\n<dt id=\"does%20this%20work%20with%20a%20headless%20or%20decoupled%20wordpress%20setup%3F\"><h3>Does this work with a headless or decoupled WordPress setup?<\/h3><\/dt>\n<dd><p>Yes. Every field is readable and writable over REST, so a headless front end, a CI job or a content-ops script can manage SEO meta the same way it manages any other structured field.<\/p><\/dd>\n<dt id=\"does%20this%20work%20with%20yoast%20seo%20free%20%2F%20premium%20%2F%20rank%20math%20free%20%2F%20pro%3F\"><h3>Does this work with Yoast SEO Free \/ Premium \/ Rank Math Free \/ PRO?<\/h3><\/dt>\n<dd><p>Yes. The plugin uses the postmeta keys that all editions read from, so it works regardless of which tier is installed.<\/p><\/dd>\n<dt id=\"can%20i%20run%20both%20yoast%20and%20rank%20math%20at%20the%20same%20time%3F\"><h3>Can I run both Yoast and Rank Math at the same time?<\/h3><\/dt>\n<dd><p>You shouldn't. They conflict with each other in the front-end (duplicate meta tags). This plugin auto-detects whichever is active; if both are active, Yoast wins.<\/p><\/dd>\n<dt id=\"is%20it%20safe%20to%20install%20on%20production%3F\"><h3>Is it safe to install on production?<\/h3><\/dt>\n<dd><p>Yes. The plugin does nothing until something calls the REST endpoints, and every call needs a signed-in user who could make the same change in wp-admin. SEO fields, schema, alt text and titles are checked per post. Since 2.0.0 page copy can be edited too: each write is cleaned with the post content allowlist, stores a backup first, and can be reverted. The plugin cannot create users, change settings outside its own, or run code that was sent to it. The only thing it can install is its own update from WordPress.org (through <code>\/update<\/code>, for users who may update plugins).<\/p><\/dd>\n<dt id=\"can%20claude%20or%20chatgpt%20edit%20an%20elementor%20or%20beaver%20builder%20page%3F\"><h3>Can Claude or ChatGPT edit an Elementor or Beaver Builder page?<\/h3><\/dt>\n<dd><p>Yes, since 2.0.0. <code>GET \/content?id=&lt;page id&gt;<\/code> returns the page's headings, paragraphs, button labels and list items as nodes, each with the element id and setting name it lives in. Send the ones you want changed to <code>POST \/content<\/code> as <code>updates: [{\"el\": \"...\", \"setting\": \"...\", \"value\": \"...\"}]<\/code>. Only those values change. Add <code>\"dry_run\": true<\/code> to see the result without writing.<\/p><\/dd>\n<dt id=\"how%20do%20i%20undo%20an%20edit%3F\"><h3>How do I undo an edit?<\/h3><\/dt>\n<dd><p>Every write through <code>\/content<\/code> stores the previous version first and returns its id as <code>backup_ts<\/code>. <code>POST \/content\/revert<\/code> with the page <code>id<\/code> and that <code>ts<\/code> restores it, and the revert can itself be undone. <code>GET \/content\/backups?id=&lt;page id&gt;<\/code> lists the newest ten. A backup can only be restored onto the page it was taken from.<\/p><\/dd>\n<dt id=\"who%20can%20edit%20page%20content%20through%20the%20api%3F\"><h3>Who can edit page content through the API?<\/h3><\/dt>\n<dd><p>Only a user who could edit that page in wp-admin. The plugin checks <code>edit_post<\/code> on the page (or <code>edit_term<\/code> on the term) for every read and write. Where the page builder has its own rule it is followed as well: a role that Elementor excludes from its editor, or that Beaver Builder's Builder Access setting keeps out, is refused here too.<\/p><\/dd>\n<dt id=\"can%20i%20send%20scripts%20or%20iframes%20in%20page%20copy%3F\"><h3>Can I send scripts or iframes in page copy?<\/h3><\/dt>\n<dd><p>No. Every value is cleaned with WordPress's post content allowlist (<code>wp_kses_post<\/code>) before it is stored, whoever sends it, administrators included. A site owner who needs the unfiltered behaviour WordPress gives administrators in the editor can switch it on with <code>add_filter('bulkseme_allow_unfiltered_html', '__return_true');<\/code>. Even then the caller must hold the <code>unfiltered_html<\/code> capability and the request must ask for it with <code>unsafe_html: 1<\/code>.<\/p><\/dd>\n<dt id=\"how%20do%20api%20keys%20work%2C%20and%20are%20they%20safe%3F\"><h3>How do API keys work, and are they safe?<\/h3><\/dt>\n<dd><p>An administrator makes a key on <strong>Tools &gt; Agent API Keys<\/strong>. It is shown once and only its SHA-256 hash is stored, so a copy of the database holds nothing that can be replayed. A key signs REST API requests in as the account that made it, with that account's capabilities, and is removed when that account is deleted. It is read only for REST API requests over HTTPS, so wp-login.php, wp-admin, admin-ajax and XML-RPC never see it.<\/p>\n\n<p>By default a key is accepted on this plugin's own routes and nowhere else (and not on <code>\/update<\/code>, which installs a new version of the plugin). If a tool also needs WordPress's own REST routes (to list posts or upload media, for example), make the key with the wider scope. Such a key is as powerful as an application password for your account, so store it like a password. Revoke any key on the same screen. To switch key sign-in off entirely: <code>add_filter('bulkseme_api_key_auth_enabled', '__return_false');<\/code>.<\/p><\/dd>\n<dt id=\"how%20do%20i%20keep%20the%20plugin%20up%20to%20date%20on%20sites%20i%20manage%20over%20the%20api%3F\"><h3>How do I keep the plugin up to date on sites I manage over the API?<\/h3><\/dt>\n<dd><p>Since 2.0.2 there is a route for it. <code>GET \/update<\/code> shows the installed version and the version WordPress.org offers. <code>POST \/update {\"install\": true}<\/code> installs the offered version straight away, the way the Update button does: the plugin stays active and the site shows its maintenance notice for a few seconds. <code>POST \/update {\"check\": true}<\/code> asks WordPress to look for a new version now. The route runs WordPress's own update code, takes the build from WordPress.org and nowhere else, and cannot be given a file or an address. Installing needs the right to update plugins (an administrator, or a network administrator on a multisite), and a site that forbids file changes is left alone. An API key with the default scope cannot use this route: it takes a key with the wider scope, an application password or a browser session. The plugin does not change how WordPress updates plugins; automatic updates stay as you set them on the Plugins screen.<\/p><\/dd>\n<dt id=\"how%20do%20redirects%20work%3F\"><h3>How do redirects work?<\/h3><\/dt>\n<dd><p>POST \/redirect with <code>from<\/code> (a path on your site), <code>to<\/code> (a path or a full URL) and <code>type<\/code> (301, 302, 307 or 410). If Rank Math's Redirections module is on, the rule is saved through Rank Math and shows in its own list. Otherwise the plugin stores the rule and serves it. Redirects need an administrator, and Rank Math's own Redirections capability when the rule goes through Rank Math.<\/p><\/dd>\n<dt id=\"what%20is%20left%20behind%20if%20i%20delete%20the%20plugin%3F\"><h3>What is left behind if I delete the plugin?<\/h3><\/dt>\n<dd><p>Deleting the plugin removes its API key hashes, so no way of signing in is left behind. Stored schema, redirect rules, the merchant policy and content backups are kept in the database, because a site may still want them, and they come back into use if the plugin is installed again.<\/p><\/dd>\n<dt id=\"does%20it%20work%20with%20woocommerce%20products%3F\"><h3>Does it work with WooCommerce products?<\/h3><\/dt>\n<dd><p>Yes. Meta is registered on every public post type, so products, custom post types, etc. are all covered. Product category and product tag archives (and any other custom taxonomy) are also editable via the term endpoints (v1.3.0+).<\/p><\/dd>\n<dt id=\"can%20i%20edit%20category%20%2F%20tag%20archive%20seo%20meta%20in%20bulk%3F\"><h3>Can I edit category \/ tag archive SEO meta in bulk?<\/h3><\/dt>\n<dd><p>Yes (v1.3.0+). Pass <code>?include_terms=1<\/code> to <code>\/export<\/code> to pull category, tag, and any custom-taxonomy archive rows alongside posts. Post <code>\/bulk<\/code> items with <code>kind: \"term\"<\/code> and the <code>taxonomy<\/code> slug to update them. Yoast term meta is stored in the <code>wpseo_taxonomy_meta<\/code> option; Rank Math uses standard term meta. Both are handled transparently, you pass plugin-neutral aliases (<code>title<\/code>, <code>description<\/code>, <code>og_title<\/code>, etc.) and the plugin writes to the right place.<\/p><\/dd>\n<dt id=\"why%20am%20i%20getting%20401%20unauthorized%20on%20localhost%3F\"><h3>Why am I getting 401 Unauthorized on localhost?<\/h3><\/dt>\n<dd><p>WordPress disables Application Passwords on non-HTTPS sites by default. For local development only, drop a small mu-plugin into <code>wp-content\/mu-plugins\/<\/code> with <code>add_filter('wp_is_application_passwords_available', '__return_true');<\/code>.<\/p><\/dd>\n<dt id=\"why%20does%20%60curl%60%20work%20but%20my%20python%2Fnode%20script%20gets%20403%20forbidden%3F\"><h3>Why does `curl` work but my Python\/Node script gets 403 Forbidden?<\/h3><\/dt>\n<dd><p>The host's web-application firewall (Apache <code>mod_security<\/code>, Wordfence, Solid Security, Cloudflare WAF) is rejecting the request based on User-Agent before WordPress sees it. Default UAs like <code>python-requests\/2.x<\/code> and Node's bare <code>node-fetch<\/code> are on most WAF blocklists. Send a regular browser User-Agent header on every REST call (e.g. <code>Mozilla\/5.0 ... Chrome\/124.0 Safari\/537.36<\/code>) and the 403 disappears. The bundled MCP server (v1.4.2+) already does this automatically, override with <code>MCP_USER_AGENT<\/code> if your host has a stricter rule. Symptom signature: <code>curl -u 'user:pass' https:\/\/site\/wp-json\/seo-meta-bridge\/v1\/status<\/code> returns 200, but the same call from Python\/Node with the same credentials returns 403 with an Apache HTML error page (not a WP JSON error).<\/p><\/dd>\n<dt id=\"can%20the%20plugin%20be%20used%20to%20write%20arbitrary%20postmeta%3F\"><h3>Can the plugin be used to write arbitrary postmeta?<\/h3><\/dt>\n<dd><p>No. Only meta keys belonging to the active SEO plugin (Yoast or Rank Math) are accepted, plus the plugin's own <code>schema<\/code> and <code>schema_mode<\/code> fields and, since 1.9.0, <code>post_title<\/code>. Other keys are rejected with <code>unknown_or_disallowed_key<\/code>.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>2.3.2<\/h4>\n\n<ul>\n<li>Live SEO panel: no price, offer or product checks on pages that are not WooCommerce products, so a service page is no longer told to add a price nobody set.<\/li>\n<li>Live SEO panel: fewer busywork warnings. A short title or description and a page without structured data are notes now; the Social row warns only without a share image; nested organizers and sponsors are not checked as the business.<\/li>\n<li>Live SEO panel: the canonical check ignores tracking parameters (utm_*, gclid, fbclid and similar) and parameter order.<\/li>\n<li><code>GET \/status<\/code> reports <code>merchant_enrichment.enabled<\/code> false when WooCommerce is not active.<\/li>\n<\/ul>\n\n<h4>2.3.1<\/h4>\n\n<ul>\n<li>Fix: <code>POST \/update<\/code> with <code>check<\/code> answered \"up to date\" without asking WordPress.org when the stored time of the last update check was in the future. The \"checked less than a minute ago\" shortcut took any future time as recent, so the check was skipped until the server's clock caught up (seen on a live site, about an hour ahead). Only a last check between 0 and 60 seconds ago counts now; a future one is treated as stale and the check runs. If WordPress.org does not answer, what WordPress knew is put back and the route answers 503, as before; a last check stamped in the future is put back as 0, so WordPress's own scheduled checks are not held back either.<\/li>\n<li><code>GET \/update<\/code> also reports <code>server_time<\/code> and <code>checked_in_future<\/code>, so a stored check time ahead of the server's clock is visible.<\/li>\n<\/ul>\n\n<h4>2.3.0<\/h4>\n\n<ul>\n<li>New: live SEO panel. Signed-in users who may edit the page in view get an \"SEO\" item in the admin bar with a count of problems. It opens a panel over the page with four tabs. Overview: the search result and every tag read from the rendered page (title, description, robots including an X-Robots-Tag header, canonical, H1, social tags, images, links), each judged. Edit: SEO title, description and focus keyword with length checks, saved into Yoast SEO or Rank Math. Schema: every JSON-LD block with the plugin that printed it, missing required and recommended properties, duplicate Organization or WebSite nodes, broken JSON, links to an @id that is not on the page; custom schema can be added or set to replace the page's schema. History: the SEO changes on this post.<\/li>\n<li>New: change history. Every change to a post's SEO title, description, keyword, canonical, robots, social fields or custom schema is kept on the post (the last 30), with the time, the user and where it came from: the editor, an API key, the REST API, WP-CLI or the live panel.<\/li>\n<li>New: \"Live on the site\" in the block editor's document sidebar and as a side box in the classic editor: what the published page sends, read again after every save, with the last changes.<\/li>\n<li>New route <code>POST \/live<\/code>, for the panel only. It needs a signed-in browser session with a REST nonce (keys and application passwords use <code>\/bulk<\/code>), the right to edit that post or term, and each field passes the same checks as in <code>\/bulk<\/code>, including the SEO plugin's own role rules for its settings groups.<\/li>\n<\/ul>\n\n<h4>2.2.0<\/h4>\n\n<ul>\n<li>New: Elementor choice settings in <code>\/content<\/code>. Dropdown, choose and switch controls on a widget's Content tab, including the fields of repeater rows (for example a Post Info row's <code>type<\/code>, <code>taxonomy<\/code> and <code>link<\/code>), are listed with <code>kind: choice<\/code> and their <code>options<\/code>. A write is accepted only when the value is one of those options. Heading levels stay <code>kind: tag<\/code>. Needs Elementor running, because the options come from its registered controls.<\/li>\n<li>New: add an Elementor element by copying an existing one. <code>POST \/content {\"inserts\": [{\"ref\": \"&lt;element id&gt;\", \"position\": \"after\", \"copy_from\": {\"post_id\": 123, \"el\": \"&lt;element id&gt;\"}}]}<\/code> clones an element from the same page or another Elementor page you can edit, with new element and row ids and without its CSS ID, before, after or inside <code>ref<\/code>. Elements only go where Elementor accepts them (a section placed in a column becomes an inner section). Up to 20 per request, applied before <code>updates[]<\/code>, with the usual dry run, base_hash, backup and revert. New <code>atomic=1<\/code> writes nothing when any update or insert in the request fails.<\/li>\n<li>Security: the copy source passes the same checks as editing it (edit rights, the builder gate and Elementor's own permission). Unless the site allows raw HTML (the <code>bulkseme_allow_unfiltered_html<\/code> filter) and the request sends <code>unsafe_html=1<\/code>, a copy, including an administrator's, keeps only the standard element keys (no local styles), its text is cleaned like a direct write (the result says <code>sanitized: true<\/code> when anything was cleaned or left out), and they cannot copy HTML, shortcode, template, code or WordPress widgets, any widget with a code control, Custom CSS, custom attributes, dynamic tags, or any other setting that holds markup or a script link; nor set a choice to a raw output value. Size limits per copy, per request and per page (8 MB, 24 levels) stop runaway growth.<\/li>\n<li><code>\/status<\/code> reports <code>supports_choice_settings<\/code> and <code>supports_inserts<\/code>.<\/li>\n<\/ul>\n\n<h4>2.1.0<\/h4>\n\n<ul>\n<li>New: add a row to an Advanced Custom Fields repeater or flexible content field. Send an update <code>{\"op\": \"add_row\", \"el\": \"&lt;field&gt;\", \"position\": 2, \"layout\": \"&lt;layout&gt;\", \"values\": {\"&lt;sub field&gt;\": \"text\"}}<\/code> in <code>POST \/content<\/code>. <code>GET \/content<\/code> lists every repeater and flexible content field under <code>containers<\/code>, with its row count and the sub fields a new row may fill. The rows below the insert move down with every value they hold, images and nested rows included. Text is cleaned like any other write, the field's maximum row count holds, and a stored value that belongs to another field, or that names a PHP class, stops the insert before anything is written. The backup holds the whole field, so <code>\/content\/revert<\/code> puts the rows back exactly.<\/li>\n<li>New: Advanced Custom Fields on taxonomy terms. With <code>kind=term<\/code>, <code>GET \/content<\/code> lists the term's ACF text fields and rows under <code>acf<\/code>, and <code>POST \/content<\/code> with <code>updates[]<\/code> edits them and adds rows, with the same backup and revert as on posts. The term description is left alone.<\/li>\n<li>Fix: a write to text kept in post meta (ACF, Beaver Builder, SiteOrigin, Accordions, and Elementor when its own resync is not available) bumped the modified date by saving the post again. For a user without unfiltered_html that ran the stored title, content and excerpt through kses and could rewrite markup the write never aimed at, such as the trailing semicolon of a <code>style<\/code> attribute. Only the date changes now.<\/li>\n<li>Security hardening: <code>kind=term<\/code> no longer reaches a taxonomy that has neither an edit screen nor a REST route (such as post formats), the same reach WordPress itself gives. Present since 2.0.0.<\/li>\n<li>Security hardening: on a network, the fallback that reads ACF field definitions from stored meta when no field group matches is for super admins only.<\/li>\n<li>New filter <code>bulkseme_user_can_edit_term_acf<\/code> keeps a role out of a term's ACF fields, as <code>bulkseme_user_can_edit_with_builder<\/code> does for posts.<\/li>\n<li><code>\/status<\/code> reports <code>supports_acf_rows<\/code> and <code>supports_term_acf<\/code>.<\/li>\n<\/ul>\n\n<h4>2.0.2<\/h4>\n\n<ul>\n<li>Security: <code>\/import<\/code> read an uploaded CSV file line by line, so one quoted cell that held line breaks was read as several rows. A post title can hold line breaks and <code>\/export<\/code> wrote it as it was, so an Author could plant rows in a title, and they were applied with the rights of whoever re-imported that export: SEO fields of posts the Author may not edit, and the site-wide title settings. The file is now read as a whole, the way RFC 4180 defines it, and line breaks and other control characters are taken out of every exported cell. If you read an export in a script, use a CSV reader over the whole text, never line by line. Present in every earlier version. Found in an independent review; not known to have been used.<\/li>\n<li>Security: stored JSON-LD schema is no longer printed on a password-protected post until the visitor has entered the password.<\/li>\n<li>Security hardening: a key with the default scope signs in on this plugin's own routes by name, and no longer on everything under its REST namespace, so it cannot reach a route another plugin registers there.<\/li>\n<li>Fix: <code>\/export<\/code> ended with a PHP error when <code>post_type<\/code>, <code>status<\/code> or <code>taxonomy<\/code> was sent as a list (<code>?status[]=draft<\/code>). Lists are accepted now. A list sent where one value belongs is refused cleanly, and a list sent as an <code>id<\/code> is no longer read as post 1.<\/li>\n<li>Fix: a URL field (canonical, Open Graph image, Twitter image) holding bytes that are not valid UTF-8, written through WordPress's own REST route, was answered with a 500. Such bytes no longer reach the database.<\/li>\n<li>Hardening: WPBakery design rules rebuilt with <code>rebuild_wpb_css<\/code> go through the filter WordPress uses for inline styles. HTML written to a SiteOrigin widget or Beaver Builder module that the plugin has no field map for has the quotes in its text stored as entities.<\/li>\n<li><code>\/import<\/code> of a file: 10 MB limit, a file that ends inside a quoted cell is refused, and rows whose cell count does not match the header are counted in <code>skipped_rows<\/code>.<\/li>\n<li>New: <code>GET<\/code> and <code>POST \/update<\/code>. See which version is installed and which one WordPress.org offers, and install the offered version without opening wp-admin. It runs WordPress's own update code, so the build can only come from WordPress.org; installing needs the right to update plugins, a site that forbids file changes is left alone, and a key with the default scope cannot use the route.<\/li>\n<li>Security hardening: a key that is revoked here is also taken out of the copy a companion build keeps of the keys it handed over, whether or not that build is active at the time.<\/li>\n<li>MCP server 1.7.2: <code>list_posts<\/code> lists only post types the site exposes, every tool argument that reaches a request path is checked, requests time out after 60 seconds, an answer over 20 MB is refused, and the dependency lock file is refreshed.<\/li>\n<li>The GitHub repository moved to github.com\/puneetindersingh\/bulk-ai-cli-connector.<\/li>\n<\/ul>\n\n<h4>2.0.1<\/h4>\n\n<ul>\n<li>Security hardening: <code>\/clone<\/code> now checks a stored meta value in exactly the form WordPress reads it back. A value planted straight into the database with a NUL byte in front of a serialised object could get past the check that keeps <code>\/clone<\/code> from recreating objects of other classes. Using it needed a row written outside WordPress and a caller with the <code>unfiltered_html<\/code> capability. Found in an independent review; not known to have been used.<\/li>\n<\/ul>\n\n<h4>2.0.0<\/h4>\n\n<ul>\n<li>Renamed from \"Bulk SEO Meta Editor for AI Agents\" to \"Bulk AI CLI Connector\". The plugin address, folder, REST routes and stored data are unchanged.<\/li>\n<li>New: page content. <code>GET \/content<\/code> and <code>POST \/content<\/code> read and edit the visible text of a page for Elementor (classic and Elementor 4 atomic widgets, heading level, link URLs and per-element custom CSS), Beaver Builder, SiteOrigin Page Builder (core text widgets and the SiteOrigin Widgets Bundle: editor, headline, button, call to action, accordion, tabs, hero, features, testimonials and image), the Accordions plugin and Advanced Custom Fields text fields, and the post content of Gutenberg, classic, Divi and WPBakery pages. <code>POST \/content\/replace<\/code> runs a literal find and replace. Add <code>kind=term&amp;taxonomy=&lt;slug&gt;<\/code> for a term description, which keeps headings, lists and tables.<\/li>\n<li>New: a backup before every content write, with <code>GET \/content\/backups<\/code> and <code>POST \/content\/revert<\/code>. Backups are kept in a hidden post type that nobody can read or edit outside these two routes, the newest ten per item, and a backup can only be restored onto the item it came from.<\/li>\n<li>New: <code>dry_run<\/code> and <code>base_hash<\/code> on content writes.<\/li>\n<li>New: <code>POST \/clone<\/code> copies a page or post into a new draft with its template, design meta, featured image and terms. SEO fields are not copied.<\/li>\n<li>New: <code>GET<\/code>, <code>POST<\/code> and <code>DELETE \/redirect<\/code>. Rules go through Rank Math's Redirections when that module is on, otherwise the plugin stores and serves them.<\/li>\n<li>New: <code>GET<\/code>, <code>POST<\/code> and <code>DELETE \/merchant-policy<\/code>. Adds return policy, shipping details, brand and a price start date to WooCommerce Product schema that the SEO plugin left without them. Opt out with <code>add_filter('bulkseme_merchant_enrichment', '__return_false')<\/code>.<\/li>\n<li>New: API keys. Made on Tools &gt; Agent API Keys, shown once, stored as SHA-256 hashes, REST only, HTTPS only. Send a key in the <code>X-Bulkseme-Key<\/code> header or as the password of HTTP Basic auth. A key is accepted on this plugin's own routes only, unless it is made with the wider scope, which covers every REST route. A key is tied to the account that made it and is removed when that account is deleted. <code>POST \/register-key<\/code> stores the hash of a key from an administrator's own browser session.<\/li>\n<li>Security: content routes check <code>edit_post<\/code> or <code>edit_term<\/code> per item and follow the page builder's own access rules (Elementor's excluded roles, Beaver Builder's Builder Access). Every value is cleaned with <code>wp_kses_post<\/code>, administrators included. Storing unfiltered HTML needs the <code>bulkseme_allow_unfiltered_html<\/code> filter, the <code>unfiltered_html<\/code> capability and <code>unsafe_html: 1<\/code> in the request.<\/li>\n<li>Security: redirects and merchant policy need <code>manage_options<\/code>. Redirects written through Rank Math also need its <code>rank_math_redirections<\/code> capability.<\/li>\n<li>Security: posts of a type that has no edit screen and no REST route of its own are now out of reach of every route, including <code>\/bulk<\/code>, <code>\/export<\/code> and <code>\/schema<\/code>. Before this, a user could rename such a record through <code>post_title<\/code> in <code>\/bulk<\/code> if WordPress counted them as its author, and that included a privacy request they had filed themselves, whose title is the address the request acts on. Recommended update for every site.<\/li>\n<li>Security: stored JSON-LD is limited to 128 KB and 5,000 values per post or term (<code>schema_too_large<\/code>), because it is read on every view of the page. Filters: <code>bulkseme_schema_max_bytes<\/code>, <code>bulkseme_schema_max_values<\/code>.<\/li>\n<li>Security: on a site with neither Yoast SEO nor Rank Math, storing schema now needs <code>edit_others_posts<\/code> (Editor and above), in line with Yoast SEO's own default for schema settings. Filter: <code>bulkseme_user_can_edit_field_group<\/code>.<\/li>\n<li>Fix: titles and descriptions that use replacement variables are stored intact. WordPress's general text cleaner removed anything that looked like a percent-encoded byte, so <code>%%category%%<\/code> was stored as <code>%tegory%%<\/code> and Rank Math's <code>%date%<\/code> as <code>te%<\/code>. The active SEO plugin's own text cleaner is now used. Post titles keep their percent signs too.<\/li>\n<li>Fix: a merchant policy number that is not finite (for example <code>1e400<\/code>) is refused. It could not be printed as JSON and emptied the product schema block.<\/li>\n<li><code>\/status<\/code> reports the new capabilities: <code>supports_content<\/code>, <code>supports_redirects<\/code>, <code>supports_api_key<\/code>, <code>supports_clone<\/code>, <code>builders<\/code> and more. Page builder version numbers, the redirect engine and the merchant policy summary are only shown to administrators. Filter: <code>bulkseme_status<\/code>.<\/li>\n<li>MCP server 1.7.1: post ids and post type names are checked before they are used in a request path.<\/li>\n<li>The plugin is now split into files under <code>includes\/<\/code>. No setting, stored value or existing route changed name.<\/li>\n<li>Deleting the plugin now removes its API key hashes, on every site of a network.<\/li>\n<\/ul>\n\n<h4>1.9.1<\/h4>\n\n<ul>\n<li>Security: SEO fields registered on the core <code>wp\/v2<\/code> routes are now returned in the <code>edit<\/code> context only. Before this, a plain <code>GET \/wp\/v2\/posts\/&lt;id&gt;<\/code> included them for anyone who could read the post, logged in or not, and that included the focus keyword, which is not printed anywhere on the page. Clients that read <code>meta<\/code> from a core route must add <code>?context=edit<\/code>. The bundled MCP server already does.<\/li>\n<li>Security: the active SEO plugin's own role rules now apply to writes through <code>\/bulk<\/code>, <code>\/import<\/code> and the core routes. Yoast SEO: canonical, robots and breadcrumb title fields, and custom schema, need <code>wpseo_edit_advanced_metadata<\/code> or <code>wpseo_manage_options<\/code> while Yoast's \"Security: no advanced or schema settings for authors\" setting is on. Rank Math: its <code>rank_math_onpage_general<\/code>, <code>rank_math_onpage_advanced<\/code>, <code>rank_math_onpage_social<\/code> and <code>rank_math_onpage_snippet<\/code> capabilities. A refused field is reported as <code>forbidden_field:&lt;key&gt;<\/code>. Filter: <code>bulkseme_user_can_edit_field_group<\/code>.<\/li>\n<li>Security: archive and global scope settings now need the SEO plugin's own settings capability (<code>wpseo_manage_options<\/code> or <code>rank_math_titles<\/code>) as well as <code>manage_options<\/code>. This matters on multisite and on sites that use a role manager.<\/li>\n<li>Security: <code>\/bulk-alts<\/code> now also requires <code>upload_files<\/code>, so only users who can manage media can call it.<\/li>\n<li>Security: <code>\/export<\/code> now neutralises spreadsheet formulas. A cell that starts with <code>=<\/code>, <code>+<\/code>, <code>-<\/code>, <code>@<\/code>, a tab or a carriage return is written with a leading apostrophe, so Excel, Google Sheets and LibreOffice show it as text instead of running it. <code>\/import<\/code> removes that apostrophe again, so a CSV round trip gives back the stored value unchanged. Use <code>\/bulk<\/code> for values that must be taken literally.<\/li>\n<li>Security hardening: text fields sent as an array or an object are now refused with <code>invalid_value_type:&lt;key&gt;<\/code> instead of being stored unsanitised. Before this, such a value could also end the request with a PHP error when it reached a URL field. Booleans and numbers are stored as text, and the two Rank Math list fields (<code>robots<\/code>, advanced robots) still accept lists.<\/li>\n<li><code>schema<\/code> sent as a JSON object or array, not only as a JSON string, is now validated and stored. It used to be read as empty, which cleared the stored schema.<\/li>\n<li><code>rank_math_primary_&lt;taxonomy&gt;<\/code> is only accepted for a taxonomy that exists.<\/li>\n<li>Items and rows that are not objects are answered with <code>invalid_item<\/code> or <code>invalid_row<\/code>.<\/li>\n<\/ul>\n\n<h4>1.9.0<\/h4>\n\n<ul>\n<li>New: custom JSON-LD schema on taxonomy terms. <code>\/bulk<\/code> items with <code>kind: \"term\"<\/code> accept the same <code>schema<\/code> and <code>schema_mode<\/code> fields as posts, stored in term meta under the same keys, and <code>GET \/schema?id=&lt;term_id&gt;&amp;kind=term&amp;taxonomy=&lt;slug&gt;<\/code> reads them back. The schema renders on the first page of the term archive only. On Rank Math the archive keeps its <code>CollectionPage<\/code> type next to an injected <code>FAQPage<\/code> instead of losing it.<\/li>\n<li>New: schema output when no SEO plugin graph is printed. With no Yoast or Rank Math installed, or with their schema output switched off, stored schema is printed as the plugin's own JSON-LD block (encoded with JSON_HEX_TAG and JSON_HEX_AMP, on top of the existing markup checks). When a graph is printed, nothing changes: the nodes still merge into it. Filter <code>bulkseme_print_standalone_schema<\/code> disables the standalone block.<\/li>\n<li>New: <code>post_title<\/code> accepted in <code>\/bulk<\/code> items. Plain text only, slug untouched, <code>edit_post<\/code> required, empty values rejected with <code>empty_post_title<\/code>. <code>\/import<\/code> keeps treating <code>post_title<\/code> as a read-only column.<\/li>\n<li>New: <code>POST \/sitemap\/flush<\/code> clears the Rank Math and Yoast sitemap caches (<code>edit_others_posts<\/code> required) so sitemaps pick up REST edits without a wp-admin save.<\/li>\n<li>New: invalid Product schema guard. <code>Product<\/code> nodes without <code>offers<\/code>, <code>review<\/code> or <code>aggregateRating<\/code> are removed from the Yoast and Rank Math graphs, together with any dangling <code>@id<\/code> references to them. Priced products are untouched. Disable with <code>add_filter('bulkseme_drop_invalid_product_schema', '__return_false')<\/code>.<\/li>\n<li><code>\/status<\/code> now reports <code>supports_term_schema<\/code>, <code>supports_post_title<\/code>, <code>supports_sitemap_flush<\/code> and <code>drops_invalid_product_schema<\/code>.<\/li>\n<li>Requires WordPress 5.7 or later (was 5.6) for <code>wp_print_inline_script_tag()<\/code>.<\/li>\n<\/ul>\n\n<h4>1.8.2<\/h4>\n\n<ul>\n<li>Security: the custom JSON-LD schema field now rejects any value that contains a raw HTML angle bracket (&lt; or &gt;) when saved, and strips angle brackets from stored schema on output. This closes a stored cross-site scripting issue on Rank Math sites, where a crafted JSON-LD object key could break out of the schema script block. Yoast sites were not affected. No valid JSON-LD needs raw angle brackets, so normal schema is untouched. Reported responsibly by Junhui Mo (Guangzhou University).<\/li>\n<\/ul>\n\n<h4>1.8.1<\/h4>\n\n<ul>\n<li>Security: <code>GET \/export<\/code> now capability-filters every row it returns, using the same capability the matching write path enforces. Term rows require the <code>edit_term<\/code> meta capability on each term, so taxonomies registered with their own capabilities are respected. CPT archive rows require <code>manage_options<\/code>, 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 <code>edit_post<\/code> per post instead of relying on the <code>edit_others_posts<\/code> pre-filter, which covers only the core post capabilities and not custom post types that declare their own <code>capability_type<\/code>. Rows the caller cannot manage are omitted, so <code>\/export<\/code> no longer returns SEO metadata for objects the caller could not open in wp-admin.<\/li>\n<li>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. <code>row_count<\/code> still reports the number of rows actually emitted.<\/li>\n<\/ul>\n\n<h4>1.8.0<\/h4>\n\n<ul>\n<li>Security: every bulk write now enforces a per-object capability check in addition to the route-level <code>edit_posts<\/code> gate. <code>POST \/bulk-alts<\/code> checks <code>edit_post<\/code> on each resolved attachment, term updates check the <code>edit_term<\/code> meta capability on each term, and CPT archive updates now require <code>manage_options<\/code> (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.<\/li>\n<li>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 <code>WPSEO_Taxonomy_Meta::set_values()<\/code> (merged with the term's current values so untouched fields are preserved) and archive\/global titles with <code>WPSEO_Options::set()<\/code>, 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.<\/li>\n<li>Internal: the plugin version constant is now a plain define kept in sync with the plugin header, replacing the runtime header parse.<\/li>\n<\/ul>\n\n<h4>1.7.0<\/h4>\n\n<ul>\n<li>Changed: <code>GET \/export<\/code> now returns JSON in the shape <code>{ \"filename\", \"row_count\", \"csv\" }<\/code> instead of streaming raw CSV bytes. The <code>csv<\/code> 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 <code>\/import<\/code> accepts the same CSV as before.<\/li>\n<li>Upgraders: scripts that saved the old raw response body to a file should now read the <code>csv<\/code> field of the JSON response. The bundled MCP server 1.7.0 handles both shapes automatically.<\/li>\n<\/ul>\n\n<h4>1.6.1<\/h4>\n\n<ul>\n<li>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-1024x683.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.<\/li>\n<\/ul>\n\n<h4>1.6.0<\/h4>\n\n<ul>\n<li>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 <code>wpseo_schema_graph<\/code> filter, Rank Math via <code>rank_math\/json_ld<\/code>). The plugin emits no markup itself, so there is no extra output-escaping surface, and the nodes join the existing <code>@graph<\/code> instead of creating a second competing block (no duplicate-schema conflict).\n\n<ul>\n<li>Write with the <code>schema<\/code> field on <code>\/bulk<\/code> or <code>\/import<\/code>, e.g. <code>{ id: 12, meta: { schema: \"{...JSON-LD...}\", schema_mode: \"add\" } }<\/code>. JSON is validated on write; invalid JSON is rejected with <code>invalid_json_schema<\/code>.<\/li>\n<li><code>schema_mode<\/code>: <code>add<\/code> (default) merges your nodes into the graph; <code>replace<\/code> makes your nodes the whole graph for that page.<\/li>\n<li>Accepts a full document (<code>{ \"@context\": ..., \"@graph\": [...] }<\/code>), a bare list of nodes, or a single node object. Per-node <code>@context<\/code> is dropped so the SEO plugin sets one top-level context.<\/li>\n<li>New <code>GET \/schema?id=123<\/code> reads back the stored schema and mode for a post.<\/li>\n<li><code>\/status<\/code> now reports <code>supports_schema: true<\/code>.<\/li>\n<\/ul><\/li>\n<\/ul>\n\n<h4>1.5.1<\/h4>\n\n<ul>\n<li>Internal: renamed all internal PHP function and define prefixes from <code>seo_meta_bridge_*<\/code> \/ <code>SEO_META_BRIDGE_*<\/code> to <code>bulkseme_*<\/code> \/ <code>BULKSEME_*<\/code> so the prefix derives directly from the plugin slug. No public REST surface changes: endpoints still live at <code>\/wp-json\/seo-meta-bridge\/v1\/*<\/code>. No migration required.<\/li>\n<li>Internal: removed <code>if (!function_exists(...))<\/code> 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.<\/li>\n<li>Code clarity: added comments next to <code>update_option('wpseo_titles', ...)<\/code> and <code>update_option('rank-math-options-titles', ...)<\/code> 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.<\/li>\n<\/ul>\n\n<h4>1.5.0<\/h4>\n\n<ul>\n<li><strong>New: bulk update image alt text.<\/strong> New endpoint <code>POST \/seo-meta-bridge\/v1\/bulk-alts<\/code> accepts <code>{ items: [{ image_url, new_alt }, ...] }<\/code> (max 200\/req) and updates <code>_wp_attachment_image_alt<\/code> on the matching media-library attachment. Resolves URLs via <code>attachment_url_to_postid()<\/code> with two fallbacks: strips <code>-WIDTHxHEIGHT.ext<\/code> size suffix (so a <code>-300x200.jpg<\/code> thumb still maps to its parent attachment), then strips any query string. Each row reports <code>status: ok | unchanged | skipped | error<\/code> plus <code>attachment_id<\/code>, <code>matched_via<\/code>, and <code>previous_alt<\/code> 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). <code>\/status<\/code> now reports <code>supports_alts: true<\/code>.<\/li>\n<\/ul>\n\n<h4>1.4.2<\/h4>\n\n<ul>\n<li><strong>Security: <code>\/export<\/code> now filters by author for low-privilege roles.<\/strong> Authors and Contributors (anyone with <code>edit_posts<\/code> but not <code>edit_others_posts<\/code>) calling <code>\/export<\/code> 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 <code>WP_Query<\/code> to the calling user's own authored posts when they lack <code>edit_others_posts<\/code>, 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.<\/li>\n<\/ul>\n\n<h4>1.4.1<\/h4>\n\n<ul>\n<li><strong>Bug fix: term meta updates now invalidate Yoast's Indexable cache.<\/strong> Previously, writing to <code>wpseo_taxonomy_meta<\/code> via <code>\/bulk<\/code> (kind=term) updated the option correctly and <code>\/export<\/code> read the new value back, but the FRONT-END kept rendering the old meta description because Yoast 14+ caches rendered SEO meta in the <code>yoast_indexable<\/code> 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 <code>do_action('wpseo_save_taxonomy_meta', $term_id, $taxonomy)<\/code> and the standard <code>do_action('edited_term', ...)<\/code>, 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).<\/li>\n<\/ul>\n\n<h4>1.4.0<\/h4>\n\n<ul>\n<li>CPT archive page support: write SEO meta to custom-post-type archive pages (e.g. <code>\/challenges\/<\/code>, <code>\/news\/<\/code>) for any CPT registered with <code>has_archive=true<\/code>. Yoast: stored in <code>wpseo_titles<\/code> option (<code>title-ptarchive-{ptype}<\/code>, <code>metadesc-ptarchive-{ptype}<\/code>); Rank Math: stored in <code>rank-math-options-titles<\/code> (<code>pt_{ptype}_archive_title<\/code>, <code>pt_{ptype}_archive_description<\/code>).<\/li>\n<li>Global SEO scopes: write meta for <code>author_archive<\/code>, <code>date_archive<\/code>, <code>search<\/code>, <code>p404<\/code>, <code>home<\/code> (latest-posts mode) via a single registry. New scopes can be added in one place without touching dispatch code.<\/li>\n<li><code>\/status<\/code> reports <code>supports_archives<\/code>, <code>supports_globals<\/code>, <code>archive_fields<\/code>, and <code>global_scopes<\/code> (alias map per active scope).<\/li>\n<li><code>\/export?include_archives=1<\/code> appends one synthetic row per CPT-with-archive (id=0, kind=cpt_archive). Backwards compatible: pre-1.4 consumers ignore unknown kind values.<\/li>\n<li><code>\/bulk<\/code> accepts the new kinds:\n\n<ul>\n<li><code>{ id: 0, kind: \"cpt_archive\", post_type: \"challenges\", meta: {...} }<\/code><\/li>\n<li><code>{ kind: \"author_archive\" | \"date_archive\" | \"search\" | \"p404\" | \"home\", meta: {...} }<\/code><\/li>\n<\/ul><\/li>\n<li><code>\/import<\/code> reads cpt_archive and global rows from CSV for round-trip edits.<\/li>\n<li>Permission: cpt_archive requires the post type's <code>edit_posts<\/code> cap; global scopes require <code>manage_options<\/code> (admin-only: they affect site-wide SEO settings).<\/li>\n<\/ul>\n\n<h4>1.3.0<\/h4>\n\n<ul>\n<li><strong>Taxonomy term archives are now editable<\/strong>: categories, tags, and any custom taxonomy archive (e.g. WooCommerce <code>product_cat<\/code>, <code>product_tag<\/code>, 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.<\/li>\n<li><code>\/status<\/code> now reports <code>supports_terms: true<\/code> and a <code>term_fields<\/code> alias map so clients can detect the capability.<\/li>\n<li><code>\/export?include_terms=1<\/code> appends term archive rows to the CSV. New trailing <code>kind<\/code> and <code>taxonomy<\/code> columns flag term rows; post rows have <code>kind=post<\/code> with an empty <code>taxonomy<\/code>. Filter to specific taxonomies with <code>?taxonomy=category,product_cat<\/code>. Backwards compatible: v1.2.x clients that ignore the trailing columns see the same column shape.<\/li>\n<li><code>\/export?post_type=any<\/code> now correctly returns all public post types (previously needed an explicit comma list of CPTs).<\/li>\n<li><code>\/bulk<\/code> accepts term updates via <code>{ id: &lt;term_id&gt;, kind: \"term\", taxonomy: \"category\", meta: {...} }<\/code>. Existing post-update payloads are unchanged.<\/li>\n<li><code>\/import<\/code> reads <code>kind<\/code> and <code>taxonomy<\/code> columns from CSV uploads so a mixed posts+terms export round-trips cleanly.<\/li>\n<li>Yoast term meta is stored in the <code>wpseo_taxonomy_meta<\/code> option (read-modify-write per call). Rank Math term meta uses <code>wp_termmeta<\/code>. Per-taxonomy <code>edit_terms<\/code> capability is enforced on every write.<\/li>\n<\/ul>\n\n<h4>1.2.0 to 1.2.6<\/h4>\n\n<ul>\n<li>First releases: <code>\/status<\/code>, <code>\/bulk<\/code>, <code>\/export<\/code> and <code>\/import<\/code> for Yoast and Rank Math, with per-post permission checks and the bundled MCP server. Later 1.2.x builds added the length columns and UTF-8 BOM in <code>\/export<\/code>, field aliases in <code>\/bulk<\/code>, and empty CSV cells left alone by <code>\/import<\/code>.<\/li>\n<\/ul>","raw_excerpt":"Connect Claude, ChatGPT and MCP agents to WordPress. Bulk edit SEO meta, schema, redirects and Elementor page copy over the REST API. With undo.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/306360","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=306360"}],"author":[{"embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/puneetindersingh"}],"wp:attachment":[{"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=306360"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=306360"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=306360"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=306360"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=306360"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/ko.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=306360"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}