Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add/webfonts loading #1573

Closed
wants to merge 66 commits into from
Closed

Conversation

aristath
Copy link
Member

@aristath aristath commented Aug 11, 2021

This PR adds functions for a webfonts API and is a first iteration covering the basics, something we can build upon in future patches to add more features and expand on what we do here.

  • wp_register_webfont
  • wp_deregister_webfont
  • wp_enqueue_webfont
  • wp_dequeue_webfont
  • wp_webfont_is
  • wp_webfont_add_data

The syntax of all these functions is identical to their style counterparts, so wp_register_webfont is the same as wp_register_style and so on. The only difference is the use of $params in lieu of $deps for practical reasons (see below)

Notes:

  • The styles registered for webfonts automatically get a webfont- prefix
  • Since webfonts don't have dependencies, the $deps argument was replaced wirth $params. These params can be used to register a webfont from local files, and auto-generates the CSS for @font-face.

In addition to the above functions, this PR also adds a wp_webfont_generate_styles function. This accepts an array of params, and returns the generated CSS for the webfoot.


Example 1: enqueuing a webfont from google-fonts

add_action( 'wp_enqueue_scripts', function() {
	wp_enqueue_webfont( 'roboto-400', '', array(
		'provider'     => new WP_Fonts_Provider_Google(),
		'font-family'  => 'Roboto',
		'font-style'   => 'normal',
		'font-weight'  => '400',
	) );
} );

Example 2: Getting the styles for a google-font and attaching them to an existing stylesheet:

add_action( 'wp_enqueue_scripts', function() {
	wp_enqueue_style( 'my-theme-styles', get_theme_file_uri( 'style.css' ) );
	$roboto_styles = wp_webfont_generate_styles( array(
		'provider'    => new WP_Fonts_Provider_Google(),
		'font-family' => 'Roboto',
		'font-weight' => '400',
	) );
	wp_add_inline_style( 'my-theme-styles', $roboto_styles );
} );

Example 3: enqueueing a webfont from local files

add_action( 'wp_enqueue_scripts', function() {
	wp_enqueue_webfont( 'my-font', '', array(
		'provider'     => new WP_Fonts_Provider_Local(),
		'font-family'  => 'My Font',
		'font-display' => 'swap',
		'font-style'   => 'normal',
		'font-weight'  => '400',
		'src'          => array(
			get_template_directory_uri() . '/fonts/font.woff2',
			get_template_directory_uri() . '/fonts/font.woff',
		),
	) );
} );

Registering a webfont from an API with CSS files

Most APIs provide CSS for their webfonts. Registering a webfont in this manner is easy as we can simply call wp_enqueue_webfont( $handle, $url );. No extra args are required, this is the simplest scenario.

Registering a webfont from local files

To register a webfont from local files, we can use the $params arg. This is formatted as an array and accepts all valid CSS props of @font-face as its array keys. Any extra args are ignored. The list of valid descriptors was taken from MDN.
Using a font-family is mandatory, and skipping that results in no CSS getting generated.

The src

If we only want to define a single file for the webfont, then we can add it as a string ('src' => $url).
If we have multiple files for the webfont (different formats to support older browsers), then we can use an array ('src' => [ $url1, $url2 ]). In this case, the URLs get internally reordered for browser support (woff2, woff, ttf, eot, otf). SVG for webfonts is not supported because they have been deprecated (see caniuse.com/svg-fonts), so if provided it gets removed (like any other invalid type).

Note: The src can also accept data-urls.

Variable fonts

The font-variation-settings property accepts either a string (normal), or, an array of key/value pairs (e.g., ["wght" => 637, "wdth" => 100]), and returns a string of these values (e.g., wght 637, wdth 100).


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

@aristath aristath force-pushed the add/webfonts-loading branch 2 times, most recently from 5853496 to 1f6f2f6 Compare September 8, 2021 07:13
@aristath aristath marked this pull request as ready for review September 10, 2021 10:18
@aristath
Copy link
Member Author

Note: As discussed on https://wordpress.slack.com/archives/C02QB2JS7/p1631135661385600 we should be able to register a local webfont from a theme.json file. When this patch gets merged we'll need to submit a PR in Gutenberg to allow registering a webfont when parsing the theme.json file.

Copy link
Member

@felixarntz felixarntz left a comment

Choose a reason for hiding this comment

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

I left some additional feedback on the fonts provider implementation. I'm still leaning towards it making more sense to register a fonts provider outside of the wp_register_webfont / wp_enqueue_webfont functions and then in those calls only referencing a registered provider. Breaking out the registration may especially make sense given my other point below, that provider classes should also be able to "orchestrate" multiple fonts together.

Comment on lines 106 to 119
$remote_url = $this->build_api_url();
$transient_name = 'google_fonts_' . md5( $remote_url );
$css = get_site_transient( $transient_name );

