Helping Organisations get organised!

Architecture notes

Code layout

Since 1.08 the plugin follows a one-class-per-ability layout. Before that, a single 30 KB class held everything, and each ability was spread across four places in it: a registration block, two schema methods and an executor. The PHP class and file names still carry the LH_MCP_Block_Transformer prefix from before the 2.0.0 rename; only the plugin’s identity (name, text domain, hook prefix and ability names) changed.

  • includes/class-lh-mcp-block-transformer.php holds identity, bootstrap, category registration, the filterable list of ability classes (lh_mcp_content_transformer_ability_classes), the permission check, and error_code().
  • includes/class-lh-mcp-block-transformer-post-batch.php is the shared write loop for any ability that saves to real posts: existence check, per-post edit_post, the live-content confirm gate, and the save.
  • includes/abilities/ holds one class per ability, each extending an abstract base that builds the wp_register_ability() call, so category, permission callback and MCP exposure are identical across abilities by construction.

A new ability is a new file in includes/abilities/ plus an entry in the ability class list. A new ability that writes to posts supplies only a per-post processor and inherits every guard from the post-batch class; repair-sourced-attributes (1.09) was the first to be built this way.

Conversion lives in LH Content Converter

Every conversion is a call to LH_Content_Converter::convert() in LH Content Converter, a required plugin, which wraps FormatBridge::convertResult() from automattic/blocks-engine-php-transformer. That call is symmetric across its html, markdown and blocks adapters; this plugin supplies the source and target format and otherwise stays out of the way. The abilities call it directly and pass its errors through, so there is no separate availability check here.

Until 1.10.0 the library was vendored here and reached through an internal bridge class, which lh-mcp-general-abilities also called directly. Moving the library and that code into LH Content Converter means every LocalHero plugin converts with one engine and one library version, instead of two copies whose autoloaders could silently serve each other. The bridge survived one release as a deprecated wrapper and was removed in 2.0.0.

Markdown input is a genuinely weaker source than HTML, for a structural reason rather than a quality one: the richer block patterns are triggered by inline styles and CSS shapes, and Markdown has no way to declare them. Use HTML for anything needing layout, media, buttons or styling; Markdown is fine for plain prose.

Sourced attributes

The transformer library serialises every attribute into the block comment JSON, including ones the block type reads from its HTML, such as the rich-text content of paragraphs, headings and list items. The block editor never does this. The blocks still render correctly, but WP_Block_Type::prepare_attributes_for_render() validates every delimiter attribute against the block’s schema, and rich-text is not a JSON-schema type, so each affected block raised a rest_validate_value_from_schema notice on every render.

Since 1.09 all block output has any delimiter attribute whose registered schema declares a source removed, which is the same rule the editor’s own serializer applies, re-serialising only when something was actually removed. That step now lives in LH_Content_Converter::strip_sourced_attributes(), and repair-sourced-attributes applies it to content saved before then. It is a local guard around upstream behaviour; if the library changes, the strip becomes a no-op rather than a conflict.

Why convert-post-to-blocks exists separately

It would appear redundant: a caller could call convert-to-blocks and hand the resulting markup to some other ability’s content parameter. In practice that two-step composition requires transcribing a large, deeply-escaped block-markup string by hand between two calls. On a real run that transcription silently dropped a closing tag and corrupted a live entry, caught only by chance on review.

convert-post-to-blocks removes that failure mode structurally rather than warning against it: only a post ID crosses the boundary, never the markup. This is worth preserving as a design constraint, not just an implementation detail.

Capability model

Every ability is gated to edit_posts rather than the install_plugins default used by LocalHero developer tooling. This is editorial tooling: nothing in it touches code, the filesystem or diagnostics, and the people who need it are the ones writing and editing content, so it follows the convention for editor-facing content operations.

The ability-level gate is coarse and answers one question: does this caller edit content here. Authorisation for a specific post is a separate question, answered per ID in the post-batch class via current_user_can( 'edit_post', $id ). Routing through edit_post rather than a hand-rolled author comparison means map_meta_cap applies, so published-post restrictions, custom capability types, and any plugin filtering map_meta_cap are all honoured rather than bypassed.

The confirm gate fails closed. It lists the statuses known never to be visible outside the editor (draft, pending, auto-draft) rather than the ones known to be live, so a custom status registered by another plugin requires confirmation until someone deliberately says otherwise through the lh_mcp_content_transformer_unpublished_statuses filter.

Dependency

The plugin declares Requires Plugins: lh-content-converter and has no Composer dependencies of its own. The library lives in LH Content Converter’s vendor/ directory, so it is there that composer install must be re-run after a zip install. If that library is missing, the converting abilities return LH Content Converter’s lh_content_converter_missing_vendor error (per post, for convert-post-to-blocks) and nothing is saved. get-pattern-guide and repair-sourced-attributes do not need the library.

If you like this plugin please consider making a donation to further development