Introducing Plugin Dependencies in WordPress 6.5

Overview

#22316 introduces 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 Dependencies to WordPress.

Extensibility of WordPress through plugins and the 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. 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. is one of its most beneficial features. There are many plugins that act purely as extensions of others, building functionality on top. The Plugin Dependencies feature aims to make the process of installing and activating addons (dependents) and the plugins they rely on (dependencies) consistent and easy.

New Plugin 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.

A new Requires Plugins header has been introduced.

This must contain a comma-separated list of WordPress.orgWordPress.org The community site where WordPress code is created and shared by the users. This is where you can download the source code for WordPress core, plugins and themes as well as the central location for community conversations and organization. https://wordpress.org/-formatted slugs for its dependencies, such as my-plugin (my-plugin/my-plugin.php is not supported). It does not support commas in plugin slugs.

How to use the new header

/**
* Plugin Name: Express Payment Gateway Checkout for Shop
* Requires Plugins: shop, payment-gateway
 */

Requirements

Dependent plugins

The following requirements are placed on dependent plugins:

  • Cannot be installed until its dependencies are installed.
  • Cannot be activated until its dependencies are activated.

Dependency plugins

The following requirements are placed on dependency plugins:

  • Cannot be deactivated while its dependents are activated.
  • Cannot be deleted while its dependents are installed.

What happens if a dependency is no longer met?

If a dependency plugin is deleted via FTPFTP FTP is an acronym for File Transfer Protocol which is a way of moving computer files from one computer to another via the Internet. You can use software, known as a FTP client, to upload files to a server for a WordPress website. https://codex.wordpress.org/FTP_Clients. or deployment, a notice will be displayed on the administration’s plugin screens, informing the user that there are missing dependencies to install and/or activate. Additionally, each dependent whose dependencies are no longer met will have an error notice in their plugin row.

What happens if a plugin update has a new dependency?

The update will be allowed for now, and the dependent will remain active. A notice will be displayed on the administration’s plugin screens, informing the user that there are missing dependencies to install and/or activate.

What happens if there are circular dependencies?

A circular dependency is when two or more plugins form a loopLoop The Loop is PHP code used by WordPress to display posts. Using The Loop, WordPress processes each post to be displayed on the current page, and formats it according to how it matches specified criteria within The Loop tags. Any HTML or PHP code in the Loop will be processed on each post. https://codex.wordpress.org/The_Loop. in their requirements.

For example: Plugin A requires Plugin B requires Plugin C requires Plugin A

Plugin Dependencies includes circular dependency detection, and will display a notice to inform users of plugins whose requirements are invalidinvalid A resolution on the bug tracker (and generally common in software development, sometimes also notabug) that indicates the ticket is not a bug, is a support request, or is generally invalid.. The plugins cannot be activated, and users should contact the plugin authors so that this circular reference can be broken where appropriate.

Is defensive coding still needed?

Yes. Plugin Dependencies makes it easier for the user to install and activate required plugins, and informs them when these are not met. This means plugin authors can safely remove checks and notices when their dependencies are not installed or activated.

However, at this time, Plugin Dependencies does not include minimum or maximum version support for dependencies, nor does it account for the loading order of plugins. Plugin authors should therefore continue to use function|class|interface_exists() and version checks where their plugin relies on specific functionality being available at a given time.

Does Plugin Dependencies affect WP-CLIWP-CLI WP-CLI is the Command Line Interface for WordPress, used to do administrative and development tasks in a programmatic way. The project page is http://wp-cli.org/ https://make.wordpress.org/cli/?

Plugin Dependencies does not prevent the installation of dependent plugins without their dependencies via WP-CLI, as it is assumed that those using WP-CLI are advanced users that are aware of their dependency stack. However, to avoid missing dependencies going unnoticed, dependent plugins cannot be activated using WP-CLI until their dependencies are activated.

This affects wp plugin activate --all, which may need to be run multiple times if a dependent appears alphabetically earlier than its dependencies. We plan to collaborate with WP-CLI maintainers on possible ways of easing this burden by exploring loading order in WordPress CoreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress., WP-CLI, or both, if appropriate.

Limitations

Plugins hosted on WordPress.org

Dependent plugins hosted on WordPress.org can only declare dependencies that are also hosted on WordPress.org. If your plugin hosted on WordPress.org requires plugins that are not hosted there, it is recommended that you do not use the Requires Plugins header in your plugin at this time.

Plugins not hosted on WordPress.org

Dependent plugins not hosted on WordPress.org can declare dependencies whether hosted on WordPress.org or elsewhere. However, the UIUI User interface will not provide an installation link for the third-party dependencies, and these must continue to be installed manually by the user.

Must-Use plugins as dependencies

Must-Use plugins as dependencies are not officially supported by WordPress Core at this time. Discussion will continue in #60504 and we will release further information when decisions about possible future support have been made.

Themes that require plugins

Themes that require plugins are not supported by Plugin Dependencies at this time, and theme authors should continue to use the checks and messaging that they have in place.

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.

The wp_plugin_dependencies_slug filter hook has been introduced to allow for alterations to dependency slugs. For example, if a dependent plugin declares my-plugin as a dependency, and a premium version of my-plugin exists, the premium version can filter the slug and convert it to my-plugin-pro so that it can be detected by Plugin Dependencies.

  • Parameters
    string $slug – The slug.

Example usage

add_filter( 'wp_plugin_dependencies_slug', 'convert_myplugin_to_myplugin_pro' );

function convert_myplugin_to_myplugin_pro( $slug ) {
if ( 'my-plugin' === $slug ) {
$slug = 'my-plugin-pro';
}
return $slug;
}

UI Changes

Plugins > Installed plugins

The following changes are made:

  • Dependent plugin rows now contain a list of their dependencies, linked to the respective plugin modal to install and activate the dependency.
  • Dependency plugin rows now contain a list of their dependents.
  • If a plugin has dependencies that are not installed and active, the Activate link is disabled.
  • If a plugin has dependents that are active, the Deactivate and Delete links are disabled.
  • Bulk Actions are disabled for dependency plugins.

Before

Three plugin rows, with one plugin requiring the other two. However, there is no indication or enforcement of the requirements.
No dependents or dependencies are listed.

After

Three plugin rows, with one plugin requiring the other two. The requirements are listed under "Requires" and "Required by" sections. The Bulk Actions checkbox is disabled, as well as the Activate, Deactivate, or Delete actions, depending on the status of the plugins.
Dependents and dependencies are listed, and actions are enabled/disabled based on the status of requirements.

Plugins > Add New

The following changes are made:

  • If a plugin has unmet dependencies, the Install Now and Activate buttons are disabled, both in their plugin card and their plugin information modal.
  • Dependent plugin cards now contain a notice listing their dependencies, with a More Details link to the dependency’s information modal which contains Install Now or Activate buttons based on their current installation status.
  • Plugin information modals are now persistent after button clicks, and modal-based plugin installation and activation are now performed through AJAX directly within the modal.

Before

Plugin card with no dependencies listed and an active Install Now button despite unmet dependencies.

After

Plugin card with dependencies listed with modal links to install and activate each dependency, and the Install Now button disabled while dependencies are unmet.

Onboarding experiences

Due to the unified AJAX approach now used on the Plugins > Add New screen, activation of a plugin will no longer automatically redirect to the Plugins > Installed plugins screen, or to onboarding experiences implemented by plugin authors. This allows users to install and activate multiple plugins without leaving their current context.

Plugins with onboarding experiences usually incorporate checks so that if, for example, their plugin is installed and activated using WP-CLI, the onboarding experience will be triggered when the user enters one of the plugin’s settings screens. Such implementations will be unaffected by Plugin Dependencies, as will activation from the Plugins > Installed plugins screen Activate link.

New WP_Plugin_Dependencies Class

A new WP_Plugin_Dependencies class has been introduced.

The following public API methods are available:

static ::initialize()
Initializes Plugin Dependencies by reading dependencies from the Requires Plugins header, and fetching Plugins API data for dependencies. This runs only once per execution.

static ::has_dependents( $plugin_file )
Determines whether the plugin has plugins that depend on it.

  • Parameters
    string $plugin_file – The plugin file, relative to the plugins directory.
  • Return value
    bool Whether the plugin has plugins that depend on it.

static ::has_dependencies( $plugin_file )
Determines whether the plugin has plugin dependencies.

  • Parameters
    string $plugin_file – The plugin file, relative to the plugins directory.
  • Return value
    bool Whether the plugin has plugin dependencies.

static ::has_active_dependents( $plugin_file )
Determines whether the plugin has active dependents.

  • Parameters
    string $plugin_file – The plugin file, relative to the plugins directory.
  • Return value
    bool Whether the plugin has active dependents.