// Get remote response and cache the CSS if it hasn't been cached already.
if ( ! $css ) {
// Get the remote URL contents.
$response = wp_remote_get(
$remote_url,
array(
// Use a modern user-agent, to get woff2 files.
'user-agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:73.0) Gecko/20100101 Firefox/73.0',
)
);
Copy link
Member

Choose a reason for hiding this comment

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

Do we really want to fetch the CSS and include it inline for API-based web fonts? Would that be a notable performance benefit over using the recommended link tag, given the extra code and maintenance cost?

I'm particularly wary about expecting and modifying certain code to be in the CSS returned from the URL, which is happening below. We don't control that CSS, so I think it'd be safer to only load the URL somehow and not do any tweaks on it.

* @param array $params The webfont's parameters.
* @return void
*/
public function set_params( $params ) {
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 the WP_Fonts_Provider should tie in at a higher level than per individual font. Right now, it is limited to returning CSS per font, but the reality for some font providers is that they can cover multiple fonts in one, which e.g. would result in fewer requests needed if let's say a site enqueues 5 different Google fonts. Currently this results in 5 requests, but it could be combined into 1.

I think it would be more appropriate for a fonts provider class to be able to process multiple font declarations in combination if the respective API supports it (every fonts provider could have a flag like supports_multiple or something). For example, instead of get_css() returning data based on the $params for a single font, it should be able to return data based on the $params for multiple fonts.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree that there should be a way to collect like fonts and then make one external request. This is something @aristath and I have been talking about here. And it can be included in Phase 1 of this API.

return;
}

return wp_enqueue_style( "webfont-$handle" );
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 using WP_Styles here under the hood is an elegant solution, but at the same time it may not be as versatile as we need, particularly related to my other point where it would be good to be able to combine the requests to font APIs for multiple fonts into one.

So maybe we need a WP_Webfonts class (that could probably extend WP_Styles) after all 🤔

* @access public
* @since 5.9.0
* @param array $params The webfont's parameters.
* @return void
Copy link
Member

Choose a reason for hiding this comment

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

Nit-pick, but WP core standards don't expect a @return void, in that case the tag should be omitted.

@hellofromtonya
Copy link
Contributor

hellofromtonya commented Oct 6, 2021

I left some additional feedback on the fonts provider implementation. I'm still leaning towards it making more sense to register a fonts provider outside of the wp_register_webfont / wp_enqueue_webfont functions and then in those calls only referencing a registered provider. Breaking out the registration may especially make sense given my other point below, that provider classes should also be able to "orchestrate" multiple fonts together.

My first impressions are:

  • This is too complex for theme authors
  • Bridging theme.json into this API current implementation could be challenging and would force undesired complexity to wrangle the flavor of schema into this registration and enqueuing process

I propose we start with the end in mind (i.e. theme.json webfonts collection schema) and then build the baseline (Phase 1) of this API as a stepping stone towards that future. In doing so, the concept of declaring the webfonts a theme is using becomes more straightforward with less code and steps for theme authors. It also can include registering custom providers and combining of like fonts for optimizing fetching and processing.

The discussion is captured here in Ari's fork. Feedback is welcomed and encouraged. If consensus, then the next version of this API (for Phase 1) can be built rather quickly for review.

@felixarntz
Copy link
Member

@hellofromtonya

This is too complex for theme authors

Which part you mean is too complex? I'm envisioning the API to be the same as what's proposed in #4. Registering a webfont provider isn't something that a theme author would typically be expected to do. I think core should provide the common webfont providers (e.g. local, Google, Adobe) out of the box, so registration would only be necessary when using a less popular or entirely custom provider (let's say if a company had their own fonts CDN). Maybe that has been unclear in my comment before, but registering a webfont provider wouldn't be required as long as you use one of the common ones that WP core would provide.

My proposed approach would facilitate the same API that is currently descibed in #4, where when registering a webfont you simply need to specify a provider slug (like "local", "google", "adobe",...), or it would default to the "local" one probably.

Feedback is welcomed and encouraged. If consensus, then the next version of this API (for Phase 1) can be built rather quickly for review.

Could you clarify what is "Phase 1"? If we want to implement certain parts later, we should still ensure the architecture we decide on now will allow for that. Specifically about webfont provider registration, I agree we could also build the registration part later, which would mean that in the beginning you would really only be able to use the webfont providers that WP core implements out of the box.

@hellofromtonya
Copy link
Contributor

Registering a webfont provider isn't something that a theme author would typically be expected to do. I think core should provide the common webfont providers (e.g. local, Google, Adobe) out of the box, so registration would only be necessary when using a less popular or entirely custom provider (let's say if a company had their own fonts CDN). Maybe that has been unclear in my comment before, but registering a webfont provider wouldn't be required as long as you use one of the common ones that WP core would provide.

