Allow plugins to include a theme.json file in the same way themes do
Aljullu opened this issue · 24 comments
Description
Currently, if a plugin wants to set default styles for its blocks, it can either write the styles in CSS or write them in the block default attributes.
None of these approaches allow themes to easily override the styles of the plugin using theme.json
.
A solution would be to allow plugins to include a theme.json
file in the same way themes are already doing it. Styles specified in the theme.json
of a plugin, would have lower priority than the styles from the theme's theme.json
and user styles.
This way, plugins can specify the default styles of their blocks in their theme.json
and themes can override the styles from their theme.json
.
Step-by-step reproduction instructions
Screenshots, screen recording, code snippet
No response
Environment info
No response
Please confirm that you have searched existing issues in the repo.
Yes
Please confirm that you have tested with all plugins deactivated except Gutenberg.
Yes
Another example of how this will be useful (for a plugin like WooCommerce) is that the plugin provided theme.json
could also provide an index of the blocks (and patterns) registered in the plugin.
I would suggest however that the hierarchy of properties be something like:
User > Block > Theme theme.json
> Plugin theme.json
Potential implementation options
It looks like we would need to add some theme.json
detection for plugins as a part of the WP_Theme_JSON_Resolver::get_merged_data
function. There's a few routes (to kick start discussion) that could be taken here in terms of how the theme.json
in a plugin would be discovered:
1. Filter for plugin to register theme.json
path
The filter would likely be within the get_merged_data
function.
2. Path defined via property in the plugin header
Similar to how other plugin details are set in the plugin header, there could be potentially a theme.json
property where the relative path could be set for the theme.json (or even a flag that just indicates the plugin has one defined?).
One advantage of having this in the plugin header is that that is already parsed by WP so it could be an aid to discoverability (only hitting the filesystem for the file if needed). There's also some potential benefits for surfacing this more readily via the WP.org plugin listing (or other automations) as well.
3. Automatic discovery via specific location for file
Having a requirement that the theme.json
for a plugin is at a specific path (eg. in the plugin root folder or theme/theme.json
). During activation/update WordPress would automatically check to see if plugins have a theme.json
and set the plugin slug to a registry for later reading/caching when WP_Theme_JSON_Resolver::get_merged_data
is invoked.
Personally, I'm leaning more towards options 2 and 3 (perhaps a combination of both) because it still provides an opt-in path for plugins yet keeps implementation details handled by WordPress.
I think the plugin should come before the theme as a fallback and be merged into it, given the theme might want to style and control the plugin's blocks specifically, it should supercede if it's targeting those blocks.
Any suggestions for how the API might work from a plugin's perspective? The merging is fairly trivial, so it's more about the ergonomics. Consider also that WordPress bundles a theme.json as well for reference. There's been some requests to unload that bundled fallback before, so that also needs to be taken into account.
I think the plugin should come before the theme as a fallback and be merged into it
Agreed, what I listed in the hierarchy flow is intended to represent priority of control from left to right. So, user overrides blocks which override theme which override plugin (I may be wrong on the blocks -> theme order, I think that might have changed recently?)
Any suggestions for how the API might work from a plugin's perspective?
I'm not sure what specifically you're asking about here? I'm guessing this isn't a question about how the plugin theme.json
would be surfaced to WordPress?
Consider also that WordPress bundles a theme.json as well for reference.
Yes, I see that, and the theme takes precedence over anything defined by core's theme.json
. I think it would make sense to have the plugin sit between core and the theme in merging order.
There's been some requests to unload that bundled fallback before, so that also needs to be taken into account.
Unload as in remove it completely, or to have an interface for external control over unloading it?
Got it, was reading the sequence wrong. Unload as being able to remove it if you want to supply your own without any dependencies or fallbacks.
I'm asking how should a plugin register or make its json file available, assuming it'd exist as a separate file.
I'm asking how should a plugin register or make its json file available, assuming it'd exist as a separate file.
That's what I was addressing in my original comment. Ideally I think the plugin shouldn't need to explicitly register it other than maybe an option in the plugin header and then having the theme.json
in a indicated path. So for instance maybe something like this for WooCommerce:
/**
* Plugin Name: WooCommerce
* Plugin URI: https://woocommerce.com/
* Description: An eCommerce toolkit that helps you sell anything. Beautifully.
* Version: 6.5.1
* Author: Automattic
* Author URI: https://woocommerce.com
* Text Domain: woocommerce
* Domain Path: /i18n/languages/
* theme.json Path: /theme
* Requires at least: 5.7
* Requires PHP: 7.2
*
* @package WooCommerce
*/
The above would signal to WordPress core when parsing the plugin header on plugin activation/update that:
- The plugin has a theme.json file.
- It's located in the
theme
folder (relative to the plugin's root folder).
WordPress core could just register the paths for all plugins indicating they have a theme.json
and then use that to load and merge when the initial resolution of all theme.json
files happens on WP_Theme_JSON_Resolver::get_merged_data
. Of course, we'd also have to account for when the resolution cache is cleared.
I love the idea.
Two thoughts:
-
I'd import it automatically if it exists. I don't see much value in making it opt-in when it's a new file that didn't exist previously.
-
If we have
block.json
andtheme.json
, I'd call this oneplugin.json
. That way, it could have its own version and schema. And there may be settings fromtheme.json
that won't make sense in a plugin and vice versa.
This is a fascinating idea. One additional consideration would be merging settings from multiple plugins.
Styles specified in the theme.json of a plugin, would have lower priority than the styles from the theme's theme.json and user styles.
This way, plugins can specify the default styles of their blocks in their theme.json and themes can override the styles from their theme.json.
With that, we would also need to figure out how to ensure that settings from multiple plugins are processed and merged in a predictable way.
If we have
block.json
andtheme.json
, I'd call this oneplugin.json
. That way, it could have its version and schema. And there may be settings from theme.json that won't make sense in a plugin and vice versa.
I don't have a formed opinion about the name of the file. I only wanted to surface potential challenges if we were to use translatable fields. We would have to create the list of translatable fields and implement the extraction of translations from plugin.json
in WP CLI. I'm not sure how quickly we would run into that issue, but it's also worth emphasizing that we don't know how much the theme.json
for plugins would overlap with theme.json
from themes and how complex the merging strategy might become.
The latest comments have raised additional questions. I'm still thinking through this but wanted to at least document them here in case others have answers/thoughts. I definitely think we should keep the initial scope fairly limited, but some of the questions raised could influence the initial structure of this, hence I'm asking.
- Should the plugin-provided properties only allow enhancing only a subset of potential
theme.json
properties? If so which? Keep in mind,theme.json
would always override what is provided by the plugin. - Should this be named differently and allow for potential separate versioning and other potential future uses?
- If multiple plugins provide the same properties, how do we handle merging them? Which plugin would "win" in any possible merge conflicts?
- Do we implement the ability to completely unload the default merging strategy (and potentially replace it) as a part of work on this issue?
I'll leave a comment in this issue at some point once I've had a chance to think through the implications for various answers to the above questions.
It's not that a plugin's theme.json
like the one described here would only serve to configure the default styles of its blocks, but as that was the use case Albert was referring to when he opened this issue, I think it'd be interesting if you could take a look at #41873. It's also trying to solve that problem, but by letting plugins define those default styles in their block.json
file instead.
I'd love to hear what you think and how it overlaps with your needs here.
It's not that a plugin's
theme.json
like the one described here would only serve to configure the default styles of its blocks, but as that was the use case Albert was referring to when he opened this issue, I think it'd be interesting if you could take a look at #41873. It's also trying to solve that problem, but by letting plugins define those styles in theirblock.json
file instead.I'd love to hear what you think and how it overlaps with your needs here.
Thanks for sharing the link @luisherranz! I think the approach in #41873 is definitely interesting and especially that it keeps the default styles at the block level for blocks. If I'm understanding things correctly, that means if themes don't define any styles for specific blocks, then the registered default styles from the blocks are used. It also seems like this could potentially be useful for defining style variations at the block level here. This would also resolve the merging issue of multiple plugin-provided theme.json
files that provide styles for the same blocks.
What is still unclear at this point is how that might solve for other potential use-cases around a plugin provided theme.json
(that I outlined in the PR):
- expose custom properties that can be overridden by themes.
- provide and expose a set of custom templates for post types.
- indicate what patterns to register from the Patterns directory, or potentially, automatically register patterns included by the plugin.
Granted, the above use cases aren't supported in the initial iteration of the PR and it's not clear if they would definitely be needed by plugins or if a plugin provided theme.json
is the best mechanism for solving. The primary impetus behind these use-cases is that it would potentially allow a plugin a single mechanism for enhancing its support of all block themes without block themes having to explicitly opt-in to supporting multiple plugins. Rather than having to register that support via multiple different interfaces, theme.json
would provide that single interface. Custom blocks and block.json
is only one aspect of that type of support.
👋 I've take a look at this and see different goals:
- To allow themes to easily override the styles of the plugin using theme.json
For this, I see #34180 as a more promising path. It gives a predictable hierarchy:
core (theme.json) > block (block.json) > theme (theme.json) > user (styles coming from the global styles sidebar)
Note that, unlike for plugins, all of the existing sources are unique (there's only a single WordPress install, a single block type, a single active theme, a single user preferences). Personally, for settings & styles, I see the existing path as more promising to investigate. Though we haven't done much work since that PR was merged. Perhaps an tracking issue listing and clarifying next steps for that API (trying new blocks, new styles, etc) would help people to be informed about the progress? cc @scruffian @adamziel @MaggieCabrera
- Expose custom properties that can be overriden by themes.
We've talked about this a number of times and the conclusion was that if we allow exposing custom things, we won't be furthering the original goal of theme.json
: provide consistency across themes & blocks to the user by leveraging a common set of styles & settings.
- I'm not super familiar with the other goals, but perhaps we can draw inspiration from things that are working for themes already. For example, can we automatically register patterns stored under
/patterns
in the plugin's directory? etc.
On the topic of using theme.json
for providing template metadata (titles, etc) I just remembered that there was some conversations about this and how counter-intuitive that felt. There was an exploration to place this metadata in the file itself at #38984
I've take a look at this and see different goals:
The primary problem we're trying to solve for here is for plugins (such as WooCommerce) to have the ability to define defaults for any blocks and/or templates it provides so that block themes don't have to explicitly support those plugins (but can still override). With #41873 now closed, what alternatives are being explored, besides what's proposed here?
I guess what you're proposing is that focus is redirected to something similar to what was in #41873, but that aligns more with what was started in #34180?
#41873 proposed to move the styles from one key to another and made that key stable. My concerns were that the initial work at #34180 had been partially reverted and no other blocks or styles were using the API yet. If the core blocks don't use the API how do we know it provides value to 3rd-party blocks?
I still think we should validate the approach started at #34180 by expanding its use and see how it performs.
As an analogy, after the initial explorations with block supports we laid out some next steps and found some limitations that we tracked at #28913 This helped anyone interested to learn about the API, understand its boundaries and help to move it forward. Perhaps if we do something similar that initial work will get more attention and momentum?
I spent some time investigating what the plugin author can expect when shipping custom blocks. This is what I tested using customised emptytheme
from Gutenberg and a block scaffolded with npx @wordpress/create-block -t @wordpress/create-block-tutorial-template
:
block/block.json
{
"name": "woo/gutenpride",
"supports": {
"html": false,
"__experimentalBorder": {
"color": true,
"radius": true,
"style": true,
"width": true
},
"typography": {
"lineHeight": true
}
}
}
The default setting coming from WordPress Core in theme.json
is set to:
https://github.com/WordPress/wordpress-develop/blob/1d456706549ccf4f0cc867b8a17d1dceaabf8a77/src/wp-includes/theme.json#L4-L10
https://github.com/WordPress/wordpress-develop/blob/1d456706549ccf4f0cc867b8a17d1dceaabf8a77/src/wp-includes/theme.json#L222
WP/wp-includes/theme.json
{
"settings": {
"appearanceTools": false,
"border": {
"color": false,
"radius": false,
"style": false,
"width": false
},
"typography": {
"lineHeight": false,
}
}
}
That means if the theme doesn't opt in to enable those settings for all blocks, then the plugin doesn't have a simple way to enable those border and typography controls in the UI.
In the WordPress Core there is an exception to some blocks to handle border differently:
WP/wp-includes/theme.json
{
"settings": {
"blocks": {
"core/button": {
"border": {
"radius": true
}
},
"core/pullquote": {
"border": {
"color": true,
"radius": true,
"style": true,
"width": true
}
}
}
}
}
However, the challenge is that to update settings for this blocks, the theme needs to explicitly override the same setting using the same syntax with settings.blocks
. It is also very unlikely that the theme would take into account custom blocks when defining the settings. In effect, even when the theme would like to disable all border controls with:
theme/theme.json
{
"settings": {
"border": {
"color": false,
"radius": false,
"style": false,
"width": false
}
}
}
It won't take effect for the core/button
and core/pullquote
. It's something that might be problematic when introducing theme.json
for plugins because themes with the current resolution algorithm weren't able to override the defaults provided by plugins through blocks
section.
The styles are determined using the following hierarchy:
- Core theme.json
- Current theme's theme.json
- User styles
One idea would be to add a few extension points to make it:
- Core theme.json
- Plugin-registered theme.json
- Current theme's theme.json
- Plugin-registered theme.json
- User styles
- Plugin-registered theme.json
I spent some time investigating what the plugin author can expect when shipping custom blocks. This is what I tested using customised
emptytheme
from Gutenberg and a block scaffolded withnpx @wordpress/create-block -t @wordpress/create-block-tutorial-template
:
My experiment continued. I removed some styles from the custom block so they don't override settings in JSON files. I added the following changes to the block.json
file to check how (default?) styles get applied:
block/block.json
:
{
"name": "woo/gutenpride",
"supports": {
"html": false,
"__experimentalBorder": {
"color": true,
"radius": true,
"style": true,
"width": true
},
"typography": {
"lineHeight": true
},
"__experimentalStyle": {
"typography": {
"fontSize": "4em",
"lineHeight": "4em"
}
}
}
}
I also added similar styles in other places to find out how that hierarchy works.
theme/theme.json
{
"styles": {
"typography": {
"fontSize": "3em",
"lineHeight": "3em"
},
"blocks": {
"woo/gutenpride": {
"typography": {
"fontSize": "3.5em",
"lineHeight": "3.5em"
}
}
}
}
}
I also tweaked the theme.json
in WordPress Core:
WP/theme.json
{
"styles": {
"typography": {
"fontSize": "2em",
"lineHeight": "2em"
},
"blocks": {
"woo/gutenpride": {
"typography": {
"fontSize": "2.5em",
"lineHeight": "2.5em"
}
}
}
}
}
Those styles get applied in the editor as follows:
Block-specific entry in the theme wins.
The same styles get applied on the front end a bit different:
Theme's general entry wins...
It looks like the order of importance in the editor resolves as follows:
theme theme.json:styles:blocks
> block block.json:supports:__experimentalStyle
> WordPress theme.json:styles:blocks
> theme theme.json:styles
> WordPress theme.json:styles
The styles are determined using the following hierarchy:
- Core theme.json
- Current theme's theme.json
- User styles
@adamziel, it's only partially true. It looks like its more nuanced when you define styles targeting individual blocks. They always take precedence over default styles defined in any theme.json
. Well, at least it's true in the editor. It seems to work differently for custom static block types that implement save
as follows:
export default function save( { attributes } ) {
const blockProps = useBlockProps.save();
return <div { ...blockProps }>{ attributes.message }</div>;
}
In effect, even when the theme would like to disable all border controls with:
[...]
It won't take effect for the core/button and core/pullquote.
I think there's a difference between:
- I want to change the configuration for any block that hasn't been explicitly configured.
- I want to override the configuration of all the blocks, including those that have been explicitly configured.
I shared an idea about how that could be expressed here, but there could be other ways to do it:
I used the following resources when learning about theme.json
:
I wanted to share a couple of thoughts based on my recent experiments. The __experimentalStyle
thing might work for certain cases that @oandregal and @nerrad discussed, but I had difficulties to configure it correctly in my testing. This might be issue on my side or the API is still limited and requires further work. It definitely needs to be better documented so it's clear how to make the most of it.
There are other use cases discussed that can't be covered with __experimentalStyle
defined for individual blocks in the block.json
file. Let's take for example CSS custom properties:
{
"version": 2,
"settings": {
"custom": {
"line-height": {
"body": 1.7,
"heading": 1.3
}
}
}
}
It’s something you don’t want to add to every block but add instead once for the custom functionality your plugin built to use it in the CSS provided. You could always define them inside the CSS manually, but it would be nice to let the Global Styles to handle it based on its resolution system.
Another example would Block styles defined once for multiple blocks:
"styles": {
"typography": {
"fontSize": "var(--wp--preset--font-size--normal)"
}
}
Here, the idea from @luisherranz would fit perfectly:
"styles": {
"blocks": {
"woo/*": {
"typography": {
"fontSize": "var(--wp--preset--font-size--normal)"
}
}
}
}
One final thought. I tend to agree with @adamziel that pointed out to me when we were discussing extending block.json
with theme-related capability might be difficult to process. We have this mental model when thinking about styles:
- theme has opinions about blocks, and can override it on the block level
- now blocks would have opinions about the same things that theme owned before, so we need to learn what has the final saying and in which configuration
I think that I have a clear picture now of how the proposal explained by @Aljullu and @nerrad would benefit plugins like Woo that add the whole set of functionalities to the WordPress site. This way you can have default CSS variables, CSS styles, that you can use with custom blocks and templates in your plugin and they are subject to all rules that Global Styles introduce. In case of Woo, the additional benefit is that the plugin can offer default state for any block theme without block themes having to explicitly support Woo. However, they can opt in and reuse some CSS custom properties or presets whenever they want to. Themes can still optionally override that default state if they want.
#44015 is very relevant as it might allow plugins to effectively define its own theme.json by hooking and modifying. This is a lower level API, but it might allow a pathway to then formalizing registration of theme.json extensions.
#44015 is very relevant as it might allow plugins to effectively define its own theme.json by hooking and modifying. This is a lower level API, but it might allow a pathway to then formalizing registration of theme.json extensions.
Now, we landed filters for theme.json
at different processing stages with #44015. Can we consider this issue resolved or do you think we should continue the process of improving the development experience with the initial idea to include theme.json
file in the plugin?
I think we can consider this closed and ask plugins attempting to extend to provide feedback on what is working, what isn't, and what possible registration hooks might make sense to reduce boilerplate.
Personally, for settings & styles, I see the existing path as more promising to investigate. Though we haven't done much work since that PR was merged. Perhaps an tracking issue listing and clarifying next steps for that API (trying new blocks, new styles, etc) would help people to be informed about the progress?
I have gone ahead and created #45198 to track the next steps for the __experimentalStyle
API.