Whitespace changes in navigation for 4.7

In [38523] the argument item_spacing was introduced for the functions wp_nav_menu(), wp_list_pages(), and wp_page_menu() to:

  • ensure whitespace equivalent output when wp_nav_menu() falls back to wp_list_pages(), and,
  • allow theme authors to control how whitespace is output.

Backward compatibility changes.

There is a backward compatibility breakage when wp_nav_menu() is empty and falls back to using wp_list_pages().

By default, wp_nav_menu() outputs markup with whitespace between each list item (</li> <li>). Prior to WordPress 4.7 when falling back to wp_list_pages() there would be no whitespace between list items (</li><li>). This caused layout to change when certain styles were applied.

From WordPress 4.7 onward when falling back to a page list, wp_nav_menu() will output markup with whitespace between each list item.

There is no change to the default behaviour when calling wp_list_pages() directly, it will not have any whitespace between each item in the menu.

Controlling how menus and page lists output whitespace.

From WordPress 4.7 onwards, theme authors will be able to control whether whitespace in wp_nav_menu(), wp_list_pages() and wp_page_menu() with the item_spacing argument.

The item_spacing argument accepts either preserve or discard. To discard the whitespace in a menu, call the function with:

wp_nav_menu( array(
	'theme_location' => 'top',
	'menu_id'        => 'top-menu',
	'item_spacing'   => 'discard', // default 'preserve'
) );

The same argument can be used for wp_list_pages(), and wp_page_menu() although the default is discard.

#4-7, #dev-notes, #menus

Changed loading order for current user in 4.7

With the introduction of user locales it’s required to load the current user earlier in the bootstrap. Since WordPress 3.4 this is already the case for the customizer, see #24169 and the following simplified function stack:

{main}()                              .../customize.php:0
require_once( '/wp-admin/admin.php' ) .../customize.php:13
require_once( '/wp-load.php' )        .../admin.php:31
require_once( '/wp-config.php' )      .../wp-load.php:44
require_once( '/wp-settings.php' )    .../wp-config.php:118
do_action( 'plugins_loaded' )         .../wp-settings.php:295
_wp_customize_include()               .../plugin.php:524
WP_Customize_Manager->__construct()   .../theme.php:2086
WP_Customize_Widgets->__construct()   .../class-wp-customize-manager.php:266
current_user_can()                    .../class-wp-customize-widgets.php:97
wp_get_current_user()                 .../capabilities.php:448

For other requests the stack looks like this:

{main}()                              .../index.php:0
require_once( '/wp-admin/admin.php' ) .../index.php:10
require_once( '/wp-load.php' )        .../admin.php:31
require_once( '/wp-config.php' )      .../wp-load.php:44
require_once( '/wp-settings.php' )    .../wp-config.php:118
WP->init()                            .../wp-settings.php:398
wp_get_current_user()                 .../class-wp.php:595

WP->init() runs between the after_setup_theme and the init action.

With WordPress 4.7 the function stack for admin requests will look like this:

{main}()                              .../index.php:0
require_once( '/wp-admin/admin.php' ) .../index.php:10
require_once( '/wp-load.php' )        .../admin.php:31
require_once( '/wp-config.php' )      .../wp-load.php:42
require_once( '/wp-settings.php' )    .../wp-config.php:127
load_default_textdomain()             .../wp-settings.php:389
get_user_locale()                     .../l10n.php:665
wp_get_current_user()                 .../l10n.php:92

That’s because load_default_textdomain() needs to know the locale of the current user. load_default_textdomain() is called after setup_theme and before after_setup_theme (which is before WP->init()).
If you compare this with the stack for the customizer then you’ll notice that wp_get_current_user() is still loaded much later.

get_user_locale() is also used in the other text domain loading functions like load_plugin_textdomain() or load_theme_textdomain(). For backward compatibility we’ve made sure that no fatal errors are thrown when one of them is called before WordPress is fully initialized, see [39127] and [39134].

Until recently BuddyPress and bbPress had a custom notice when a user was initialized without using WP->init(). This was fixed in #7305-buddypress and #2309-bbpress together with a new wp_roles_init filter in core. The new filter allows plugins to add their own custom roles whenever they’re initialized, see #23016.

#4-7, #bootstrap-load, #dev-notes

User Admin Languages and Locale Switching in 4.7

In WordPress 4.7 users will be able to select their preferred locale (language) when editing their profile. 🎉🎉 This allows for greater personalization of the WordPress admin and therefore a better user experience.

