Heading Block CSS Specificity Fix in WordPress 6.9

WordPress 6.9 fixes a specificity issue with the Heading block’s background padding. Previously, padding styles applied to headings with backgrounds were affecting other blocks that use heading elements, such as the Accordion Heading blockBlock Block is the abstract term used to describe units of markup that, composed together, form the content or layout of a webpage using the WordPress editor. The idea combines concepts of what in the past may have achieved with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience.. This fix ensures that background padding is only applied to actual Heading blocks.

What’s changed?

The CSSCSS Cascading Style Sheets. selector for applying padding to headings with backgrounds has been made more specific. The selector now targets .wp-block-heading.has-background instead of just heading element tags (h1h2, etc) with the .has-background class.

Before:

h1, h2, h3, h4, h5, h6 {
  &.has-background {
    padding: ...;
  }
}

After:

h1, h2, h3, h4, h5, h6 {
  &:where(.wp-block-heading).has-background {
    padding: ...;
  }
}

The use of :where() ensures the CSS specificity remains at 0-1-1, minimizing impact on existing theme styles. As the CSS specificity remains unchanged, existing theme styles targeting heading elements should continue to work as expected.

What does this mean for themes?

If a theme applies the .has-background class to heading elements that are not Heading blocks (e.g., <h1 class="page-title has-background">Hello World</h1>), these elements will no longer receive the default block background padding. If a theme relies on this behavior, it will need to be updated to include explicit padding styles for these elements.

Props to @priethor for reviewing.

#6-9, #css, #dev-notes, #dev-notes-6-9

Changes to the Interactivity API in WordPress 6.9

Standardized APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways. to set unique IDs in directives

HTMLHTML HyperText Markup Language. The semantic scripting language primarily used for outputting content in web browsers. does not allow multiple attributes with the same name on a single element, yet multiple plugins may need to add directives of the same type to the same element. To address this, WordPress 6.9 introduces a standardized way to assign unique IDs to Interactivity API directives, allowing an element to have multiple directives of the same type, as long as each directive has a different ID.

To assign a unique ID to a directive, append a triple-dash (---) followed by the ID, as shown in the following example.

<div
    data-wp-watch---my-unique-id="callbacks.firstWatch"
    data-wp-watch---another-id="callbacks.secondWatch"
></div>

The triple-dash syntax unambiguously differentiates unique IDs from suffixes. In the example below, click is the suffix, while plugin-a and plugin-b are the unique IDs. The order is also important: the unique ID must always appear at the end of the attribute name.

<button
    data-wp-on--click---plugin-a="plugin-a::actions.someAction"
    data-wp-on--click---plugin-b="plugin-b::actions.someAction"
>
  Button example
</button>

See #72161 for more details.

Props to @luisherranz and @santosguillamot for the implementation.

Deprecated data-wp-ignore directive

The data-wp-ignore directive was designed to prevent hydration of a specific part of an interactive region. However, its implementation broke context inheritance and caused potential issues with the client-side navigation feature of the @wordpress/interactivity-router module, without addressing a real use case.

Since WordPress 6.9, this directive is deprecated and will be removed in subsequent versions.

See #70945 for more details.

Props to @Sahil1617 for the implementation.

New AsyncAction and TypeYield type helpers

WordPress 6.9 introduces two new TypeScript helper types, AsyncAction<ReturnType> and TypeYield<T>, to the Interactivity API. These types help developers address potential TypeScript issues when working with asynchronous actions.

AsyncAction<ReturnType>

This helper lets developers explicitly type the return value of an asynchronous action (generator). By using any for yielded values, it breaks circular type dependencies when state is used within yield expressions or in the final return value.

TypeYield<T extends (...args: any[]) => Promise<any>

This helper lets developers explicitly type the value that a yield expression resolves to by providing the type of the async function or operation being yielded.

For more information and examples, see the Typing asynchronous actions section in the Interactivity API reference > Core Concepts > Using TypeScript guide.

See #70422 for more details.

Props to @luisherranz for the implementation.

#6-9, #dev-notes, #dev-notes-6-9, #interactivity-api

Interactivity API’s client navigation improvements in WordPress 6.9

In WordPress 6.9, the client-side navigation feature provided by the @wordpress/interactivity-router module has been expanded to cover additional use cases that were previously unsupported.

Support for new script modules and stylesheets