static ::get_dependents( $slug )
Gets filepaths of plugins that require the dependency.

  • Parameters
    string $slug – The dependency’s slug.
  • Return value
    array An array of dependent plugin filepaths, relative to the plugins directory.

static ::get_dependencies( $plugin_file )
Gets the slugs of plugins that the dependent requires.

  • Parameters
    string $plugin_file – The dependent plugin’s filepath, relative to the plugins directory.
  • Return value
    array An array of dependency plugin slugs.

static ::get_dependent_filepath( $slug )
Gets a dependent plugin’s filepath.

  • Parameters
    string $slug – The dependent plugin’s slug.
  • Return value
    string|false The dependent plugin’s filepath, relative to the plugins directory, or false if the plugin has no dependencies.

static ::get_dependency_filepath( $slug )
Gets the filepath for a dependency, relative to the plugin’s directory.

  • Parameters
    string $slug – The dependency’s slug.
  • Return value
    string|false If installed, the dependency’s filepath relative to the plugins directory, otherwise false.

static ::has_unmet_dependencies( $plugin_file )
Determines whether the plugin has unmet dependencies.

  • Parameters
    string $plugin_file – The plugin’s filepath, relative to the plugins directory.
  • Return value
    bool Whether the plugin has unmet dependencies.

static ::has_circular_dependency( $plugin_file )
Determines whether the plugin has a circular dependency.

  • Parameters
    string $plugin_file – The plugin’s filepath, relative to the plugins directory.
  • Return value
    bool Whether the plugin has a circular dependency.

static ::get_dependent_names( $plugin_file )
Gets the names of plugins that require the plugin.

  • Parameters
    string $plugin_file – The plugin’s filepath, relative to the plugins directory.
  • Return value
    array An array of dependent names.

static ::get_dependency_names( $plugin_file )
Gets the names of plugins required by the plugin.

  • Parameters
    string $plugin_file – The dependent plugin’s filepath, relative to the plugins directory.
  • Return value
    array An array of dependency names.

static ::get_dependency_data( $slug )
Returns API data for the dependency.

  • Parameters
    string $slug – The dependency’s slug.
  • Return value
    array|false The dependency’s API data on success, otherwise false.

FAQs

Where can I stay updated on the future development of Plugin Dependencies?

The Plugin Dependencies feature plugin will no longer receive updates now that the Plugin Dependencies feature has been merged into WordPress Core. The feature will be primarily maintained by contributors to the Upgrade/Install and Plugins components. You can stay updated on Plugin Dependencies and other developments by subscribing to the blog.

How can I get involved in the future development of Plugin Dependencies?

You can join us in the #core-upgrade-install channel on Slack. Our weekly meetings are held every Wednesday at 18:00 UTC. Tickets will be opened on WordPress Core Trac.

How can I report an issue?

You can search for an existing ticket or create a new ticket with the type of defect (bugbug A bug is an error or unexpected result. Performance improvements, code optimization, and are considered enhancements, not defects. After feature freeze, only bugs are dealt with, with regressions (adverse changes from the previous version) being the highest priority.) on WordPress Core TracTrac An open source project by Edgewall Software that serves as a bug tracker and project management tool for WordPress..

How can I request an enhancementenhancement Enhancements are simple improvements to WordPress, such as the addition of a hook, a new feature, or an improvement to an existing feature.?

You can search for an existing ticket or create a new ticket with the type of enhancement on WordPress Core Trac.

Props to @afragen and @swissspidy for technical review, and @stevenlinx for copy review.

In memory of Alex Mills and Alex King.
WordPress Remembers.

#6-5, #dev-notes, #dev-notes-6-5

Unification of the site and post editors in 6.5

In WordPress 6.5, the site editor has been refactored to match the post editor UIUI User interface and behavior closely. 

Preferences 

The preferences have been unified between the post and site editor. For instance, if a user enables the top toolbar in the post editor, it will be honored in the site editor and vice versa. 

This means that the following preferences have been moved from the core/edit-post or core/edit-site scopes into the core scope.

  • allowRightClickOverrides
  • distractionFree
  • editorMode
  • fixedToolbar
  • focusMode
  • hiddenBlockTypes
  • inactivePanels
  • keepCaretInsideBlock
  • mostUsedBlocks
  • openPanels
  • showBlockBreadcrumbs
  • showIconLabels
  • showListViewByDefault

Accessing the preferences using the previous scope will continue to work but it’s deprecated, you can now access and modify these preferences using:

const isFixedToolbar = window.wp.data.select( 'core/preferences' ).get( 'core', 'fixedToolbar' );
window.wp.data.dispatch( 'core/preferences' ).get( 'core', 'fixedToolbar', true );

Actions and selectors

For the same reasons, the following selectors and actions from ‘edit-post’ store to the ‘editor’ store:

  • setIsInserterOpened
  • setIsListViewOpened
  • isInserterOpened
  • isListViewOpened
  • isEditorPanelEnabled
  • isEditorPanelOpened
  • isEditorPanelRemoved
  • removeEditorPanel
  • toggleEditorPanelEnabled
  • toggleEditorPanelOpened
const isInserterOpened = window.wp.data.select( 'core/editor' ).isInserterOpened();

Editor Store

The site editor now relies on the editor package, thie means that in order to retrieve the currently edited post or entity, you can use the editor store selectors:

const editedPostType = wp.data.select( 'core/editor' ).getCurrentPostType();
const editedPostId = wp.data.select( 'core/editor' ).getCurrentPostId();

The rest of the editor store selectors and actions should also be now usable within the site editor.

Props to @audrasjb for the technical review and @leonnugraha for the copy review

#6-5, #dev-notes, #dev-notes-6-5

Updates to the HTML API in 6.5

WordPress 6.5 brings significant updates to the HTMLHTML HyperText Markup Language. The semantic scripting language primarily used for outputting content in web browsers. 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.. Notably, the Tag Processor received a major overhaul allowing it to scan every token in an HTML document and unlocking a broad array of new functionality. The HTML Processor supports much more of the HTML specification than it did when it was minimally introduced in WordPress 6.4.

New functionality in the Tag Processor.

The Tag Processor was designed to scan every tagtag A directory in Subversion. WordPress uses tags to store a single snapshot of a version (3.6, 3.6.1, etc.), the common convention of tags in version control systems. (Not to be confused with post tags.) in an HTML document, skipping all of the non-tag tokens. In WordPress 6.5 it can scan everything. Tokens are the fundamental pieces that make up a document; in the case of HTML, they are the tags, comments, doctype definitions, and text nodes. This means that it’s now possible to read the text content of an HTML document, trivializing previously-complicated operations like stripping tags or truncating HTML.

Supporting this work is the introduction of a new concept called modifiable text. Modifiable text represents content within token boundaries, or content which can be changed without affecting the structure of the document as a whole. Different tokens contain different types of modifiable text:

  • The entire span of a text node is modifiable text.
  • The inside contents of an HTML comment is modifiable text.
  • The content between the opening and closing tags of special elements is modifiable content.

In this case, special elements refer to elements whose inner contents cannot contain other HTML tags. These include the contents of SCRIPT and STYLE elements as well as the contents of the TITLE and TEXTAREA elements. There are a few more representing deprecated or invalidinvalid A resolution on the bug tracker (and generally common in software development, sometimes also notabug) that indicates the ticket is not a bug, is a support request, or is generally invalid. syntax: see the class documentation for a comprehensive list.

New Methods.

  • next_token() advances to the next token in the document, including closing tags. Unlike next_tag() there is no query for this method. It scans everything.
  • get_token_type() indicates what kind of token was found, e.g. a #tag or #text or #comment.
  • get_token_name() returns a value roughly corresponding to the DOM nodeName, e.g. SPAN or #text or html (for doctype declarations).
  • get_modifiable_text() returns the properly-decoded text content for a matched token.
  • get_comment_type() indicates why the token is a comment, since different kinds of invalid markup become HTML comments. For example, <​![CDA​TA[​text]​]> is an HTML comment of the type COMMENT_AS_CDATA_LOOKALIKE because it appears as though it was intended to be a CDATA section, which doesn’t exist within HTML content.
  • paused_at_incomplete_token() indicates if the Tag Processor reached the end of the document in the middle of a token. For example, it may have been truncated in the middle of a tag.

Example usage:

$title = null;
$text_content = '';
$processor = new WP_HTML_Tag_Processor( $html );
while ( $processor->next_token() ) {
    if ( '#text' === $processor->get_token_type() ) {
        $text_content .= $processor->get_modifiable_text();
    } elseif ( null === $title && 'TITLE' === $processor->get_token_name() ) {
        $title = $processor->get_modifiable_text();
    }
}

