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

Webfonts API - webfonts collection implementation #1736

Closed
wants to merge 94 commits into from

Conversation

hellofromtonya
Copy link
Contributor

@hellofromtonya hellofromtonya commented Oct 8, 2021

This API solves the problem of inefficient font loading which is a known "performance bottleneck" that can impact the user experience.

What the API Provides

This API provides:

  1. A standard for registering webfonts
  2. A performant way of fetching and processing from external font services (one request instead of individual requests)
  3. Performant HTML and CSS including preconnect links
  4. Extensibility for extenders

What the API does

  • Exposes a standard way for theme.json parser, themes without theme.json, and plugins to register their webfonts using wp_register_webfonts()

  • Allows for different ways to get the webfont file: from an external font service or locally hosted within a theme or plugin

    • Providers bundled in the API:
      • google: handles requesting from the Google Fonts service
      • local: handles locally hosted webfont files
    • Extenders can create custom providers to connect to other external font services
  • Validates what is registered:

    • For required configuration parameters: validates the keys exist and the values are of the data type required. Why?

      • To guard Core and users against fatal errors of attempting to access a key that does not exist.
      • To avoid unnecessary processing if those required parameters do not exist.
      • To alert developers where schema issues exist to help them debug issues with their code.
    • For optional configuration parameters: checks if key ones are valid, if not, sets a default. Why?

      • Avoids unnecessary processing such as making an invalid request to an external font service.
      • Protects the user from delays, errors, FOUC, and/or unexpected styling issues.
  • Orchestrates webfonts organization:

    • for performance and improved user experience by:
      - doing a single external API roundtrip (when requesting an external font service), instead of individual fetches
      - adding preconnect links
    • to support plans to preview font-families and font pairing in the block editor
    • for extensibility extenders the ability to build custom experiences
  • Validation (more on this below)

Benefits for extenders:

  • A standard webfont schema for defining webfonts - meaning the structure of the schema is consistent whether it's defined in theme.json, in a theme's PHP, or in a plugin
  • Less code to maintain in their plugin or theme
  • Create a custom provider to connect to an external font service other than Google Fonts

Benefits for users:

  • Faster perceived rendering
  • Connect sooner to external font services to minimize page shifts
  • Less FOUT
  • Better CLS score

Trac ticket: https://core.trac.wordpress.org/ticket/46370


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.

@hellofromtonya hellofromtonya force-pushed the try/webfonts-alt-api-1 branch from ca8f410 to 18f7807 Compare October 8, 2021 17:48
aristath added a commit to aristath/twentytwentytwo that referenced this pull request Oct 13, 2021
@aristath
Copy link
Member

Styles for webfonts now get properly enqueued in the editor.
I created a draft PR in the twentytwentytwo theme implementing the webfonts API, we can use that to test the implementation 👍

@hellofromtonya hellofromtonya force-pushed the try/webfonts-alt-api-1 branch 2 times, most recently from 007ef64 to 1d02ed4 Compare October 14, 2021 00:16
@azaozz
Copy link
Contributor

azaozz commented Oct 18, 2021