Previously, only the HTMLHTML HyperText Markup Language. The semantic scripting language primarily used for outputting content in web browsers. of the new page was updated, keeping the styles present in the initial page and ignoring any new script modules. This worked for basic client-side navigation cases, but it didn’t handle more complex situations, such as when new blocks appear on the next page.

With this update, WordPress now replaces stylesheets and loads any script modules after client-side navigation.

  • The new algorithm reuses the stylesheets shared with the previous page to minimize networknetwork (versus site, blog) requests, loads any new stylesheet not present in the previous navigations, and disables those that no longer apply.
  • The new algorithm also loads all the script modules belonging to interactive blocks that didn’t exist on the previous pages. To correctly support module dependencies, new importmap definitions are also supported.
  • To maintain the experience of instant navigations, prefetching a page also prefetches all the stylesheets and script modules that were not previously prefetched or loaded.

For details on the implementation, see #70353.

Support for router regions inside interactive elements

Router regions are those elements marked with the data-wp-router-region directive. When the navigate() action from @wordpress/interactivity-router is invoked, the content of these regions is updated to match the newly requested page.

In previous WordPress versions, router regions needed to match a root interactive element (i.e., one of the top-most elements with a data-wp-interactive directive). This meant that if the data-wp-router-region directive was used anywhere else in an interactive region, its content wouldn’t be updated.

<div data-wp-interactive="example">
    <button data-wp-on--click="actions.doSomething">Click me!</button>
    <div
      data-wp-interactive="example"
      data-wp-router-region='example/region-1'
    >
      I wasn't updated on client navigation (now I am!)
    </div>
</div>

Now, router regions are updated whenever they are placed in an interactive region. The only requirement is that they must still be used alongside the data-wp-interactive directive so they receive the corresponding namespace.

For more details, refer to the related issue #71519 and pull request #71635.

New attachTo option for router regions

In WordPress 6.9, router regions accept an attachTo property that can be defined inside the data-wp-router-region directive, allowing the region to be rendered even when it was missing on the initial page. This option supports cases like overlays that are necessary for a blockBlock Block is the abstract term used to describe units of markup that, composed together, form the content or layout of a webpage using the WordPress editor. The idea combines concepts of what in the past may have achieved with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience., but may appear outside all the regions.

The attachTo property should be a valid CSSCSS Cascading Style Sheets. selector that points to the parent element where the new router region should be rendered. For example, the following router region would be rendered in <body> if it appears on a visited page, even if it wasn’t initially rendered.

<div
  data-wp-interactive="example"
  data-wp-router-region='{ "id": "example/region", "attachTo": "body" }'
>
  I'm in a new region!
</div>

See #70421 for more details.

Improved getServerState and getServerContext functions

When using the Interactivity APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways. with client-side navigation, the getServerState() and getServerContext() functions now properly handle the following scenarios:

  1. Properties that are modified on the client but should reset to server values on navigation
    Now, whenever getServerState or getServerContext is tracking a value that doesn’t change after a client-side navigation, it will still trigger an invalidation so that it can be used to reset values, such as:
   const { state } = store( 'myPlugin', {
    // ...
    callbacks: {
        resetCounter() {
            const serverState = getServerState(); // Always { counter: 0 };
            state.counter = serverState.counter; // Reset to 0;
        },
    },
   } );
  1. Properties that only exist on certain pages
    Server state and contexts are now fully overwritten: only the properties present on the current page are retained, and those from previous pages are removed. This allows having the certainty of knowing if a property doesn’t exist in the server state, even if it was present on the previous page.
   store( 'myPlugin', {
    // ...
    callbacks: {
        onlyWhenSomethingExists() {
            const serverState = getServerState();
            if ( serverState.something ) {
                // Do something...
            }
        },
    },
   } );

Additionally, these functions now include proper type definitions and error messages in debug mode, among other improvements.

See #72381 for more details.

Props to @darerodz and @luisherranz for the implementations.

Login to Reply<\/a><\/li><\/ul><\/div>","commentTrashedActions":"

Preparing the Post Editor for Full iframe Integration

As part of an ongoing effort to modernize the editing experience, WordPress is moving toward running the post editor inside an iframeiframe iFrame is an acronym for an inline frame. An iFrame is used inside a webpage to load another HTML document and render it. This HTML document may also contain JavaScript and/or CSS which is loaded at the time when iframe tag is parsed by the user’s browser.. This work builds upon the original iframe migrationMigration Moving the code, database and media files for a website site from one server to another. Most typically done when changing hosting companies. in the template editor and introduces new compatibility measures in WordPress 6.9, ahead of the full transition planned for WordPress 7.0.