echo $title ? $title : '(untitled post)';
echo "\n\n";
echo $text_content;

Expanded support in the HTML Processor.

WordPress is now able to recognize and properly parse most HTML. There are only a few major exceptions.

  • The HTML Processor will bail any time it encounters a FORM, MATH, SVG, TABLE, or TEMPLATE tag. These elements introduce complicated parsing rules that require additional care, testing, and design work.
  • There are times when an HTML parser needs to implicitly create an element that wasn’t in the HTML itself, or when it needs to move a tag to an earlier place in the document. This requires further design work to properly communicate that the document has retroactively changed.
  • If the HTML Processor is fed full HTML documents or snippets that exist outside of a BODY element (e.g. a document HEAD), then it will abort, as it currently only supports parsing HTML inside a BODY context.

The last point highlights that the HTML Processor has been developed following the needs of 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.-related code and focused on rules in the HTML specification relating to how tags ought to be interpreted inside a BODY element. Once this section is complete, the others will be added. Thankfully, this is the hardest section by a long shot, and parsing full documents should follow quickly after the primary work is done.

Notes for experimenters.

If you’ve been experimenting with the HTML API and building custom subclasses of the Tag Processor then it’s important to understand two changes: handling of incomplete documents, and special HTML tags with modifiable text.

Incomplete documents.

Previously, when the Tag Processor reached the end of a document and there was an incomplete token (for instance, “<div cl“), it would return false and finish parsing the document. Now that the Tag Processor can scan and visit every token in the document, however, it will indicate if it encountered such a situation. When it does, it will still return false, but it will back up to the beginning of the token and freeze. In a future release, it will be possible to extend the document and continue processing. For now, if paused_at_incomplete_token() returns true, then you can know that there might have been more to the original HTML that was sent into the processor than it received.

Special HTML tags with modifiable text.

The HTML Processor properly tracks document structure and is the appropriate method for determining when an element starts and ends. Many simpler approaches will often seem correct enough for practical use, but these will all break down in common situations. While the Tag Processor makes it easier to estimate structure than regex-based approaches, be cautioned whenever skipping the rules that the HTML Processor implements, as even normative HTML often breaks simple mental models for translating HTML text into DOM structure.

For those who are relying on the Tag Processor to estimate structure, there’s a change in behavior that might break your subclasses: the Tag Processor no longer visits the closing tag for the group of special elements. These are tags like SCRIPT, STYLE, and TITLE. The reason for this change is that these elements cannot contain other tags inside of them. It will often look like they do (for instance, with <title>an <img> is plain text</title>), but the inner content is parsed as text and not as HTML. This change prevents misinterpreting those inner contents as HTML tags.

When the Tag Processor encounters a SCRIPT tag or any of these special elements, it will continue parsing until it finds the associated closing tag and then the inner contents will be available as modifiable text. For algorithms tracking depth in a document, they will not only need to check if the tag is_void(), but also if it’s a special element since the closing tag will no longer be separately visited.

Note that it’s still possible to visit a closing tag for a special element, but only if they are unexpected and come without a corresponding opening tag.


Follow along in the CoreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress. SlackSlack Slack is a Collaborative Group Chat Platform https://slack.com/. The WordPress community has its own Slack Channel at https://make.wordpress.org/chat/. channel #core-html-api or watch here for updated posts to be part of the conversation driving and developing this API.

Props to @stevenlinx for copy review on this post.

#dev-notes #dev-notes-6-5 #6-5

Interactivity API in 6.5

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. provides a standard way for developers to add interactions to the frontend of their blocks.

This standard aims to make it easier for developers to create rich, interactive user experiences, from simple cases like counters or pop-ups to more complex features like instant page navigation, instant search, carts, or checkouts.

Blocks can share data, actions, and callbacks between them. This makes communication between blocks simpler and less error-prone. For example, clicking on an “add to cart” 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. can seamlessly update a separate “cart” block.

To understand better the reasoning behind it, you can take a look at the original proposal, where it is explained in more detail.

More information about it can be found in the merge announcement, the status update post, and the Trac ticket for the Interactivity API.

This 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. covers the APIs included in 6.5 and how to use the Interactivity API.

How to create interactions using the Interactivity API

It’s important to highlight that the block creation workflow doesn’t change.

Until now, WordPress has been intentionally unopinionated about the different solutions used on the frontend of blocks. The Interactivity API changes that. It adds a new standard way to easily add frontend interactivity to blocks while the APIs handling the Block Editor remain the same.

You need first to declare its compatibility with the API by adding the interactivity property inside  supports, in the block.json file:

"supports": {
    "interactivity": true
},

Refer to the Block Editor handbook to get a more detailed description of the interactivity support property.

The Interactivity API script requires using the new script modules coming in WordPress 6.5, so blocks should enqueue the JavaScriptJavaScript JavaScript or JS is an object-oriented computer programming language commonly used to create interactive effects within web browsers. WordPress makes extensive use of JS for a better user experience. While PHP is executed on the server, JS executes within a user’s browser. https://www.javascript.com/. by using viewScriptModule:

// block.json
{
   ...
   "viewScriptModule": "file:./view.js"
}

You can easily scaffold and test an interactive block following this quick start guide, which explains how to use a CLICLI Command Line Interface. Terminal (Bash) in Mac, Command Prompt in Windows, or WP-CLI for WordPress. command to speed up this process.

With that in mind, in order to add interactivity to blocks powered by the Interactivity API, developers would need to:

  1. Add directives to the markup to add specific interactions to the block.
  2. Create a store with the logic (state, actions, or callbacks) for interactivity.

Let’s use a simple example to explain it: a button that shows and hides some text. Let’s also send a message in the console whenever the button is hidden or revealed.

1. Add the directives

Directives are custom attributes that are added to the markup of your block to add interactions to its DOM elements. They are placed in the render.php file (for dynamic blocks).

The very first step is to add the data-wp-interactive directive. This is used to “activate” the Interactivity API in a DOM element and its children, and its value must be the unique namespace of your 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 block:

<div data-wp-interactive="myPlugin">
    <!-- Interactivity API zone -->
</div>

The rest of the directives can be added with the desired interactions.

// render.php

$context = array('isOpen' => false);

<div
  <?php echo get_block_wrapper_attributes(); ?>
  <?php echo wp_interactivity_data_wp_context($context) ?>
  data-wp-interactive='myPlugin'
  data-wp-watch="callbacks.logIsOpen"
>
  <button
      data-wp-on--click="actions.toggle"
      data-wp-bind--aria-expanded="context.isOpen"
  >
    Toggle
  </button>
  <p id="p-1" data-wp-bind--hidden="!context.isOpen">
    This element is now visible!
  </p>
</div>

Additionally, directives can also be injected dynamically using the HTML Tag Processor.

Don’t worry if you don’t understand how it works yet. So far, the important part is that the example above uses directives like wp-on and wp-bind to add interactivity to the HTMLHTML HyperText Markup Language. The semantic scripting language primarily used for outputting content in web browsers.. This is the list of directives available in WordPress 6.5:

You can find a deeper explanation of each directive and examples of how to use it in the relevant links.

  • wp-interactive: This attribute must be set to the unique identifier of your plugin or block in order for it to use the Interactivity API.
  • wp-context: It provides a local state available to a specific HTML node and its children. It accepts stringified 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. as a value. It’s recommended to use wp_interactivity_data_wp_context() to set it in PHPPHP The web scripting language in which WordPress is primarily architected. WordPress requires PHP 5.6.20 or higher.
  • wp-bind: It allows HTML attributes to be set on elements based on a boolean or string value. It follows the syntax data-wp-bind--[attribute]. (like data-wp-bind–value)
  • wp-class: It adds or removes a class to an HTML element, depending on a boolean value. It follows the syntax data-wp-class--[classname].
  • wp-style: It adds or removes inline style to an HTML element, depending on its value. It follows the syntax data-wp-style--[css-property].
  • wp-text: It sets the inner text of an HTML element. It only accepts strings as the parameter.
  • wp-on: It runs code on dispatched DOM events like click or keyup. Its syntax is data-wp-on--[event] (like data-wp-on--click or data-wp-on--keyup).
  • wp-on-window: It allows to attach global window events like resize, copy, focus and then execute a defined callback when those happen. Its syntax is data-wp-on-window--[window-event] (like data-wp-on-window--resize or data-wp-on-window--languagechange).
  • wp-on-document: It allows to attach global document events like scroll, mousemove, keydown and then execute a defined callback when those happen. Its syntax is data-wp-on-document--[document-event] (like data-wp-on-document--keydown or data-wp-on-document--selectionchange).
  • wp-watch: It runs a callback when the node is created and runs it again when the state or context changes.
  • wp-init: It runs a callback only when the node is created.
  • wp-run: It runs the passed callback during node’s render execution.
  • wp-key: It assigns a unique key to an element to help the Interactivity API identify it when iterating through arrays of elements.
  • wp-each: It is intended to render a list of elements.
  • wp-each-child: Ensures hydration works as expected, is added automatically on the server processing of wp-each directive.

