tag string, the * handle, and the src. We bail when: * - the user excluded this handle / src substring, * - the tag already has defer or async (don't double-set), * - the tag has no src (inline scripts can't be deferred — would * execute synchronously regardless). * * @param string $tag * @param string $handle * @param string $src */ public static function defer_script_tag( $tag, $handle, $src ): string { if ( ! is_string( $tag ) || '' === $tag ) { return (string) $tag; } // Self-guard: even though Minifier::__construct() bails on admin/ // AJAX/REST/cron at registration, a late context switch (e.g. a // custom wp_print_scripts() call inside an admin page render) can // leave the filter attached. Skipping here keeps the React admin // bundle's fallback so * users with JS disabled still get styles applied (via media="all"). * * @param string $tag * @param string $handle */ public static function async_style_tag( $tag, $handle ): string { if ( ! is_string( $tag ) || '' === $tag ) { return (string) $tag; } if ( self::skip_in_non_frontend_context() ) { return $tag; } // Only operate on with a media attribute // we can swap. Skip anything custom (preload, etc.) — we don't // want to fight with explicit author intent. if ( false === stripos( $tag, 'rel=\'stylesheet\'' ) && false === stripos( $tag, 'rel="stylesheet"' ) ) { return $tag; } // Avoid double-wrapping. if ( false !== stripos( $tag, 'data-xs-async' ) ) { return $tag; } $async = (string) preg_replace_callback( '#\bmedia\s*=\s*(["\'])([^"\']*)\1#i', static function ( $m ) { $orig = $m[2]; return 'media="print" onload="this.media=\'' . esc_attr( $orig ) . '\'" data-xs-async="' . esc_attr( $orig ) . '"'; }, $tag, 1 ); // If no media= was present (rare), inject one. if ( $async === $tag ) { $async = (string) preg_replace( '#. return $async . ''; } /** * Filter: `style_loader_src` + `script_loader_src` — strip the * ?ver=X.Y query string that WP appends for cache busting. Some * CDNs / reverse proxies cache better when the URL has no query. * * Skip URLs whose query carries non-ver params — those might be * intentional (e.g. a CDN providing per-image transforms). * * @param string $src */ public static function strip_version_query( $src ): string { if ( ! is_string( $src ) || '' === $src ) { return (string) $src; } if ( self::skip_in_non_frontend_context() ) { return $src; } $parts = wp_parse_url( $src ); if ( ! is_array( $parts ) || empty( $parts['query'] ) ) { return $src; } parse_str( $parts['query'], $query ); if ( ! is_array( $query ) ) { return $src; } // Only strip 'ver' — keep anything else the asset URL needs. unset( $query['ver'] ); $new_query = http_build_query( $query ); $new_url = ( $parts['scheme'] ?? 'http' ) . '://' . ( $parts['host'] ?? '' ); if ( isset( $parts['port'] ) ) { $new_url .= ':' . $parts['port']; } $new_url .= $parts['path'] ?? ''; if ( '' !== $new_query ) { $new_url .= '?' . $new_query; } if ( ! empty( $parts['fragment'] ) ) { $new_url .= '#' . $parts['fragment']; } return $new_url; } /** * Defensive context guard for filter callbacks. Mirrors the registration- * time bail in Minifier::__construct() so a late context flip (admin page * render kicked off mid-request, REST_REQUEST set after plugins_loaded, * etc.) doesn't let frontend tag rewrites leak into wp-admin / AJAX / * REST / cron responses. * * Specifically prevents the React admin bundle's