For background, see the original post.

What’s Changing in WordPress 6.9

Starting with WordPress 6.9, several important updates have been introduced to prepare for this change, which will be completed in WordPress 7.0.

Browser console warnings for legacy blocks

To help developers identify legacy blocks, WordPress 6.9 now displays a warning in the browser console when a blockBlock Block is the abstract term used to describe units of markup that, composed together, form the content or layout of a webpage using the WordPress editor. The idea combines concepts of what in the past may have achieved with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience. is registered with apiVersion 2 or lower. This serves as an early signal to update existing blocks before the post editor becomes fully iframed in WordPress 7.0.

When a block is registered with apiVersion 2 or lower, the post editor runs in a non-iframe context as before to maintain backward compatibility. Developers are encouraged to migrate their blocks to apiVersion 3 and test them within the iframe-based editor to ensure full compatibility with future WordPress releases.

See #70783 for more details.

block.jsonJSON JSON, or JavaScript Object Notation, is a minimal, readable format for structuring data. It is used primarily to transmit data between a server and web application, as an alternative to XML. schema now only allows apiVersion: 3

Alongside these compatibility measures, the block.json schema has been updated to only allow apiVersion: 3 for new or updated blocks. Older versions (1 or 2) will no longer pass schema validation.

See #71107 for more details.

Why iframe the Post Editor?

The main goal of iframing the editor is isolation. By loading the editor content within an iframe, styles from the WordPress adminadmin (and super admin) no longer interfere with the editor canvas.

This separation ensures that the editing experience more closely mirrors what users see on the front end.

From a technical perspective, the iframe approach offers several benefits:

  • Admin styles no longer leak into the editor, eliminating the need to reset admin CSSCSS Cascading Style Sheets..
  • Viewport-relative units (vwvh) and media queries now behave naturally within the editor.

The iframed Post Editor will make life easier for block and theme authors by reducing styling conflicts and improving layout accuracy.

Props to @mamaduka for helping review this dev-note.

Login to Reply<\/a><\/li><\/ul><\/div>","commentTrashedActions":"

Block Bindings improvements in WordPress 6.9

For WordPress users.

The BlockBlock Block is the abstract term used to describe units of markup that, composed together, form the content or layout of a webpage using the WordPress editor. The idea combines concepts of what in the past may have achieved with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience. Bindings user interface has been upgraded to improve how different data sources are displayed in the editor.
Users can now easily switch between sources, as well as bind and unbind attributes with a single click.

For WordPress developers.

On the server

A new filterFilter Filters are one of the two types of Hooks https://codex.wordpress.org/Plugin_API/Hooks. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output.block_bindings_supported_attributes_{$block_type}, allows developers to customize which of a block’s attributes can be connected to a Block Bindings source.

On the editor

Developers can now register custom sources in the editor UIUI User interface by adding a getFieldsList method to their source registration function.

This function must return an array of objects with the following properties:

  • label (string): Defines the label shown in the dropdown selector. Defaults to the source label if not provided.
  • type (string): Defines the attribute value type. It must match the attribute type it binds to; otherwise, it won’t appear in the UI.Example: An id attribute that accepts only numbers should only display fields that return numeric values.
  • args (object): Defines the source arguments that are applied when a user selects the field from the dropdown.

This is an example that can be tried directly in the console from the Block Editor:

wp.blocks.registerBlockBindingsSource({
	name: 'state-word/haikus',
	label: 'Haikus',
	useContext: [ 'postId', 'postType' ],
	getValues: ( { bindings } ) => {

	        // this getValues assumes you're on a paragraph
		if ( bindings.content?.args?.haiku === 'one' ) {
			return {
				content:
					'Six point nine arrives,\nBlock bindings bloom like spring flowers,\nEditors rejoice.',
			};
		}
		if ( bindings.content?.args?.haiku === 'two' ) {
			return {
				content:
					'New features unfold,\nPatterns dance with dynamic grace,\nWordPress dreams take flight.',
			};
		}
		if ( bindings.content?.args?.haiku === 'three' ) {
			return {
				content:
					"December's gift shines,\nSix nine brings the future near,\nCreators build more.",
			};
		}
		return {
			content: bindings.content,
		};
	},
	
	getFieldsList() {
		return [
			{
				label: 'First Haiku', 
				type: 'string',
				args: {
					haiku: 'one',
				},
			},
			{
				label: 'Second Haiku', 
				type: 'string',
				args: {
					haiku: 'two',
				},
			},
			{
				label: 'Third Haiku', 
				type: 'string',
				args: {
					haiku: 'three',
				},
			},
		];
	},
} );