2. Create the store

The store is used to create the logic that will link the directives with the data used inside that logic.

All stores are referenced by a unique namespace, separating the logic and avoiding name collisions between different store properties and functions.

If there are multiple stores defined with the same namespace, they will be merged into a single store.

The store is usually created in the view.js file of each block, although the state can be initialized in the backend, for example, in the render file of the block.

The state is a global object, available to all HTML nodes of the page. It is defined by the store() function. If you need a local state for just a node and its children, check the context definition.

The object can accept any property, in order to keep consistency between projects, this convention is recommended.

  • State: Defines data available to the HTML nodes of the page. Properties inside the state will be available globally. If you need to edit them, the recommended way is by using getters.
    • Derived State. If you need a modified version of any state property, getters are the recommended approach (more on deriving state below).
  • Actions: Usually triggered by the data-wp-on directive (using event listeners).
  • Callbacks: Automatically reactReact React is a JavaScript library that makes it easy to reason about, construct, and maintain stateless and stateful user interfaces. https://reactjs.org/. to state changes. Usually triggered by data-wp-on-window, data-wp-on-document or data-wp-init directives.

Returning to our example, this could be a simple store in one block, a global state has been added for having a complete sample of how a store could look like.

// view.js
import { store, getContext } from "@wordpress/interactivity";

const { state } = store( 'myPlugin', {
 state: {
  likes: 0,
  getDoubleLikes() {
    return 2 * state.likes;
  }
 },
  actions: {
    toggle: () => {
      const context = getContext();
      context.isOpen = !context.isOpen;
    },
  },
  callbacks: {
    logIsOpen: () => {
      const context = getContext();
      // Log the value of `isOpen` each time it changes.
      console.log(`Is open: ${context.isOpen}`);
    },
  },
});

There can be cases where only actions and callbacks are defined in the store.

DOM elements are connected to data stored in the state and context through directives. If data in the state or context change directives will react to those changes, updating the DOM accordingly (see diagram).

When creating the store, there are some important things to be aware of:

Using derived state

Derived state uses getters to return a computed version of the state. It can access both state and context.

// view.js
const { state } = store( "myPlugin", {
  state: {
    amount: 34,
    defaultCurrency: 'EUR',
    currencyExchange: {
      USD: 1.1,
      GBP: 0.85,
    },
    get amountInUSD() {
      return state.currencyExchange[ 'USD' ] * state.amount,
    },
    get amountInGBP() {
      return state.currencyExchange[ 'GBP' ] * state.amount,
    },
  },
} );

Accessing the store by destructuring

The store contains all the store properties, like state, actions, or callbacks. They are returned by the store() call, so you can access them by destructuring it:

const { state, actions, callbacks } = store( "myPlugin", {
  // ...
} );

Note that context is not part of the store and is accessed through the getContext function.

If you want to take a deeper view about how the store() function works, feel free to check the function documentation here.

Async actions

Async actions should use generator functions instead of async/await or promises. The Interactivity API needs to be able to track async behavior in order to restore the proper scope. Otherwise, getContext may return stale values if it was updated concurrently with the async operation. Instead of awaiting the promise, yield it from the generator function, and the Interactivity API will handle awaiting its completion.

So, instead of:

store("myPlugin", {
  state: {
    get isOpen() {
      return getContext().isOpen;
    },
  },
  actions: {
    someAction: async () => {
      state.isOpen; // This is the expected context.
      await longDelay();
      state.isOpen; // This may not get the proper context unless it's properly restored.
    },
  },
});

function longDelay() {
  return new Promise( ( resolve ) => {
    setTimeout( () => resolve(), 3_000 );
  } );
}

The store should be:

store("myPlugin", {
  state: {
    get isOpen() {
      return getContext().isOpen;
    },
  },
  actions: {
    someAction: function* () {
      state.isOpen; // This is the expected context.
      yield longDelay(); // With generators, the caller controls when to resume this function.
      state.isOpen; // This context is correct because the scope was restored before resuming after the yield.
    },
  },
});

If you want to take a deeper look at the example, check the api reference.


Working with other namespaces

Interactive blocks can share data between them, unless they are private stores.

Directives

In order to access the store of a different namespace in a directive, add the namespace before the directive value. For example:

<!-- This accesses the current store -->
<div data-wp-class--is-hidden="state.isHidden"></div>

<!-- This accesses the "otherPlugin" store -->
<button data-wp-on--click="otherPlugin::actions.addToCart>Button</button>

Context

Context from a different namespace can be accessed by providing the desired namespace as an argument to getContext( namespace ):

import { getContext } from "@wordpress/interactivity";

const otherPluginContext = getContext( "otherPlugin" );

Store

Like context, different stores can be accessed by passing the desired namespace as an argument: 

const { state: otherState, actions: otherActions } = store( "otherPlugin" );

Private stores

A store can be “locked” to prevent its content from being accessed from other namespaces. To do so, set the lock option to true in the store() call, like in the example below. When the lock is set, subsequent executions of store() with the same locked namespace will throw an error, meaning that the namespace can only be accessed where its reference was returned from the first store() call. This is especially useful for developers who want to hide part of their plugin stores so it doesn’t become accessible for extenders.

const { state } = store("myPlugin/private", {
  state: {
      messages: [ "private message" ]
    } 
  },
  { lock: true }
);

// The following call throws an Error!
store( "myPlugin/private", { /* store part */ } );

There is also a way to unlock private stores: instead of passing a boolean, you can use a string as the lock value. Such a string can then be used in subsequent store() calls to the same namespace to unlock its content. Only the code with the lock string will be able to access the protected store. This is useful for complex stores defined across multiple files.

const { state } = store("myPlugin/private", {
  state: {
      messages: [ "private message" ]
    }
  },
  { lock: PRIVATE_LOCK }
);

// The following call works as expected.
store( "myPlugin/private", { /* store part */ }, { lock: PRIVATE_LOCK } );

Interactivity API client methods

The following methods are for use in JavaScript and are provided by the wordpress/interactivity script module available in WordPress 6.5.

getContext()

The context defined with the data-wp-context attribute can be retrieved with the getContext function:

const { state } = store( "myPlugin", {
  actions: {
    someAction() {
      const context = getContext();
      const otherPluginContext = getContext( 'otherPlugin' );
      // ...
    }
  }
} );

Handbook description.

getElement()

Retrieves a representation of the element where a function from the store is being evaluated. This representation is read-only, and contains a reference to the DOM element and its attributes.

Handbook description.

getConfig()

Retrieves a configuration object that was previously defined in the server via wp_interactivity_config() function.

Configuration is immutable on the client, it cannot be modified. You can get an example later in this document.

store()

Creates the store used to link the data and actions with their respective directives. Check the main section for more information.

withScope()

Actions can depend on the scope when they are called, e.g., when you call getContext() or getElement().

When the Interactivity API runtime execute callbacks, the scope is set automatically. However, if you call an action from a callback that is not executed by the runtime, like in a setInterval() callback, you need to ensure that the scope is properly set. Use the withScope() function to ensure the scope is properly set in these cases.

An example, where actions.nextImage would trigger an undefined error without the wrapper:

store('mySliderPlugin', {
	callbacks: {
		initSlideShow: () => {
		    setInterval(
				withScope( () => {
					actions.nextImage();
				} ),
				3_000
			);
		}
	},
})

Interactivity API server functions

These are the PHP functions the Interactivity API includes:

wp_interactivity_state( $store_namespace, $state )

It is used to initialize the state on the server and ensure that the HTML sent by it, and the HTML after the client hydration are the same. And it also allows you to use any WordPress API like coreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress. translations.

// render.php

wp_interactivity_state( "movies", array(
      "1" => array(
        "id" => "123-abc",
        "movieName" => __("someMovieName", "textdomain")
      ),
) );

It receives two arguments, a string with the namespace that will be used as a reference and an associative array containing the values.

The state defined on this function gets merged with the stores defined in the view.js files.

wp_interactivity_data_wp_context( $context, $store_namespace )

Generates a data-wp-context attribute ready to be server side rendered. This function escapes the array to prevent external attacks, apart from any error that may appear when writing JSON strings manually.

$context is an array containing the keys and values of the context.

$store_namespace allows referencing different stores, and is empty by default.