I agree. Core should have built-in providers that it registers. Theme authors would then specify which provider via its associated ID such as local, google, etc.

For custom things, a custom provider can be registered as you suggested previously.

Which part you mean is too complex?

  • Individually registering each font within the theme using wp_register_webfont() or wp_enqueue_webfont()
  • Instantiating a provider as part of each of these registrations/enqueuing within the theme
  • A theme author having to figure out if they configure via src or params or both

With the new proposal being discussed, it aligns better to the future need while reducing code and complexity for theme authors.

My proposed approach would facilitate the same API that is currently descibed in 4, where when registering a webfont you simply need to specify a provider slug (like "local", "google", "adobe",...), or it would default to the "local" one probably.

I agree. That's the vision of the proposal. The theme defines all of the fonts it'll use and for each font, it associates a slug or ID for the provider. Core will expose built-in providers. Custom providers can be built and registered.

  • local => handles local fonts
  • google => handles fetching from the Google Fonts API
  • my-custom-font-provider => handles a custom provider (that is in the theme)

@felixarntz
Copy link
Member

  • Individually registering each font within the theme using wp_register_webfont() or wp_enqueue_webfont()

The currently proposed functions wp_register_webfont() and wp_enqueue_webfont() make the lower-level API, that doesn't mean we have to require theme authors to call those functions. Theme authors could specify directives in theme.json that would then be translate to calls to those functions. The functions would need to be available still, e.g. for plugin authors, who wouldn't have theme.json as a mechanism.

  • Instantiating a provider as part of each of these registrations/enqueuing within the theme

That would never be necessary. When calling the function, no provider needs to be instantiated, only the identifier for a core default provider would be passed. Registration of a provider wouldn't be necessary either unless it's a custom one.

  • A theme author having to figure out if they configure via src or params or both

As far as I've seen based on the latest API iteration (also see https://core.trac.wordpress.org/ticket/46370#comment:40), I don't think the differentiation between src and params is a thing anymore? Or am I missing something @aristath? Is there a place where you would still provide only src and not params?

@hellofromtonya
Copy link
Contributor

Could you clarify what is "Phase 1"?

That's a great question. I had thoughts here where we're initially discussing the new proposal.

Overview of Phase 1:

  • Input: the webfonts collection schema in an array data type (for registration)
  • Processing:
    • Schema validation
    • Collection of like font providers to optimize fetching from external APIs
    • Registration of providers (including custom providers)
    • Generation of CSS
  • Output: CSS

This includes:

  • the functions to register the webfonts collection and custom providers
  • built-in Core providers

What's not included:

  • Handling, parsing, or validation of the theme.json
  • An Adobe Fonts Provider

If we want to implement certain parts later, we should still ensure the architecture we decide on now will allow for that. Specifically about webfont provider registration, I agree we could also build the registration part later, which would mean that in the beginning you would really only be able to use the webfont providers that WP core implements out of the box.

I agree. What gets built now should be a stepping stone towards the future.

@hellofromtonya
Copy link
Contributor

hellofromtonya commented Oct 6, 2021

The currently proposed functions wp_register_webfont() and wp_enqueue_webfont() make the lower-level API, that doesn't mean we have to require theme authors to call those functions. Theme authors could specify directives in theme.json that would then be translate to calls to those functions. The functions would need to be available still, e.g. for plugin authors, who wouldn't have theme.json as a mechanism.

Imagine one entry into the Webfonts API that serves plugins, block themes with a theme.json file, and themes without that json file. By standardizing the schema of how themes and plugins tell Core about their webfonts, this single entry point could be achieved.

Let's think about by starting with the end in mind, i.e. theme.json.

The configuration of webfonts will part of the theme.json. Once the theme.json is parsed and the webfonts extracted from it, then it could be passed to the Web Fonts API for its processing. Imagine the webfonts part of the theme.json after it's parsed. Let's imagine it as an array. If that's the entry point into the API, then that same schema can be used as the input via a theme.json or directly passing the collection as an array to the API's registration function such as wp_register_webfont().

This could then lead to one API function for registration that handles registering webfonts from multi-sources including themes with a theme.json file, themes that directly invoke wp_register_webfont(), and plugins that directly invoke wp_register_webfont().

Let's see that in action.

This PR requires the theme or plugin to register / enqueue like this:

// Passing a URL as the src.
wp_enqueue_webfont( 'roboto', 'https://fonts.googleapis.com/css2?family=Roboto:ital@0;1&display=swap' );

wp_enqueue_webfont(
	'my-font-normal-400',
	'',
	array(
		'font-family'  => 'My Font',
		'src'          => array(
			get_template_directory_uri() . '/fonts/font.woff2',
			get_template_directory_uri() . '/fonts/font.woff',
		),
		'provider'     => new WP_Fonts_Provider_Local(),
	)
);