After executing the code above, when inserting a paragraph, a new UI selector for the Block Binding should be available for the content attribute of the paragraph.

Additional Information

More information can be found on the related tickets, changesets, and pull requests:

  • TracTrac An open source project by Edgewall Software that serves as a bug tracker and project management tool for WordPress. ticketticket Created for both bug reports and feature development on the bug tracker.: #64030
  • Changeset: [60807]
  • gutenberg repository pull request: PR-71820
  • gutenberg pull request the idea originated from: PR-70975

Props: @bernhard-reiter and @cbravobernal for implementation. @juanmaguitar for peer review and providing examples.

Login to Reply<\/a><\/li><\/ul><\/div>","commentTrashedActions":"

Theme.json Border Radius Presets Support in WordPress 6.9

WordPress now supports defining border radius presets in theme.jsonJSON JSON, or JavaScript Object Notation, is a minimal, readable format for structuring data. It is used primarily to transmit data between a server and web application, as an alternative to XML., allowing theme authors to provide a curated set of border radius values that users can select from in the BlockBlock Block is the abstract term used to describe units of markup that, composed together, form the content or layout of a webpage using the WordPress editor. The idea combines concepts of what in the past may have achieved with shortcodes, custom HTML, and embed discovery into a single consistent API and user experience. Editor’s border controls.

The Problem

Previously, users could only input custom border radius values manually through the text input and unit picker controls. This made it difficult for theme authors to maintain consistent border radius values across a site’s design system and provided no guidance to users about which border radius values fit the theme’s design language.

The Solution

Following the pattern established by spacing sizes, WordPress now supports defining border radius presets in theme.json. These presets appear as visual options in the border radius control, allowing users to quickly select from predefined values while maintaining the ability to enter custom values when needed.

How to Use Border Radius Presets

Theme authors can define border radius presets in their theme.json file under settings.border.radiusSizes :

{
  "version": 3,
  "settings": {
    "border": {
      "radiusSizes": [
        {
          "name": "None",
          "slug": "none",
          "size": "0"
        },
        {
          "name": "Small",
          "slug": "small",
          "size": "4px"
        },
        {
          "name": "Medium",
          "slug": "medium",
          "size": "8px"
        },
        {
          "name": "Large",
          "slug": "large",
          "size": "16px"
        },
        {
          "name": "Full",
          "slug": "full",
          "size": "9999px"
        }
      ]
    }
  }
}

User Experience

  • With 1-8 presets: Users see a slider with stops for each border radius preset
  • With 9 or more presets: The control displays as a dropdown select menu
  • Users can always access custom value input through the custom button to the right of the control
  • The interface automatically adapts based on the number of presets defined

Backwards Compatibility

This is a purely additive feature. Existing themes without border radius presets will continue to work as before, showing only the custom input controls. Themes can opt into this feature by adding border radius presets to their theme.json.

GitHub Pull Request #67544
Related Issue #64041

Props to @youknowriad and @aaronrobertshaw for implementation, @ramonopoly and @priethor for dev notedev note Each important change in WordPress Core is documented in a developers note, (usually called dev note). Good dev notes generally include a description of the change, the decision that led to this change, and a description of how developers are supposed to work with that change. Dev notes are published on Make/Core blog during the beta phase of WordPress release cycle. Publishing dev notes is particularly important when plugin/theme authors and WordPress developers need to be aware of those changes.In general, all dev notes are compiled into a Field Guide at the beginning of the release candidate phase. and reviews.

#dev-notes, #dev-notes-6-9

DataViews, DataForm, et al. in WordPress 6.9

This is a summary of the changes introduced in the “dataviews space” during the WordPress 6.9 cycle. They have been posted in the corresponding iteration issue as well. There’s a new issue for the WordPress 7.0 cycle, subscribe there for updates.

Continue reading

#6-9, #dev-notes, #dev-notes-6-9

Abilities API in WordPress 6.9