<?php
 $context = array(
  'id' => $post_id,
  'show' => true,
 );
?>
<div <?php echo wp_interactivity_data_wp_context($context) ?> >
  My interactive div
</div>

Will return

<div data-wp-context="{ "id": 1, "show": "true" } ">
  My interactive div
</div>

wp_interactivity_config( $store_namespace, $config )

Sets or gets configuration for an interactivity store. An immutable copy of the configuration can be read by the client.

Consider config as a global setting that can affect the full site and won’t be updated on client interactions. For example, determining if a site can handle client-side navigation or not.

<?php
// Sets configuration for the  'myPlugin' namespace.
wp_interactivity_config( 'myPlugin', array( 'setting' => true ) );

// Gets the current configuration for the 'myPlugin' namespace.
$config = wp_interactivity_config( 'myPlugin' );

This config can be retrieved in the client:

// view.js

const { setting } = getConfig();
console.log( setting ); // Will log true.

wp_interactivity_process_directives( $html )

Processes directives within HTML content, updating the markup where necessary.

This is the core functionality of the Interactivity API. It’s public so that any HTML can be processed, not just blocks.

For blocks with supports.interactivity, directives are automatically processed. Developers do not need to call wp_interactivity_process_directives in this case.

<?php
$html_content = '<div data-wp-text="myPlugin::state.message"></div>';
wp_interactivity_state( 'myPlugin', array( 'message' => 'hello world!' ) );

// Process directives in HTML content.
$processed_html = wp_interactivity_process_directives( $html_content );
// output: <div data-wp-text="myPlugin::state.message">hello world!</div>

Relevant links

Props to @gziolo, @darerodz, @santosguillamot, @luisherranz and @jonsurrell for technical review.

Props to @leonnugraha for copy review.

#6-5, #dev-notes, #dev-notes-6-5

Script Modules in 6.5

A new “Script Modules” interface has been introduced to support native JavaScript modules in WordPress. JavaScriptJavaScript JavaScript or JS is an object-oriented computer programming language commonly used to create interactive effects within web browsers. WordPress makes extensive use of JS for a better user experience. While PHP is executed on the server, JS executes within a user’s browser. https://www.javascript.com/. modules use import and export and are often referred to as ECMAScript Modules or “ESM”.

This post will refer to “scripts” and “modules” for Scripts and Script Modules concepts, respectively.

JavaScript modules are a coreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress. web technology with a number of benefits like import and export for proper scoping, forced strict mode, and deferred loading. It’s important for WordPress to support modules and encourage their use as the future of JavaScript development.

Script Modules 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.

The interface is modeled after the existing scripts interface and should feel familiar. Modules are identified by a unique identifier (ID), analogous to a script handle.

The registered module ID should be identical to the imported module name in JavaScript. For example, the @wordpress/interactivity module can be used in another module as:

import * as interactivity from '@wordpress/interactivity';

Registered modules have an array of module dependencies. Dependencies can be a simple string (the dependency module ID) or an associative array of the following form:

$dependencies = array(
'id' => '@wordpress/interactivity',
'import' => 'dynamic' // Optional.
);

The array form optionally includes an import key with the value 'static' (default) or 'dynamic'. Dependencies that may not be needed or should only be loaded under some circumstances should use 'import' => 'dynamic'.

Note that these dependencies are module dependencies, not script dependencies. Script handles should not be listed as module dependencies.

The following functions are the most relevant parts of the interface for developers working with modules:

  • wp_register_script_module( string $id, string $src, array $deps = array(), $version = false ): Registers the module.
  • wp_enqueue_script_module( string $id, string $src = '', array $deps = array(), $version = false ): Marks the module to be enqueued. An optional $src argument can be provided to enqueue and register the module.
  • wp_deregister_script_module( string $id ): Deregisters the module.
  • wp_dequeue_script_module( string $id ): Unmarks the module so it is not enqueued.

Important notes

It’s strongly recommended that developers currently utilizing JavaScript modules in their extensions migrate to the Script Modules API. This will standardize behavior, bring a number of benefits, and prevent some of the following potential issues:

  • Any code that is currently using import maps may run into compatibility issues because multiple import maps are not currently supported.
  • If a module appears before an import map, an error will be triggered that causes the import map to be ignored. This would break some critical functionality that the Script Modules API relies on.

It’s important to migrate to the new API to prevent these issues. If it’s unfeasible to migrate, moving modules script to the footer may mitigate some of the potential issues.

Available modules

Two core modules are provided with 6.5.

@wordpress/interactivity is the interface for building interactive frontends with the Interactivity API.

@wordpress/interactivity-router defines an Interactivity API store with the core/router namespace, exposing relevant state and actions for client-side navigation.

There are no other core modules available at this time. The JavaScript packages available as scripts before 6.5 are not available as modules. For example, the @wordpress/api-fetch package corresponds to the wp-api-fetch script and is not available in WordPress as a module.

Working with Script Modules

The most relevant functions for working with modules are wp_register_script_module to register and wp_enqueue_script_module to enqueue modules. For example:

// Registers a module, it can now appear as a dependency or be enqueued.
wp_register_script_module(
'@my-plugin/shared',
plugin_dir_url( __FILE__ ) . 'shared.js'
);

// Registers and enqueues a module.
wp_enqueue_script_module(
'@my-plugin/entry',
plugin_dir_url( __FILE__ ) . 'entry.js',
array( '@my-plugin/shared' )
);

// Enqueues a previously registered module.
wp_enqueue_script_module( '@my-plugin/a-registered-module' );

In this example, the @my-plugin/shared module will be available to the @my-plugin/entry module:

// "@my-plugin/entry" module at …/entry.js
import * as shared from '@my-plugin/shared';
shared.doSomethingInteresting();

Blocks

WordPress 6.5 introduces a new viewScriptModule block metadata field that will handle module registration and enqueue automatically analogous to viewScript. A full dev note about viewScriptModule can be read here.

Module registration

Developers familiar with modern JavaScript development will recognize the module system and may wonder, “Do I have to register all my modules with WordPress in order to use them?” The short answer is no, but the recommended approach is to register all the modules.

JavaScript modules can import be imported by URLs. It’s common to see imports like import { utility } from './utilities.js' in modern applications. Because the Script Modules API is built on top of native JavaScript modules, it’s possible for modules to import relative paths in WordPress. However, the recommended approach is to register modules and use the registered ID to import the module. This has a few benefits:

  • Registered modules are available globally. There’s no need to worry about the module URLURL A specific web address of a website or web page on the Internet, such as a website’s URL www.wordpress.org.
  • Registered modules have versions. The Script Modules API transparently handles associating a module ID with the correct URL and version.
  • Risk of module duplication is reduced. If a module is imported through different URLs, the browser may fetch and execute multiple copies of what is ostensibly the same module.
  • Dependencies that are imported statically will be preloaded when the module is enqueued. This allows the browser to fetch them in parallel.

Relative imports to unregistered modules may be useful for prototyping and exploration, but the best practice for production code is to register modules with the Script Modules API.

Limitations

Modules and scripts are not compatible at this time. Modules cannot depend on scripts and scripts cannot depend on modules. This means that modules cannot depend on scripts like wp-i18n, wp-api-fetch, etc.

There is a workaround to use scripts from modules. Script functionality is shared by adding variables to the window scope, for example wp-api-fetch can be accessed via window.wp.apiFetch. To use scripts from modules at this time, developers must:

  • Ensure that the script is enqueued: wp_enqueue_script( ‘wp-api-fetch’ )
  • Access the script through the global scope: const { apiFetch } = window.wp

There are a number of downsides to this workaround, but it can be an effective solution. Follow Core Trac ticket #60647 for work on script and module interoperability.

Compatibility

JavaScript modules have excellent support in most browsers today. In addition, the Script Modules API includes a polyfill for browsers with lacking or incomplete support to make the number of supported browsers even greater.

Technical details

A full explanation of how the Script Modules API works is beyond the scope of this note, but this section will provide a brief overview.

The Script Modules API will track registered and enqueued modules. For each enqueued module, a <script type="module"> tagtag A directory in Subversion. WordPress uses tags to store a single snapshot of a version (3.6, 3.6.1, etc.), the common convention of tags in version control systems. (Not to be confused with post tags.) will be added to load and execute the module.

A <script type="importmap"> tag will be added. The import map tells the browser how to map module IDs to URLs so that when it sees an import for '@wordpress/interactivity', it knows that the module should be fetched from the URL /wp-includes/js/dist/interactivity.min.js?ver=xyz. The import map includes all the dependencies of all enqueued modules, and all the dependencies of those dependencies, etc. The import map will not include dependencies of all the registered modules, only those that may be required on the page as dependencies, and it will not include the enqueued modules unless they are in the dependency graph of other enqueued modules.