The back end will be displayed in the user’s individual locale while the locale used on the front end equals the one set for the whole site. If the user didn’t specify a locale, the site’s locale will be used as a fallback. The new locale property of the WP_User class can be used to retrieve the user’s locale setting.

The new user language option when editing the profile

The new user language option

To enable sending emails in the language of the recipient and not the current user’s language, an API has been introduced to switch locales and translations at any point during the page load.

Here are some of the new API functions with usage examples.

get_user_locale( $user_id )

This function is used to retrieve the locale of any user. When no user ID is set it uses the current user. The user locale should be used for any user-facing screens in the admin.

The user locale is stored as a user meta locale. Therefore it can be modified with the get_{$meta_type}_metadata filter.
If the meta field is empty the value of get_locale() is returned.

switch_to_locale( $locale )

switch_to_locale() switches the locale and (un)loads text domains according to a given locale. This includes the global $wp_locale object as well. It can be used together with get_locale() or get_user_locale(). Core’s main purpose of this function is to be able to send emails like password/email change notifications in the recipient’s language. Admin emails, emails for  get_option( 'admin_email' ), are using get_locale(), the site language.

When switching a locale several actions are fired which allows one to perform further actions:

  • change_locale: Fires when a locale is switched or restored. Core uses it to re-init post types and taxonomies.
  • switch_locale: Fires when a locale is switched.
  • restore_previous_locale: Fires when a locale is restored to the previous one.

restore_previous_locale() and restore_current_locale()

restore_previous_locale() can be used to restore the previous locale. Example:

$locale_switched = switch_to_locale( get_user_locale() );

// Do something.