WordPress 6.9 introduces the Abilities APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways., a new foundational system that enables plugins, themes, and WordPress coreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress. to register and expose their capabilitiescapability capability is permission to perform one or more types of task. Checking if a user has a capability is performed by the current_user_can function. Each user of a WordPress site might have some permissions but not others, depending on their role. For example, users who have the Author role usually have permission to edit their own posts (the “edit_posts” capability), but not permission to edit other users’ posts (the “edit_others_posts” capability). in a standardized, machine-readable format. This API creates a unified registry of functionality that can be discovered, validated, and executed consistently across different contexts, including PHPPHP The web scripting language in which WordPress is primarily architected. WordPress requires PHP 7.4 or higher, REST APIREST API The REST API is an acronym for the RESTful Application Program Interface (API) that uses HTTP requests to GET, PUT, POST and DELETE data. It is how the front end of an application (think “phone app” or “website”) can communicate with the data store (think “database” or “file system”) https://developer.wordpress.org/rest-api/. endpoints, and future AI-powered integrations.

The Abilities API is part of the broader AI Building Blocks for WordPress initiative, providing the groundwork for AI agents, automation tools, and developers to understand and interact with WordPress functionality in a predictable manner.

What is the Abilities API?

An ability is a self-contained unit of functionality with defined inputs, outputs, permissions, and execution logic. By registering abilities through the Abilities API, developers can:

  • Create discoverable functionality with standardized interfaces
  • Define permission checks and execution callbacks
  • Organize abilities into logical categories
  • Validate inputs and outputs
  • Automatically expose abilities through REST API endpoints

Rather than burying functionality in isolated functions or custom AJAX handlers, abilities are registered in a central registry that makes them accessible through multiple interfaces.

Core Components

The Abilities API introduces three main components to WordPress 6.9:

1. PHP API

A set of functions for registering, managing, and executing abilities:

Ability Management:

  • wp_register_ability() – Register a new ability
  • wp_unregister_ability() – Unregister an ability
  • wp_has_ability() – Check if an ability is registered
  • wp_get_ability() – Retrieve a registered ability
  • wp_get_abilities() – Retrieve all registered abilities

Ability CategoryCategory The 'category' taxonomy lets you group posts / content together that share a common bond. Categories are pre-defined and broad ranging. Management:

  • wp_register_ability_category() – Register an ability category
  • wp_unregister_ability_category() – Unregister an ability category
  • wp_has_ability_category() – Check if an ability category is registered
  • wp_get_ability_category() – Retrieve a registered ability category
  • wp_get_ability_categories() – Retrieve all registered ability categories

2. REST API Endpoints

When enabled, the Abilities API can automatically expose registered abilities through REST API endpoints under the wp-abilities/v1 namespace:

  • GET /wp-abilities/v1/categories – List all ability categories
  • GET /wp-abilities/v1/categories/{slug} – Get a single ability category
  • GET /wp-abilities/v1/abilities – List all abilities
  • GET /wp-abilities/v1/abilities/{name} – Get a single ability
  • GET|POST|DELETE /wp-abilities/v1/abilities/{name}/run – Execute an ability

3. HooksHooks In WordPress theme and development, hooks are functions that can be applied to an action or a Filter in WordPress. Actions are functions performed when a certain event occurs in WordPress. Filters allow you to modify certain functions. Arguments used to hook both filters and actions look the same.

New action hooks for integrating with the Abilities API:

Actions:

  • wp_abilities_api_categories_init – Fired when the ability categories registry is initialized (register categories here)
  • wp_abilities_api_init – Fired when the abilities registry is initialized (register abilities here)
  • wp_before_execute_ability – Fired before an ability executes
  • wp_after_execute_ability – Fired after an ability finishes executing

Filters:

  • wp_register_ability_category_args – Filters ability category arguments before registration
  • wp_register_ability_args – Filters ability arguments before registration

Registering Abilities

Abilities must be registered on the wp_abilities_api_init action hook. Attempting to register abilities outside of this hook will trigger a _doing_it_wrong() notice, and the Ability registration will fail.

Basic Example

Here’s a complete example of registering an ability category and an ability:

<?php
add_action( 'wp_abilities_api_categories_init', 'my_plugin_register_ability_categories' );
/**
 * Register ability categories.
 */
function my_plugin_register_ability_categories() {
    wp_register_ability_category(
        'content-management',
        array(
            'label'       => __( 'Content Management', 'my-plugin' ),
            'description' => __( 'Abilities for managing and organizing content.', 'my-plugin' ),
        )
    );
}

add_action( 'wp_abilities_api_init', 'my_plugin_register_abilities' );
/**
 * Register abilities.
 */