A <link rel="modulepreload"> will be added for all static dependencies. Static dependencies are all dependencies that are not defined as array( 'id' => '…', 'import' => 'dynamic' ). This is an optimization hint for browsers, read more about it here.

Relevant links

Props

#6-5, #dev-notes, #dev-notes-6-5

Block metadata viewScriptModule field in 6.5

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. 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. now recognizes the viewScriptModule field in block.json metadata. This provides an experience for script modules that’s analogous to what scripts and the viewScript field currently provide. The script module declared in viewScriptModule will be enqueued when a block is rendered for viewing on the frontend.

This new field is important so that developers can use script modules on the frontend. Scripts cannot depend on script modules, so script modules are necessary to use script modules like the Interactivity API (via the @wordpress/interactivity script module).

Read more: Block metadata viewScriptModule field in 6.5

This post assumes a basic understanding of the Script Modules API, read more about it here.

Getting started

You can generate a simple block 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 using the @wordpress/create-block-interactive-template package:

npx @wordpress/create-block --template @wordpress/create-block-interactive-template

This will generate an interactive block from a template that uses viewScriptModule.

Usage

Add a viewScriptModule field to the metadata in block.json that points to the script module.

Here’s what a simple plugin might look like:

myplugin/
├── build/
│ ├── block.json
│ ├── …snip…
│ ├── view.asset.php
│ └── view.js
└── plugin.php

The only code in the plugin.php file calls register_block_type for the block:

add_action( 'init', function() {
register_block_type( __DIR__ . '/build/block.json' );
} );

The relevant metadata in build/block.json includes viewScriptModule:

{
"…snip…": "…",
"viewScriptModule": "file:./view.js"
}

build/view.js is the script module that depends on @wordpress/interactivity:

import { getContext, store } from '@wordpress/interactivity';

store( 'myplugin/demo', {
increment() {
getContext().val += 1;
},
decrement() {
getContext().val -= 1;
},
} );

The module asset file build/view.asset.php declares the view script module’s dependency on @wordpress/interactivity:

<?php return array(
'dependencies' => array('@wordpress/interactivity'),
);

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/.

A new field view_script_module_ids has been added to the Block Types REST API. The field is a list of the view script module IDs associated with a block type.

Tooling

The @wordpress/scripts package is the standard way of compiling JavaScriptJavaScript JavaScript or JS is an object-oriented computer programming language commonly used to create interactive effects within web browsers. WordPress makes extensive use of JS for a better user experience. While PHP is executed on the server, JS executes within a user’s browser. https://www.javascript.com/. assets for use in WordPress. Experimental functionality has been added to the build and start scripts to support compiling modules. It can be enabled using --experimental-modules on the command line, for example:

 wp-scripts build --experimental-modules

Script module compilation with @wordpress/scripts does not support using your own webpack file at this time.

The example plugin above was compiled using the experimental module build with @wordpress/scripts.

@wordpress/dependency-extraction-webpack-plugin has been updated to produce script module asset files. No special configuration is required. See the package documentation for details.

Relevant links

Props

#6-5, #dev-notes, #dev-notes-6-5

Updates to Block Hooks in 6.5

First introduced in WordPress 6.4, 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. 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. offer an extensibility mechanism for Block Themes that allows extenders to inject a dynamic block in a location specified by an “anchor” block and a relative position. (For example, before or after the Post Content block.)

WordPress 6.5 updates Block Hooks to make them more widely useful and includes some frequently requested new features.

New features

Modified layouts

Previously restricted to layouts – templates, template parts, and patterns – that didn’t have any user modifications, Block Hooks now also work with modified layouts. This works out of the box; extenders do not need to update their code to benefit from this. Block Hooks will continue to respect user customizations. If a user moves or deletes a hooked block in the Site Editor, the Block Hooks mechanism will not re-insert it, thus preserving the user’s express intentions.

Navigation block

Additionally, it is now possible to inject hooked blocks into the Navigation block. Previously, a hooked block could only be added before or after the Navigation block, but now it can also be added as its first or last child.

As a consequence, the hooked_block_types 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.’s $context argument can now also be a WP_Post object with its post type set to wp_navigation.

This means that a negative condition, such as if ( ! $context instanceof WP_Block_Template ), is no longer sufficient to conclude that $context is an array (i.e., a pattern). Instead, use an affirmative check, such as if ( is_array( $context ) ).

The following example adds a (hypothetical) Shopping Cart block as the Navigation block’s last child, after ensuring that the block isn’t already present elsewhere in the Navigation menuNavigation Menu A theme feature introduced with Version 3.0. WordPress includes an easy to use mechanism for giving various control options to get users to click from one place to another on a site.:

function add_shopping_cart_block_to_navigation_block( $hooked_block_types, $relative_position, $anchor_block_type, $context ) {
	// Is $context a Navigation menu?
	if ( ! $context instanceof WP_Post || 'wp_navigation' !== $context->post_type ) {
		return $hooked_block_types;
	}

	// Does the Navigation menu already contain the block?
	if ( str_contains( $context->post_content, '<!-- wp:my-ecommerce/shopping-cart' ) ) {
		return $hooked_block_types;
	}

	if ( 'last_child' === $relative_position && 'core/navigation' === $anchor_block_type ) {
		$hooked_block_types[] = 'my-ecommerce/shopping-cart';
	}
	return $hooked_block_types;
}
add_filter( 'hooked_block_types', 'add_shopping_cart_block_to_navigation_block', 10, 4 );

New filters

In order to offer more fine-grained control over hooked blocks, two new filters have been introduced: hooked_block and hooked_block_{$hooked_block_type}. Both share the following signature:

  • $parsed_hooked_block (array|null) – The hooked block, in parsed block array format, or null, indicating that a previous filter has suppressed the injection of the given block.
  • $hooked_block_type (string) – The hooked block type name.
  • $relative_position (string) – The relative position of the hooked block (can be one of before, after, first_child, or last_child).
  • $parsed_anchor_block (array) – The anchor block, in parsed block array format.
  • $context (WP_Block_Template|WP_Post|array) – The block template, template part, wp_navigation post type, or pattern that the anchor block belongs to.

One of the most frequently requested features for Block Hooks was the ability to set a hooked block’s attributes. While it is generally advised to choose default values for block attributes that work well when the block is used as a hooked block, it is now possible to set custom values for its attributes via these new filters.

In addition, the filters allow setting a hooked block’s inner blocks (and, by extension, wrapping it inside of another block), or suppressing the hooked block altogether (by returning null).

Finally, the filters are also given the anchor block instance as an argument, so it’s possible to, for example, set the hooked block’s attributes based on the anchor block’s:

function set_block_layout_attribute_based_on_adjacent_block( $hooked_block, $hooked_block_type, $relative_position, $anchor_block ) {
	// Has the hooked block been suppressed by a previous filter?
	if ( is_null( $hooked_block ) ) {
		return $hooked_block;
	}

	// Is the hooked block adjacent to the anchor block?
	if ( 'before' !== $relative_position && 'after' !== $relative_position ) {
		return $hooked_block;
	}

	// Does the anchor block have a layout attribute?
	if ( isset( $anchor_block['attrs']['layout'] ) ) {
		// Copy the anchor block's layout attribute to the hooked block.
		$hooked_block['attrs']['layout'] = $anchor_block['attrs']['layout'];
	}

	return $hooked_block;
}
add_filter( 'hooked_block_my/like-button', 'set_block_layout_attribute_based_on_adjacent_block', 10, 4 );

For more examples, refer to the snippets found in the description of the PR that introduced the new filters.

Plugins block inspector panel toggles

The toggles for each hooked block in the block inspector’s “Plugins” panel have been updated to fix a confusing behavior that would remove the toggle in certain cases after it was deactivated. Furthermore, toggles are now shown for blocks added by the hooked_block_types filter; note that due to technical limitations, this currently only works for layouts that have user modifications.

A screenshot showing a Navigation block selected in the Site Editor. The block contains 'News', 'About', and 'Logout' links, and a shopping cart icon.

The Block Inspector sidebar is open. It is showing the 'Plugins' panel with toggles for the hooked blocks: one for the Shopping Cart, and one for the Login/out link.

Under the hood

The major technical challenge to making Block Hooks work with modified layouts was to ensure they would continue respecting user customizations to a given layout. That is, when a block was persisted, moved, or deleted by the user in the Site Editor, it wouldn’t end up being re-inserted by the Block Hooks mechanism.

