diff --git a/inc/Engine/Common/Head/ElementTrait.php b/inc/Engine/Common/Head/ElementTrait.php new file mode 100644 index 0000000000..9511a1250c --- /dev/null +++ b/inc/Engine/Common/Head/ElementTrait.php @@ -0,0 +1,139 @@ +link( $args ); + } + + /** + * Preconnect link. + * + * @param array $args Element args. + * @return array|string[] + */ + protected function preconnect_link( array $args = [] ) { + $args['rel'] = 'preconnect'; + return $this->link( $args ); + } + + /** + * Dns_prefetch link. + * + * @param array $args Element args. + * @return array|string[] + */ + protected function dns_prefetch_link( array $args = [] ) { + $args['rel'] = 'dns-prefetch'; + return $this->link( $args ); + } + + /** + * Prefetch link. + * + * @param array $args Element args. + * @return array|string[] + */ + protected function prefetch_link( array $args = [] ) { + $args['rel'] = 'prefetch'; + return $this->link( $args ); + } + + /** + * Prerender link. + * + * @param array $args Element args. + * @return array|string[] + */ + protected function prerender_link( array $args = [] ) { + $args['rel'] = 'prerender'; + return $this->link( $args ); + } + + /** + * Stylesheet link. + * + * @param array $args Element args. + * @return array|string[] + */ + protected function stylesheet_link( array $args = [] ) { + $args['rel'] = 'stylesheet'; + return $this->link( $args ); + } + + /** + * Style tag. + * + * @param string $css CSS content. + * @param array $args Element args. + * @return array|string[] + */ + protected function style_tag( string $css = '', array $args = [] ) { + $element = [ + 'open_tag' => ''; + + return $element; + } + + /** + * Noscript tag. + * + * @param string $content Element contents. + * @param array $args Element args. + * @return array|string[] + */ + protected function noscript_tag( string $content = '', array $args = [] ) { + $element = [ + 'open_tag' => ''; + + return $element; + } + + /** + * Generic link tag. + * + * @param array $args Element args. + * @return array|string[] + */ + private function link( array $args = [] ) { + $element = [ + 'open_tag' => ' '', + ] + ); + + return $element; + } +} diff --git a/inc/Engine/Common/Head/ServiceProvider.php b/inc/Engine/Common/Head/ServiceProvider.php new file mode 100644 index 0000000000..885943fe7f --- /dev/null +++ b/inc/Engine/Common/Head/ServiceProvider.php @@ -0,0 +1,39 @@ +provides, true ); + } + + /** + * Registers items with the container + * + * @return void + */ + public function register(): void { + $this->getContainer()->addShared( 'common_head_subscriber', Subscriber::class ); + } +} diff --git a/inc/Engine/Common/Head/Subscriber.php b/inc/Engine/Common/Head/Subscriber.php new file mode 100644 index 0000000000..0c0148adea --- /dev/null +++ b/inc/Engine/Common/Head/Subscriber.php @@ -0,0 +1,168 @@ + 'method_name') + * * array('hook_name' => array('method_name', $priority)) + * * array('hook_name' => array('method_name', $priority, $accepted_args)) + * * array('hook_name' => array(array('method_name_1', $priority_1, $accepted_args_1)), array('method_name_2', $priority_2, $accepted_args_2))) + * + * @return array + */ + public static function get_subscribed_events() { + return [ + 'rocket_buffer' => [ 'insert_rocket_head', 100000 ], + 'rocket_head' => 'print_head_elements', + ]; + } + + /** + * Print all head elements. + * + * @param string $content Head elements HTML. + * @return string + */ + public function print_head_elements( $content ) { + /** + * Filter Head elements array. + * + * @param array $head_items Elements to be added to head after closing of title tag. + * + * Priority 10: preconnect + * Priority 30: preload + * Priority 50: styles + * @returns array + */ + $items = wpm_apply_filters_typed( 'array', 'rocket_head_items', [] ); + if ( empty( $items ) ) { + return $content; + } + + $this->head_items = []; + // Combine elements. + $elements = ''; + foreach ( $items as $item ) { + // Make sure that we don't have duplication based on `href` inside each `rel`. + if ( $this->is_duplicate( $item ) ) { + continue; + } + $elements .= "\n" . $this->prepare_element( $item ); + } + + return $content . $elements; + } + + /** + * Check if the item is duplicate. + * + * @param array $item Item to check. + * @return bool + */ + private function is_duplicate( $item ) { + if ( empty( $item['rel'] ) || empty( $item['href'] ) ) { + return false; + } + + if ( ! isset( $this->head_items[ $item['rel'] ] ) ) { + $this->head_items[ $item['rel'] ] = []; + } + + if ( ! isset( $this->head_items[ $item['rel'] ][ $item['href'] ] ) ) { + $this->head_items[ $item['rel'] ][ $item['href'] ] = true; + return false; + } + + return true; + } + + /** + * Prepare element HTML from the item array. + * + * @param array $element Item element. + * @return string + */ + private function prepare_element( $element ) { + $open_tag = ''; + if ( ! empty( $element['open_tag'] ) ) { + $open_tag = $element['open_tag']; + unset( $element['open_tag'] ); + } + + $close_tag = ''; + if ( ! empty( $element['close_tag'] ) ) { + $close_tag = $element['close_tag']; + unset( $element['close_tag'] ); + } + + $inner_content = ''; + if ( ! empty( $element['inner_content'] ) ) { + $inner_content = $element['inner_content']; + unset( $element['inner_content'] ); + } + + $attributes = []; + + ksort( $element, SORT_NATURAL ); + + foreach ( $element as $key => $value ) { + if ( is_int( $key ) ) { + $attributes[] = $value; + continue; + } + $attributes[] = $key . '="' . esc_attr( $value ) . '"'; + } + + $attributes_html = ! empty( $attributes ) ? ' ' . implode( ' ', $attributes ) : ''; + + return $open_tag . $attributes_html . '>' . $inner_content . $close_tag; + } + + /** + * Insert rocket_head into the buffer HTML + * + * @param string $html Buffer HTML. + * @return string + */ + public function insert_rocket_head( $html ) { + if ( empty( $html ) ) { + return $html; + } + + $filtered_buffer = preg_replace( + '##iU', + '' . wpm_apply_filters_typed( 'string', 'rocket_head', '' ), + $html, + 1 + ); + + if ( empty( $filtered_buffer ) ) { + return $html; + } + + return $filtered_buffer; + } +} diff --git a/inc/Engine/CriticalPath/CriticalCSSSubscriber.php b/inc/Engine/CriticalPath/CriticalCSSSubscriber.php index 8d663f8e99..38421de43b 100644 --- a/inc/Engine/CriticalPath/CriticalCSSSubscriber.php +++ b/inc/Engine/CriticalPath/CriticalCSSSubscriber.php @@ -4,6 +4,7 @@ use WP_Rocket\Admin\Options; use WP_Rocket\Admin\Options_Data; +use WP_Rocket\Engine\Common\Head\ElementTrait; use WP_Rocket\Engine\License\API\User; use WP_Rocket\Engine\Optimization\RegexTrait; use WP_Rocket\Event_Management\Subscriber_Interface; @@ -18,6 +19,7 @@ class CriticalCSSSubscriber implements Subscriber_Interface { use RegexTrait; use CommentTrait; + use ElementTrait; /** * Instance of Critical CSS. @@ -61,6 +63,13 @@ class CriticalCSSSubscriber implements Subscriber_Interface { */ protected $user; + /** + * Critical CSS contents. + * + * @var string + */ + private $critical_css_content = ''; + /** * Creates an instance of the Critical CSS Subscriber. * @@ -113,12 +122,13 @@ public static function get_subscribed_events() { [ 'async_css', 32 ], ], + 'rocket_head_items' => [ 'insert_css_in_head', 50 ], 'switch_theme' => 'maybe_regenerate_cpcss', 'rocket_excluded_inline_js_content' => 'exclude_inline_js', 'before_delete_post' => 'delete_cpcss', - 'rocket_before_rollback' => [ 'stop_critical_css_generation', 9 ], - 'wp_rocket_upgrade' => [ 'stop_critical_css_generation', 9 ], - 'admin_post_switch_to_rucss' => 'switch_to_rucss', + 'rocket_before_rollback' => [ 'stop_critical_css_generation', 9 ], + 'wp_rocket_upgrade' => [ 'stop_critical_css_generation', 9 ], + 'admin_post_switch_to_rucss' => 'switch_to_rucss', ]; // phpcs:enable WordPress.Arrays.MultipleStatementAlignment.DoubleArrowNotAligned } @@ -595,20 +605,43 @@ public function insert_critical_css_buffer( $buffer ) { return $buffer; } - $critical_css_content = str_replace( '\\', '\\\\', $critical_css_content ); - - $buffer = preg_replace( - '##iU', - '', - $buffer, - 1 - ); + $this->critical_css_content = str_replace( '\\', '\\\\', $critical_css_content ); $buffer = preg_replace( '#
#iU', $this->return_remove_cpcss_script() . '', $buffer, 1 ); return $this->add_meta_comment( 'async_css', $buffer ); } + /** + * Insert critical CSS into head. + * + * @param array $items Head elements. + * @return mixed + */ + public function insert_css_in_head( $items ) { + $css = $this->get_critical_css_content(); + if ( empty( $css ) ) { + return $items; + } + + $items[] = $this->style_tag( + $css, + [ + 'id' => 'rocket-critical-css', + ] + ); + return $items; + } + + /** + * Get critical CSS content, getter method for critical_css_content property. + * + * @return string + */ + public function get_critical_css_content() { + return $this->should_async_css() ? $this->critical_css_content : ''; + } + /** * Returns JS script to remove the critical css style from frontend. * diff --git a/inc/Engine/Optimization/GoogleFonts/AbstractGFOptimization.php b/inc/Engine/Optimization/GoogleFonts/AbstractGFOptimization.php index 271f94e6f7..7de1e16b5b 100644 --- a/inc/Engine/Optimization/GoogleFonts/AbstractGFOptimization.php +++ b/inc/Engine/Optimization/GoogleFonts/AbstractGFOptimization.php @@ -3,6 +3,7 @@ namespace WP_Rocket\Engine\Optimization\GoogleFonts; +use WP_Rocket\Engine\Common\Head\ElementTrait; use WP_Rocket\Engine\Media\Fonts\FontsTrait; /** @@ -12,6 +13,7 @@ */ abstract class AbstractGFOptimization { use FontsTrait; + use ElementTrait; /** * Allowed display values. @@ -106,32 +108,85 @@ protected function get_font_display_value(): string { } /** - * Returns the optimized markup for Google Fonts + * Check if preload google fonts is enabled or not using filter. * - * @since 3.9.1 + * @return bool + */ + protected function is_preload_enabled() { + return ! wpm_apply_filters_typed( 'boolean', 'rocket_disable_google_fonts_preload', false ); + } + + /** + * Prepare preload fonts to the head items. * - * @param string $url Google Fonts URL. + * @param array $fonts Fonts list. + * @param array $items Head items. + * @return array + */ + protected function prepare_preload_fonts_to_head( array $fonts, array $items ): array { + foreach ( $fonts as $font_url ) { + $items[] = $this->preload_link( + [ + 'href' => $font_url, + 'as' => 'style', + ] + ); + } + + return $items; + } + + /** + * Prepare stylesheets to the head. * - * @return string + * @param array $fonts Fonts list. + * @param array $items Head items. + * @return array */ - protected function get_optimized_markup( string $url ): string { - /** - * Filters whether to disable Google Fonts preloading. - * - * @since 3.18 - * - * @param bool $disable_google_fonts_preload Whether to disable Google Fonts preloading. Default false. - */ - if ( wpm_apply_filters_typed( 'boolean', 'rocket_disable_google_fonts_preload', false ) ) { - return sprintf( - '', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet - $url + protected function prepare_stylesheet_fonts_to_head( array $fonts, array $items ): array { + $preload_enabled = $this->is_preload_enabled(); + + foreach ( $fonts as $font_url ) { + $item = $this->stylesheet_link( + [ + 'href' => $font_url, + ] + ); + + if ( ! $preload_enabled ) { + $items[] = $item; + continue; + } + + $item['media'] = 'print'; + $item['onload'] = "this.media='all'"; + $items[] = $item; + + $items[] = $this->noscript_tag( + sprintf( '', $font_url ) // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet ); } - return sprintf( - '', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet - $url - ); + return $items; + } + + /** + * Insert font stylesheets into head. + * + * @param array $items Head elements. + * @return mixed + */ + public function insert_font_stylesheet_into_head( $items ) { + return $items; + } + + /** + * Insert font preloads into head. + * + * @param array $items Head elements. + * @return mixed + */ + public function insert_font_preload_into_head( $items ) { + return $items; } } diff --git a/inc/Engine/Optimization/GoogleFonts/Combine.php b/inc/Engine/Optimization/GoogleFonts/Combine.php index 9308f811ef..cd897894cb 100644 --- a/inc/Engine/Optimization/GoogleFonts/Combine.php +++ b/inc/Engine/Optimization/GoogleFonts/Combine.php @@ -32,6 +32,13 @@ class Combine extends AbstractGFOptimization { */ protected $subsets = ''; + /** + * Font urls. + * + * @var array + */ + protected $font_urls = []; + /** * Combines multiple Google Fonts links into one * @@ -42,6 +49,7 @@ class Combine extends AbstractGFOptimization { * @return string */ public function optimize( $html ): string { + $this->font_urls = []; Logger::info( 'GOOGLE FONTS COMBINE PROCESS STARTED.', [ 'GF combine process' ] ); $html_nocomments = $this->hide_comments( $html ); @@ -64,7 +72,7 @@ public function optimize( $html ): string { function ( $font ) use ( $exclusions ) { return ! $this->is_excluded( $font[0], $exclusions ); } - ); + ); $num_fonts = count( $filtered_fonts ); @@ -84,7 +92,7 @@ function ( $font ) use ( $exclusions ) { return $html; } - $html = preg_replace( '@<\/title>@i', '$0' . $this->get_optimized_markup( $this->get_combined_url() ), $html, 1 ); + $this->font_urls[] = $this->get_combined_url(); foreach ( $filtered_fonts as $font ) { $html = str_replace( $font[0], '', $html ); @@ -152,4 +160,47 @@ private function get_combined_url(): string { return esc_url( "https://fonts.googleapis.com/css?family={$this->fonts}{$this->subsets}&display={$display}" ); } + + /** + * Get font urls, getter method for font_urls property. + * + * @return array + */ + public function get_font_urls(): array { + return $this->font_urls; + } + + /** + * Insert font stylesheets into head. + * + * @param array $items Head elements. + * @return mixed + */ + public function insert_font_stylesheet_into_head( $items ) { + $font_urls = $this->get_font_urls(); + if ( empty( $font_urls ) ) { + return $items; + } + + return $this->prepare_stylesheet_fonts_to_head( $font_urls, $items ); + } + + /** + * Insert font preloads into head. + * + * @param array $items Head elements. + * @return mixed + */ + public function insert_font_preload_into_head( $items ) { + $font_urls = $this->get_font_urls(); + if ( empty( $font_urls ) ) { + return $items; + } + + if ( ! $this->is_preload_enabled() ) { + return $items; + } + + return $this->prepare_preload_fonts_to_head( $font_urls, $items ); + } } diff --git a/inc/Engine/Optimization/GoogleFonts/CombineV2.php b/inc/Engine/Optimization/GoogleFonts/CombineV2.php index b3878f064a..c92ccffa3c 100644 --- a/inc/Engine/Optimization/GoogleFonts/CombineV2.php +++ b/inc/Engine/Optimization/GoogleFonts/CombineV2.php @@ -14,6 +14,13 @@ class CombineV2 extends AbstractGFOptimization { use RegexTrait; + /** + * Font urls. + * + * @var array + */ + protected $font_urls = []; + /** * Combines multiple Google Fonts (API v2) links into one * @@ -24,6 +31,7 @@ class CombineV2 extends AbstractGFOptimization { * @return string */ public function optimize( $html ): string { + $this->font_urls = []; Logger::info( 'GOOGLE FONTS COMBINE-V2 PROCESS STARTED.', [ 'GF combine process' ] ); $processed_tags = []; @@ -73,9 +81,9 @@ function ( $tag ) use ( $exclusions ) { return $html; } - $families = array_unique( $families ); - $combined_tag = $this->get_optimized_markup( $this->get_combined_url( $families ) ); - $html = preg_replace( '@<\/title>@i', '$0' . $combined_tag, $html, 1 ); + $families = array_unique( $families ); + $combined_url = $this->get_combined_url( $families ); + $this->font_urls[] = $combined_url; foreach ( $processed_tags as $font ) { $html = str_replace( $font[0], '', $html ); @@ -85,7 +93,7 @@ function ( $tag ) use ( $exclusions ) { 'V2 Google Fonts successfully combined.', [ 'GF combine process', - 'url' => $combined_tag, + 'url' => $combined_url, ] ); @@ -161,4 +169,47 @@ private function get_concatenated_families( array $families ): string { return rtrim( $families_string, '&?' ); } + + /** + * Get font urls, getter method for font_urls property. + * + * @return array + */ + public function get_font_urls(): array { + return $this->font_urls; + } + + /** + * Insert font stylesheets into head. + * + * @param array $items Head elements. + * @return mixed + */ + public function insert_font_stylesheet_into_head( $items ) { + $font_urls = $this->get_font_urls(); + if ( empty( $font_urls ) ) { + return $items; + } + + return $this->prepare_stylesheet_fonts_to_head( $font_urls, $items ); + } + + /** + * Insert font preloads into head. + * + * @param array $items Head elements. + * @return mixed + */ + public function insert_font_preload_into_head( $items ) { + $font_urls = $this->get_font_urls(); + if ( empty( $font_urls ) ) { + return $items; + } + + if ( ! $this->is_preload_enabled() ) { + return $items; + } + + return $this->prepare_preload_fonts_to_head( $font_urls, $items ); + } } diff --git a/inc/Engine/Optimization/GoogleFonts/Subscriber.php b/inc/Engine/Optimization/GoogleFonts/Subscriber.php index 19232d36e8..28088738ca 100644 --- a/inc/Engine/Optimization/GoogleFonts/Subscriber.php +++ b/inc/Engine/Optimization/GoogleFonts/Subscriber.php @@ -55,6 +55,10 @@ public static function get_subscribed_events() { return [ 'wp_resource_hints' => [ 'preconnect', 10, 2 ], 'rocket_buffer' => [ 'process', 17 ], + 'rocket_head_items' => [ + [ 'insert_fonts_preload', 30 ], + [ 'insert_fonts_stylesheets', 50 ], + ], ]; } @@ -126,4 +130,32 @@ protected function is_allowed() { return ! is_user_logged_in() || (bool) $this->options->get( 'cache_logged_user', 0 ); } + + /** + * Insert fonts link stylesheets into head elements for v1 and v2. + * + * @param array $items Head elements. + * @return mixed + */ + public function insert_fonts_stylesheets( array $items ) { + if ( ! $this->is_allowed() ) { + return $items; + } + $items = $this->combine_v2->insert_font_stylesheet_into_head( $items ); + return $this->combine->insert_font_stylesheet_into_head( $items ); + } + + /** + * Insert fonts preloads into head elements for v1 and v2. + * + * @param array $items Head elements. + * @return mixed + */ + public function insert_fonts_preload( $items ) { + if ( ! $this->is_allowed() ) { + return $items; + } + $items = $this->combine_v2->insert_font_preload_into_head( $items ); + return $this->combine->insert_font_preload_into_head( $items ); + } } diff --git a/inc/Engine/Optimization/RUCSS/Controller/UsedCSS.php b/inc/Engine/Optimization/RUCSS/Controller/UsedCSS.php index 6c19692474..1ae6609644 100644 --- a/inc/Engine/Optimization/RUCSS/Controller/UsedCSS.php +++ b/inc/Engine/Optimization/RUCSS/Controller/UsedCSS.php @@ -5,6 +5,7 @@ use WP_Rocket\Admin\Options_Data; use WP_Rocket\Engine\Common\Context\ContextInterface; +use WP_Rocket\Engine\Common\Head\ElementTrait; use WP_Rocket\Engine\Optimization\CSSTrait; use WP_Rocket\Engine\Optimization\DynamicLists\DefaultLists\DataManager; use WP_Rocket\Engine\Optimization\RegexTrait; @@ -16,6 +17,7 @@ class UsedCSS { use RegexTrait; use CSSTrait; use CommentTrait; + use ElementTrait; /** * UsedCss Query instance. @@ -80,6 +82,20 @@ class UsedCSS { */ private $manager; + /** + * Used CSS contents. + * + * @var string + */ + private $used_css_content = ''; + + /** + * Preloaded font urls. + * + * @var array + */ + private $preloaded_fonts = []; + /** * Instantiate the class. * @@ -158,9 +174,10 @@ public function treeshake( string $html ): string { return $html; } + $this->used_css_content = $used_css_content; + $html = $this->remove_used_css_from_html( $clean_html, $html ); - $html = $this->add_used_css_to_html( $html, $used_css_content ); - $html = $this->add_used_fonts_preload( $html, $used_css_content ); + $this->add_used_fonts_preload( $used_css_content ); $html = $this->remove_google_font_preconnect( $html ); if ( ! empty( $used_css->id ) ) { @@ -354,26 +371,48 @@ private function remove_internal_styles_from_html( string $clean_html, string $h } /** - * Alter HTML string and add the used CSS style in
tag, + * Add the used CSS style in
tag,
*
- * @param string $html HTML content.
- * @param string $used_css Used CSS content.
+ * @param array $items Head items.
*
- * @return string HTML content.
+ * @return array Filtered head items.
*/
- private function add_used_css_to_html( string $html, string $used_css ): string {
- $replace = preg_replace(
- '##iU',
- '' . $this->get_used_css_markup( $used_css ),
- $html,
- 1
+ public function add_used_css_to_html( array $items ): array {
+ $used_css = $this->get_used_css_content();
+ if ( empty( $used_css ) ) {
+ return $items;
+ }
+
+ $items[] = $this->style_tag(
+ $this->get_used_css_markup( $used_css ),
+ [
+ 'id' => 'wpr-usedcss',
+ ]
);
+ return $items;
+ }
- if ( null === $replace ) {
- return $html;
+ /**
+ * Insert preload fonts into page head.
+ *
+ * @param array $items Head elements.
+ * @return mixed
+ */
+ public function insert_preload_fonts( $items ) {
+ if ( empty( $this->preloaded_fonts ) ) {
+ return $items;
+ }
+ foreach ( $this->preloaded_fonts as $font ) {
+ $items[] = $this->preload_link(
+ [
+ 'href' => esc_url( $font ),
+ 'as' => 'font',
+ 1 => 'crossorigin',
+ ]
+ );
}
- return $replace;
+ return $items;
}
/**
@@ -394,12 +433,7 @@ private function get_used_css_markup( string $used_css ): string {
$used_css = apply_filters( 'rocket_usedcss_content', $used_css );
$used_css = str_replace( '\\', '\\\\', $used_css );// Guard the backslashes before passing the content to preg_replace.
- $used_css = $this->handle_charsets( $used_css, false );
-
- return sprintf(
- '',
- $used_css
- );
+ return $this->handle_charsets( $used_css, false );
}
/**
@@ -445,12 +479,11 @@ public function get_not_completed_count() {
/**
* Add preload links for the fonts in the used CSS
*
- * @param string $html HTML content.
* @param string $used_css Used CSS content.
*
- * @return string
+ * @return void
*/
- private function add_used_fonts_preload( string $html, string $used_css ): string {
+ private function add_used_fonts_preload( string $used_css ): void {
/**
* Filters the fonts preload from the used CSS
*
@@ -459,15 +492,15 @@ private function add_used_fonts_preload( string $html, string $used_css ): strin
* @param bool $enable True to enable, false to disable.
*/
if ( ! apply_filters( 'rocket_enable_rucss_fonts_preload', true ) ) {
- return $html;
+ return;
}
if ( ! preg_match_all( '/@font-face\s*{\s*(?