function my_plugin_register_abilities() {
    wp_register_ability(
        'my-plugin/get-post-count',
        array(
            'label'              => __( 'Get Post Count', 'my-plugin' ),
            'description'        => __( 'Retrieves the total number of published posts.', 'my-plugin' ),
            'category'           => 'content-management',
            'input_schema'       => array(
                'type'       => 'string',
                'description' => __( 'The post type to count.', 'my-plugin' ),
                'default'     => 'post',
            ),
            'output_schema'      => array(
                'type'       => 'integer',
                'description' => __( 'The number of published posts.', 'my-plugin' ),
            ),
            'execute_callback'   => 'my_plugin_get_post_count',
            'permission_callback' => function() {
                return current_user_can( 'read' );
            },
        )
    );
}

/**
 * Execute callback for get-post-count ability.
 */
function my_plugin_get_post_count( $input ) {
    $post_type = $input ?? 'post';

    $count = wp_count_posts( $post_type );

    return (int) $count->publish;
}

More Complex Example

Here’s an example with more advanced input and output schemas, input validation, and error handling:

<?php
add_action( 'wp_abilities_api_init', 'my_plugin_register_text_analysis_ability' );
/**
 * Register a text analysis ability.
 */
function my_plugin_register_text_analysis_ability() {
    wp_register_ability(
        'my-plugin/analyze-text',
        array(
            'label'              => __( 'Analyze Text', 'my-plugin' ),
            'description'        => __( 'Performs sentiment analysis on provided text.', 'my-plugin' ),
            'category'           => 'text-processing',
            'input_schema'       => array(
                'type'       => 'object',
                'properties' => array(
                    'text' => array(
                        'type'        => 'string',
                        'description' => __( 'The text to analyze.', 'my-plugin' ),
                        'minLength'   => 1,
                        'maxLength'   => 5000,
                    ),
                    'options' => array(
                        'type'       => 'object',
                        'properties' => array(
                            'include_keywords' => array(
                                'type'        => 'boolean',
                                'description' => __( 'Whether to extract keywords.', 'my-plugin' ),
                                'default'     => false,
                            ),
                        ),
                    ),
                ),
                'required' => array( 'text' ),
            ),
            'output_schema'      => array(
                'type'       => 'object',
                'properties' => array(
                    'sentiment' => array(
                        'type'        => 'string',
                        'enum'        => array( 'positive', 'neutral', 'negative' ),
                        'description' => __( 'The detected sentiment.', 'my-plugin' ),
                    ),
                    'confidence' => array(
                        'type'        => 'number',
                        'minimum'     => 0,
                        'maximum'     => 1,
                        'description' => __( 'Confidence score for the sentiment.', 'my-plugin' ),
                    ),
                    'keywords' => array(
                        'type'        => 'array',
                        'items'       => array(
                            'type' => 'string',
                        ),
                        'description' => __( 'Extracted keywords (if requested).', 'my-plugin' ),
                    ),
                ),
            ),
            'execute_callback'   => 'my_plugin_analyze_text',
            'permission_callback' => function() {
                return current_user_can( 'edit_posts' );
            },
        )
    );
}

/**
 * Execute callback for analyze-text ability.
 * 
 * @param $input
 * @return array
 */
function my_plugin_analyze_text( $input ) {
    $text = $input['text'];
    $include_keywords = $input['options']['include_keywords'] ?? false;

    // Perform analysis (simplified example)
    $sentiment = 'neutral';
    $confidence = 0.75;

    $result = array(
        'sentiment'  => $sentiment,
        'confidence' => $confidence,
    );

    if ( $include_keywords ) {
        $result['keywords'] = array( 'example', 'keyword' );
    }

    return $result;
}

Ability Naming Conventions

Ability names should follow these practices:

  • Use namespaced names to prevent conflicts (e.g., my-plugin/my-ability)
  • Use only lowercase alphanumeric characters, dashes, and forward slashes
  • Use descriptive, action-oriented names (e.g., process-payment, generate-report)
  • The format should be namespace/ability-name

Categories

Abilities must be assigned to a category. Categories provide better discoverability and help organize related abilities. Categories must be registered before the abilities that reference them using the wp_abilities_api_categories_init hook.

JSONJSON JSON, or JavaScript Object Notation, is a minimal, readable format for structuring data. It is used primarily to transmit data between a server and web application, as an alternative to XML. Schema Validation

The Abilities API uses JSON Schema for input and output validation. WordPress implements a validator based on a subset of JSON Schema Version 4. The schemas serve two purposes:

  1. Automatic validation of data passed to and returned from abilities
  2. Self-documenting API contracts for developers

Defining schemas is mandatory when there is a value to pass or return.

Using REST API Endpoints