if ( $locale_switched ) {

Use restore_current_locale() if you want to empty the current stack and restore the original locale.


As the name implies, it checks whether switch_to_locale() is in effect.


switch_to_locale(), restore_previous_locale(), restore_current_locale(), and is_locale_switched() are wrapping the same named methods of a new WP_Locale_Switcher class. This class is responsible for holding a stack of locales and filtering the actual locale through the locale filter.

Note about admin-ajax.php

As admin-ajax.php is in the admin, anyone getting translated strings via Ajax will get strings in the user’s locale when they are logged in. You can use switch_to_locale( get_locale() ) to ensure the string is returned in the site’s locale, rather then the user’s locale. Or, ideally, leverage the REST API. 💪🏼

For background information on these changes see #29783, #26511, and #38485.

Related dev note: Changed loading order for current user in 4.7.

#4-7, #dev-notes, #i18n

Multisite Focused Changes in 4.7

Howdy. The 4.7 release cycle has been a chance to build on some of the work from the last couple releases in multisite.  If you’d like more detail, check out the full list of multisite focused changes in this release.

get_blog_details() replaced with get_site()

A lot of progress has been made over the last few releases to get things in place for this transition. Now that WP_Site and WP_Network objects exist and are accessible with functions like get_site() and get_network(), they can be implemented throughout core.

In WordPress 4.7, get_blog_details() was replaced throughout core code with the modern  get_site(). The roadmap for this includes deprecating get_blog_details() in WordPress 4.8, so take this cycle as a chance to move your code in that direction.

get_site() is often a direct replacement, though get_sites() can also be used to query for sites when an ID is not available.

See #37102 for details on this change.

blog_details filter deprecated

In combination with the decision to stop using get_blog_details() throughout core, the (not widely used) blog_details filter has been deprecated. It has been added to get_site() to provide backward compatibility with the above change and will fire with a deprecation notice. Plugin code should use the site_details filter instead. See #38491 for details on this change.

_network_option actions and filters get $network_id

The $network_id associated with the use of a _network_option() function is now passed to the filters and actions that fire within. This provides granular control that was not available when first introduced. See #38319, #38320, #38321, and #38322 for details on this change.

wp_get_network() deprecated

It is now recommended that get_network() is used instead. See #37553 for this change.



#4-7, #dev-notes, #multisite, #network-sites

Attributes for Resource Hints in 4.7

WordPress 4.6 added support for Resource Hints, a W3C specification that “defines the dns-prefetch, preconnect, prefetch, and prerender relationships of the HTML Link Element (<link>)”. These can be used to assist the browser in the decision process of which origins it should connect to, and which resources it should fetch and preprocess to improve page performance.

With WordPress 4.7, you’re now able to pass specific HTML attributes to these resource hints to make even better use of them. Namely, the as, crossorigin, pr, and type attributes can now be defined when using the wp_resource_hints filter. See #38121 for more information.

Here’s an example of how one can use this new feature:

function makewp_example_resource_hints_attributes( $hints, $relation_type ) {
	if ( 'prefetch' === $relation_type ) {
		$hints[] = array(
			'crossorigin' => 'use-credentials',
			'as'          => 'style',
			'pr'          => 0.5,
			'href'        => 'https://example.com/foo.css',

	return $hints;

add_filter( 'wp_resource_hints', 'makewp_example_resource_hints_attributes', 10, 2 );

While crossorigin can be used to set the CORS settings for a resource, pr indicates the expected probability that the specified resource hint will be used. The official W3C specification has more information about these attributes.

Note: preload is not yet supported by wp_resource_hints(), mainly because of a lack of browser support and benefit for core. This will continue to be reevaluated as browser support evolves for these emerging features.

#4-7, #dev-notes, #script-loader

New Post Type Labels in 4.7

In WordPress 4.7, two additional labels have been made available for custom post types. These get passed in via the labels argument when using register_post_type(). The following labels are new:

  • view_items – The post type archive label used in the toolbar on the edit screen. Default “View Posts” / “View Pages”. See #34113.
  • attributes – The label used for the title of the post attributes meta box (used to select post type templates). Default “Post Attributes”/ “Page Attributes”. See #18375.


#4-7, #dev-notes, #i18n

Post Type Templates in 4.7

WordPress has supported custom page templates for over 12 years, allowing developers to create various layouts for specific pages. While this feature is very helpful, it has always been limited to the ‘page’ post type and not was not available to other post types. With WordPress 4.7, it will be.

By opening up the page template functionality to all post types, the template hierarchy’s flexibility continues to improve.

In addition to the Template Name file header, the post types supported by a template can be specified using Template Post Type: post, foo, bar. Here’s an example:

Template Name: Full-width layout
Template Post Type: post, page, product

// … your code here

That way, you’ll be able to select this full-width template for posts, pages, and products.

When at least one template exists for a post type, the ‘Post Attributes’ meta box will be displayed in the back end, without the need to add post type support for 'page-attributes' or anything else. The ‘Post Attributes’ label can be customized per post type using the 'attributes' label when registering a post type.

Backward Compatibility

Let’s say you want to publicly release a theme with support for post type templates. WordPress versions before 4.7 will ignore the Template Post Type header and show the template in the list of page templates, even though it only works for regular posts. To prevent that, you can hook into the theme_page_templates filter to exclude it from the list. Here’s an example:

 * Hides the custom post template for pages on WordPress 4.6 and older
 * @param array $post_templates Array of page templates. Keys are filenames, values are translated names.
 * @return array Filtered array of page templates.
function makewp_exclude_page_templates( $post_templates ) {
	if ( version_compare( $GLOBALS['wp_version'], '4.7', '<' ) ) {
		unset( $post_templates['templates/my-full-width-post-template.php'] );

	return $post_templates;

add_filter( 'theme_page_templates', 'makewp_exclude_page_templates' );

That way you can support custom post type templates in WordPress 4.7 and beyond while maintaining full backward compatibility.

Note that theme_page_templates is actually a dynamic theme_{$post_type}_templates filter. The dynamic portion of the hook name, $post_type, refers to the post type supported by the templates. E.g. you can hook into theme_product_templates to filter the list of templates for the product post type.

For further information about this new feature, see the corresponding ticket #18375.

#4-7, #dev-notes

wp_list_sort() and WP_List_Util in 4.7

If you have been working with arrays of multiple objects or arrays of multiple arrays before, you might be familiar with the functions wp_list_filter(), wp_list_pluck(), and wp_filter_object_list() which is essentially a combination of the previous two. WordPress 4.7 brings a few enhancements to handling such object or array lists.

New Utility Function to Sort Lists

WordPress 4.7 introduces a new function wp_list_sort() that makes it easy to sort object or array lists by one or more of its elements’ properties.


In addition to the function’s first parameter $list which should receive the array to sort, you can pass the name of one of the elements’ properties as the second parameter $orderby and the sort direction (either ‘ASC’ or ‘DESC’) as the third parameter $order. For example, if you had a list of post objects and you needed to sort them by their dates in descending order, you would use the function as follows:

$sorted_posts = wp_list_sort( $posts, 'post_date', 'DESC' )

Similar to the orderby and order arguments of the query classes in WordPress, the function also supports sorting by multiple properties by passing an array of properties and their sort direction as the $orderby parameter. When using the function like this, the $order parameter can be omitted and will be ignored. For example, here’s how you’d first order some posts by their dates in descending order and then by their titles in ascending order afterwards:

$sorted_posts = wp_list_sort( $posts, array(
  'post_date'  => 'DESC',
  'post_title' => 'ASC',
) )

The fourth parameter $preserve_keys is a boolean and can be set to true if the array is associative and its keys should be preserved. By default the parameter is set to false, thus most appropriate for indexed arrays.


Internally the function uses PHP’s usort() by default, or uasort() if the $preserve_keys parameter is set to true. Numeric values will be compared by their amount while strings will be compared via strcmp(). The function should not be used to compare booleans or non-scalar values. Be aware that PHP’s sorting algorithms are not stable, so when two elements have identical values for all the properties compared, they will not necessarily remain in the same order as before.

Better Organization of List Utilities

While wp_list_sort() provides new functionality, existing list utilities have been revamped as well: WordPress 4.7 introduces a new WP_List_Util class as a central access point for handling lists. The previously mentioned functions have been adjusted to become wrappers for methods inside that new class.

Beside the structural improvements of moving such functionality into a centralized class, the class provides an optimized way to run multiple tasks on a list, e.g. filtering, sorting and plucking a property out of a list through one instance of the class.

By using the method WP_List_Util::get_output() you can access the list in its current state at any time, while WP_List_Util::get_input() can be used to access it in its original state. A good example on how to use the new class is wp_filter_object_list() which handles both filtering and plucking a property.

For the background discussion around these changes, see #37128.

#4-7, #dev-notes

WP_Taxonomy in 4.7

Similar to how WordPress 4.6 introduced WP_Post_Type, version 4.7 will introduce a new WP_Taxonomy class. This changes the global $wp_taxonomies to an array of WP_Taxonomy objects.

WP_Taxonomy provides methods to handle rewrite rules and hooks. These methods are used internally by register_taxonomy() and unregister_taxonomy(). Each taxonomy argument is now a property of WP_Taxonomy.

The following function has been changed to return a WP_Taxonomy object:

  • get_taxonomy()

The following hook parameters are now a WP_Taxonomy object:

  • The second parameter $taxonomy of xmlrpc_prepare_taxonomy.
  • The second parameter $tax of term_search_min_chars.

The following function accepts a WP_Taxonomy object now:

  • get_taxonomy_labels()

Just like with WP_Post_Type, introducing such a class makes further improvements much more feasible.

For more background on the change, see #36224.

#4-7, #dev-notes

Fine grained capabilities for taxonomy terms in 4.7

WordPress 4.7 introduces new capabilities for individual taxonomy terms, allowing developers to implement more fine grained control over management of terms (including categories, tags, and custom taxonomy terms). The new capabilities are meta capabilities that ultimately map back to existing capabilities, so there is no change in behaviour for existing functionality and users.

New Singular Capabilities

  • edit_term
  • delete_term
  • assign_term

In the same way that capabilities such as edit_post and delete_user are used to check that a user can perform the action for a specific post or user, these new capabilities can be used to check that a user can perform the action for a specific term. If you’re currently checking the edit_terms, delete_terms, and assign_terms capabilities, you can switch these over to the singular form and include the term ID as the second parameter. Example:

if ( current_user_can( 'edit_term', $term_id ) ) {
	printf( '<a href="%s">Edit This</a>', get_edit_term_link( $term_id ) );

The new capabilities are meta capabilities — which means they ultimately map back to the existing manage_categories capability by default, or whichever capability has been specified in the corresponding capability arguments when registering the taxonomy. The actual required capabilities can be filtered using the map_meta_cap filter like so:

add_filter( 'map_meta_cap', function( $required_caps, $cap, $user_id, $args ) {
	switch ( $cap ) {

		case 'delete_term':
			$term_id = $args[0];
			// Prevent a "protected" term from being deleted:
			if ( get_term_meta( $term_id, 'protected', true ) ) {
				$required_caps[] = 'do_not_allow';

		case 'edit_term':
			// Introduce some anarchy:
			if ( rand( 1, 6 ) == 3 ) {
				$required_caps[] = 'do_not_allow';


	return $required_caps;
}, 10, 4 );

Separate Capabilities for Tags and Categories

A related change is that the post tag taxonomy now uses separate capabilities from the category taxonomy by default (it previously used the same capabilities as the category taxonomy). This doesn’t alter existing behaviour (custom or otherwise), but it does mean that you can target capabilities for tags separately from categories. Example:

add_filter( 'map_meta_cap', function( $required_caps, $cap, $user_id, $args ) {
	switch ( $cap ) {

		case 'manage_post_tags':
		case 'edit_post_tags':
		case 'delete_post_tags':
		case 'assign_post_tags':
			// Allow Authors to manage tags:
			$required_caps = array(


	return $required_caps;
}, 10, 4 );

See Trac ticket #35614 for more information.

#4-7, #dev-notes