Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c1c5111
Templates: allow templates to be activated and deactivated
ellatrix Dec 30, 2024
1c55ea9
Migrate to active templates
ellatrix Dec 30, 2024
07fcf91
Use Badge component, add rename
ellatrix Dec 30, 2024
75f8056
Fix click handler
ellatrix Dec 30, 2024
e35f93c
Fix some e2e tests
ellatrix Dec 30, 2024
af44163
Fix preload check
ellatrix Dec 30, 2024
64873fa
All theme templates are active by default
ellatrix Dec 30, 2024
adc86c7
Add backport log entry
ellatrix Dec 30, 2024
0d3d192
Fix perf test
ellatrix Dec 31, 2024
bce1d83
Prevent active_templates from being empty
ellatrix Jul 29, 2025
a3e799b
Fix setting type
ellatrix Sep 4, 2025
4a569e4
Add templates php filter so Woo templates show up
ellatrix Sep 5, 2025
b849489
Make sure static template filtering in dataview is active
ellatrix Sep 5, 2025
cf33029
Include plugin templates in active tab
ellatrix Sep 14, 2025
d9d4c16
Move to 6.9 folder
ellatrix Sep 14, 2025
f1941b3
Make sure hook adding terms runs before default hooks
ellatrix Sep 14, 2025
e8beeea
Prevent get_block_templates from returning user templates (needed to …
ellatrix Sep 15, 2025
daa1412
Revert "Prevent get_block_templates from returning user templates (ne…
ellatrix Sep 15, 2025
fa55d1e
Fix preload
ellatrix Sep 15, 2025
fa8761d
php lint
ellatrix Sep 15, 2025
435b22c
Remove __returnsBadge
ellatrix Sep 15, 2025
5d7e2a9
Remove dataviews changes
ellatrix Sep 15, 2025
31041bb
Adjust test selector
ellatrix Sep 16, 2025
c4e30f4
Fix preload
ellatrix Sep 16, 2025
487c124
Ensure the edited post id is a string
ellatrix Sep 16, 2025
efa4291
Fix preload e2e test
ellatrix Sep 16, 2025
f03a90d
_wp_static_template => wp_registered_template
ellatrix Sep 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backport-changelog/6.8/8063.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://github.com/WordPress/wordpress-develop/pull/8063

* https://github.com/WordPress/gutenberg/pull/67125
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php

class Gutenberg_REST_Static_Templates_Controller extends WP_REST_Templates_Controller {
public function register_routes() {
// Lists all templates.
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);

// Lists/updates a single template based on the given id.
register_rest_route(
$this->namespace,
// The route.
sprintf(
'/%s/(?P<id>%s%s)',
$this->rest_base,
/*
* Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
* Excludes invalid directory name characters: `/:<>*?"|`.
*/
'([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
// Matches the template name.
'[\/\w%-]+'
),
array(
'args' => array(
'id' => array(
'description' => __( 'The id of a template' ),
'type' => 'string',
'sanitize_callback' => array( $this, '_sanitize_template_id' ),
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}

public function get_item_schema() {
$schema = parent::get_item_schema();
$schema['properties']['is_custom'] = array(
'description' => __( 'Whether a template is a custom template.' ),
'type' => 'bool',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
);
$schema['properties']['plugin'] = array(
'type' => 'string',
'description' => __( 'Plugin that registered the template.' ),
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
);
return $schema;
}

public function get_items( $request ) {
$query = array();
if ( isset( $request['area'] ) ) {
$query['area'] = $request['area'];
}
if ( isset( $request['post_type'] ) ) {
$query['post_type'] = $request['post_type'];
}
$template_files = _get_block_templates_files( 'wp_template', $query );
$query_result = array();
foreach ( $template_files as $template_file ) {
$query_result[] = _build_block_template_result_from_file( $template_file, 'wp_template' );
}

// Add templates registered in the template registry. Filtering out the ones which have a theme file.
$registered_templates = WP_Block_Templates_Registry::get_instance()->get_by_query( $query );
$matching_registered_templates = array_filter(
$registered_templates,
function ( $registered_template ) use ( $template_files ) {
foreach ( $template_files as $template_file ) {
if ( $template_file['slug'] === $registered_template->slug ) {
return false;
}
}
return true;
}
);

$query_result = array_merge( $query_result, $matching_registered_templates );

/**
* Filters the array of queried block templates array after they've been fetched.
*
* @since 5.9.0
*
* @param WP_Block_Template[] $query_result Array of found block templates.
* @param array $query {
* Arguments to retrieve templates. All arguments are optional.
*
* @type string[] $slug__in List of slugs to include.
* @type int $wp_id Post ID of customized template.
* @type string $area A 'wp_template_part_area' taxonomy value to filter by (for 'wp_template_part' template type only).
* @type string $post_type Post type to get the templates for.
* }
* @param string $template_type wp_template or wp_template_part.
*/
$query_result = apply_filters( 'get_block_templates', $query_result, $query, 'wp_template' );

$templates = array();
foreach ( $query_result as $template ) {
$item = $this->prepare_item_for_response( $template, $request );
$item->data['type'] = 'wp_registered_template';
$templates[] = $this->prepare_response_for_collection( $item );
}

return rest_ensure_response( $templates );
}

public function get_item( $request ) {
$template = get_block_file_template( $request['id'], 'wp_template' );

if ( ! $template ) {
return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
}

$item = $this->prepare_item_for_response( $template, $request );
// adjust the template type here instead
$item->data['type'] = 'wp_registered_template';
return rest_ensure_response( $item );
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the changes here compared to the regular template endpoint that we have before. This is only for "theme" and "plugin" templates right, aka "registered templates"?

Copy link
Member Author

@ellatrix ellatrix Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's correct. We shouldn't include the edits/user templates that the current one has

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will all have to be reconciled in the core back port PR

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

class Gutenberg_REST_Templates_Controller extends WP_REST_Posts_Controller {
protected function handle_status_param( $status, $request ) {
if ( 'auto-draft' === $status ) {
return $status;
}
return parent::handle_status_param( $status, $request );
}
protected function add_additional_fields_schema( $schema ) {
$schema = parent::add_additional_fields_schema( $schema );

$schema['properties']['status']['enum'][] = 'auto-draft';
return $schema;
}
}
30 changes: 30 additions & 0 deletions lib/compat/wordpress-6.9/preload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/**
* Preload necessary resources for the editors.
*
* @param array $paths REST API paths to preload.
* @param WP_Block_Editor_Context $context Current block editor context
*
* @return array Filtered preload paths.
*/
function gutenberg_block_editor_preload_paths_6_9( $paths, $context ) {
if ( 'core/edit-site' === $context->name ) {
// Only prefetch for the root. If we preload it for all pages and it's not used
// it won't be possible to invalidate.
// To do: perhaps purge all preloaded paths when client side navigating.
if ( '/' !== $_GET['p'] ) {
$paths = array_filter(
$paths,
function ( $path ) {
return '/wp/v2/templates/lookup?slug=front-page' !== $path && '/wp/v2/templates/lookup?slug=home' !== $path;
}
);
}

$paths[] = '/wp/v2/wp_registered_template?context=edit';
}

return $paths;
}
add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_block_editor_preload_paths_6_9', 10, 2 );
169 changes: 169 additions & 0 deletions lib/compat/wordpress-6.9/template-activate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php

// How does this work?
// 1. For wp_template, we remove the custom templates controller, so it becomes
// a normal posts endpoint, modified slightly to allow auto-drafts.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the right thing to do but I'm a bit concerned about potential breaking changes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The part about changing it to a normal posts endpoint? That's valid. The alternative would be to create a different CPT or endpoint 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just wondering if the endpoint is going to continue working the exact same way or not? Do we have some fallbacks in place when previous "slug ids" are used?. I think a good summary of the endpoints changes would be good.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a good summary of the endpoints changes would be good.

Came here to ask the same 🙇

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a fallback for a slug id in a follow-up as discussed. Right there should be a summary. The main change is that whatever imitation of the post type endpoint there was for wp_template is now replaced with a real post type endpoint. And another one is added to handle theme/plugin templates separately.

add_filter( 'register_post_type_args', 'gutenberg_modify_wp_template_post_type_args', 10, 2 );
function gutenberg_modify_wp_template_post_type_args( $args, $post_type ) {
if ( 'wp_template' === $post_type ) {
$args['rest_base'] = 'wp_template';
$args['rest_controller_class'] = 'Gutenberg_REST_Templates_Controller';
$args['autosave_rest_controller_class'] = null;
$args['revisions_rest_controller_class'] = null;
}
return $args;
}

// 2. We maintain the routes for /templates and /templates/lookup. I think we'll
// need to deprecate /templates eventually, but we'll still want to be able
// to lookup the active template for a specific slug, and probably get a list
// of all _active_ templates. For that we can keep /lookup.
add_action( 'rest_api_init', 'gutenberg_maintain_templates_routes' );
function gutenberg_maintain_templates_routes() {
// This should later be changed in core so we don't need initialise
// WP_REST_Templates_Controller with a post type.
global $wp_post_types;
$wp_post_types['wp_template']->rest_base = 'templates';
$controller = new WP_REST_Templates_Controller( 'wp_template' );
$wp_post_types['wp_template']->rest_base = 'wp_template';
$controller->register_routes();
}

// 3. We need a route to get that raw static templates from themes and plugins.
// I registered this as a post type route because right now the
// EditorProvider assumes templates are posts.
add_action( 'init', 'gutenberg_setup_static_template' );
function gutenberg_setup_static_template() {
global $wp_post_types;
$wp_post_types['wp_registered_template'] = clone $wp_post_types['wp_template'];
$wp_post_types['wp_registered_template']->name = 'wp_registered_template';
$wp_post_types['wp_registered_template']->rest_base = 'wp_registered_template';
$wp_post_types['wp_registered_template']->rest_controller_class = 'Gutenberg_REST_Static_Templates_Controller';

register_setting(
'reading',
'active_templates',
array(
'type' => 'object',
'show_in_rest' => array(
'schema' => array(
'type' => 'object',
// properties can be integers or false (deactivated).
'additionalProperties' => true,
),
),
'default' => array(),
'label' => 'Active Templates',
)
);
}

add_filter( 'pre_wp_unique_post_slug', 'gutenberg_allow_template_slugs_to_be_duplicated', 10, 5 );
function gutenberg_allow_template_slugs_to_be_duplicated( $override, $slug, $post_id, $post_status, $post_type ) {
return 'wp_template' === $post_type ? $slug : $override;
}

add_filter( 'pre_get_block_templates', 'gutenberg_pre_get_block_templates', 10, 3 );
function gutenberg_pre_get_block_templates( $output, $query, $template_type ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have some comments just to clarify what these overrides do.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was removed btw, the filtering is way less hacky

if ( 'wp_template' === $template_type && ! empty( $query['slug__in'] ) ) {
$active_templates = (array) get_option( 'active_templates', array() );
$slugs = $query['slug__in'];
$output = array();
foreach ( $slugs as $slug ) {
if ( isset( $active_templates[ $slug ] ) ) {
if ( false !== $active_templates[ $slug ] ) {
$post = get_post( $active_templates[ $slug ] );
if ( $post && 'publish' === $post->post_status ) {
$output[] = _build_block_template_result_from_post( $post );
}
} else {
// Deactivated template, fall back to next slug.
$output[] = array();
}
}
}
if ( empty( $output ) ) {
$output = null;
}
}
return $output;
}

// Whenever templates are queried by slug, never return any user templates.
// We are handling that in gutenberg_pre_get_block_templates.
function gutenberg_remove_tax_query_for_templates( $query ) {
if ( isset( $query->query['post_type'] ) && 'wp_template' === $query->query['post_type'] ) {
// We don't have templates with this status, that's the point. We want
// this query to not return any user templates.
$query->set( 'post_status', array( 'pending' ) );
}
}

add_filter( 'pre_get_block_templates', 'gutenberg_tax_pre_get_block_templates', 10, 3 );
function gutenberg_tax_pre_get_block_templates( $output, $query, $template_type ) {
// Do not remove the tax query when querying for a specific slug.
if ( 'wp_template' === $template_type && ! empty( $query['slug__in'] ) ) {
add_action( 'pre_get_posts', 'gutenberg_remove_tax_query_for_templates' );
}
return $output;
}

add_filter( 'get_block_templates', 'gutenberg_tax_get_block_templates', 10, 3 );
function gutenberg_tax_get_block_templates( $output, $query, $template_type ) {
if ( 'wp_template' === $template_type && ! empty( $query['slug__in'] ) ) {
remove_action( 'pre_get_posts', 'gutenberg_remove_tax_query_for_templates' );
}
return $output;
}

// We need to set the theme for the template when it's created. See:
// https://github.com/WordPress/wordpress-develop/blob/b2c8d8d2c8754cab5286b06efb4c11e2b6aa92d5/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php#L571-L578
// Priority 9 so it runs before default hooks like
// `inject_ignored_hooked_blocks_metadata_attributes`.
add_action( 'rest_pre_insert_wp_template', 'gutenberg_set_active_template_theme', 9, 2 );
function gutenberg_set_active_template_theme( $changes, $request ) {
$template = $request['id'] ? get_block_template( $request['id'], 'wp_template' ) : null;
if ( $template ) {
return $changes;
}
$changes->tax_input = array(
'wp_theme' => isset( $request['theme'] ) ? $request['theme'] : get_stylesheet(),
);
return $changes;
}

// Migrate existing "edited" templates. By existing, it means that the template
// is active.
add_action( 'init', 'gutenberg_migrate_existing_templates' );
function gutenberg_migrate_existing_templates() {
$active_templates = get_option( 'active_templates' );

if ( $active_templates ) {
return;
}

// Query all templates in the database. See `get_block_templates`.
$wp_query_args = array(
'post_status' => 'publish',
'post_type' => 'wp_template',
'posts_per_page' => -1,
'no_found_rows' => true,
'lazy_load_term_meta' => false,
'tax_query' => array(
array(
'taxonomy' => 'wp_theme',
'field' => 'name',
'terms' => get_stylesheet(),
),
),
);

$template_query = new WP_Query( $wp_query_args );
$active_templates = array();

foreach ( $template_query->posts as $post ) {
$active_templates[ $post->post_name ] = $post->ID;
}

update_option( 'active_templates', $active_templates );
}
4 changes: 4 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/compat/wordpress-6.8/rest-api.php';

// WordPress 6.9 compat.
require __DIR__ . '/compat/wordpress-6.9/class-gutenberg-rest-static-templates-controller.php';
require __DIR__ . '/compat/wordpress-6.9/class-gutenberg-rest-templates-controller.php';
require __DIR__ . '/compat/wordpress-6.9/template-activate.php';
require __DIR__ . '/compat/wordpress-6.9/block-bindings.php';
require __DIR__ . '/compat/wordpress-6.9/post-data-block-bindings.php';
require __DIR__ . '/compat/wordpress-6.9/term-data-block-bindings.php';
Expand Down Expand Up @@ -84,6 +87,7 @@ function gutenberg_is_experiment_enabled( $name ) {
// WordPress 6.9 compat.
require __DIR__ . '/compat/wordpress-6.9/customizer-preview-custom-css.php';
require __DIR__ . '/compat/wordpress-6.9/command-palette.php';
require __DIR__ . '/compat/wordpress-6.9/preload.php';

// Experimental features.
require __DIR__ . '/experimental/block-editor-settings-mobile.php';
Expand Down
Loading
Loading