Developers can also enable Abilities to support the default REST API endpoints. This is possible by setting the meta.show_in_rest argument to true when registering an ability.

       wp_register_ability(
        'my-plugin/get-post-count',
        array(
            'label'              => __( 'Get Post Count', 'my-plugin' ),
            'description'        => __( 'Retrieves the total number of published posts.', 'my-plugin' ),
            'category'           => 'content-management',
            'input_schema'       => array(
                'type'       => 'string',
                'description' => __( 'The post type to count.', 'my-plugin' ),
                'default'     => 'post',
            ),
            'output_schema'      => array(
                'type'       => 'integer',
                'description' => __( 'The number of published posts.', 'my-plugin' ),
            ),
            'execute_callback'   => 'my_plugin_get_post_count',
            'permission_callback' => function() {
                return current_user_can( 'read' );
            },
            'meta'               => array(
                'show_in_rest' => true,
            )
        )
    );

Access to all Abilities REST API endpoints requires an authenticated user. The Abilities API supports all WordPress REST API authentication methods:

  • Cookie authentication (same-origin requests)
  • Application passwords (recommended for external access)
  • Custom authentication plugins

Once enabled, it’s possible to list, fetch, and execute Abilities via the REST API endpoints:

List All Abilities:

curl -u 'USERNAME:APPLICATION_PASSWORD' \
  https://example.com/wp-json/wp-abilities/v1/abilities

Get a Single Ability:

curl -u 'USERNAME:APPLICATION_PASSWORD' \
https://example.com/wp-json/wp-abilities/v1/abilities/my-plugin/get-post-count

Execute an Ability:

curl -u 'USERNAME:APPLICATION_PASSWORD' \
  -X POST https://example.com/wp-json/wp-abilities/v1/abilities/my-plugin/get-post-count/run \
  -H "Content-Type: application/json" \
  -d '{"input": {"post_type": "page"}}'

The API automatically validates the input against the ability’s input schema, checks permissions via the ability’s permission callback, executes the ability, validates the output against the ability’s output schema, and returns the result as JSON.

Checking and Retrieving Abilities

You can check if an ability exists and retrieve it programmatically:

<?php
// Check if an ability is registered
if ( wp_has_ability( 'my-plugin/get-post-count' ) ) {
    // Get the ability object
    $ability = wp_get_ability( 'my-plugin/get-post-count' );

    // Access ability properties
    echo $ability->get_label();
    echo $ability->get_description();
}

// Get all registered abilities
$all_abilities = wp_get_abilities();

foreach ( $all_abilities as $ability ) {
    echo $ability->get_name();
}

Error Handling

Abilities should handle errors gracefully by returning WP_Error objects:

<?php
function my_plugin_delete_post( $input ) {
    $post_id = $input['post_id'];

    if ( ! get_post( $post_id ) ) {
        return new WP_Error(
            'post_not_found',
            __( 'The specified post does not exist.', 'my-plugin' ),
        );
    }

    $result = wp_delete_post( $post_id, true );

    if ( ! $result ) {
        return new WP_Error(
            'deletion_failed',
            __( 'Failed to delete the post.', 'my-plugin' ),
        );
    }

    return array(
        'success' => true,
        'post_id' => $post_id,
    );
}

Backward Compatibility

The Abilities API is a new feature in WordPress 6.9 and does not affect existing WordPress functionality. Plugins and themes can adopt the API incrementally without breaking existing code.

For developers who want to support both WordPress 6.9+ and earlier versions, check if the API functions exist before using them:

<?php
if ( function_exists( 'wp_register_ability' ) ) {
    add_action( 'wp_abilities_api_init', 'my_plugin_register_abilities' );
}

Or

if ( class_exists( 'WP_Ability' ) ) {
 add_action( 'wp_abilities_api_init', 'my_plugin_register_abilities' );}

Further Resources

Props to @gziolo for pre-publish review.

Login to Reply<\/a><\/li><\/ul><\/div>","commentTrashedActions":"

Prettier Emails: Supporting Inline Embedded Images

An embedded email image is an image file included directly within the email’s content, allowing it to be displayed inline (i.e., in the body of an HTMLHTML HyperText Markup Language. The semantic scripting language primarily used for outputting content in web browsers. email) without requiring the recipient to download it separately. This is achieved by referencing the image via a Content-ID (CID) in the email’s HTML markup. Embedded images are useful for enhancing visual emails, such as newsletters or branded notifications, but they only work in HTML-formatted emails. By default, WordPress sends emails in plain text format. To take advantage of embedded images, you must set the Content-Type headerHeader The header of your site is typically the first thing people will experience. The masthead or header art located across the top of your page is part of the look and feel of your website. It can influence a visitor’s opinion about your content and you/ your organization’s brand. It may also look different on different screen sizes. to text/html when calling wp_mail().