It turned out that this problem could be solved by essentially a “finer-grained” version of the same principle (i.e., modified vs. unmodified). This is achieved by storing information about hooked block types in an ignoredHookedBlocks array inside the global metadata attribute on the anchor block. Specifically, it works via the two following operations:

On write (i.e., persisting a modified layout to the database): All hooked block types that were present during the edit are persisted to their respective anchor block’s ignoredHookedBlocks arrays. (Note that this is regardless of the nature of the edit: It works the same both for when the hooked block is persisted and when it is removed.)

On read (i.e., rendering on the front end or loading into the Site Editor): The Block Hooks mechanism will now look for the presence of a given hooked block type inside its anchor block’s ignoredHookedBlocks array. If a hooked block’s type name is found in that array, it will be ignored (i.e., not injected).

See this Trac ticket for more information on how this solution was conceived.

For controlled inner blocks: For inner blocks that aren’t present inside the containing block but loaded separately at runtime, the same ignoredHookedBlocks array is stored, but in a new post metaMeta Meta is a term that refers to the inside workings of a group. For us, this is the team that works on internal WordPress sites like WordCamp Central and Make WordPress. field _wp_ignored_hooked_blocks instead of in the containing block’s attributes. This is how hooked blocks work in the Navigation block with its external wp_navigation post.


With these new features in place and several important limitations lifted, Block Hooks can now be used across a large number of layouts, without paying heed to whether the user has modified a layout or not. Specialized filters allow you to fine-tune hooked blocks by setting their attributes and inner blocks while also taking information about the anchor block instance into account when doing so. This makes Block Hooks a more intuitive and powerful tool for extending Block Themes.

We’re looking forward to seeing what extenders will create with this 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.!

Props to @dmsnell, @gziolo, @ndiego, and @leonnugraha for peer review.

#dev-notes

Performance improvements for registering block variations with callbacks

Background

In WordPress 5.8, the 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. for registering 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. variations was introduced, bringing new capabilities to developers. However, it soon became evident that generating these variations had a non-trivial performance cost, particularly when used for template-part and navigation-link in WordPress 5.9 and post-terms in WordPress 6.1.

The coreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress. of this issue was that these variations were being generated on every page load due to their registration using the init hook. This was inefficient, as the variations were only needed in the editor or for returning block data in the 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/.. This redundancy in loading led to unnecessary performance overhead, underscoring the need for a more streamlined approach.

What is changed

In WordPress 6.5, we’ve introduced $variation_callback as a new attribute in WP_Block_Type. This feature allows developers to register variation-building functions as callbacks rather than building variations before registration. With this approach, variations are constructed from this callback only when they are accessed for the first time. 

Consequently, the WP_Block_Type::get_variations() method was introduced as the primary means to access variations. This method skillfully checks for a callback in variation_callback and triggers the callback, but only during the first access of the variations. This efficient approach ensures that the callback is executed just once, optimizing performance by avoiding redundant processing on subsequent accesses. Additionally, this strategy aids in lazy loading, as variations no longer need to be loaded during registration.

Moreover, it integrates the get_block_type_variations 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., which provides a versatile mechanism for developers to modify the resulting variations. These enhancements reflect WordPress’s ongoing commitment to improving its developer-centric features and maintaining compatibility with existing functionalities.
In WordPress 6.5, a significant alteration is made to WP_Block_Type::$variations. The notable change involved transitioning the visibility of variations from public to private. This modification was specifically designed to necessitate the use of the PHPPHP The web scripting language in which WordPress is primarily architected. WordPress requires PHP 5.6.20 or higher magic __get method for accessing variations only via WP_Block_Type::get_variations().

Please note that the modifications to WP_Block_Type::$variations may introduce certain limitations that require developer attention. These are detailed in the “Developer Action” section below.

Lazy loading variations using variation_callback

To facilitate the lazy loading of variations, we can replace the traditional method of loading variations with a callback. This approach is exemplified in the case of GutenbergGutenberg The Gutenberg project is the new Editor Interface for WordPress. The editor improves the process and experience of creating new content, making writing rich content much simpler. It uses ‘blocks’ to add richness rather than shortcodes, custom HTML etc. https://wordpress.org/gutenberg/ PR #56952, which is aimed at enhancing performance.

Previously, template-part variations were loaded as follows:

register_block_type_from_metadata(
    __DIR__ . '/template-part',
    array(
        'render_callback'    => 'render_block_core_template_part',
        'variations'         => build_template_part_block_variations(),
    )
);

To optimize this, we have transitioned to using a variation_callback:

register_block_type_from_metadata(
    __DIR__ . '/template-part',
    array(
        'render_callback'    => 'render_block_core_template_part',
        'variation_callback' => 'build_template_part_block_variations',
    )
);

It’s important to note that if both variations and variation_callback are defined, like in the following example:

register_block_type_from_metadata(
'block-name',
array(
'variations' => array( 'test-variation' => array( 'test-variations' ) ),
'variation_callback' => 'get_test_variations',
)
);

In this scenario, the variations array takes precedence, and the variation_callback will not be called.

Developer action required

With the addition of variations to the existing magic function framework in WP_Block_Type, a significant limitation emerges: the inability to modify variations by reference directly. This change impacts how variations were traditionally updated.

Consider these examples of updating variations by reference, which will no longer function as expected in WordPress 6.5:

$block_type->variations[] = array( 'test' => array( 'test' ) );
// or
$block_type->variations['example1'] = array( 'test' );

To overcome this limitation in WordPress 6.5, a workaround involving a temporary variable can be used. Here’s how you can modify the temporary variable and then reassign it back to $block_type->variations:

$variations = $block_type->variations;$variations[] = array( 'test' => array( 'test' ) );
$block_type->variations = $variations;
// Similarly$variations = $block_type->variations;
$variations['example1'] = array( 'test' );$block_type->variations = $variations;

Alternatively, to align with the latest updates and best practices in WordPress, you might consider using the get_block_type_variations filter (reference link).

Utilizing the get_block_type_variations filter for enhanced variation management

The introduction of the get_block_type_variations filter in WordPress 6.5 allows for the manipulation and customization of the variations array, providing greater flexibility in handling block variations.

Here’s an implementation example of the get_block_type_variations filter:

function modify_block_type_variations( $variations, $block_type ) {
// Example: Add a new variation
if ( 'block-name' === $block_type->name ) {
$variations[] = array(
'name' => 'new-variation',
'title' => __( 'New Variation', 'textdomain' ),
// Other variation properties...
);
}

return $variations;
}
add_filter( 'get_block_type_variations', 'modify_block_type_variations', 10, 2 );

In this example, the modify_block_type_variations function is hooked to the get_block_type_variations filter. It checks if the block type is ‘block-name‘ and then adds a new variation to the variations array. This function then returns the modified variations array.

By leveraging this filter, developers can dynamically adjust the variations according to specific requirements or conditions.

Please refer to #59969 for additional details on these changes.

Props to @adamsilverstein, @westonruter, @joemcgill, and @leonnugraha for reviewing this post.

#6-5, #block-api, #dev-notes, #dev-notes-6-5

I18N Improvements in 6.5 (Performant Translations)

Various internationalization (i18n) improvements are in WordPress 6.5, and this developers note will focus on these.

New localization system with improved performance

Over the past year, WordPress contributors have meticulously analyzed performance of the existing i18ni18n Internationalization, or the act of writing and preparing code to be fully translatable into other languages. Also see localization. Often written with a lowercase i so it is not confused with a lowercase L or the numeral 1. Often an acquired skill. system in WordPress and ultimately created a new Performant Translations feature pluginFeature Plugin A plugin that was created with the intention of eventually being proposed for inclusion in WordPress Core. See Features as Plugins. that provided a completely overhauled system with significantly better performance. After thousands of betaBeta A pre-release of software that is given out to a large group of users to trial under real conditions. Beta versions have gone through alpha testing in-house and are generally fairly close in look, feel and function to the final product; however, design changes often occur as part of the process. testers and a merge announcement late last year, this new library is now included in WordPress 6.5! See #59656 for all the details.

The Performant Translations 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 is still useful and will continue to be maintained to build on top of the coreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress. solution with a distinct additional feature. As is already the case today, the plugin will automatically convert any .mo files to PHPPHP The web scripting language in which WordPress is primarily architected. WordPress requires PHP 5.6.20 or higher files if a PHP file does not currently exist. This is useful for sites where translations are not coming from translate.wordpress.org or only exist locally on that server.

This new library is faster at loading binary .mo files and uses less memory. It even supports loading multiple locales at the same time, which makes locale switching faster. In addition to that, it supports translations contained in PHP files, avoiding a binary file format and leveraging OPCache if available.

The new library is so fast, in fact, that it paves the way for the Preferred Languages feature plugin to merge translations of multiple locales by default starting with WordPress 6.5.

