id : '';
}
return $hook && in_array( $hook, self::$hooks, true );
}
/** v1.0 lived under Settings → GlobeSign Speed; keep old bookmarks working. */
public static function legacy_redirect() {
global $pagenow;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( 'options-general.php' === $pagenow && isset( $_GET['page'] ) && self::SLUG === $_GET['page'] && current_user_can( 'manage_options' ) ) {
wp_safe_redirect( self::url() );
exit;
}
}
public static function action_links( $links ) {
array_unshift( $links, 'Settings' );
return $links;
}
public static function footer_text( $text ) {
if ( ! self::is_our_screen() ) {
return $text;
}
$brand = GSSO_Brand::get();
return 'Thanks for speeding up with ' . esc_html( $brand['company'] ) . '.';
}
public static function assets( $hook ) {
if ( ! self::is_our_screen( $hook ) ) {
return;
}
wp_enqueue_style( 'gsso-admin', GSSO_URL . 'assets/admin.css', array(), GSSO_VERSION );
wp_enqueue_script( 'gsso-admin', GSSO_URL . 'assets/admin.js', array(), GSSO_VERSION, true );
wp_localize_script(
'gsso-admin',
'GSSO',
array(
'ajax' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'gsso_ajax' ),
'rules' => GSSO_Assets::rules(),
'home' => home_url( '/' ),
'locked' => GSSO_Assets::PROTECTED_HANDLES,
)
);
}
/** First admin load after activation: write server files and report problems. */
public static function apply_pending() {
if ( ! get_transient( 'gsso_apply_pending' ) || ! current_user_can( 'manage_options' ) ) {
return;
}
delete_transient( 'gsso_apply_pending' );
self::flash( GSSO_Plugin::apply_settings(), 'warning' );
self::flash( array( 'Active with safe defaults. Open GlobeSign Speed → Settings in the admin menu to fine-tune.' ), 'success' );
}
/* ======================================================================
* Messages
* ==================================================================== */
public static function flash( array $messages, $type = 'success' ) {
if ( empty( $messages ) ) {
return;
}
$stored = get_transient( self::NOTICE_KEY );
$stored = is_array( $stored ) ? $stored : array();
foreach ( $messages as $message ) {
$stored[] = array( 'type' => $type, 'text' => $message );
}
set_transient( self::NOTICE_KEY, $stored, 5 * MINUTE_IN_SECONDS );
}
public static function notices() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$stored = get_transient( self::NOTICE_KEY );
if ( is_array( $stored ) ) {
delete_transient( self::NOTICE_KEY );
foreach ( $stored as $notice ) {
printf( '
GlobeSign Speed Optimization: %s
', esc_attr( $notice['type'] ), esc_html( $notice['text'] ) );
}
}
// Ongoing status notices are limited to our own pages and the Plugins screen (Guideline 11).
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( ! self::is_our_screen() && ! ( $screen && 'plugins' === $screen->id ) ) {
return;
}
$conflicts = GSSO_Conflicts::active();
if ( $conflicts ) {
$names = wp_list_pluck( $conflicts, 'name' );
printf(
'
GlobeSign Speed Optimization: %s is also active. To avoid conflicts, the overlapping GlobeSign Speed features are paused automatically. For best results, use one optimization plugin.
GlobeSign Speed Optimization is in test mode: CSS/JS/HTML optimizations are only visible to you via ?gsso_test=1. Turn off test mode when you are happy with the result.
';
}
}
/* ======================================================================
* Admin bar
* ==================================================================== */
public static function admin_bar( $bar ) {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$bar->add_node(
array(
'id' => 'gsso',
'title' => 'Speed',
'href' => self::url(),
)
);
$bar->add_node(
array(
'parent' => 'gsso',
'id' => 'gsso-clear-all',
'title' => 'Clear all cache',
'href' => wp_nonce_url( add_query_arg( 'gsso_bar', 'clear_all' ), 'gsso_bar' ),
)
);
if ( ! is_admin() ) {
$bar->add_node(
array(
'parent' => 'gsso',
'id' => 'gsso-clear-page',
'title' => 'Clear this page',
'href' => wp_nonce_url( add_query_arg( 'gsso_bar', 'clear_page' ), 'gsso_bar' ),
)
);
$bar->add_node(
array(
'parent' => 'gsso',
'id' => 'gsso-test',
'title' => 'Preview optimized version',
'href' => add_query_arg( 'gsso_test', '1' ),
)
);
}
}
public static function handle_bar_actions() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( empty( $_GET['gsso_bar'] ) || ! current_user_can( 'manage_options' ) ) {
return;
}
check_admin_referer( 'gsso_bar' );
$action = sanitize_key( wp_unslash( $_GET['gsso_bar'] ) );
// phpcs:enable
$current = remove_query_arg( array( 'gsso_bar', '_wpnonce' ) );
if ( 'clear_all' === $action ) {
GSSO_Purge::purge_all();
self::flash( array( 'All cache cleared. Pages will be rebuilt in the background.' ) );
} elseif ( 'clear_page' === $action ) {
$host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : (string) wp_parse_url( home_url(), PHP_URL_HOST );
GSSO_Purge::purge_url( ( is_ssl() ? 'https://' : 'http://' ) . $host . $current );
}
wp_safe_redirect( $current );
exit;
}
/* ======================================================================
* Save + actions
* ==================================================================== */
public static function save() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Not allowed.' );
}
check_admin_referer( 'gsso_save' );
$problems = self::do_save();
self::flash( array( 'Settings saved and cache cleared.' ) );
self::flash( $problems, 'warning' );
$tab = isset( $_POST['gsso_tab'] ) ? sanitize_key( wp_unslash( $_POST['gsso_tab'] ) ) : '';
wp_safe_redirect( self::url( $tab ) );
exit;
}
/** @return string[] Problems from applying the settings. */
private static function do_save() {
// phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified by both callers (save/ajax_save); values sanitized in GSSO_Settings::sanitize().
$input = isset( $_POST[ GSSO_Settings::OPTION ] ) && is_array( $_POST[ GSSO_Settings::OPTION ] ) ? wp_unslash( $_POST[ GSSO_Settings::OPTION ] ) : array();
GSSO_Settings::update( $input );
$problems = GSSO_Plugin::apply_settings();
GSSO_Purge::purge_all();
self::forget_stats();
return $problems;
}
/** AJAX save: same work as save(), but the page doesn't reload. */
public static function ajax_save() {
if ( ! current_user_can( 'manage_options' ) || ! check_ajax_referer( 'gsso_save', '_wpnonce', false ) ) {
wp_send_json_error( array( 'message' => 'Your session expired. Reload the page and try again.' ), 403 );
}
$problems = self::do_save();
$reminder = GSSO_Host_Cache::reminder();
if ( $reminder ) {
$problems[] = $reminder;
}
wp_send_json_success(
array(
'message' => 'Settings saved and cache cleared.',
'problems' => array_values( $problems ),
)
);
}
/** Quick actions that are safe to run without a page reload. */
public static function ajax_action() {
if ( ! current_user_can( 'manage_options' ) || ! check_ajax_referer( 'gsso_ajax', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => 'Your session expired. Reload the page and try again.' ), 403 );
}
$do = isset( $_POST['gsso_do'] ) ? sanitize_key( wp_unslash( $_POST['gsso_do'] ) ) : '';
if ( ! in_array( $do, array( 'clear_cache', 'preload', 'clear_assets' ), true ) ) {
wp_send_json_error( array( 'message' => 'Unknown action.' ), 400 );
}
$result = self::perform( $do );
$stats = self::stats( true );
$extra = in_array( $do, array( 'clear_cache', 'clear_assets' ), true ) ? GSSO_Host_Cache::reminder() : '';
wp_send_json_success(
array(
'message' => trim( implode( ' ', $result['messages'] ) . ' ' . $extra ),
'stats' => array(
'pages' => number_format_i18n( $stats['pages']['files'] ),
'pages_size' => size_format( $stats['pages']['size'] ) ? size_format( $stats['pages']['size'] ) : '0 B',
'queue' => GSSO_Preload::remaining(),
'assets' => number_format_i18n( $stats['assets']['files'] ),
),
)
);
}
/** Database counts run several COUNT queries, so they load only when the Database tab is opened. */
public static function ajax_db_counts() {
if ( ! current_user_can( 'manage_options' ) || ! check_ajax_referer( 'gsso_ajax', 'nonce', false ) ) {
wp_send_json_error( null, 403 );
}
$counts = get_transient( self::DB_KEY );
if ( ! is_array( $counts ) ) {
$counts = GSSO_Database::counts();
set_transient( self::DB_KEY, $counts, 10 * MINUTE_IN_SECONDS );
}
$out = array();
foreach ( $counts as $key => $count ) {
$out[ $key ] = array(
'count' => (int) $count,
'label' => number_format_i18n( $count ) . ( 'db_optimize_tables' === $key ? ' tables' : '' ),
);
}
wp_send_json_success( $out );
}
/** Host cache status for the dashboard and Page Cache tab (loopback request, so loaded on demand). */
public static function ajax_host_cache() {
if ( ! current_user_can( 'manage_options' ) || ! check_ajax_referer( 'gsso_ajax', 'nonce', false ) ) {
wp_send_json_error( null, 403 );
}
$host = GSSO_Host_Cache::detect( ! empty( $_POST['fresh'] ) );
if ( ! $host ) {
wp_send_json_success( array( 'html' => '' ) );
}
if ( ! empty( $host['purgeable'] ) ) {
$advice = 'GlobeSign Speed purges it automatically whenever it clears its own cache.';
} elseif ( GSSO_Settings::get( 'host_cache_nocache' ) ) {
$advice = 'GlobeSign Speed is telling it not to store pages, so GlobeSign Speed\'s own cache is used. Flush it once so old copies disappear: ' . $host['how'];
} else {
$advice = 'It sits in front of WordPress, so visitors and PageSpeed keep getting its stored copy until it is flushed. After changing settings: ' . $host['how'] . ' Or turn on "Let GlobeSign Speed manage page caching" in the Page Cache tab.';
}
$html = '
Your server runs . GlobeSign's rules are compatible with it. If you also enable a server-level cache from your hosting panel, clear it after changing settings here.
Recommended rollout
Already on: page cache, browser caching, GZIP, lazy loading, emoji removal. These are safe on virtually every site.
Go to Safety & Tools and turn on Test mode.
Enable one item at a time in CSS & JavaScript (start with Minify CSS, then Defer JS).
Use Preview optimized homepage and click through your key pages: menus, sliders, forms, checkout.
Happy? Turn test mode off so visitors get the optimized version.
To check caching: open your site in a private window, then look for the X-GSSO-Cache: HIT response header in your browser's developer tools (Network tab).
Page cache
0 = never expire.', 'hours' );
self::toggle( 'cache_mobile_separate', 'Separate cache for mobile devices', 'Only needed if your theme outputs different HTML for phones (not just responsive CSS).', 'test' );
?>
Hosting server cache
Checking whether your host caches pages…
Exclusions
* is a wildcard, e.g. /members/*.' );
self::textarea( 'cache_exclude_cookies', 'Never cache when these cookies are present', 'One cookie name (or prefix) per line. Login, password-protected-post and comment-author cookies are always excluded.' );
self::textarea( 'cache_exclude_agents', 'Never cache for these user agents', 'One user-agent fragment per line.', 2 );
self::textarea( 'cache_ignore_params', 'Query parameters to ignore', 'URLs with only these parameters still get the cached page (marketing tags). Any other parameter bypasses the cache.' );
?>
Browser caching & compression
Your server is not Apache/LiteSpeed, so these rules cannot be applied from WordPress. Ask your host to enable GZIP/Brotli and static file caching (most do by default).
Rules are added to .htaccess between clearly marked comments, wrapped in <IfModule>, and verified with a test request. If your server rejects them they are rolled back automatically.
These options change how your theme's code loads. Turn on Test mode (Safety & Tools) first and preview with ?gsso_test=1 before showing them to visitors.
HTML
pre, textarea, script, style and code is never touched.', 'test' ); ?>
CSS
@import are left where they are. If a combined file fails validation, the original files are used. On HTTP/2 servers the gain is usually small; try it with Test mode and compare.', 'test' );
self::textarea( 'combine_css_exclude', 'Exclude from combining', 'One filename or keyword per line. Excluded files stay separate and keep their position.', 2 );
self::toggle( 'google_fonts_swap', 'Google Fonts: display=swap', 'Shows text immediately in a fallback font while Google Fonts load (fixes the "Ensure text remains visible" PageSpeed warning).' );
self::toggle( 'font_display_swap', 'Theme & icon fonts: font-display swap', 'Adds font-display: swap to self-hosted fonts in your theme\'s inline styles and minified stylesheets (fixes the "Font display" PageSpeed insight).' );
?>
JavaScript
.min.js) are skipped.', 'test' );
self::textarea( 'minify_js_exclude', 'Exclude from JS minification', 'One filename or keyword per line.', 2 );
self::toggle( 'defer_js', 'Defer JavaScript', 'Lets the page render before scripts run (fixes "Eliminate render-blocking resources"). Execution order is preserved, including inline scripts. Any excluded script automatically keeps the scripts it may depend on loading normally.', 'test' );
self::textarea( 'defer_js_exclude', 'Never defer', 'One filename or keyword per line. jQuery is excluded by default for maximum compatibility; remove it only after testing.', 3 );
self::toggle( 'delay_js', 'Delay JavaScript until user interaction', 'Scripts load only when the visitor scrolls, clicks, taps or types. Huge gains for analytics, chat widgets and pixels.', 'test' );
self::select(
'delay_js_mode',
'What to delay',
array(
'listed' => 'Only the scripts listed below (recommended)',
'all' => 'All scripts except exclusions (advanced)',
),
'"Only listed" is safe because it targets third-party trackers that don\'t affect how the page looks.'
);
self::textarea( 'delay_js_list', 'Scripts to delay', 'One domain, filename or code keyword per line.', 6 );
self::textarea( 'delay_js_exclude', 'Never delay', 'Used in "All scripts" mode. One keyword per line.', 3 );
self::number( 'delay_js_timeout', 'Load anyway after', 'If the visitor doesn\'t interact, load delayed scripts after this many seconds. 0 = only on interaction.', 'seconds' );
?>
Lazy loading
loading="lazy": no JavaScript, works with every theme, and images load normally if a browser doesn\'t support it.' );
self::toggle( 'lazy_iframes', 'Lazy load iframes & videos', 'YouTube, Vimeo, Google Maps and other embeds load when scrolled into view.' );
self::number( 'lazy_skip_first', 'Skip the first', 'The first images on the page (logo, hero) load immediately so your Largest Contentful Paint isn\'t delayed.', 'images' );
self::textarea( 'lazy_exclude', 'Exclude images containing', 'Any class, filename or attribute. One per line.', 3 );
?>
Layout stability
width/height to local images that lack them, reducing layout shift (CLS).', 'test' ); ?>
Cache preloading
Resource hints
https://fonts.gstatic.com. Keep this short (2–4 entries).', 3 );
self::textarea( 'dns_prefetch', 'DNS prefetch', 'Origins used later on the page, e.g. https://www.googletagmanager.com.', 3 );
self::textarea( 'preload_fonts', 'Preload fonts', 'Full URLs of .woff2 fonts used above the fold.', 3 );
self::textarea( 'preload_images', 'Preload key images (LCP)', 'The main image visitors see first, usually a hero or background image. PageSpeed names it under LCP breakdown. One URL per line. Prefix with mobile: or desktop: to load it only on that screen size, e.g. mobile: https://example.com/wp-content/uploads/hero-mobile.jpg', 3 );
?>
Remove unneeded WordPress features
Server load
'WordPress default',
'reduce' => 'Reduce to once a minute',
'disable_front' => 'Disable on the front end',
'disable_all' => 'Disable everywhere except the post editor',
),
'Heartbeat sends regular background requests from open admin tabs. Autosave and post locking in the editor are always kept.'
);
self::number( 'limit_revisions', 'Limit post revisions', 'Maximum revisions stored per post. 0 = WordPress default (unlimited).', 'revisions' );
?>
Back up your database before the first cleanup. Deleted data cannot be recovered.
Stop plugin files from loading on pages that don't use them, for example a contact form's CSS and JavaScript on every page except /contact/. Scan a page to see what it loads, choose what to unload, then save. Test mode is on: rules only apply when you preview with ?gsso_test=1.' : 'Preview with ?gsso_test=1 or a private window after saving.'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static markup. ?>
Scan a page
The page is loaded once as a logged-out visitor. Files another loaded file needs are always kept, and core files such as jQuery can't be unloaded.
Your rules
Paths support * wildcards, for example /shop/*. One path per line.
Clean now
Safety
only when you (an admin) add ?gsso_test=1 to a URL. Visitors keep seeing the unchanged site. Caching, lazy loading and browser caching keep working normally.' );
self::textarea( 'optimize_exclude_urls', 'Never optimize these URLs', 'Pages listed here are served exactly as your theme outputs them (they can still be cached). One path per line, * wildcard. You can also switch off optimization on any single page from its editor sidebar.', 3 );
self::toggle( 'debug_log', 'Debug logging', 'Records informational events in the log below. Errors are always logged.' );
?>
If something looks wrong
Turn off the last option you enabled and save. The cache clears automatically.
Or switch off optimization for just that page from its editor sidebar.
Or click Reset to safe defaults below.
Locked out entirely? Add define( 'GSSO_DISABLE', true ); to wp-config.php, or rename the plugin folder via FTP. Deactivating the plugin removes every change it made to your server files.
Import / export
Copy these settings to another site.
Activity log
Nothing logged. Any optimization that is skipped to protect your site will appear here.