From 6b3b716970402f5d148e3337fcf9152a6838c081 Mon Sep 17 00:00:00 2001 From: Adnan Haque Date: Sun, 20 Oct 2024 16:55:30 +0300 Subject: [PATCH 01/27] Add a dummy foundation pages class --- projects/plugins/boost/app/lib/Foundation_Pages.php | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 projects/plugins/boost/app/lib/Foundation_Pages.php diff --git a/projects/plugins/boost/app/lib/Foundation_Pages.php b/projects/plugins/boost/app/lib/Foundation_Pages.php new file mode 100644 index 0000000000000..870407f5e4624 --- /dev/null +++ b/projects/plugins/boost/app/lib/Foundation_Pages.php @@ -0,0 +1,9 @@ + Date: Sun, 20 Oct 2024 16:56:23 +0300 Subject: [PATCH 02/27] changelog --- projects/plugins/boost/changelog/add-foundation-pages | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/plugins/boost/changelog/add-foundation-pages diff --git a/projects/plugins/boost/changelog/add-foundation-pages b/projects/plugins/boost/changelog/add-foundation-pages new file mode 100644 index 0000000000000..95745ea51bf07 --- /dev/null +++ b/projects/plugins/boost/changelog/add-foundation-pages @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Foundation Pages: Added foundation pages functionality From 99cd265de790c959bed8cd8c9f1ba8b0268c8ab9 Mon Sep 17 00:00:00 2001 From: Adnan Haque <3737780+haqadn@users.noreply.github.com> Date: Mon, 21 Oct 2024 11:02:15 +0200 Subject: [PATCH 03/27] Boost: Implement partial cloud CSS regeneration (#39833) * Improve the jetpack_boost_critical_css_urls filter * Add a filter for the source providers * Remove front_page as provider when foundation page is setup * Add context support for core providers * Implement partial regeneration * Regenerate blog page if blog page itself is updated * Cast core page ids to int --- .../plugins/boost/app/class-jetpack-boost.php | 4 +++ .../boost/app/lib/Foundation_Pages.php | 21 ++++++++++++++- .../boost/app/lib/critical-css/Regenerate.php | 2 +- .../source-providers/Source_Providers.php | 22 ++++++++++------ .../providers/WP_Core_Provider.php | 26 ++++++++++++------- .../optimizations/cloud-css/Cloud_CSS.php | 16 +++++++----- .../cloud-css/Cloud_CSS_Followup.php | 2 +- 7 files changed, 66 insertions(+), 27 deletions(-) diff --git a/projects/plugins/boost/app/class-jetpack-boost.php b/projects/plugins/boost/app/class-jetpack-boost.php index edf3e4e422f6c..3d28fabf5bda0 100644 --- a/projects/plugins/boost/app/class-jetpack-boost.php +++ b/projects/plugins/boost/app/class-jetpack-boost.php @@ -27,6 +27,7 @@ use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_State; use Automattic\Jetpack_Boost\Lib\Critical_CSS\Critical_CSS_Storage; use Automattic\Jetpack_Boost\Lib\Critical_CSS\Generator; +use Automattic\Jetpack_Boost\Lib\Foundation_Pages; use Automattic\Jetpack_Boost\Lib\Setup; use Automattic\Jetpack_Boost\Lib\Site_Health; use Automattic\Jetpack_Boost\Lib\Status; @@ -108,6 +109,9 @@ public function __construct() { $modules_setup = new Modules_Setup(); Setup::add( $modules_setup ); + $foundation_pages = new Foundation_Pages(); + Setup::add( $foundation_pages ); + // Initialize the Admin experience. $this->init_admin( $modules_setup ); diff --git a/projects/plugins/boost/app/lib/Foundation_Pages.php b/projects/plugins/boost/app/lib/Foundation_Pages.php index 870407f5e4624..8d1c231e8ccfd 100644 --- a/projects/plugins/boost/app/lib/Foundation_Pages.php +++ b/projects/plugins/boost/app/lib/Foundation_Pages.php @@ -2,7 +2,26 @@ namespace Automattic\Jetpack_Boost\Lib; -class Foundation_Pages { +use Automattic\Jetpack_Boost\Contracts\Has_Setup; + +class Foundation_Pages implements Has_Setup { + + public function setup() { + add_filter( 'jetpack_boost_critical_css_providers', array( $this, 'remove_ccss_front_page_provider' ), 10, 2 ); + } + + public function remove_ccss_front_page_provider( $providers ) { + $filtered_providers = array(); + + foreach ( $providers as $provider ) { + if ( $provider['key'] !== 'core_front_page' ) { + $filtered_providers[] = $provider; + } + } + + return $filtered_providers; + } + public function get_pages() { return array(); } diff --git a/projects/plugins/boost/app/lib/critical-css/Regenerate.php b/projects/plugins/boost/app/lib/critical-css/Regenerate.php index 6b25fdf434c3c..cab4b7fc805ec 100644 --- a/projects/plugins/boost/app/lib/critical-css/Regenerate.php +++ b/projects/plugins/boost/app/lib/critical-css/Regenerate.php @@ -35,7 +35,7 @@ public function start() { // If this is a cloud CSS request, we need to trigger the generation // of the CSS and return the URL to the CSS file. $cloud_css = new Cloud_CSS(); - $cloud_css->regenerate_cloud_css( Cloud_CSS::REGENERATE_REASON_USER_REQUEST ); + $cloud_css->regenerate_cloud_css( Cloud_CSS::REGENERATE_REASON_USER_REQUEST, $cloud_css->get_all_providers() ); Cloud_CSS_Followup::schedule(); } diff --git a/projects/plugins/boost/app/lib/critical-css/source-providers/Source_Providers.php b/projects/plugins/boost/app/lib/critical-css/source-providers/Source_Providers.php index 30840fae12c14..c3f342c97cae9 100644 --- a/projects/plugins/boost/app/lib/critical-css/source-providers/Source_Providers.php +++ b/projects/plugins/boost/app/lib/critical-css/source-providers/Source_Providers.php @@ -113,10 +113,10 @@ public function get_current_request_css() { * * @return array */ - public function get_provider_sources() { + public function get_provider_sources( $context_posts = array() ) { $sources = array(); - $wp_core_provider_urls = WP_Core_Provider::get_critical_source_urls(); + $wp_core_provider_urls = WP_Core_Provider::get_critical_source_urls( $context_posts ); $flat_wp_core_urls = array(); foreach ( $wp_core_provider_urls as $urls ) { $flat_wp_core_urls = array_merge( $flat_wp_core_urls, $urls ); @@ -127,7 +127,7 @@ public function get_provider_sources() { // For each provider, // Gather a list of URLs that are going to be used as Critical CSS source. - foreach ( $provider::get_critical_source_urls() as $group => $urls ) { + foreach ( $provider::get_critical_source_urls( $context_posts ) as $group => $urls ) { if ( empty( $urls ) ) { continue; } @@ -146,25 +146,31 @@ public function get_provider_sources() { $key = $provider_name . '_' . $group; - // For each URL + // For each provider // Track the state and errors in a state array. $sources[] = array( 'key' => $key, 'label' => $provider::describe_key( $key ), /** - * Filters the URLs used by Critical CSS + * Filters the URLs used by Critical CSS for each provider. * * @param array $urls The list of URLs to be used to generate critical CSS - * + * @param string $provider The provider name. * @since 1.0.0 */ - 'urls' => apply_filters( 'jetpack_boost_critical_css_urls', $urls ), + 'urls' => apply_filters( 'jetpack_boost_critical_css_urls', $urls, $provider ), 'success_ratio' => $provider::get_success_ratio(), ); } } - return $sources; + /** + * Filters the list of Critical CSS source providers. + * + * @param array $sources The list of Critical CSS source providers. + * @since $$next-version$$ + */ + return apply_filters( 'jetpack_boost_critical_css_providers', $sources ); } /** diff --git a/projects/plugins/boost/app/lib/critical-css/source-providers/providers/WP_Core_Provider.php b/projects/plugins/boost/app/lib/critical-css/source-providers/providers/WP_Core_Provider.php index e2b63769a6784..4689d904714b4 100644 --- a/projects/plugins/boost/app/lib/critical-css/source-providers/providers/WP_Core_Provider.php +++ b/projects/plugins/boost/app/lib/critical-css/source-providers/providers/WP_Core_Provider.php @@ -27,24 +27,30 @@ class WP_Core_Provider extends Provider { public static function get_critical_source_urls( $context_posts = array() ) { $urls = array(); - // TODO: Limit to provided context posts. + $front_page = (int) get_option( 'page_on_front' ); + $posts_page = (int) get_option( 'page_for_posts' ); - $front_page = get_option( 'page_on_front' ); - if ( ! empty( $front_page ) ) { + if ( ! empty( $front_page ) && empty( $context_posts ) ) { $permalink = get_permalink( $front_page ); if ( ! empty( $permalink ) ) { $urls['front_page'] = array( $permalink ); } } - $posts_page = get_option( 'page_for_posts' ); - if ( ! empty( $posts_page ) ) { - $permalink = get_permalink( $posts_page ); - if ( ! empty( $permalink ) ) { - $urls['posts_page'] = array( $permalink ); + $context_post_types = wp_list_pluck( $context_posts, 'post_type' ); + $context_post_ids = wp_list_pluck( $context_posts, 'ID' ); + + // The blog page is only in context if the context posts include a 'post' post_type. + // Or, if the blog page itself is in context. + if ( empty( $context_post_types ) || in_array( 'post', $context_post_types, true ) || in_array( $posts_page, $context_post_ids, true ) ) { + if ( ! empty( $posts_page ) ) { + $permalink = get_permalink( $posts_page ); + if ( ! empty( $permalink ) ) { + $urls['posts_page'] = array( $permalink ); + } + } else { + $urls['posts_page'] = (array) home_url( '/' ); } - } else { - $urls['posts_page'] = (array) home_url( '/' ); } return $urls; diff --git a/projects/plugins/boost/app/modules/optimizations/cloud-css/Cloud_CSS.php b/projects/plugins/boost/app/modules/optimizations/cloud-css/Cloud_CSS.php index 8664505d583cf..64db2088cc0c4 100644 --- a/projects/plugins/boost/app/modules/optimizations/cloud-css/Cloud_CSS.php +++ b/projects/plugins/boost/app/modules/optimizations/cloud-css/Cloud_CSS.php @@ -155,11 +155,11 @@ public function handle_save_post( $post_id, $post ) { // phpcs:ignore VariableAn return; } - $this->regenerate_cloud_css( self::REGENERATE_REASON_SAVE_POST ); + $this->regenerate_cloud_css( self::REGENERATE_REASON_SAVE_POST, $this->get_all_providers( array( $post ) ) ); } - public function regenerate_cloud_css( $reason ) { - $result = $this->generate_cloud_css( $reason, $this->get_existing_sources() ); + public function regenerate_cloud_css( $reason, $providers ) { + $result = $this->generate_cloud_css( $reason, $providers ); if ( is_wp_error( $result ) ) { $state = new Critical_CSS_State(); $state->set_error( $result->get_error_message() )->save(); @@ -171,18 +171,22 @@ public function regenerate_cloud_css( $reason ) { * Called when stored Critical CSS has been invalidated. Triggers a new Cloud CSS request. */ public function handle_critical_css_invalidated() { - $this->regenerate_cloud_css( self::REGENERATE_REASON_INVALIDATED ); + $this->regenerate_cloud_css( self::REGENERATE_REASON_INVALIDATED, $this->get_all_providers() ); Cloud_CSS_Followup::schedule(); } + public function get_all_providers( $context_posts = array() ) { + $source_providers = new Source_Providers(); + return $source_providers->get_provider_sources( $context_posts ); + } + public function get_existing_sources() { $state = new Critical_CSS_State(); $data = $state->get(); if ( ! empty( $data['providers'] ) ) { $providers = $data['providers']; } else { - $source_providers = new Source_Providers(); - $providers = $source_providers->get_provider_sources(); + $providers = $this->get_all_providers(); } return $providers; diff --git a/projects/plugins/boost/app/modules/optimizations/cloud-css/Cloud_CSS_Followup.php b/projects/plugins/boost/app/modules/optimizations/cloud-css/Cloud_CSS_Followup.php index dd97fe0985a8e..197c5ec597a9f 100644 --- a/projects/plugins/boost/app/modules/optimizations/cloud-css/Cloud_CSS_Followup.php +++ b/projects/plugins/boost/app/modules/optimizations/cloud-css/Cloud_CSS_Followup.php @@ -29,7 +29,7 @@ public static function run() { $state = new Critical_CSS_State(); if ( $state->has_errors() ) { $cloud_css = new Cloud_CSS(); - $cloud_css->regenerate_cloud_css( Cloud_CSS::REGENERATE_REASON_FOLLOWUP ); + $cloud_css->regenerate_cloud_css( Cloud_CSS::REGENERATE_REASON_FOLLOWUP, $cloud_css->get_existing_sources() ); } } From f56920b44f3f1101a1be538b2189814b6819400a Mon Sep 17 00:00:00 2001 From: Peter Petrov Date: Tue, 22 Oct 2024 17:46:55 +0300 Subject: [PATCH 04/27] Boost: Add foundation pages UI (#39832) --- .../foundation-pages/foundation-pages.tsx | 23 +++ .../lib/stores/foundation-pages.ts | 32 ++++ .../foundation-pages/meta/meta.module.scss | 111 +++++++++++ .../features/foundation-pages/meta/meta.tsx | 178 ++++++++++++++++++ .../src/js/features/module/module.module.scss | 4 + .../settings-item/settings-item.module.scss | 28 +++ .../ui/settings-item/settings-item.tsx | 23 +++ .../src/js/lib/utils/get-support-link.ts | 10 + .../app/assets/src/js/pages/index/index.tsx | 2 + .../app/data-sync/Foundation_Pages_Entry.php | 56 ++++++ .../boost/app/lib/Foundation_Pages.php | 10 + .../boost/changelog/add-foundation-pages-ui | 5 + projects/plugins/boost/wp-js-data-sync.php | 6 + 13 files changed, 488 insertions(+) create mode 100644 projects/plugins/boost/app/assets/src/js/features/foundation-pages/foundation-pages.tsx create mode 100644 projects/plugins/boost/app/assets/src/js/features/foundation-pages/lib/stores/foundation-pages.ts create mode 100644 projects/plugins/boost/app/assets/src/js/features/foundation-pages/meta/meta.module.scss create mode 100644 projects/plugins/boost/app/assets/src/js/features/foundation-pages/meta/meta.tsx create mode 100644 projects/plugins/boost/app/assets/src/js/features/ui/settings-item/settings-item.module.scss create mode 100644 projects/plugins/boost/app/assets/src/js/features/ui/settings-item/settings-item.tsx create mode 100644 projects/plugins/boost/app/assets/src/js/lib/utils/get-support-link.ts create mode 100644 projects/plugins/boost/app/data-sync/Foundation_Pages_Entry.php create mode 100644 projects/plugins/boost/changelog/add-foundation-pages-ui diff --git a/projects/plugins/boost/app/assets/src/js/features/foundation-pages/foundation-pages.tsx b/projects/plugins/boost/app/assets/src/js/features/foundation-pages/foundation-pages.tsx new file mode 100644 index 0000000000000..d676f1e5e3235 --- /dev/null +++ b/projects/plugins/boost/app/assets/src/js/features/foundation-pages/foundation-pages.tsx @@ -0,0 +1,23 @@ +import { __ } from '@wordpress/i18n'; +import Meta from './meta/meta'; +import SettingsItem from '$features/ui/settings-item/settings-item'; + +const FoundationPages = () => { + return ( + + { __( + 'List the most important pages of your site. They will be optimized. The Page Speed scores are based on the first foundation page.', + 'jetpack-boost' + ) } +

+ } + > + +
+ ); +}; + +export default FoundationPages; diff --git a/projects/plugins/boost/app/assets/src/js/features/foundation-pages/lib/stores/foundation-pages.ts b/projects/plugins/boost/app/assets/src/js/features/foundation-pages/lib/stores/foundation-pages.ts new file mode 100644 index 0000000000000..5e3f2a128bc3b --- /dev/null +++ b/projects/plugins/boost/app/assets/src/js/features/foundation-pages/lib/stores/foundation-pages.ts @@ -0,0 +1,32 @@ +import { useDataSync } from '@automattic/jetpack-react-data-sync-client'; +import { z } from 'zod'; + +/** + * Hook to get the Foundation Pages. + */ +export function useFoundationPages(): [ string[], ( newValue: string[] ) => void ] { + const [ { data }, { mutate } ] = useDataSync( + 'jetpack_boost_ds', + 'foundation_pages_list', + z.array( z.string() ) + ); + + function updatePages( newValue: string[] ) { + mutate( newValue ); + } + + return [ data || [], updatePages ]; +} + +const FoundationPagesProperties = z.object( { max_pages: z.number() } ); +type FoundationPagesProperties = z.infer< typeof FoundationPagesProperties >; + +export function useFoundationPagesProperties(): FoundationPagesProperties | undefined { + const [ { data } ] = useDataSync( + 'jetpack_boost_ds', + 'foundation_pages_properties', + FoundationPagesProperties + ); + + return data; +} diff --git a/projects/plugins/boost/app/assets/src/js/features/foundation-pages/meta/meta.module.scss b/projects/plugins/boost/app/assets/src/js/features/foundation-pages/meta/meta.module.scss new file mode 100644 index 0000000000000..2c4b87a1db4c0 --- /dev/null +++ b/projects/plugins/boost/app/assets/src/js/features/foundation-pages/meta/meta.module.scss @@ -0,0 +1,111 @@ +.wrapper { + font-size: 14px; + line-height: 22px; + + :global(button ~ button) { + margin-left: 20px !important; + } + + .head { + display: flex; + flex-direction: row; + align-items: flex-start; + + :global(svg) { + fill: var(--jetpack-green-40); + } + + .summary { + flex-grow: 1; + margin-right: 5px; + color: var(--gray-40); + } + } + + .body { + margin-top: 16px; + + .section { + padding: 16px; + border-radius: 4px; + background-color: var(--gray-0); + + & ~ .section { + margin-top: 16px; + } + + &.has-error { + textarea { + border-color: var( --red-40 ); + } + + .error-message { + display: block; + color: var( --red-40 ); + } + + .error { + color: var( --red-40 ); + } + } + + .title { + margin-bottom: 16px; + font-size: 16px; + line-height: 1; + font-weight: 600; + } + + textarea { + display: block; + margin-bottom: 8px; + width: 100%; + padding: 12px 16px; + border-radius: 4px; + border: 1px solid var( --gray-10 ); + background: var( --primary-white ); + color: #000; + } + + .error-message { + display: none; + } + + .description { + margin-top: 8px; + font-size: 14px; + line-height: 1.5; + color: var( --gray-60 ); + font-weight: 400; + } + + label { + display: block; + font-size: 16px; + line-height: 1.5; + + &:not(:last-child) { + margin-bottom: 8px; + } + } + } + + .button { + margin-top: 16px; + } + } + + .see-logs-link { + font-size: 16px; + line-height: 1.5; + } + + .logging-toggle { + margin-right: 1em; + float: left; + } + + .clearfix { + clear: both; + } +} diff --git a/projects/plugins/boost/app/assets/src/js/features/foundation-pages/meta/meta.tsx b/projects/plugins/boost/app/assets/src/js/features/foundation-pages/meta/meta.tsx new file mode 100644 index 0000000000000..3e10c3c6f1742 --- /dev/null +++ b/projects/plugins/boost/app/assets/src/js/features/foundation-pages/meta/meta.tsx @@ -0,0 +1,178 @@ +import { Button, Notice } from '@automattic/jetpack-components'; +import { __, _n, sprintf } from '@wordpress/i18n'; +import ChevronDown from '$svg/chevron-down'; +import ChevronUp from '$svg/chevron-up'; +import React, { useEffect, useState } from 'react'; +import clsx from 'clsx'; +import styles from './meta.module.scss'; +import { useFoundationPages, useFoundationPagesProperties } from '../lib/stores/foundation-pages'; +import { createInterpolateElement } from '@wordpress/element'; +import { recordBoostEvent } from '$lib/utils/analytics'; +import getSupportLink from '$lib/utils/get-support-link'; + +const Meta = () => { + const [ isExpanded, setIsExpanded ] = useState( false ); + const [ foundationPages, setFoundationPages ] = useFoundationPages(); + const foundationPagesProperties = useFoundationPagesProperties(); + + const updateFoundationPages = ( newValue: string ) => { + const newItems = newValue.split( '\n' ).map( line => line.trim() ); + + setFoundationPages( newItems ); + }; + + let content = null; + + if ( foundationPagesProperties !== undefined ) { + content = ( + + ); + } else { + content = ( + +

+ { createInterpolateElement( + __( + 'Refresh the page and try again. If the issue persists, please contact support.', + 'jetpack-boost' + ), + { + link: ( + // eslint-disable-next-line jsx-a11y/anchor-has-content + { + recordBoostEvent( 'foundation_pages_properties_failed', {} ); + } } + /> + ), + } + ) } +

+
+ ); + } + + return ( +
+
+
+ { foundationPagesProperties && + sprintf( + /* translators: %1$d is the number of foundation pages added, %2$d is the maximum number allowed */ + __( '%1$d / %2$d added', 'jetpack-boost' ), + foundationPages.length, + foundationPagesProperties.max_pages + ) } +
+
+ +
+
+ { isExpanded &&
{ content }
} +
+ ); +}; + +type ListProps = { + items: string; + setItems: ( newValue: string ) => void; + maxItems: number; +}; + +const List: React.FC< ListProps > = ( { items, setItems, maxItems } ) => { + const [ inputValue, setInputValue ] = useState( items ); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [ inputInvalid, setInputInvalid ] = useState( false ); + + const inputRows = Math.min( maxItems, 10 ); + + const validateInputValue = ( value: string ) => { + setInputValue( value ); + setInputInvalid( ! validateItems( value ) ); + }; + + const validateItems = ( value: string ) => { + const lines = value + .split( '\n' ) + .map( line => line.trim() ) + .filter( line => line.trim() !== '' ); + + // There should always be at least one foundation page. + if ( lines.length === 0 ) { + return false; + } + + // Check if the number of items exceeds maxItems + if ( lines.length > maxItems ) { + return false; + } + + return true; + }; + + useEffect( () => { + setInputValue( items ); + }, [ items ] ); + + function save() { + setItems( inputValue ); + } + + return ( +
+