While this is in large part a silent and backward-compatible under-the-hood change, there are still a few things to be aware of:

New .l10n.php translationtranslation The process (or result) of changing text, words, and display formatting to support another language. Also see localization, internationalization. file format

When downloading language packs from WordPress.orgWordPress.org The community site where WordPress code is created and shared by the users. This is where you can download the source code for WordPress core, plugins and themes as well as the central location for community conversations and organization. https://wordpress.org/, there will be a new .l10n.php file in addition to the .mo and .po files you are already familiar with. If an .mo translation file has a corresponding .l10n.php file, the latter will be loaded instead, making things even faster and use even less memory.

This is a progressive enhancementenhancement Enhancements are simple improvements to WordPress, such as the addition of a hook, a new feature, or an improvement to an existing feature., so if there’s only an .mo file but no PHP file, translations will still be loaded as expected. However, the opposite is also true! So you can theoretically use only .l10n.php translation files in your project and features such as the just-in-time translation loading continue to work. Right now, WordPress still expects corresponding .po and .mo files for things like update checks. However, this limitation will be addressed in the future, see #60554 for more information.

Note: if you don’t see any .l10n.php translation files in wp-content/languages yet, it might be that the language pack hasn’t been updated in a while, i.e. there were no new translations.

Here’s an example of a PHP translation file as supported by WordPress 6.5:

<?php
return [
'project-id-version' => 'WordPress - 6.5.x - Development',
'report-msgid-bugs-to' => 'polyglots@example.com',
'messages' =>
[
'original' => 'translation',
'contextEOToriginal with context' => 'translation with context',
'plural0' => 'translation0' . "\0" . 'translation1',
'contextEOTplural0 with context' => 'translation0 with context' . "\0" . 'translation1 with context',
'Product' => 'Produkt' . "\0" . 'Produkte',
],
];

Note: EOT here stands for the “End of Transmission” character (U+0004, or "\4" in PHP). It’s the same delimiter as in gettext used to glue the context with the singular string.

Generating PHP translation files

If you would like to generate these PHP translation files yourself, version 4.0 of GlotPress, the plugin that powers translate.WordPress.org, already supports the new .l10n.php format.

In addition to that, WP-CLI 2.10.0 (i18n-command 2.6.0) provides a new wp i18n make-php command to create these PHP files from a given .po file. Examples:

# Create PHP files for all PO files in the current directory.
$ wp i18n make-php .

# Create a PHP file from a single PO file in a specific directory.
$ wp i18n make-php example-plugin-de_DE.po languages

If you are developing a WordPress plugin that deals with translations, you can also use the new WP_Translation_File class to convert an .mo file into a PHP file. Example:

$contents = WP_Translation_File::transform( $mofile, 'php' );
if ( $contents ) {
file_put_contents( $path_to_php_file, $contents );
}

New filters to customize this behavior

If you would like to disable the support for PHP files for some reason; for example, if you don’t have any yet in your project and want to prevent the extra file lookup operation, you can use the new translation_file_format 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. to change the preferred format (default is php) like so:

add_filter(
'translation_file_format',
static function () {
return 'mo';
}
);

The existing load_textdomain_mofile filter can still be used to filter the .mo file path for loading translations for a specific text domain. However, it only works for .mo files. To filter the path for a translation file, be it a .l10n.php or a .mo file, use the new load_translation_file filter.

Working with the $l10n global variable

Previously, when loading translations, WordPress would store an instance of the MO class in the $l10n global variable. With WordPress 6.5, this will be an instance of a new WP_Translations class that acts as a shim with similar characteristics. If your project directly works with this global variable or the MO class in some way, this is an area to keep an eye on.

Cached list of language file paths

This another slight performance improvement but unrelated to the new localization library covered above.

In places such as get_available_languages() and WP_Textdomain_Registry, WordPress used to directly use the glob() function to retrieve all .mo files in a specific directory. This is important for just-in-time translation loading and generally knowing which translations are installed. However, on sites with a large number of language files, the glob() operation can become expensive.

Because of this, a new caching mechanism was introduced in #58919 / [57287]. The file lookup is now handled centrally in WP_Textdomain_Registry and stored in the object cache in the translations group, with the cache key having the format cached_mo_files_<hash>, where <hash> is the MD5 hash of the scanned directory, e.g. wp-content/languages. The cache is cleared whenever language packs are updated.

Also, the lookup now also scans for .l10n.php files in addition to .mo files, in case only the former exist on a site.

More questions? Please let us know

If you have any questions, please leave a comment below or file a new ticket on Trac under the I18N component if you’ve encountered any bugs.

Props to @joemcgill, @stevenlinx for review.

#6-5, #dev-notes, #dev-notes-6-5, #i18n

WordPress 6.5 adds AVIF support 

WordPress 6.5 supports AVIF, a modern image format that offers significant improvements in image quality and compression over previous formats like JPEG, PNG, and even WebP.  AVIF images can be up to 50% smaller than JPEGs while maintaining the same image quality. AVIF images also support a wide range of colors (including HDR) and produce sharper images than JPEGs, especially in areas with high detail.

From WordPress version 6.5 forward, you can upload and use AVIF images in WordPress just like you would a JPEG or PNG image today – as long as your hosting environment supports AVIF. Switching to the AVIF format for your images is likely to improve your site’s performance and your site visitor’s experience. 

How AVIF helps you

AVIF images are significantly smaller than their JPEG equivalents, so pages load more quickly and take less bandwidth to transmit. AVIF images still get all of the benefits of the responsive images, Fetch Priority, and lazy loading that WordPress supports by default. Finally, AVIFs are supported in all major browsers, so most sites can start using them today.

Creating AVIF images

Many image editing tools support exporting to AVIF. You can also use command line conversion tools or web based open sourceOpen Source Open Source denotes software for which the original source code is made freely available and may be redistributed and modified. Open Source **must be** delivered via a licensing model, see GPL. tools like Squoosh. Once you save your images as AVIF, upload them to WordPress and use them like you would any other image. WordPress can also create AVIFs for you automatically, for more details, see the FAQ item below.

Using AVIF images in WordPress

AVIF images work like any other image format in WordPress, with a few important notes:

AVIF in WordPress depends on support in your web server’s image processing library (WordPress has built-in support for both Imagick and LibGD for image processing). You can check for AVIF support in wp-adminadmin (and super admin) by visiting Tools -> Site Health, clicking the “Info” tab and expanding the “Media Handling” section, then finally looking for “AVIF” in the list of supported formats.

If your audience includes a significant number of users on an unsupported browser, either avoid using AVIF images, or enqueue a browser polyfill.

FAQ

Q: How do I adjust the compression level used for generated AVIF images?

The wp_editor_set_quality 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. can be used to set the quality setting. The passed mime type enables setting by type, for example:

// Use a quality setting of 75 for AVIF images.
function filter_avif_quality( $quality, $mime_type ) {
if ( 'image/avif' === $mime_type ) {
return 75;
}
return $quality;
}
add_filter( 'wp_editor_set_quality', 'filter_avif_quality', 10, 2 );

How can I output lossless AVIF images?

Using a compression level of 100 will set AVIF to its lossless mode.

Q: Can WordPress create AVIF images when I upload JPEGs?

Yes. Developers can use the image_editor_output_format filter to specify this type of transformation for uploads. Here is an example:

// Output AVIFs for uploaded JPEGs
function filter_image_editor_output_format( $formats ) {
$formats['image/jpeg'] = 'image/avif';
return $formats;
}
add_filter( 'image_editor_output_format', 'filter_image_editor_output_format' );

If I use WordPress multisitemultisite Used to describe a WordPress installation with a network of multiple blogs, grouped by sites. This installation type has shared users tables, and creates separate database tables for each blog (wp_posts becomes wp_0_posts). See also network, blog, site, will all my sites work with AVIF images?

No. Multisite stores the file types that users are allowed to upload when a site is created. We are still working on improving this in #53167. In the meantime, to ensure all existing sites on a networknetwork (versus site, blog) allow AVIF files, you can use the site_option filter in a network mu-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 to add avif to the allowed file types for all network sites:

// Ensure all network sites include AVIF support.
function filter_site_option_upload_filetypes( $filetypes ) {
$filetypes = explode( ' ', $filetypes );
if ( ! in_array( 'avif', $filetypes, true ) ) {
$filetypes[] = 'avif';
}
return implode( ' ', $filetypes );
}
add_filter( 'site_option_upload_filetypes', 'filter_site_option_upload_filetypes' );


Thanks to @stevenlinx and @westonruter for reviewing this post.

#6-5, #core-images, #dev-notes, #dev-notes-6-5, #images