or like this:

wp_enqueue_webfont(
	'roboto-400',
	'',
	array(
		'fontFamily'  => 'Roboto',
		'fontStyle'   => 'normal',
		'fontWeight'  => '400',
		'provider'    => new WP_Fonts_Provider_Google(),
	)
);

wp_enqueue_webfont(
	'roboto-italic-400',
	'',
	array(
		'fontFamily'  => 'Roboto',
		'fontStyle'   => 'italic',
		'fontWeight'  => '400',
		'provider'    => new WP_Fonts_Provider_Google(),
	)
);

wp_enqueue_webfont(
	'my-font-normal-400',
	'',
	array(
		'font-family'  => 'My Font',
		'src'          => array(
			get_template_directory_uri() . '/fonts/font.woff2',
			get_template_directory_uri() . '/fonts/font.woff',
		),
		'provider'     => new WP_Fonts_Provider_Local(),
	)
);

With the new proposal, it can simplified to this:

wp_register_webfont(
	array(
		'roboto-normal-400' => array(
			'fontFamily'  => 'Roboto',
			'fontStyle'   => 'normal',
			'fontWeight'  => '400',
			'provider'    => 'google',
		),
		'roboto-italic-400' => array(
			'fontFamily'  => 'Roboto',
			'fontStyle'   => 'italic',
			'fontWeight'  => '400',
			'provider'    => 'google',
		),
		'my-font-normal-400' => array(
			'fontFamily'  => 'My Font',
			'fontDisplay' => 'swap',
			'fontStyle'   => 'normal',
			'fontWeight'  => '400',
			'src'         => array(
				get_template_directory_uri() . '/fonts/font.woff2',
				get_template_directory_uri() . '/fonts/font.woff',
			),
			'provider'     => 'local',
		),
	)
);

@hellofromtonya
Copy link
Contributor

Notice in the last example how the array follows the theme.json schema. That's what I meant by standardizing the input to specify the webfonts collection schema.

@felixarntz
Copy link
Member

// Passing a URL as the src.
wp_enqueue_webfont( 'roboto', 'https://fonts.googleapis.com/css2?family=Roboto:ital@0;1&display=swap' );

To my above question, is that still relevant given the latest iteration supports font providers? I think it'd be more appropriate to register e.g. Google fonts by registering a font like in the other examples and specifying google. The example you have here with the URL would be just another way to accomplish that, which I'm not sure we want to allow for instead of the params approach which relies on structured data.

I agree with pretty much everything you're saying otherwise. Regarding your last example though (wp_register_webfont with an array to register multiple fonts), I'm still unsure why that's better than wp_register_webfont with a single font, like it is now. The theme.json input could as well translate into a foreach loop where then wp_register_webfont is called accordingly for each. Last but not least, if we decide to go with that array approach, the function should probably called wp_register_webfonts, since it wouldn't be just one webfont that is registered.

@hellofromtonya
Copy link
Contributor

The example you have here with the URL would be just another way to accomplish that, which I'm not sure we want to allow for instead of the params approach which relies on structured data.

I agree with you. I think we want to standardize on the params structure and not an optional src or params.

Why?

It could make collecting the like fonts a little bit easier rather than parsing separate URLs that have been registered by a theme and/or plugin.

I'm still unsure why that's better than wp_register_webfont with a single font, like it is now. The theme.json input could as well translate into a foreach loop where then wp_register_webfont is called accordingly for each. Last but not least, if we decide to go with that array approach, the function should probably called wp_register_webfonts, since it wouldn't be just one webfont that is registered.

Why better? A couple of things come to my mind:

  • Supporting classic and block themes and/or migrating some day to theme.json. Converting from an array to JSON is fairly straightforward process (i.e. in the theme)
  • Potentially eliminating multiple iterators, i.e. foreach. How so? Imagine a case where a plugin and/or theme makes it DRY by using an array and then iterating through the webfont definition records to invoke the registration for each.

@felixarntz
Copy link
Member

+1 to both of that. I'd be leaning towards having a higher-level wp_register_webfonts( $fonts ) which accepts an array of one or more webfont entries. That function would then iterate over them and call wp_register_webfont for each.

@hellofromtonya
Copy link
Contributor

I'd be leaning towards having a higher-level wp_register_webfonts( $fonts ) which accepts an array of one or more webfont entries. That function would then iterate over them and call wp_register_webfont for each.

+1 Agreed!

I'll start a new branch on Ari's fork with this new approach and optimized architecture. Why? To retain this PoC PRs history and implementation.

Also plan to include a suite of unit/integration tests. Once ready, performance benchmarks will be needed too.

@aristath
Copy link
Member Author

Closing this PR as it has been replaced by #1736

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants