diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index db08306..296338e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -47,6 +47,8 @@ peptide-starter/ │ ├── newsletter-admin.php # Subscriber viewer + CSV-safe export + │ │ # /newsletter-unsubscribe route. │ ├── mail-diagnostic.php # [v1.5.1] Tools → Mail Test — wp_mail probe. +│ ├── perf-asset-policy.php # [v1.6.0] Mobile perf: dequeue + font slim +│ │ # + preconnect + defer. │ └── page-setup.php # Auto-create pages on activation + │ # v1.5.0 user migration (enrol into verify). │ @@ -265,7 +267,7 @@ for out-of-band updates. ## Version -**Current:** v1.5.2 (2026-04-14) +**Current:** v1.6.0 (2026-04-23) Bump `style.css` header line 7 and `PEPTIDE_STARTER_VERSION` in `functions.php` together on release. diff --git a/CHANGELOG.md b/CHANGELOG.md index a5786be..69c9de0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,49 @@ All notable changes to the Peptide Starter Theme are documented in this file. +## [1.6.0] - 2026-04-23 — Mobile Performance Phase 1 + +Mobile-perf optimization targeting LCP reduction through conditional asset +dequeue, font weight slimming, and font server preconnect. Measured baseline +LCP on Moto G Power (Slow 4G): 5.4s; target after Phase 1: ~2.5s. + +### Added + +- `inc/perf-asset-policy.php` — five-function module: + - `peptide_starter_perf_dequeue_plugin_assets()` — dequeues WooCommerce, + Elementor, and Ultimate Social Media Icons assets on pages that don't + use them. Hook: `wp_enqueue_scripts` priority 100. + - `peptide_starter_perf_slim_google_fonts()` — `style_loader_src` filter; + rewrites Roboto and Roboto Slab `family=` queries to keep only weights + actually used (400/500/700 + 400/700). Drops 72 font faces to 5. + - `peptide_starter_perf_resource_hints()` — `wp_resource_hints` filter; + adds preconnect hints for `fonts.googleapis.com` and `fonts.gstatic.com`. + - `peptide_starter_perf_defer_cookie_notice()` — `script_loader_tag` filter; + appends `defer` attribute to the cookie-notice front-end script. + - `peptide_starter_page_uses_elementor( int $post_id )` — helper. +- Kill-switch constant `PEPTIDE_STARTER_PERF_DEQUEUE` (default `true`). + Define `false` in `wp-config.php` to disable all dequeues instantly. +- Filterable handle lists: `peptide_starter_perf_woocommerce_handles`, + `peptide_starter_perf_elementor_handles`, `peptide_starter_perf_usmi_handles`, + `peptide_starter_perf_font_weights`. +- `tests/test-perf-asset-policy.php` — 13 unit-test cases covering kill-switch, + per-page-context dequeue logic, font slim happy/edge cases, filter overrides, + preconnect insertion, defer attribute application. + +### Changed + +- `style.css` Version header bumped 1.5.2 → 1.6.0. +- `PEPTIDE_STARTER_VERSION` constant bumped to `1.6.0`. + +### Why + +PageSpeed Insights mobile score on `https://peptiderepo.com/` was 74 with +LCP 5.4s, FCP 2.6s. Desktop scored ~95 — the gap was mobile-specific +(weaker CPU + slower network amplify the cost of 13 render-blocking CSS +files in ``, of which the homepage actually needed at most 4). +Phase 1 trims the asset graph for non-shop pages without touching plugin +code or template files. + ## [1.5.2] - 2026-04-14 — Security Same-day follow-up to v1.5.1. Closes four issues the v1.5.1 post-merge diff --git a/CONVENTIONS.md b/CONVENTIONS.md index c3f2344..54919af 100644 --- a/CONVENTIONS.md +++ b/CONVENTIONS.md @@ -129,6 +129,43 @@ do_action( 'peptide_starter_hero_before' ); - **languages/:** Translation files - **template-parts/:** Reusable template parts +## How to add a performance policy rule + +When adding a new asset-perf rule (e.g., a new plugin's CSS to dequeue on +non-relevant pages, or a new font family to slim): + +1. **For dequeues:** edit `inc/perf-asset-policy.php`. Add the plugin's + handle list to a new function (or extend the existing arrays via the + matching filter — `peptide_starter_perf__handles`). Hook the + new dequeue function on `wp_enqueue_scripts` priority 100, gated by + `PEPTIDE_STARTER_PERF_DEQUEUE` and a context predicate (e.g., + `is_front_page()`, `! is_woocommerce()`). + +2. **For font slimming:** add the family to the default array in + `peptide_starter_perf_slim_google_fonts()`, or override at runtime + via the `peptide_starter_perf_font_weights` filter. Specify only + weights actually used by the design system; defaulting to "all weights" + defeats the purpose. + +3. **For new resource hints:** extend `peptide_starter_perf_resource_hints()` + with the new domain in the `preconnect` array. WP renders the + `` tag automatically; don't echo your own. + +4. **For deferring third-party scripts:** add the script handle to the + list in `peptide_starter_perf_defer_cookie_notice()` (or rename the + function if it grows beyond cookie-notice). + +5. **Always add a test case** in `tests/test-perf-asset-policy.php` + covering the new rule's happy path AND its negation (the page-context + where the rule must NOT apply). The dequeue logic is silent if a + handle name is wrong — only tests catch this before deploy. + +6. **Verify on production handles** before committing. Plugin handle + names are not always what you'd guess. SSH to Hostinger and run + `wp eval 'global $wp_styles; do_action("wp_enqueue_scripts"); + foreach($wp_styles->registered as $h=>$o){echo $h.PHP_EOL;}'` to + dump the actual handle names. + ## Step-by-Step Guides ### How to Add a New Template diff --git a/functions.php b/functions.php index 62fc211..2f3847b 100644 --- a/functions.php +++ b/functions.php @@ -15,6 +15,7 @@ * @see inc/page-setup.php — auto-create pages + v1.5.0 user migration * @see inc/mail-diagnostic.php — admin deliverability test tool * @see inc/helpers.php — nav walker, menu fallback, gates, utilities + * @see inc/perf-asset-policy.php — asset dequeue + font slim + preconnect + defer * * @package peptide-starter */ @@ -25,7 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) { } // Theme constants. -define( 'PEPTIDE_STARTER_VERSION', '1.5.2' ); +define( 'PEPTIDE_STARTER_VERSION', '1.6.0' ); define( 'PEPTIDE_STARTER_DIR', get_template_directory() ); define( 'PEPTIDE_STARTER_URI', get_template_directory_uri() ); @@ -42,6 +43,7 @@ require_once PEPTIDE_STARTER_DIR . '/inc/contact-handler.php'; require_once PEPTIDE_STARTER_DIR . '/inc/page-setup.php'; require_once PEPTIDE_STARTER_DIR . '/inc/newsletter-admin.php'; require_once PEPTIDE_STARTER_DIR . '/inc/mail-diagnostic.php'; +require_once PEPTIDE_STARTER_DIR . '/inc/perf-asset-policy.php'; /** * Set up theme defaults and register support for WordPress features. @@ -69,6 +71,7 @@ function peptide_starter_setup() { load_theme_textdomain( 'peptide-starter', PEPTIDE_STARTER_DIR . '/languages' ); } add_action( 'after_setup_theme', 'peptide_starter_setup' ); +add_action( 'after_setup_theme', 'peptide_starter_perf_asset_policy_init' ); /** * Register and enqueue stylesheets and scripts. diff --git a/inc/perf-asset-policy.php b/inc/perf-asset-policy.php new file mode 100644 index 0000000..da75c9a --- /dev/null +++ b/inc/perf-asset-policy.php @@ -0,0 +1,313 @@ +queue ) ) { + $elementor_post_styles = array(); + foreach ( $wp_styles->queue as $handle ) { + if ( isset( $wp_styles->registered[ $handle ] ) ) { + $src = $wp_styles->registered[ $handle ]->src; + // Detect Elementor per-post CSS by path pattern. + if ( $src && preg_match( '/uploads\/elementor\/css\/post-\d+\.css/', $src ) ) { + $elementor_post_styles[] = $handle; + } + } + } + + foreach ( $elementor_post_styles as $handle ) { + wp_dequeue_style( $handle ); + } + } + } + + // Ultimate Social Media Icons: dequeue on homepage, archives, and blog index. + if ( is_front_page() || is_home() || is_archive() ) { + $usmi_handles = apply_filters( + 'peptide_starter_perf_usmi_handles', + array( + 'SFSImainCss', + 'SFSIjqueryModernizr', + 'SFSIjqueryShuffle', + 'SFSIjqueryrandom-shuffle', + 'SFSICustomJs', + ) + ); + + foreach ( $usmi_handles as $handle ) { + wp_dequeue_style( $handle ); + wp_dequeue_script( $handle ); + } + } +} + +/** + * Slim Google Fonts to essential weights only. + * + * Rewrites fonts.googleapis.com URLs to keep only: + * - Roboto: 400, 500, 700 (no italics) + * - Roboto Slab: 400, 700 (no italics) + * + * This reduces the font CSS from ~6KB to <1KB and the font payload by ~80%. + * Filterable per family via 'peptide_starter_perf_font_weights'. + * + * @param string $src Stylesheet URL. + * @param string $handle Stylesheet handle. + * @return string Rewritten URL or original. + */ +function peptide_starter_perf_slim_google_fonts( $src, $handle ) { + if ( ! $src || false === strpos( $src, 'fonts.googleapis.com' ) ) { + return $src; + } + + // Parse the URL to get the query string. + $parsed = wp_parse_url( $src ); + if ( empty( $parsed['query'] ) ) { + return $src; + } + + // Extract family parameter. + parse_str( $parsed['query'], $query_vars ); + if ( empty( $query_vars['family'] ) ) { + return $src; + } + + $families_str = $query_vars['family']; + + // Decode the family string (it may be URL-encoded). + $families_str = rawurldecode( $families_str ); + + // Split families by + (multiple families in one request). + $families = explode( '+', $families_str ); + + // Get configured font weights from filter. + $font_weights_config = apply_filters( + 'peptide_starter_perf_font_weights', + array( + 'Roboto' => array( 400, 500, 700 ), + 'Roboto Slab' => array( 400, 700 ), + ) + ); + + $rewritten_families = array(); + foreach ( $families as $family ) { + // Clean up the family name (URL decode + trim). + $family = trim( rawurldecode( $family ) ); + + // Check if this family has a configured weight list. + $matched = false; + foreach ( $font_weights_config as $config_family => $weights ) { + if ( 0 === strcasecmp( $family, $config_family ) ) { + // Rewrite with only the specified weights. + $weights_str = implode( ',', $weights ); + $rewritten_families[] = $config_family . ':' . $weights_str; + $matched = true; + break; + } + } + + // If no config match, pass through unchanged. + if ( ! $matched ) { + $rewritten_families[] = $family; + } + } + + // If nothing changed, return original. + if ( count( $rewritten_families ) === count( $families ) ) { + // Check if any actually changed (compare raw values). + $original_str = implode( '+', $families ); + $rewritten_str = implode( '+', $rewritten_families ); + + if ( $original_str === $rewritten_str ) { + return $src; + } + } + + // Rebuild the URL with the new family parameter. + $new_query_vars = $query_vars; + $new_query_vars['family'] = implode( '+', $rewritten_families ); + $new_query_str = http_build_query( $new_query_vars ); + + // Reconstruct the full URL. + $new_src = $parsed['scheme'] . '://' . $parsed['host'] . $parsed['path'] . '?' . $new_query_str; + + return $new_src; +} + +/** + * Add preconnect hints for font servers. + * + * Reduces TLS + DNS latency for Google Fonts requests on cold cache. + * Adds preconnect for fonts.googleapis.com and fonts.gstatic.com. + * + * @param array $urls List of URLs to add hints for. + * @param string $rel The relationship type (preconnect, dns-prefetch, etc.). + * @return array Modified URL list. + */ +function peptide_starter_perf_resource_hints( $urls, $rel ) { + // Only add hints for preconnect relationships. + if ( 'preconnect' !== $rel ) { + return $urls; + } + + // Add font servers if not already present. + $font_urls = apply_filters( + 'peptide_starter_perf_font_preconnect_urls', + array( + 'https://fonts.googleapis.com', + 'https://fonts.gstatic.com', + ) + ); + + foreach ( $font_urls as $url ) { + if ( ! in_array( $url, $urls, true ) ) { + $urls[] = $url; + } + } + + return $urls; +} + +/** + * Defer the cookie-notice script to avoid render-blocking. + * + * Adds 'defer' attribute to cookie-notice JS while preserving DOM-ready order. + * This ensures the cookie banner loads without blocking initial rendering. + * + * @param string $tag The script tag HTML. + * @param string $handle The script handle. + * @param string $src The script URL (unused). + * @return string Modified script tag. + */ +function peptide_starter_perf_defer_cookie_notice( $tag, $handle, $src ) { + // Only modify the cookie-notice script. + if ( 'cookie-notice-front' !== $handle ) { + return $tag; + } + + // Add defer attribute if not already present. + if ( false === strpos( $tag, 'defer' ) ) { + // Insert defer before the closing >. + $tag = str_replace( ''; + $result = peptide_starter_perf_defer_cookie_notice( $tag, 'cookie-notice-front', 'http://example.com/cookie-notice.js' ); + + $this->assertStringContainsString( 'defer', $result ); + } + + /** + * Test: Defer not applied to other scripts. + * + * The defer filter should only affect cookie-notice-front, not other handles. + */ + public function test_defer_not_applied_to_other_scripts() { + $tag = ''; + $result = peptide_starter_perf_defer_cookie_notice( $tag, 'other-script', 'http://example.com/other.js' ); + + // Should not have defer added. + $this->assertStringNotContainsString( 'defer', $result ); + } + + /** + * Test: page_uses_elementor helper returns false for non-singular. + */ + public function test_page_uses_elementor_non_singular() { + // Go to homepage (not singular). + $this->go_to( home_url( '/' ) ); + + $result = peptide_starter_page_uses_elementor( 0 ); + $this->assertFalse( $result ); + } +}