Historically, the only way to add inline embedded images was to directly call $phpmailer->AddEmbeddedImage method during phpmailer_init, like this:

function embed_images( $phpmailer ) {
    $phpmailer->AddEmbeddedImage( '/path/to/logo.png', 'logo.png' );
}
add_action( 'phpmailer_init', 'embed_images' );

This worked, but this specific implementation depended on the PHPMailer library.

The changes added in #28059/[60698] aim to provide a native option that facilitates some degree of abstraction from the current mailing library and also sets the stage to deprecate this native injection with phpmailer_init in the future, simplifying the process and ensuring long-term maintainability.

This change updates the wp_mail function signature to add a new argument for $embeds:

wp_mail( $to, $subject, $message, $headers = '', $attachments = array(), $embeds = array())

This new parameter can accept a newline-separated(\n) string of images paths, an array of image path strings, or an associative array of image paths where each key is the Content-ID.

Content-ID formation

When using the $embeds parameter to embed images for use in HTML emails, no changes are necessary for plain text emails. Reference the embedded file in your HTML code with a cid: URLURL A specific web address of a website or web page on the Internet, such as a website’s URL www.wordpress.org whose value matches the file’s Content-ID.

Example email markup:

<img src="cid:0" alt="Logo">
<img src="cid:my-image" alt="Image">


As mentioned above, Content-ID (cid)s for each image path to be embedded can be passed using an associative array of cid/image path pairs. If $embeds is a newline-separated string or a non-associative array, the cid is a zero-based index of either the exploded string or the element’s position in the array.

New wp_mail_embed_args filterFilter Filters are one of the two types of Hooks https://codex.wordpress.org/Plugin_API/Hooks. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output.

A new wp_mail_embed_args filter has been introduced to allow each individual embed to be filtered during processing. With this new hook, it’s possible to customize most of the properties of each embed before passing to $phpmailer->AddEmbeddedImage().

$embed_args = apply_filters(
    'wp_mail_embed_args',
    array(
        'path'        => $embed_path,
        'cid'         => $key,
        'name'        => basename( $embed_path ),
        'encoding'    => 'base64',
        'type'        => '',
        'disposition' => 'inline',
    )
);

Each of these values represents the following:

  • path – The path to the image file
  • cid – The Content-ID
  • name – The filename of the image
  • encoding – The encoding of the image
  • type – The MIME Content-Type
  • disposition – The disposition of the image

This provides a 1-to-1 representation of the PHPMailer’s addEmbeddedImage method via this hook.

Example: Set the correct Content-Type when embedding SVG images:

add_filter( 'wp_mail_embed_args', function ( $args ) {
    if ( isset( $args['path'] ) && '.svg' === substr( $args['path'], -4 ) ) {
        $args['type'] = 'image/svg+xml';
    }
    return $args;
} );

Backward Compatibility Recommendations

If you are maintaining code that completely replaces the wp_mail() function (e.g. via a custom implementation in a pluginPlugin A plugin is a piece of software containing a group of functions that can be added to a WordPress website. They can extend functionality or add new features to your WordPress websites. WordPress plugins are written in the PHP programming language and integrate seamlessly with WordPress. These can be free in the WordPress.org Plugin Directory https://wordpress.org/plugins/ or can be cost-based plugin from a third-party or theme), take the following steps to improve the implementation with this update:

  1. Update your implementation to expect and handle the new $embeds parameter in the function signature. This is optional, as $embeds receives an empty array by default.
  2. Consider adding support for the wp_mail_embed_args filter to ensure that any plugins or themes making use of it will have their changes reflected in your implementation of wp_mail().
  3. If you are currently supporting images in your emails, they won’t be affected by this update. Although, adding this implementation will ensure greater integration with WordPress to safeguard against future deprecations.

As a reminder, fully replacing wp_mail() is generally discouraged. The pre_wp_mail filter introduced in WordPress 5.7 can accomplish the same result. For more details, see the developer note on overriding wp_mail() behavior.

Props to @TimothyBlynJacobs, @mukesh27, @davidbaumwald, @desrosj, and @johnbillion helping review this dev-note.

#6-9, #dev-notes, #dev-notes-6-9, #mail