Been looking at this patch for couple of days and thinking it may be going a bit too far/in a wrong direction. Few things:

  1. As @hellofromtonya mentioned here: https://core.trac.wordpress.org/ticket/46370#comment:42 this seems very big and very complex, both as an API for use by themes and plugins and as code. Unfortunately it seems it has become even larger and more complex since then.
    Currently a theme can add a webfont with about 40 lines of code. That code is typically copied and reused from one of the default themes, making it really simple. Looking at the requirements here, it seems quite more difficult at add a webfont by using the proposed API.
  2. Looking at the code there is a lot of validation going on, and is even separated in another subclass. What is the purpose of that validation/verification? What are the expected use cases that are covered by it? Are new webfonts going to be registered on every page load in production? Or are the webfonts settings going to be provided by the users (direct user input)?
    Looking at expected use cases, this is an internal API not expected to be accessed from outside of WordPress like for example the REST API. The webfonts settings are pretty much going to be hard-coded in themes and plugins. So all the validation, including all the regex, etc. will be running many millions of times per day on these constant settings, completely redundantly. The only possible case where that validation might eventually be useful for a short time is while a developer is starting a new theme or plugin and doesn't follow the (still missing) documentation and examples. In all other cases it's just churn.
  3. As mentioned above documentation and examples need to be added, perhaps at the top of webfonts-api.php (assuming that's the expected way to use the API). There are some examples on the trac ticket, but they need to be in the docblock :)
  4. The whole structure seems overly complex, perhaps overengineered. There are several classes and subclasses that don't really do much apart from storing coupe of values and providing couple of methods. Perhaps simplifying it would make the code shorter/clearer/better/easier to read and understand. I understand this may be modeled after the WP_Dependencies classes and functions but the main purpose there is to calculate the load order of JS and CSS files, and to provide a way for enqueuing and outputting of inline scripts and styles before and after an enqueued file. Here the case seems quite simpler.
  5. There are two issues that are not addressed yet. We talked with @hellofromtonya few days ago about these, don't see them mentioned here yet.
  • User privacy concerns. Using a non-local webfont is pretty much the same like using a "pixel" or a third-party analytics service. In some regions, most notably in Europe, a website visitor may need to be asked to provide consent for their data being send to a third party. I understand that's not part of this API, but at least a proper (inline) documentation should be provided, explaining the implications to theme and plugin authors so they can take care of their users. This is necessary as WordPress will eventually have the Google webfonts URLs hard-coded as part of this patch.
  • Generated CSS. I may be wrong but some commercial webfonts come with quite restrictive licenses and usage directions, and seem to provide all necessary files and formats, thanks to @skorasaurus for the example. Could this CSS generation be seen as perhaps breaking of such licenses? Also why is this still necessary (more inline docs opportunities).

@azaozz
Copy link
Contributor

azaozz commented Oct 18, 2021

Uh, sorry about the long comment above. I'm also a bit worried that this is far too complex, not only as "code design" but also as usage. What would be the compelling reason a theme would prefer to switch to using this API rather than continue to hard-code loading of the web fonts it needs, like it has been doing for the last five years or so?

@hellofromtonya
Copy link
Contributor Author

hellofromtonya commented Oct 18, 2021

Currently a theme can add a webfont with about 40 lines of code. That code is typically copied and reused from one of the default themes, making it really simple. Looking at the requirements here, it seems quite more difficult at add a webfont by using the proposed API.

It's less code for themes and plugins.

How so?

Without this API, themes and plugins:

  • Register, enqueue, and inline styles for wp_enqueue_scripts and 'wp-block-library'
  • Generate and return @font-face CSS for the inline
  • And handle the preconnect link for externally hosted fonts

With this API, themes and plugins:

  • Define their webfonts in theme.json or in a PHP array passed to wp_register_webfonts()

See this action 👀

Take a look at the new TT2 theme's PR 95:

  • Reduces the theme's webfont code by ~42%:
    • Removes 52 lines of code
    • Adds 18 lines to define the webfonts collection (i.e. an array of arrays)
    • Adds 4 lines of PHP code to pass the array into Core

This PR is an example of how plugins and non-theme.json themes can simplify their code while gaining the benefits of the API.

Note: Once this API is committed, then theme.json parser can be updated. Then TT2's webfonts collection will move into its theme.json.

What are benefits for theme and plugin authors and extenders? 🚀

  • Less code to write and maintain.
  • Their webfont collection (configurations) are portable. They can copy/paste between different themes, plugins, and projects. How so? The structure is standardized and agnostic.
  • The code is more readable and maintainable. How so? It uses CSS properties to define and configure the webfonts. No CSS to write. And very very little PHP.
  • As web performance evolves, their products / projects are no longer affected. Core handles it for them. Less time to be aware, update, and maintain changes in their existing products / projects.

@hellofromtonya
Copy link
Contributor Author

hellofromtonya commented Oct 18, 2021

What is the purpose of that validation/verification? What are the expected use cases that are covered by it? Are new webfonts going to be registered on every page load in production?

The webfonts settings are pretty much going to be hard-coded in themes and plugins. So all the validation, including all the regex, etc. will be running many millions of times per day on these constant settings, completely redundantly.

The validation code protects users and provides debug help for extenders.

For performance:

  • It only runs when a webfont is registered.
  • It's super duper fast about < 5 microseconds for it to validate a webfont (See the profiler in action here.).
  • There is no negative impact on performance.

What's the purpose of it? Benefits

  • For required configuration parameters: validate the keys exist and the values are of the data type required. Why?

    • To guard Core and users against fatal errors of attempting to access a key that does not exist
    • To avoid unnecessary processing if those required parameters do not exist
    • To alert developers where schema issues exist to help them debug issues in their code
  • For optional configuration parameters: check if keys and values are valid, if no, set to a default. Why?

    • Avoids unnecessary processing such as making an invalid request to an external font service
    • Protects the user from delays, errors, FOUC, and/or unexpected styling issues

Or are the webfonts settings going to be provided by the users (direct user input)?

Yes that's possible.

@hellofromtonya
Copy link
Contributor Author

The whole structure seems overly complex, perhaps overengineered. There are several classes and subclasses that don't really do much apart from storing coupe of values and providing couple of methods.

This architecture is designed to:

  • Simplify how extenders register webfonts
  • Standardize registering and handling of webfonts
  • Be agnostic to where the webfonts are defined (could be theme.json powered theme, a theme without a theme.json, a plugin, or interface where the user selects them)
  • Performance:
    • Fetch and process from external font services (one request instead of individual requests)
    • Add HTML and CSS including preconnect links
    • Orchestration of webfont collections to reduce additional sorting iterations
  • Extensible for extenders
  • Support font family and font pairing rendering in the block editor
  • Future enhanceable
  • Be robust (not fragile)
  • Be readable and understandable as code is (a) broken up into areas of responsibility and (b) with minimized coupling between the pieces

How so?

I hadn't yet detailed the architecture of what each piece does and why. Now is a good time. Let's start with the pieces that are needed and then move to have each class is delivering those pieces.

The Pieces Needed

For the webfonts (WP_Webfonts_Registry):

  • A mechanism to register a webfont
  • An in-memory storage container to house the registered webfonts until it's time to process them
  • Ability to handle both font-family (kebab-case) and fontFamily (theme.json is in camelCase but the CSS needs kebab-case -> building in the ability to handle both types)
  • A way to pass the webfonts organized by font-family to their respective providers
  • A way to query the webfonts (i.e. get them for usage outside of the API)

For the providers (WP_Webfonts_Provider_Registry):

  • A mechanism to register a provider
  • An in-memory storage container to house the registered provider
  • A mechanism to instantiate a provider
  • The ability to receive its webfonts
  • A trigger to tell the provider to do its work on the given webfonts

For the API (WP_Webfonts_Controller):

  • A manager to orchestrate all the above
  • The glue between all of the pieces

For local or external fetching and CSS (the providers):

  • A mechanism to handle locally hosted web files and generate its @font-face
  • A mechanism to handle Google Fonts and generate its @font-face
  • The ability for custom providers to be registered to connect to other external font services

The Registry

  • Pattern: Repository pattern
  • Common pattern in Core: WP_Hook, WP_Sitemaps_Registry, WP_Block_List, WP_Block_Pattern_Categories_Registry, WP_Block_Patterns_Registry, WP_Block_Styles_Registry, WP_Block_Type_Registry, WP_Styles, WP_Scripts, and more.

An in-memory storage mechanism is needed for the API to collect and then organize the optimizations to be done. Instead of register and processing each individual webfont, the goal is to organize the collections for each provider in such a way that each provider can do its work in an optimum way without duplicating the sorting process.

The knowledge of how to work with the webfonts or providers is contained within their respective registry. As each has an in-memory container to house the registered items, further optimizations can be done. These registries provide the mechanisms to handle the work of registering, preparing, verifying, and querying. That know-how is contained inside of each of these registries.

Benefits:

  • Know-how encapsulated inside of each registry: they know how to handle the work of registering, preparing, verifying, and querying. That know-how is contained inside of each of these registries.
  • Faster fetching from external font services. How so?
    • Fetch all of the webfonts in one request (instead of multiple requests).
  • Faster rendering. How so?
    • The block editor and/or plugins querying the webfonts get them in this pre-organized (sorted) structure.
  • Faster processing. How so?
    • One loop when registering the webfonts that handles all the work to prepare, check, sort, and then store each. This means other code doesn't need to do redundant work including the providers, block editor, and plugins.
    • The combination of the Controller and Registry means filtering is not needed (as filtering adds memory, CPU cycles, and the potential to alter or even break the schema). Instead, the registry exposes methods for getting and querying. No filtering needed.
  • Eliminates duplicates.
    - It's possible that a user adds a plugin to their site that is using the same webfont. With the registry, only one of the same type can be registered.
    - This prevents unnecessary work.
  • It's built for the future, i.e. to allow future enhancements. For example, there may be a need to remove a webfont (deregister).
  • It's testable

The Providers

The provider is a processor that encapsulates the business logic of how to generate the @font-face CSS. It's built for future enhancements.

Benefits:

  • Extensibility: extenders can create their own custom providers to connect to other font services
  • Encapsulates the know-how for processing a specific type of webfont src
  • It's built for the future, i.e. to allow future enhancements
  • It's testable

The Controller

Pattern: Controller
Common pattern in Core: heavily used in the REST API (for site healthy, taxonomies, templates, search, block patterns, users, widget types), sitemaps, and more.

The Controller is the glue. It's the manager directly the flow of work between the pieces and keeping the coupling between to a minimum.

Benefits:

  • Changing the processing order or hooks is readily found within it
  • The coupling between the objects is minimized
  • It's straightforward to trace through what gets called and when
  • It's built for the future, i.e. to allow future enhancements
  • It's testable

@hellofromtonya
Copy link
Contributor Author

hellofromtonya commented Oct 19, 2021

Perhaps simplifying it would make the code shorter/clearer/better/easier to read and understand. ...Here the case seems quite simpler.

It's doing a lot more than those highly specific 40 lines of code you mentioned are frequently copied/pasted into themes and plugins. (See the details above of all that is delivers.)

How much code does it add to Core?
~570 lines of executable code. It's small for all that it does.

@azaozz do you still feel it's overengineered? If yes, what ideas do you have to simplify it and still deliver everything it needs to do?

@aristath
Copy link
Member

User privacy concerns. Using a non-local webfont is pretty much the same like using a "pixel" or a third-party analytics service. In some regions, most notably in Europe, a website visitor may need to be asked to provide consent for their data being send to a third party. I understand that's not part of this API, but at least a proper (inline) documentation should be provided, explaining the implications to theme and plugin authors so they can take care of their users. This is necessary as WordPress will eventually have the Google webfonts URLs hard-coded as part of this patch.

This is not the end of the implementation, it is the beginning. Before we discuss privacy concerns, loading remote files etc, we need to have a base we can build on and improve.
In the previous implementation/POC we already had a method to grab the remote files and host them locally. This will improve both privacy and performance, but we chose not to include such functionality in this PR, and instead do a followup patch to add it, once this gets merged.
Strictly speaking, it is not exactly part of the API definition, and further discussion on it will probably be needed. Discussions about this behavior have the potential to torpedo and delay merging the initial implementation of the API - and so it was split off the initial PR and will be a separate followup.
For reference, the code in the previous POC that downloads the font-files and stores them locally can be seen here:

/**
* Download files mentioned in our CSS locally.
*
* @access public
* @since 1.0.0
* @return array Returns an array of remote URLs and their local counterparts.
*/
public function get_local_files_from_css( $css ) {
$font_faces = explode( '@font-face', $css );
$font_files = array();
// Loop all our font-face declarations.
foreach ( $font_faces as $font_face ) {
// Make sure we only process styles inside this declaration.
$style = explode( '}', $font_face )[0];
// Sanity check.
if ( false === strpos( $style, 'font-family' ) ) {
continue;
}
// Get an array of our font-families.
preg_match_all( '/font-family.*?\;/', $style, $matched_font_families );
// Get an array of our font-files.
preg_match_all( '/url\(.*?\)/i', $style, $matched_font_files );
// Get the font-family name.
$font_family = 'unknown';
if ( isset( $matched_font_families[0] ) && isset( $matched_font_families[0][0] ) ) {
$font_family = rtrim( ltrim( $matched_font_families[0][0], 'font-family:' ), ';' );
$font_family = trim( str_replace( array( "'", ';' ), '', $font_family ) );
$font_family = sanitize_key( strtolower( str_replace( ' ', '-', $font_family ) ) );
}
// Make sure the font-family is set in our array.
if ( ! isset( $font_files[ $font_family ] ) ) {
$font_files[ $font_family ] = array();
}
// Get files for this font-family and add them to the array.
foreach ( $matched_font_files as $match ) {
// Sanity check.
if ( ! isset( $match[0] ) ) {
continue;
}
// Add the file URL.
$font_files[ $font_family ][] = rtrim( ltrim( $match[0], 'url(' ), ')' );
}
// Make sure we have unique items.
// We're using array_flip here instead of array_unique for improved performance.
$font_files[ $font_family ] = array_flip( array_flip( $font_files[ $font_family ] ) );
}
$stored = get_site_option( 'downloaded_font_files', array() );
$change = false; // If in the end this is true, we need to update the cache option.
$filesystem = $this->get_filesystem();
if ( ! defined( 'FS_CHMOD_DIR' ) ) {
define( 'FS_CHMOD_DIR', ( 0755 & ~ umask() ) );
}
// If the fonts folder don't exist, create it.
if ( ! file_exists( trailingslashit( WP_CONTENT_DIR ) . '/fonts' ) ) {
$filesystem->mkdir( trailingslashit( WP_CONTENT_DIR ) . '/fonts', FS_CHMOD_DIR );
}
foreach ( $font_files as $font_family => $files ) {
// The folder path for this font-family.
$folder_path = trailingslashit( WP_CONTENT_DIR ) . "/fonts/$font_family";
// If the folder doesn't exist, create it.
if ( ! file_exists( $folder_path ) ) {
$filesystem->mkdir( $folder_path, FS_CHMOD_DIR );
}
foreach ( $files as $url ) {
// Get the filename.
$filename = basename( wp_parse_url( $url, PHP_URL_PATH ) );
// Check if the file already exists.
if ( file_exists( "$folder_path/$filename" ) ) {
// Skip if already cached.
if ( isset( $stored[ $url ] ) ) {
continue;
}
// Add file to the cache and change the $changed var to indicate we need to update the option.
$stored[ $url ] = "$folder_path/$filename";
$change = true;
// Since the file exists we don't need to proceed with downloading it.
continue;
}
/**
* If we got this far, we need to download the file.
*/
// require file.php if the download_url function doesn't exist.
if ( ! function_exists( 'download_url' ) ) {
require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/file.php' );
}
// Download file to temporary location.
$tmp_path = download_url( $url );
// Make sure there were no errors.
if ( is_wp_error( $tmp_path ) ) {
continue;
}
// Move temp file to final destination.
$success = $filesystem->move( $tmp_path, "$folder_path/$filename", true );
if ( $success ) {
$stored[ $url ] = "$folder_path/$filename";
$change = true;
}
}
}
// If there were changes, update the option.
if ( $change ) {
// Cleanup the option and then save it.
foreach ( $stored as $url => $path ) {
if ( ! file_exists( $path ) ) {
unset( $stored[ $url ] );
}
}
update_site_option( 'downloaded_font_files', $stored );
}
return $stored;
}

@aristath
Copy link
Member

Generated CSS. I may be wrong but some commercial webfonts come with quite restrictive licenses and usage directions, and seem to provide all necessary files and formats, thanks to @skorasaurus for the example. Could this CSS generation be seen as perhaps breaking of such licenses? Also why is this still necessary (more inline docs opportunities).

Users have the freedom to use this API however they want... If the user downloads non-free webfont files, then bundles them in their theme and registers them using this API, the CSS we generate is not an issue. The issue is the font-files they used.

@hellofromtonya hellofromtonya marked this pull request as ready for review October 19, 2021 21:32
@aristath aristath force-pushed the try/webfonts-alt-api-1 branch from c99f96f to 6a211c8 Compare October 20, 2021 05:09
aristath added a commit to aristath/twentytwentytwo that referenced this pull request Oct 20, 2021
@aristath aristath mentioned this pull request Oct 20, 2021
@aristath
Copy link
Member

Testing instructions for webfonts registration added in https://core.trac.wordpress.org/ticket/46370#comment:66

@aristath aristath force-pushed the try/webfonts-alt-api-1 branch 2 times, most recently from 1c02d77 to e79b46d Compare October 25, 2021 06:24
hellofromtonya and others added 5 commits November 18, 2021 15:25
A filter is provided to grant permission to do
remote requests to external service providers.
By default, this filter is set to `false`, meaning
the API will not do remote requests or generate
`@font-face` styles for external service providers.

Removes the `is_external()` logic as all providers
are external except for the local provider bundled
in core.

Removes the `is-external` parameter from the webfonts
schema and validation code.
Further documents the API with examples.

Includes code review fixes.
@aristath aristath force-pushed the try/webfonts-alt-api-1 branch from 2ab1403 to aa08030 Compare November 18, 2021 13:51
aristath added a commit to WordPress/gutenberg that referenced this pull request Nov 19, 2021
aristath added a commit to WordPress/gutenberg that referenced this pull request Nov 23, 2021
aristath added a commit to WordPress/gutenberg that referenced this pull request Nov 24, 2021
@azaozz
Copy link
Contributor

azaozz commented Nov 24, 2021

@felixarntz Sorry for the delay in responding. Have some health problems.

You're pointing out why feature plugins are generally useful...

Right. I believe this API, like all other larger/more complex new features in WordPress, would benefit from being a feature plugin and get tested in all sorts of environments, staging, etc. That includes testing at very large sites, multi-site/network installs, etc.

People installing a plugin with this API in their sites will give them nothing to test..

Correct, that's a similar case like with the REST API. Feature plugins can be targeted at extenders too, not just at site admins.

The alternative would be to add a brand new, untested, potentially unrefined API which would "commit" WP to support it "forever" even if there are large problems with it (I don't think there are, but it is a possibility). And it is not an MVP, it adds a lot of more advanced features. Hence my recommendation to be on the cautious side, and I believe the patch was not merged following that recommendation. Also identified two "blockers" (documentation, user privacy) that were fixed in time.

@azaozz
Copy link
Contributor

azaozz commented Nov 24, 2021

@aristath Again, sorry about the delay, have some health problems at the moment.

I haven't tried adding Adobe fonts...

Specifically asked about Adobe as they have probably the largest fonts library and guessing they are the top candidate for being added by plugins that want to extend the WP webfonts API. Also their APIs work in a pretty different way. As far as I see they expect the users to have an account on their website and to manage the webfonts features from there. Here are some examples in this tutorial: https://helpx.adobe.com/fonts/using/add-fonts-website.html. As far as I see it works by providing an URL that doesn't contain any fonts information, just points to the user's account on their site. So all the fonts settings that are required by this patch are not available.

As far as I see there is no way to make this work with the current patch. Perhaps if the patch was more extensible a plugin may be able to bypass most of the current requirements and enable use of Adobe fonts, but still looks like a pretty hard thing to do.

...to be sure that we can add other providers in the future

I'm actually still wondering about having the "providers" registration in the first place. Could you perhaps tell me how did you come up with that idea, to separate the font URL into two parts and what benefits that brings?

At the moment, no matter from what angle I try to look at it, it seems that the "providers" abstraction is not needed/not performing a particular function. Having the fonts registered together with the providers (i.e. the providers are part of the font registration) seems more feasible. That would also reduce the current complexity quite a lot, and make the API easier to use.

Another idea/concern/enhancement would be to separate the font settings that are for use by other parts of WP (like the editor) and the actual URL used to get the font(s). For example looking at how Google serves webfonts, having to "extract" the settings from the URL that is generated by the Google API, then enter them as PHP array key/value pairs, then the API would try to regenerate the original URL as given by the remote API from the settings is not the best way for this to work imho.

Perhaps a better solution would be to distinguish between "functional" webfonts settings (as needed for the webfont to work) and settings intended for other parts of WP. Then a theme or plugin will be able to expose some of the settings to the editor UI, but keep others "hidden" from the users: a lot better control and a lot simpler/safer as the original third-party API URL will not be disassembled by the plugin author and then reassembled by WP.

Please note: this is just an idea, not a "blocker" or anything like that :)

@aristath
Copy link
Member

sorry about the delay, have some health problems at the moment.

I'm sorry to hear than Andrew, I hope things go well and you feel better soon ❤️

Specifically asked about Adobe as they have probably the largest fonts library and guessing they are the top candidate for being added by plugins that want to extend the WP webfonts API. Also their APIs work in a pretty different way.

I'm sorry but I'm not going to try and fix the way the Adobe API works. If Adobe wants, they can build a class themselves - or fix their API to be more friendly/reasonable.

I'm actually still wondering about having the "providers" registration in the first place. Could you perhaps tell me how did you come up with that idea, to separate the font URL into two parts and what benefits that brings?

The benefit is consistency. All webfonts can be defined the same way - regardless of whether they are local, google-fonts, or something else. This is necessary so that webfonts can be defined in a theme.json file using a consistent format regardless of their source of origin. That can't be done efficiently without some concept of a provider.

For example looking at how Google serves webfonts, having to "extract" the settings from the URL that is generated by the Google API, then enter them as PHP array key/value pairs, then the API would try to regenerate the original URL as given by the remote API from the settings is not the best way for this to work imho.

That's a false premise... You're missing a step here, and that step is the very 1st one: The user chooses a webfont they want to use. Users don't "extract the settings from the generated URL to then enter them as PHP array key/value pairs"... Users don't have a URL in their mind when they want to use the Open-Sans font with weights 400 & 700. They start with what they want to use which is a font-family, some font-weights etc. Instead of going to the google-fonts (or any other API) to generate a URL, they just enter what they want as key/value pairs - either in PHP or JSON in their theme.json file.

Currently:

  • The user decides what webfonts they want to use
  • They go to the google-fonts site, select their fonts
  • The google-fonts API generates a URL which the user then has to use.
  • The user then needs to add that to their site. If they add it using add_action( 'wp_head', ... ) they can just inject it directly (hacky solution). If they use wp_enqueue_style (the currently "right" way), then it doesn't work when they have multiple fonts in the same google-fonts request.

With the Webfonts API:

  • The user decides what webfonts they want to use
  • They enter the font details in an array format
  • The API takes care of the rest.

I don't see how the current, unmanaged version is any better than a more managed solution which doesn't require users to go to a 3rd-party site, search for webfonts, then get a URL and manually handle adding it everywhere.
If someone wants to do things the "old" way there's nothing preventing them to do that. But in that case they need to manually take care of adding the webfont in the frontend and the editor, add it to font-family pickers, manage privacy concerns, optimize delivery and so on.
This API does not break current behaviors. It doesn't prevent anyone from doing something they were previously doing. If someone wants to manually inject webfonts, styles, scripts and whatnot on their site they'll always be able to do that. Having an API makes the process easier for those that need it.

Perhaps a better solution would be to distinguish between "functional" webfonts settings (as needed for the webfont to work) and settings intended for other parts of WP.

What would be a non-functional webfont-setting exactly? The API simply uses @font-face props - which by definition makes them all functional. The only "arbitrary" setting is defining the provider which simply tells the API "hey, this webfont should be fetched from a 3rd-party server". Is that the non-functional part?

@azaozz
Copy link
Contributor

azaozz commented Nov 25, 2021

@aristath

I hope things go well and you feel better soon

Thank you!

I'm not going to try and fix the way the Adobe API works

Hehe, of course not. But not being able to use the Adobe fonts API/CDN with the WordPress webfonts API seems like big drawback. Themes and plugins should be able to do that.

On the other hand looking at the other font foundries that provide webfonts, most would let you download the font and host it locally (whether is it paid or free to use). There is also https://fontsource.org/ and its repo: https://github.com/fontsource/fontsource that seem to contain the fonts that are available from Google.

Then the consideration becomes: does WordPress want to facilitate usage of third party webfonts APIs/CDNs at this stage? Seems quite better to make the WP API work only with local fonts for now. This makes it quite simpler, and also resolves any concerns about user privacy issues. Also removes the necessity of integration with a future "user consent API" that may be added later. In that case not being able to extend the API to use Adobe webfonts CDNs for now seems appropriate. Frankly I'm starting to think this is the proper way forward.

The benefit is consistency. All webfonts can be defined the same way - regardless of whether they are local, google-fonts, or something else.

Hmm, yes, I can see that but generally a webfont is defined by its URL. Third-party/remote webfonts would have absolute URLs, local fonts would probably need to have relative URLs. This is the only difference, and is really "easy to handle". Don't think the abstraction is needed there.

Furthermore this abstraction seems to only be used when using the Google CDN. Considering the above thoughts (only local fonts for now) think the need for abstracting the fonts "sources" disappears completely.

You're missing a step here, and that step is the very 1st one

Okay, this only concerns using the Google webfonts API/CDN so probably not that important for now. When you select a webfont there (with few options) you're given an URL like this: https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,900;1,100&display=swap. This URL cannot be used in the WP webfonts API as given by the remote service. It has to be "translated", right? This is the step that seems "strange", WP not being able to accept what the third party APIs suggest and expect.

I understand that the different options that were selected have to be "known" so they can be used by the editor UI for example. My suggestion in this particular case was to separate the "working URL as given by Google" from the options that are presented to the WP UI.

This is something to maybe examine more closely in the future if WordPress decides to add support for using the Google CDN. At that time a decision would have to be made about using the Adobe CDN too.

@azaozz
Copy link
Contributor

azaozz commented Nov 29, 2021

Re-posting this here from WordPress/gutenberg#36394 (having 2 PRs for the same thing is hard/confusing...).

Been looking at this implementation for a while. It seems that it can be made a lot better by removing the "providers" abstraction. Few reasons:

  • It will remove several possible (theoretical) edge cases that are a concern: registering the same font from multiple providers, different plugins registering the same "provider" (conflicting settings?), etc.

  • Seems that the "providers" abstraction can only be used to add support for the Google webfonts API/CDN. Seems the Adobe API/CDN cannot be made to work with this implementation as it expects the user to manage all features from the Adobe website (doesn't pass font settings in the URL like for the Google's CDN).

  • All webfonts available from the Google's API/CDN seem to also be available for downloading/including from https://fontsource.org/ and their repo at https://github.com/fontsource/fontsource as NPM packages.

Advantages of using local webfonts (from Fontsource):

  • Self-hosting brings significant performance gains as loading fonts from hosted services, such as Google Fonts, lead to an extra (render blocking) network request. To provide perspective, for simple websites it has been seen to double visual load times. Benchmarks can be found here and here.

  • Fonts remain version locked. Google often pushes updates to their fonts without notice, which may interfere with your live production projects. Manage your fonts like any other NPM dependency.

  • Commit to privacy. Google does track the usage of their fonts and for those who are extremely privacy concerned, self-hosting is an alternative.

  • Your fonts load offline. On top of benefiting PWAs, often there may be situations, like working in an airplane or train, leaving you stranded without access to your online hosted fonts. Have the ability to keep working under any circumstance.

  • Support for fonts outside the Google Font ecosystem. This repository is constantly evolving with other Open Source fonts. Feel free to contribute!

In these terms my recommendations are:

  1. Add support only for local fonts for now. If WordPress decides to include support for the Google CDN later on, the implementation will have to consider web privacy laws and restrictions and be tied with an eventual User Consent API, etc.
  2. Because of the above the "providers" abstraction can be removed as it is not needed.
  3. The "WP webfont settings validation" functionality (a.k.a. "schema" in the patch) should be moved out of the low-level API and into a separate function. The API should only check parameter types (to prevent fatal errors as it seems this may be broken in new versions of PHP). However regex is not needed to check/set function param types, that is redundant there.

@hellofromtonya hellofromtonya changed the base branch from master to trunk November 29, 2021 20:24
@hellofromtonya
Copy link
Contributor Author

The Webfonts API has moved to Gutenberg (see first implementation in WordPress/gutenberg#36394). Closing this PR, thought it will remain available for the ongoing work in Gutenberg.

@azaozz
Copy link
Contributor

azaozz commented Nov 29, 2021

I'm still not convinced the Webfonts API should be "merged" with Gutenberg. It is a separate API after all. Another reason is that it still may benefit from being made into a feature plugin (no point to bypass the WP development process).

On the other hand if it is significantly simpler and more straightforward, perhaps it would be okay to "keep it" in Gutenberg as that would make it a bit easier to test with the block editor. Ideally it should be a plugin though, just like any other new WP feature that needs more testing.

aristath added a commit to WordPress/gutenberg that referenced this pull request Dec 2, 2021
aristath added a commit to WordPress/gutenberg that referenced this pull request Dec 6, 2021
aristath added a commit to WordPress/gutenberg that referenced this pull request Dec 8, 2021
aristath added a commit to WordPress/gutenberg that referenced this pull request Dec 14, 2021
@aristath aristath mentioned this pull request Feb 24, 2022
7 tasks
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.