Changeset 3783147
- Timestamp:
- 11/04/2026 06:02:59 AM (4 days ago)
- Location:
- searchus/trunk
- Files:
-
- 1 added
- 11 edited
-
core/api-client.php (modified) (12 diffs)
-
core/query-cache.php (modified) (1 diff)
-
core/query-guard.php (modified) (3 diffs)
-
core/tier.php (modified) (3 diffs)
-
fw-search-indexer.php (modified) (4 diffs)
-
languages/searchus.pot (modified) (40 diffs)
-
readme.txt (modified) (3 diffs)
-
serp/page-template-404-recovery.php (modified) (1 diff)
-
serp/search-request-guard.php (added)
-
serp/search-results.php (modified) (4 diffs)
-
serp/serp-content-block.php (modified) (1 diff)
-
serp/smart-404.php (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
searchus/trunk/core/api-client.php
r3769819 r3783147 1861 1861 } 1862 1862 1863 if ( ! defined( 'FW_SI_OPTION_API_BACKOFF' ) ) { 1864 /** Option: visitor searches skip the API until this time (after HTTP 429 Retry-After, or the API not answering). */ 1865 define( 'FW_SI_OPTION_API_BACKOFF', 'fw_si_api_backoff' ); 1866 } 1867 1868 /** 1869 * Active API pause for visitor searches: ['until' => unix time, 'code' => 429 or 0], or [] when none. 1870 * Stored autoloaded, so checking it on every search costs no database query. 1871 * 1872 * @return array{until?:int,code?:int} 1873 */ 1874 function fw_si_api_backoff(): array { 1875 $v = get_option( FW_SI_OPTION_API_BACKOFF, null ); 1876 if ( $v === null ) { 1877 add_option( FW_SI_OPTION_API_BACKOFF, [], '', true ); 1878 return []; 1879 } 1880 return ( is_array( $v ) && (int) ( $v['until'] ?? 0 ) > time() ) ? $v : []; 1881 } 1882 1883 /** 1884 * Pause visitor searches for $seconds (1–120). A longer pause already running is kept. 1885 * 1886 * @param int $seconds Pause length. 1887 * @param int $code 429 (service asked to slow down) or 0 (service did not answer). 1888 */ 1889 function fw_si_api_backoff_start( int $seconds, int $code ): void { 1890 $until = time() + max( 1, min( 120, $seconds ) ); 1891 $cur = fw_si_api_backoff(); 1892 if ( (int) ( $cur['until'] ?? 0 ) >= $until ) { 1893 return; 1894 } 1895 update_option( FW_SI_OPTION_API_BACKOFF, [ 'until' => $until, 'code' => $code ], true ); 1896 } 1897 1898 /** 1899 * Start a pause when the service answered 429 (uses its Retry-After) or could not be reached in time. 1900 * Without it, every search during the burst or outage holds a PHP worker for the full HTTP timeout. 1901 * 1902 * @param array|WP_Error $response wp_remote_post() result. 1903 */ 1904 function fw_si_api_backoff_note_response( $response ): void { 1905 if ( is_wp_error( $response ) ) { 1906 /** 1907 * Seconds visitor searches skip the API after it could not be reached in time. 1908 * 1909 * @param int $seconds Default 10. 1910 */ 1911 fw_si_api_backoff_start( (int) apply_filters( 'fw_si_api_unreachable_pause_seconds', 10 ), 0 ); 1912 return; 1913 } 1914 if ( (int) wp_remote_retrieve_response_code( $response ) === 429 ) { 1915 $retry = (int) wp_remote_retrieve_header( $response, 'retry-after' ); 1916 fw_si_api_backoff_start( $retry > 0 ? $retry : 5, 429 ); 1917 } 1918 } 1919 1863 1920 /** 1864 1921 * Tell the service to count a visitor search that was answered from the local cache. … … 1867 1924 * @param string $query Search text the visitor submitted. 1868 1925 * @param string $live_session Optional live search session id; the service counts one search per session. 1926 * @param bool $is_preview True for a Predictive Search preview. 1927 * @param string $source 'smart404' for a Smart 404 recovery search; empty otherwise. 1928 * @param string $crawler Search engine or AI crawler short name; empty for visitors. 1929 * @param string $crawler_ip The crawler request's address, verified by the service. 1869 1930 * @return bool True when the service counted the search. 1870 1931 */ 1871 function fw_si_report_cached_search_usage( string $query, string $live_session = '', bool $is_preview = false ): bool {1932 function fw_si_report_cached_search_usage( string $query, string $live_session = '', bool $is_preview = false, string $source = '', string $crawler = '', string $crawler_ip = '' ): bool { 1872 1933 $site_id = function_exists( 'fw_si_get_site_id' ) ? fw_si_get_site_id() : ''; 1873 1934 if ( $site_id === '' || $query === '' ) { … … 1875 1936 } 1876 1937 if ( function_exists( 'fw_si_is_junk_query' ) && fw_si_is_junk_query( $query ) ) { 1938 return false; 1939 } 1940 // The cached results are still shown; the search is just not counted while the service asked for a pause. 1941 if ( fw_si_api_backoff() ) { 1877 1942 return false; 1878 1943 } … … 1882 1947 if ( function_exists( 'fw_si_tier' ) ) { 1883 1948 $body['tier'] = fw_si_tier(); 1949 } 1950 if ( $source === 'smart404' ) { 1951 $body['source'] = 'smart404'; 1952 } 1953 if ( $crawler !== '' && ! $is_preview ) { 1954 $body['crawler'] = $crawler; 1955 $body['crawler_ip'] = $crawler_ip; 1884 1956 } 1885 1957 if ( $live_session !== '' ) { … … 1901 1973 $url, 1902 1974 [ 1903 'timeout' => 8, 1975 /** 1976 * Seconds a visitor's page waits for the service to count a cached search. 1977 * 1978 * @param int $timeout Default 3. 1979 */ 1980 'timeout' => (int) apply_filters( 'fw_si_query_count_timeout', 3 ), 1904 1981 'headers' => $headers, 1905 1982 'body' => $encoded, 1906 1983 ] 1907 1984 ); 1985 fw_si_api_backoff_note_response( $response ); 1908 1986 if ( is_wp_error( $response ) ) { 1909 1987 return false; … … 1933 2011 * ['live_session' => string] = live search session id (one counted search per session). 1934 2012 * ['live_preview' => int] = live search preview: completes a partial last word, top N results. 2013 * ['source' => 'smart404'] = Smart 404 recovery search (the service drops scanner path probes). 1935 2014 * @return array|null Payload (all, content, media, ...) or debug payload (debug_results); null on failure. 1936 2015 */ … … 1972 2051 : ''; 1973 2052 $live_preview = isset( $options['live_preview'] ) ? max( 0, (int) $options['live_preview'] ) : 0; 2053 $search_source = ( isset( $options['source'] ) && $options['source'] === 'smart404' ) ? 'smart404' : ''; 2054 $crawler = ( $live_preview === 0 && isset( $options['crawler'] ) && is_string( $options['crawler'] ) ) ? sanitize_key( $options['crawler'] ) : ''; 2055 $crawler_ip = ( $crawler !== '' && isset( $options['crawler_ip'] ) && is_string( $options['crawler_ip'] ) ) ? $options['crawler_ip'] : ''; 1974 2056 // Transient cache applies to all tiers so repeat queries avoid POST /v1/query. Return true from 1975 2057 // fw_si_query_api_skip_transient_cache to bypass (e.g. if service-supplied attribution HTML must stay in lockstep with usage before TTL). … … 1987 2069 fw_si_query_cache_extend_ttl( $cache_key ); 1988 2070 } 1989 if ( empty( $options['skip_log'] ) && function_exists( 'fw_si_report_cached_search_usage' ) && fw_si_report_cached_search_usage( $query, $live_session, $live_preview > 0 ) ) {2071 if ( empty( $options['skip_log'] ) && function_exists( 'fw_si_report_cached_search_usage' ) && fw_si_report_cached_search_usage( $query, $live_session, $live_preview > 0, $search_source, $crawler, $crawler_ip ) ) { 1990 2072 $cached['_fw_usage_counted'] = true; 1991 2073 } … … 2010 2092 } 2011 2093 } 2094 $backoff = $is_debug ? [] : fw_si_api_backoff(); 2095 if ( $backoff ) { 2096 $wait = max( 1, (int) $backoff['until'] - time() ); 2097 return [ 2098 '_fw_error' => true, 2099 '_fw_error_code' => (int) ( $backoff['code'] ?? 0 ), 2100 /* translators: %d: seconds until searches are sent to the service again */ 2101 '_fw_error_detail' => sprintf( __( 'Searches paused for %d seconds after the service was busy or unreachable.', 'searchus' ), $wait ), 2102 ]; 2103 } 2012 2104 $base = fw_si_get_api_base_url(); 2013 2105 $url = $is_debug ? ( $base . '/v1/debug/query' ) : ( $base . '/v1/query' ); … … 2029 2121 $body['preview_limit'] = $live_preview; 2030 2122 } 2123 if ( $search_source !== '' ) { 2124 $body['source'] = $search_source; 2125 } 2126 if ( $crawler !== '' ) { 2127 $body['crawler'] = $crawler; 2128 $body['crawler_ip'] = $crawler_ip; 2129 } 2031 2130 if ( ! $is_debug && function_exists( 'fw_si_query_cache_take_refresh' ) && fw_si_query_cache_take_refresh( $cache_key ) ) { 2032 2131 $body['cache_refresh'] = true; … … 2047 2146 2048 2147 $qt_http_start = $qt_api_debug ? microtime( true ) : 0; 2148 // A visitor's page holds a PHP worker while it waits; debug and pre-cache runs are not on a visitor's page. 2149 $visitor_wait = $is_debug || ! empty( $options['skip_log'] ) ? 15 : (int) apply_filters( 'fw_si_query_api_timeout', 6, $live_preview > 0 ); 2049 2150 $response = wp_remote_post( 2050 2151 $url, 2051 2152 [ 2052 'timeout' => 15,2153 'timeout' => $visitor_wait, 2053 2154 'headers' => $headers, 2054 2155 'body' => $query_encoded, 2055 2156 ] 2056 2157 ); 2158 if ( ! $is_debug ) { 2159 fw_si_api_backoff_note_response( $response ); 2160 } 2057 2161 if ( $qt_api_debug ) { fw_si_plugin_diagnostic_log( '[FW_SI_QT] api_source=http query=' . substr( $query, 0, 40 ) . ' http_ms=' . round( ( microtime( true ) - $qt_http_start ) * 1000 ) ); } 2058 2162 if ( is_wp_error( $response ) ) { … … 2169 2273 fw_si_query_cache_register_doc_ids( $cache_key, $decoded ); 2170 2274 } 2171 if ( function_exists( 'fw_si_query_cache_ trim_to_top' ) ) {2172 fw_si_query_cache_ trim_to_top();2275 if ( function_exists( 'fw_si_query_cache_schedule_trim' ) ) { 2276 fw_si_query_cache_schedule_trim(); 2173 2277 } 2174 2278 if ( ! $is_debug && empty( $options['skip_log'] ) && function_exists( 'fw_si_log_query_for_precache' ) ) { -
searchus/trunk/core/query-cache.php
r3749625 r3783147 357 357 } 358 358 } 359 360 /** 361 * Trim the cache in a cron run once it holds more than FW_SI_QUERY_CACHE_MAX_ENTRIES searches. 362 * Trimming asks the service for its top tokens, which a visitor's search should not wait for. 363 */ 364 function fw_si_query_cache_schedule_trim() { 365 $map = get_option( FW_SI_OPTION_QUERY_CACHE_MAP, [] ); 366 if ( ! is_array( $map ) || count( $map ) <= (int) FW_SI_QUERY_CACHE_MAX_ENTRIES ) { 367 return; 368 } 369 if ( ! wp_next_scheduled( 'fw_si_query_cache_trim' ) ) { 370 wp_schedule_single_event( time(), 'fw_si_query_cache_trim' ); 371 } 372 } 373 add_action( 'fw_si_query_cache_trim', 'fw_si_query_cache_trim_to_top' ); 359 374 360 375 /** -
searchus/trunk/core/query-guard.php
r3749625 r3783147 122 122 } 123 123 124 // Scanner signatures that reach search as words once "/" and "-" become spaces (Smart 404), 125 // and torrent listings pasted as searches. Same rules as the SearchUs API. 126 if ( preg_match( '/\bproc\s+self\b|(?:^|\s)@fs(?:\s|$)|\btfvars\b|\btfstate\b|\bapi_server\b|\bbafk[a-z2-7]{20,}|\bz9x8c7v6b5\b/i', $lower ) ) { 127 return true; 128 } 129 if ( preg_match( '/\b[0-9a-f]{32,40}\b|&tr=|magnet:|urn:btih/i', $lower ) ) { 130 return true; 131 } 132 if ( preg_match( '/[\'"]\s*>|>\s*[\'"]/', $q ) ) { 133 return true; 134 } 135 // Only symbols ("**", "'>\""). A lone "*" is the browse-all query. 136 if ( $q !== '*' && ! preg_match( '/[\p{L}\p{N}]/u', $q ) ) { 137 return true; 138 } 139 124 140 return false; 141 } 142 143 /** 144 * True when a Smart 404 search (words from a missing URL) is only scanner path words and numbers, 145 * e.g. /admin/login/ or /api/v1/config/. Not used for typed searches: a visitor may search "login" on purpose. 146 * 147 * @param string $query Query derived from the missing URL. 148 * @return bool 149 */ 150 function fw_si_is_smart404_probe_query( $query ): bool { 151 static $probe = null; 152 if ( $probe === null ) { 153 $probe = array_flip( 154 preg_split( 155 '/\s+/', 156 'admin administrator login logout signin signup register auth oauth sso user users account accounts profile ' 157 . 'api apis rest graphql v1 v2 v3 v4 config configs configuration settings env environment environ secret secrets ' 158 . 'token tokens key keys credential credentials private public backup backups bak old new test tests tmp temp dev ' 159 . 'staging stage prod production debug console server status health healthz metrics actuator ui uploads upload ' 160 . 'wp wordpress includes content ajax json xmlrpc cgi bin vendor plugin plugins theme themes assets core js css ' 161 . 'jquery min map maint maintenance install setup shell cmd db database sql dump git svn docs doc cron log logs ' 162 . 'phpmyadmin adminer dashboard panel cpanel manager portal internal system root home swagger openapi schema' 163 ) 164 ); 165 } 166 $words = preg_split( '/[^a-z0-9]+/', strtolower( (string) $query ), -1, PREG_SPLIT_NO_EMPTY ); 167 if ( ! is_array( $words ) || ! $words ) { 168 return false; 169 } 170 $has_probe_word = false; 171 foreach ( $words as $w ) { 172 if ( isset( $probe[ $w ] ) ) { 173 $has_probe_word = true; 174 } elseif ( ! ctype_digit( $w ) ) { 175 return false; 176 } 177 } 178 return $has_probe_word; 125 179 } 126 180 … … 141 195 return $is_bot; 142 196 } 197 $is_bot = fw_si_is_bot_user_agent_request(); 198 return $is_bot; 199 } 200 201 /** 202 * Same test as fw_si_is_bot_request() for use before `init`: a login cookie stands in for is_user_logged_in(), 203 * because asking for the current user that early can run before plugins add their login handlers. 204 * 205 * @return bool 206 */ 207 function fw_si_is_bot_request_early(): bool { 208 if ( ( defined( 'WP_CLI' ) && WP_CLI ) || wp_doing_cron() ) { 209 return false; 210 } 211 if ( defined( 'LOGGED_IN_COOKIE' ) && isset( $_COOKIE[ LOGGED_IN_COOKIE ] ) ) { 212 return false; 213 } 214 return fw_si_is_bot_user_agent_request(); 215 } 216 217 /** 218 * Search engine or AI crawler named by the user agent, as the short name the SearchUs service verifies 219 * ('google', 'bing', 'openai', …), or ''. Anyone can send these user agents: the service checks the 220 * request's address, and shows results only to the real crawler. Crawler searches are never counted. 221 * 222 * @return string 223 */ 224 function fw_si_search_crawler_name(): string { 225 static $name = null; 226 if ( $name !== null ) { 227 return $name; 228 } 229 $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? strtolower( sanitize_text_field( wp_unslash( (string) $_SERVER['HTTP_USER_AGENT'] ) ) ) : ''; 230 $found = ''; 231 if ( $ua !== '' ) { 232 $crawlers = array( 233 'google' => 'googlebot|google-inspectiontool', 234 'bing' => 'bingbot|adidxbot', 235 'duckduckgo' => 'duckduckbot|duckassistbot', 236 'yandex' => 'yandexbot', 237 'baidu' => 'baiduspider', 238 'apple' => 'applebot', 239 'yahoo' => 'slurp', 240 'naver' => 'yeti\/', 241 'seznam' => 'seznambot', 242 'qwant' => 'qwantbot|qwantify', 243 'openai' => 'gptbot|oai-searchbot|chatgpt-user', 244 'anthropic' => 'claudebot|claude-user|claude-searchbot', 245 'perplexity' => 'perplexitybot|perplexity-user', 246 ); 247 foreach ( $crawlers as $short => $pattern ) { 248 if ( preg_match( '/' . $pattern . '/', $ua ) ) { 249 $found = $short; 250 break; 251 } 252 } 253 } 254 /** 255 * Search engine or AI crawler for the current request ('' for none). Smart 404 pages, and results pages 256 * on the Free plan, show such crawlers real results and the attribution; other bots get the theme's 404 257 * or the short results page. 258 * 259 * @param string $found Short crawler name, or ''. 260 * @param string $ua Lowercased user agent. 261 */ 262 $name = (string) apply_filters( 'fw_si_search_crawler', $found, $ua ); 263 return $name; 264 } 265 266 /** 267 * True on the Free plan, where results pages carry the SearchUs attribution and search engines are 268 * welcome to crawl them. On paid plans results pages are kept away from crawlers. 269 * 270 * @return bool 271 */ 272 function fw_si_crawlers_read_results_pages(): bool { 273 $free = function_exists( 'fw_si_tier' ) && fw_si_tier() === 'free'; 274 /** 275 * Whether search engine and AI crawlers get full results pages (and robots.txt does not block them). 276 * 277 * @param bool $allow Default: true on the Free plan, false on paid plans. 278 */ 279 return (bool) apply_filters( 'fw_si_crawlers_read_results_pages', $free ); 280 } 281 282 /** 283 * User agent half of bot detection (no login check). 284 * 285 * @return bool 286 */ 287 function fw_si_is_bot_user_agent_request(): bool { 143 288 $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? strtolower( sanitize_text_field( wp_unslash( (string) $_SERVER['HTTP_USER_AGENT'] ) ) ) : ''; 144 289 // Every browser sends a user agent; an empty one is a script. … … 155 300 * @param string $ua Lowercased user agent. 156 301 */ 157 $is_bot = (bool) apply_filters( 'fw_si_is_bot_request', $is_bot, $ua ); 158 return $is_bot; 302 return (bool) apply_filters( 'fw_si_is_bot_request', $is_bot, $ua ); 159 303 } 160 304 -
searchus/trunk/core/tier.php
r3713689 r3783147 7 7 8 8 /** 9 * Plan/tier detection. Single source of truth: plan name (Free, Growth, Pro, Business, Enterprise, Custom).9 * Plan/tier detection. Single source of truth: plan name (Free, Starter, Growth, Scale, Pro, Enterprise, Custom). 10 10 * API and plugin use the same concept; binary "free vs paid" is derived from plan. 11 11 * … … 14 14 15 15 /** Canonical plan names (API and plugin). */ 16 define( 'FW_SI_PLANS', 'Free, Growth,Pro,Business,Enterprise,Custom' );16 define( 'FW_SI_PLANS', 'Free,Starter,Growth,Scale,Pro,Enterprise,Custom' ); 17 17 18 18 /** … … 90 90 91 91 /** 92 * Current plan name: 'Free' | ' Growth' | 'Pro' | 'Business' | 'Enterprise' | 'Custom'.92 * Current plan name: 'Free' | 'Starter' | 'Growth' | 'Scale' | 'Pro' | 'Enterprise' | 'Custom'. 93 93 * From activation response (stored in fw_si_license_plan); default 'Free' if no license or not set. 94 94 */ 95 95 function fw_si_plan(): string { 96 $plans = array( 'Free', ' Growth', 'Pro', 'Business', 'Enterprise', 'Custom' );96 $plans = array( 'Free', 'Starter', 'Growth', 'Scale', 'Pro', 'Enterprise', 'Custom' ); 97 97 98 98 // From activation response (stored in fw_si_license_plan) -
searchus/trunk/fw-search-indexer.php
r3769819 r3783147 5 5 * Plugin Name: SearchUs – Site Search for WordPress and WooCommerce 6 6 * Description: Intelligent indexing, intent scoring, and SERP control for WordPress. 7 * Version: 3.1. 37 * Version: 3.1.4 8 8 * Author: FunnelWon 9 9 * Author URI: https://searchus.io … … 129 129 ========================================================= */ 130 130 131 define( 'FW_SI_VERSION', '3.1. 3' );131 define( 'FW_SI_VERSION', '3.1.4' ); 132 132 define( 'FW_SI_PATCH', 13 ); 133 133 define( 'FW_SI_PLUGIN_DIR', __DIR__ ); … … 340 340 $fw_si_lock_bypass[] = 'fw_si_search_fallback'; 341 341 $fw_si_lock_bypass[] = 'fw_si_attribution_check'; 342 $fw_si_lock_bypass[] = 'fw_si_api_backoff'; 342 343 if ( in_array( $option, $fw_si_lock_bypass, true ) ) { 343 344 return $value; … … 889 890 } 890 891 require_once FW_SI_PLUGIN_DIR . '/serp/search-integration.php'; 892 require_once FW_SI_PLUGIN_DIR . '/serp/search-request-guard.php'; 891 893 require_once FW_SI_PLUGIN_DIR . '/serp/smart-404.php'; 892 894 require_once FW_SI_PLUGIN_DIR . '/admin/result-control-admin.php'; -
searchus/trunk/languages/searchus.pot
r3769819 r3783147 3 3 msgid "" 4 4 msgstr "" 5 "Project-Id-Version: SearchUs – Site Search for WordPress and WooCommerce 3.1. 3\n"5 "Project-Id-Version: SearchUs – Site Search for WordPress and WooCommerce 3.1.4\n" 6 6 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/searchus\n" 7 7 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" … … 10 10 "Content-Type: text/plain; charset=UTF-8\n" 11 11 "Content-Transfer-Encoding: 8bit\n" 12 "POT-Creation-Date: 2026-1 0-28T03:06:56+00:00\n"12 "POT-Creation-Date: 2026-11-04T05:37:56+00:00\n" 13 13 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 14 "X-Generator: WP-CLI 2.12.0\n" … … 942 942 #. translators: %d: Minimum excerpt length in characters (UTF-8). 943 943 #: admin/result-control-admin.php:470 944 #: fw-search-indexer.php:51 7944 #: fw-search-indexer.php:518 945 945 #, php-format 946 946 msgid "Required for this type: at least %d characters (UTF-8). For products this is the short description under the price." … … 1185 1185 #: admin/search-embeds-admin.php:265 1186 1186 #: admin/settings-page.php:4776 1187 #: fw-search-indexer.php:4 291187 #: fw-search-indexer.php:430 1188 1188 msgid "Delete" 1189 1189 msgstr "" … … 1251 1251 #: admin/settings-page.php:956 1252 1252 #: admin/settings-page.php:982 1253 #: fw-search-indexer.php:47 81253 #: fw-search-indexer.php:479 1254 1254 msgid "Another user" 1255 1255 msgstr "" 1256 1256 1257 1257 #: admin/settings-page.php:242 1258 #: fw-search-indexer.php:4 791258 #: fw-search-indexer.php:480 1259 1259 msgid "Editing in progress." 1260 1260 msgstr "" … … 1385 1385 #: admin/settings-page.php:1256 1386 1386 #: admin/settings-page.php:3968 1387 #: serp/serp-content-block.php:228 41388 #: serp/serp-content-block.php:27 091387 #: serp/serp-content-block.php:2287 1388 #: serp/serp-content-block.php:2712 1389 1389 msgid "Read more" 1390 1390 msgstr "" … … 1487 1487 #: admin/settings-page.php:1480 1488 1488 #: analytics/analytics-ui.php:763 1489 #: serp/serp-content-block.php:185 21490 #: serp/serp-content-block.php:27 391489 #: serp/serp-content-block.php:1855 1490 #: serp/serp-content-block.php:2742 1491 1491 msgid "Previous" 1492 1492 msgstr "" … … 1494 1494 #: admin/settings-page.php:1494 1495 1495 #: analytics/analytics-ui.php:763 1496 #: serp/serp-content-block.php:18 671497 #: serp/serp-content-block.php:275 41496 #: serp/serp-content-block.php:1870 1497 #: serp/serp-content-block.php:2757 1498 1498 msgid "Next" 1499 1499 msgstr "" … … 1565 1565 #: admin/settings-register.php:1020 1566 1566 #: core/hmac-auth.php:213 1567 #: fw-search-indexer.php:73 41568 #: fw-search-indexer.php:79 61567 #: fw-search-indexer.php:735 1568 #: fw-search-indexer.php:797 1569 1569 #: serp/search-page.php:695 1570 1570 msgid "Unauthorized" … … 1793 1793 1794 1794 #: admin/settings-page.php:3198 1795 #: fw-search-indexer.php:43 81795 #: fw-search-indexer.php:439 1796 1796 msgid "Disable Crawling" 1797 1797 msgstr "" 1798 1798 1799 1799 #: admin/settings-page.php:3198 1800 #: fw-search-indexer.php:4 391800 #: fw-search-indexer.php:440 1801 1801 msgid "Enable Crawling" 1802 1802 msgstr "" … … 2012 2012 2013 2013 #: admin/settings-page.php:3760 2014 #: fw-search-indexer.php:43 62014 #: fw-search-indexer.php:437 2015 2015 msgid "Reindex in progress." 2016 2016 msgstr "" … … 2132 2132 #: admin/settings-page.php:4046 2133 2133 #: admin/setup-wizard.php:78 2134 #: fw-search-indexer.php:48 42134 #: fw-search-indexer.php:485 2135 2135 msgid "Unlock Settings" 2136 2136 msgstr "" 2137 2137 2138 2138 #: admin/settings-page.php:4046 2139 #: fw-search-indexer.php:48 52139 #: fw-search-indexer.php:486 2140 2140 msgid "Lock Settings" 2141 2141 msgstr "" … … 2167 2167 #: admin/settings-page.php:5104 2168 2168 #: admin/settings-page.php:5223 2169 #: fw-search-indexer.php:42 82169 #: fw-search-indexer.php:429 2170 2170 msgid "Expand" 2171 2171 msgstr "" … … 2866 2866 2867 2867 #: admin/settings-page.php:5350 2868 #: fw-search-indexer.php:43 52868 #: fw-search-indexer.php:436 2869 2869 msgid "Minimize" 2870 2870 msgstr "" … … 3813 3813 #. translators: 1: Current page number, 2: Total number of pages. 3814 3814 #: analytics/analytics-ui.php:761 3815 #: serp/serp-content-block.php:18 593816 #: serp/serp-content-block.php:274 63815 #: serp/serp-content-block.php:1862 3816 #: serp/serp-content-block.php:2749 3817 3817 #, php-format 3818 3818 msgid "Page %1$d of %2$d" … … 3828 3828 3829 3829 #: analytics/analytics-ui.php:763 3830 #: serp/serp-content-block.php:190 53830 #: serp/serp-content-block.php:1908 3831 3831 msgid "Results Per Page:" 3832 3832 msgstr "" … … 4160 4160 msgctxt "HTML link in API notice" 4161 4161 msgid "Manage your plan or limits: %s" 4162 msgstr "" 4163 4164 #. translators: %d: seconds until searches are sent to the service again 4165 #: core/api-client.php:2101 4166 #, php-format 4167 msgid "Searches paused for %d seconds after the service was busy or unreachable." 4162 4168 msgstr "" 4163 4169 … … 4496 4502 4497 4503 #: crawler/crawl-confirm.php:512 4498 #: fw-search-indexer.php:44 64504 #: fw-search-indexer.php:447 4499 4505 msgid "Auto-Crawl Disabled" 4500 4506 msgstr "" … … 4502 4508 #. translators: %d: Number of pages sent to the service and waiting for it to confirm they are indexed. 4503 4509 #: crawler/crawl-confirm.php:516 4504 #: fw-search-indexer.php:4 494510 #: fw-search-indexer.php:450 4505 4511 #, php-format 4506 4512 msgid "Waiting for confirmation (%d)" … … 4508 4514 4509 4515 #: crawler/crawl-confirm.php:519 4510 #: fw-search-indexer.php:44 74516 #: fw-search-indexer.php:448 4511 4517 msgid "Active — Processing queue" 4512 4518 msgstr "" 4513 4519 4514 4520 #: crawler/crawl-confirm.php:521 4515 #: fw-search-indexer.php:45 04521 #: fw-search-indexer.php:451 4516 4522 msgid "Idle — No queued items" 4517 4523 msgstr "" … … 4545 4551 msgstr "" 4546 4552 4547 #: fw-search-indexer.php:41 34553 #: fw-search-indexer.php:414 4548 4554 msgid "Result" 4549 4555 msgstr "" 4550 4556 4551 #: fw-search-indexer.php:41 44557 #: fw-search-indexer.php:415 4552 4558 msgid "Injected" 4553 4559 msgstr "" 4554 4560 4555 4561 #. translators: %s: Previous title or label for a reordered search result. 4556 #: fw-search-indexer.php:41 64562 #: fw-search-indexer.php:417 4557 4563 #, php-format 4558 4564 msgid "(Previously %s)" 4559 4565 msgstr "" 4560 4566 4561 #: fw-search-indexer.php:42 54567 #: fw-search-indexer.php:426 4562 4568 msgid "Error" 4563 4569 msgstr "" 4564 4570 4565 #: fw-search-indexer.php:42 64571 #: fw-search-indexer.php:427 4566 4572 msgid "Reindex failed on server." 4567 4573 msgstr "" 4568 4574 4569 #: fw-search-indexer.php:42 74575 #: fw-search-indexer.php:428 4570 4576 msgid "Collapse" 4571 4577 msgstr "" 4572 4578 4573 #: fw-search-indexer.php:43 04579 #: fw-search-indexer.php:431 4574 4580 msgid "Deleting..." 4575 4581 msgstr "" 4576 4582 4577 4583 #. translators: %s = comma-separated list of query phrases where the URL is a duplicate 4578 #: fw-search-indexer.php:43 24584 #: fw-search-indexer.php:433 4579 4585 #, php-format 4580 4586 msgid "This URL already appears in organic search results for: %s. Duplicate injections are automatically skipped on the SERP, so this injection will not display. Remove the URL from the injection or from the index first." 4581 4587 msgstr "" 4582 4588 4583 #: fw-search-indexer.php:43 34589 #: fw-search-indexer.php:434 4584 4590 msgid "Save is taking longer than expected. The page did not finish loading — check whether your changes were saved, then try again." 4585 4591 msgstr "" 4586 4592 4587 #: fw-search-indexer.php:43 44593 #: fw-search-indexer.php:435 4588 4594 msgid "Close" 4589 4595 msgstr "" 4590 4596 4591 #: fw-search-indexer.php:43 74597 #: fw-search-indexer.php:438 4592 4598 msgid "Search index is rebuilding. Controls are temporarily locked." 4593 4599 msgstr "" 4594 4600 4595 #: fw-search-indexer.php:44 04601 #: fw-search-indexer.php:441 4596 4602 msgid "Active — Reindexing" 4597 4603 msgstr "" 4598 4604 4599 #: fw-search-indexer.php:44 14605 #: fw-search-indexer.php:442 4600 4606 msgid "Active" 4601 4607 msgstr "" 4602 4608 4603 #: fw-search-indexer.php:44 24609 #: fw-search-indexer.php:443 4604 4610 msgid "Paused" 4605 4611 msgstr "" 4606 4612 4607 #: fw-search-indexer.php:44 34613 #: fw-search-indexer.php:444 4608 4614 msgid "Indexing is paused." 4609 4615 msgstr "" 4610 4616 4611 #: fw-search-indexer.php:44 44617 #: fw-search-indexer.php:445 4612 4618 msgid "1 or more items are queued for crawling. Once crawling is enabled, these will be processed automatically." 4613 4619 msgstr "" 4614 4620 4615 #: fw-search-indexer.php:44 54621 #: fw-search-indexer.php:446 4616 4622 msgid "Unknown" 4617 4623 msgstr "" 4618 4624 4619 #: fw-search-indexer.php:45 14625 #: fw-search-indexer.php:452 4620 4626 msgid "Reindexing…" 4621 4627 msgstr "" 4622 4628 4623 #: fw-search-indexer.php:45 24629 #: fw-search-indexer.php:453 4624 4630 msgid "Settings are Locked - Click Unlock Settings to make edits" 4625 4631 msgstr "" 4626 4632 4627 #: fw-search-indexer.php:45 34633 #: fw-search-indexer.php:454 4628 4634 msgid "Settings are locked. Unlock settings to clear cache." 4629 4635 msgstr "" 4630 4636 4631 #: fw-search-indexer.php:45 44637 #: fw-search-indexer.php:455 4632 4638 msgid "Failed to clear cache." 4633 4639 msgstr "" 4634 4640 4635 #: fw-search-indexer.php:45 54641 #: fw-search-indexer.php:456 4636 4642 msgid "Settings are locked. Unlock settings to run pre-cache." 4637 4643 msgstr "" 4638 4644 4639 #: fw-search-indexer.php:45 64645 #: fw-search-indexer.php:457 4640 4646 msgid "Pre-cache failed." 4641 4647 msgstr "" 4642 4648 4643 #: fw-search-indexer.php:45 74649 #: fw-search-indexer.php:458 4644 4650 msgid "Reset endpoint is not available. Please refresh and try again." 4645 4651 msgstr "" 4646 4652 4647 #: fw-search-indexer.php:45 84653 #: fw-search-indexer.php:459 4648 4654 msgid "Settings are locked. Unlock settings to reset." 4649 4655 msgstr "" 4650 4656 4651 #: fw-search-indexer.php:4 594657 #: fw-search-indexer.php:460 4652 4658 msgid "Reset all settings to defaults? This cannot be undone." 4653 4659 msgstr "" 4654 4660 4655 #: fw-search-indexer.php:46 04661 #: fw-search-indexer.php:461 4656 4662 msgid "Final confirmation: this will overwrite your current settings. Continue?" 4657 4663 msgstr "" 4658 4664 4659 #: fw-search-indexer.php:46 14665 #: fw-search-indexer.php:462 4660 4666 msgid "Failed to reset settings." 4661 4667 msgstr "" 4662 4668 4663 #: fw-search-indexer.php:46 24669 #: fw-search-indexer.php:463 4664 4670 msgid "Replace all existing synonyms?" 4665 4671 msgstr "" 4666 4672 4667 #: fw-search-indexer.php:46 34673 #: fw-search-indexer.php:464 4668 4674 msgid "Replace all existing variant option aliases?" 4669 4675 msgstr "" 4670 4676 4671 #: fw-search-indexer.php:46 44677 #: fw-search-indexer.php:465 4672 4678 msgid "Replace all existing crawl exclusion rules?" 4673 4679 msgstr "" 4674 4680 4675 #: fw-search-indexer.php:46 54681 #: fw-search-indexer.php:466 4676 4682 msgid "Suggesting…" 4677 4683 msgstr "" 4678 4684 4679 #: fw-search-indexer.php:46 64685 #: fw-search-indexer.php:467 4680 4686 msgid "Suggest failed. Try again." 4681 4687 msgstr "" 4682 4688 4683 #: fw-search-indexer.php:46 74689 #: fw-search-indexer.php:468 4684 4690 msgid "Unlock settings to use Suggest." 4685 4691 msgstr "" 4686 4692 4687 #: fw-search-indexer.php:46 84693 #: fw-search-indexer.php:469 4688 4694 msgid "" 4689 4695 "Rebuild the search index now?\n" … … 4692 4698 msgstr "" 4693 4699 4694 #: fw-search-indexer.php:4 694700 #: fw-search-indexer.php:470 4695 4701 msgid "Settings are locked. Unlock settings to change crawling." 4696 4702 msgstr "" 4697 4703 4698 #: fw-search-indexer.php:47 04704 #: fw-search-indexer.php:471 4699 4705 msgid "Enabling..." 4700 4706 msgstr "" 4701 4707 4702 #: fw-search-indexer.php:47 14708 #: fw-search-indexer.php:472 4703 4709 msgid "Disabling..." 4704 4710 msgstr "" 4705 4711 4706 #: fw-search-indexer.php:47 24712 #: fw-search-indexer.php:473 4707 4713 msgid "Settings are locked. Unlock settings to reindex." 4708 4714 msgstr "" 4709 4715 4710 #: fw-search-indexer.php:47 34716 #: fw-search-indexer.php:474 4711 4717 msgid "Analyzing..." 4712 4718 msgstr "" 4713 4719 4714 #: fw-search-indexer.php:47 44720 #: fw-search-indexer.php:475 4715 4721 msgid "Run Full Reindex" 4716 4722 msgstr "" 4717 4723 4718 #: fw-search-indexer.php:47 54724 #: fw-search-indexer.php:476 4719 4725 msgid "Reindex failed. Check your license and try again." 4720 4726 msgstr "" 4721 4727 4722 #: fw-search-indexer.php:47 64728 #: fw-search-indexer.php:477 4723 4729 msgid "OK" 4724 4730 msgstr "" 4725 4731 4726 #: fw-search-indexer.php:47 74732 #: fw-search-indexer.php:478 4727 4733 msgid "Reindex request failed. This may be a timeout on large sites. Refresh the page to check if the reindex completed in the background." 4728 4734 msgstr "" 4729 4735 4730 4736 #. translators: %s = editor display name 4731 #: fw-search-indexer.php:48 14737 #: fw-search-indexer.php:482 4732 4738 #, php-format 4733 4739 msgid "%s is currently editing FunnelWon Search Engine settings. You can view configuration, but editing is temporarily disabled." … … 4735 4741 4736 4742 #. translators: %s: Remaining time until editing may be available (human-readable, e.g. countdown). 4737 #: fw-search-indexer.php:48 34743 #: fw-search-indexer.php:484 4738 4744 #, php-format 4739 4745 msgid "Editing access may become available in %s." 4740 4746 msgstr "" 4741 4747 4742 #: fw-search-indexer.php:48 64748 #: fw-search-indexer.php:487 4743 4749 msgid "- Settings are read-only to prevent accidental changes." 4744 4750 msgstr "" 4745 4751 4746 #: fw-search-indexer.php:48 74752 #: fw-search-indexer.php:488 4747 4753 msgid "- Changes are enabled. Remember to lock settings when finished." 4748 4754 msgstr "" 4749 4755 4750 #: fw-search-indexer.php:48 84756 #: fw-search-indexer.php:489 4751 4757 msgid "Unlocking..." 4752 4758 msgstr "" 4753 4759 4754 #: fw-search-indexer.php:4 894760 #: fw-search-indexer.php:490 4755 4761 msgid "Locking..." 4756 4762 msgstr "" 4757 4763 4758 #: fw-search-indexer.php:49 04764 #: fw-search-indexer.php:491 4759 4765 msgid "Dismiss this notice." 4760 4766 msgstr "" 4761 4767 4762 #: fw-search-indexer.php:49 14768 #: fw-search-indexer.php:492 4763 4769 msgid "" 4764 4770 "Disable FunnelWon Search?\n" … … 4770 4776 4771 4777 #. translators: %s = user display name 4772 #: fw-search-indexer.php:49 34778 #: fw-search-indexer.php:494 4773 4779 #, php-format 4774 4780 msgid "%s has taken over. Refreshing…" 4775 4781 msgstr "" 4776 4782 4777 #: fw-search-indexer.php:49 44783 #: fw-search-indexer.php:495 4778 4784 msgid "Settings are currently locked." 4779 4785 msgstr "" 4780 4786 4781 4787 #. translators: %s = countdown timer string like "2:30" 4782 #: fw-search-indexer.php:49 64788 #: fw-search-indexer.php:497 4783 4789 #, php-format 4784 4790 msgid " Try again in %s." … … 4786 4792 4787 4793 #. translators: %s = editor display name 4788 #: fw-search-indexer.php:49 84794 #: fw-search-indexer.php:499 4789 4795 #, php-format 4790 4796 msgid "%s is currently editing these settings." 4791 4797 msgstr "" 4792 4798 4793 #: fw-search-indexer.php: 4994799 #: fw-search-indexer.php:500 4794 4800 msgid "" 4795 4801 "\n" … … 4798 4804 msgstr "" 4799 4805 4800 #: fw-search-indexer.php:50 04806 #: fw-search-indexer.php:501 4801 4807 msgid "Could not verify edit session. Please refresh the page and try again." 4802 4808 msgstr "" 4803 4809 4804 #: fw-search-indexer.php:50 14810 #: fw-search-indexer.php:502 4805 4811 msgid "Military Time:" 4806 4812 msgstr "" 4807 4813 4808 #: fw-search-indexer.php:50 24809 #: serp/serp-content-block.php:18 494810 #: serp/serp-content-block.php:273 64814 #: fw-search-indexer.php:503 4815 #: serp/serp-content-block.php:1852 4816 #: serp/serp-content-block.php:2739 4811 4817 msgid "Pagination" 4812 4818 msgstr "" 4813 4819 4814 #: fw-search-indexer.php:50 34820 #: fw-search-indexer.php:504 4815 4821 msgid "Enter a query above and click \"Show results\" to load the preview list." 4816 4822 msgstr "" 4817 4823 4818 #: fw-search-indexer.php:50 44824 #: fw-search-indexer.php:505 4819 4825 msgid "No results for this query." 4820 4826 msgstr "" 4821 4827 4822 #: fw-search-indexer.php:50 54828 #: fw-search-indexer.php:506 4823 4829 msgid "(Promoted)" 4824 4830 msgstr "" 4825 4831 4826 #: fw-search-indexer.php:50 64832 #: fw-search-indexer.php:507 4827 4833 msgid "Could not load results." 4828 4834 msgstr "" 4829 4835 4830 #: fw-search-indexer.php:50 74836 #: fw-search-indexer.php:508 4831 4837 msgid "Removing reorder rule…" 4832 4838 msgstr "" 4833 4839 4834 #: fw-search-indexer.php:50 84840 #: fw-search-indexer.php:509 4835 4841 msgid "Could not remove reorder rule." 4836 4842 msgstr "" 4837 4843 4838 #: fw-search-indexer.php:5 094844 #: fw-search-indexer.php:510 4839 4845 msgid "Saving reorder rule…" 4840 4846 msgstr "" 4841 4847 4842 #: fw-search-indexer.php:51 04848 #: fw-search-indexer.php:511 4843 4849 msgid "Reorder rule save failed." 4844 4850 msgstr "" 4845 4851 4846 #: fw-search-indexer.php:51 14852 #: fw-search-indexer.php:512 4847 4853 msgid "Removing…" 4848 4854 msgstr "" 4849 4855 4850 #: fw-search-indexer.php:51 24856 #: fw-search-indexer.php:513 4851 4857 msgid "Remove failed" 4852 4858 msgstr "" 4853 4859 4854 #: fw-search-indexer.php:51 34860 #: fw-search-indexer.php:514 4855 4861 msgid "Paste a single embed URL (https://www.youtube.com/embed/… or player.vimeo.com/…) or the full iframe block from the host's share dialog (we read src). Watch links are converted when possible. Must end up as https on an allowed host. After the first save, this cannot be changed." 4856 4862 msgstr "" 4857 4863 4858 #: fw-search-indexer.php:51 44864 #: fw-search-indexer.php:515 4859 4865 msgid "Not used for video embed injections. The SERP shows an \"Embed\" line; the modal uses an iframe player." 4860 4866 msgstr "" 4861 4867 4862 #: fw-search-indexer.php:51 54868 #: fw-search-indexer.php:516 4863 4869 msgid "Not used for this media type. The SERP shows size and format from the file URL when available (same as organic media results)." 4864 4870 msgstr "" 4865 4871 4866 #: fw-search-indexer.php:51 84872 #: fw-search-indexer.php:519 4867 4873 msgid "Please select a content type before saving an injection." 4868 4874 msgstr "" 4869 4875 4870 4876 #. translators: %1$d = min chars, %2$d = entered chars 4871 #: fw-search-indexer.php:52 04877 #: fw-search-indexer.php:521 4872 4878 #, php-format 4873 4879 msgid "Excerpt must be at least %1$d characters (%2$d entered). Use Clear form if you only want to save other settings." … … 4875 4881 4876 4882 #. translators: 1: result type (e.g. Pages), 2: new position, 3: number of rows. Read out after reordering the All tab. 4877 #: fw-search-indexer.php:55 24883 #: fw-search-indexer.php:553 4878 4884 #, php-format 4879 4885 msgid "%1$s: position %2$d of %3$d" … … 5122 5128 5123 5129 #. translators: %d: number of image results 5124 #: serp/search-results.php:3 535130 #: serp/search-results.php:379 5125 5131 #, php-format 5126 5132 msgid "See %d image" … … 5130 5136 5131 5137 #. translators: %d: number of video results 5132 #: serp/search-results.php:3 575138 #: serp/search-results.php:383 5133 5139 #, php-format 5134 5140 msgid "See %d video" … … 5138 5144 5139 5145 #. translators: %d: number of document results 5140 #: serp/search-results.php:3 615146 #: serp/search-results.php:387 5141 5147 #, php-format 5142 5148 msgid "See %d document" … … 5146 5152 5147 5153 #. translators: %d: number of audio results 5148 #: serp/search-results.php:3 655154 #: serp/search-results.php:391 5149 5155 #, php-format 5150 5156 msgid "See %d audio file" … … 5154 5160 5155 5161 #. translators: %d: number of product results 5156 #: serp/search-results.php:3 695162 #: serp/search-results.php:395 5157 5163 #, php-format 5158 5164 msgid "See %d product" … … 5161 5167 msgstr[1] "" 5162 5168 5163 #: serp/search-results.php: 3755169 #: serp/search-results.php:401 5164 5170 msgid "No pages found." 5165 5171 msgstr "" … … 5209 5215 5210 5216 #. translators: %s: suggested search term (link to corrected search) 5211 #: serp/serp-content-block.php:174 05217 #: serp/serp-content-block.php:1743 5212 5218 #, php-format 5213 5219 msgid "Did you mean %s?" … … 5215 5221 5216 5222 #. translators: %s: suggested search term (link to corrected search) 5217 #: serp/serp-content-block.php:17 485223 #: serp/serp-content-block.php:1751 5218 5224 #, php-format 5219 5225 msgid "Search for %s" 5220 5226 msgstr "" 5221 5227 5222 #: serp/serp-content-block.php:17 675228 #: serp/serp-content-block.php:1770 5223 5229 msgid "Search result types" 5224 5230 msgstr "" 5225 5231 5226 #: serp/serp-content-block.php:179 55232 #: serp/serp-content-block.php:1798 5227 5233 msgid "Page not found; here are similar results." 5228 5234 msgstr "" 5229 5235 5230 5236 #. translators: %s: search query used to find similar results 5231 #: serp/serp-content-block.php:1 7995237 #: serp/serp-content-block.php:1802 5232 5238 #, php-format 5233 5239 msgid "Page not found; here are similar results for %s" … … 5235 5241 5236 5242 #. translators: %s: search query 5237 #: serp/serp-content-block.php:180 65243 #: serp/serp-content-block.php:1809 5238 5244 #, php-format 5239 5245 msgid "Search Results for: %s" … … 5241 5247 5242 5248 #. translators: 1: matched query term, 2: canonical synonym 5243 #: serp/serp-content-block.php:181 45249 #: serp/serp-content-block.php:1817 5244 5250 #, php-format 5245 5251 msgid " (%1$s is a synonym of %2$s)" … … 5247 5253 5248 5254 #. translators: %d: number of results 5249 #: serp/serp-content-block.php:184 25250 #: serp/serp-content-block.php:273 05255 #: serp/serp-content-block.php:1845 5256 #: serp/serp-content-block.php:2733 5251 5257 #, php-format 5252 5258 msgid "%d Results" 5253 5259 msgstr "" 5254 5260 5255 #: serp/serp-content-block.php:276 65261 #: serp/serp-content-block.php:2769 5256 5262 msgid "Continue to search results" 5257 5263 msgstr "" 5258 5264 5259 #: serp/serp-content-block.php:277 35265 #: serp/serp-content-block.php:2776 5260 5266 msgid "Authentication failed" 5261 5267 msgstr "" 5262 5268 5263 #: serp/serp-content-block.php:277 55269 #: serp/serp-content-block.php:2778 5264 5270 msgid "Rate limited" 5265 5271 msgstr "" 5266 5272 5267 #: serp/serp-content-block.php:27 775273 #: serp/serp-content-block.php:2780 5268 5274 msgid "Could not reach search API" 5269 5275 msgstr "" 5270 5276 5271 #: serp/serp-content-block.php:27 795277 #: serp/serp-content-block.php:2782 5272 5278 msgid "Search API error" 5273 5279 msgstr "" 5274 5280 5275 #: serp/serp-content-block.php:278 25281 #: serp/serp-content-block.php:2785 5276 5282 msgid "Search is temporarily unavailable. Please try again later." 5277 5283 msgstr "" 5278 5284 5279 #: serp/serp-content-block.php:278 55285 #: serp/serp-content-block.php:2788 5280 5286 msgid "Try a more specific search. Common words like “and” or “the” are skipped." 5281 5287 msgstr "" 5282 5288 5283 #: serp/serp-content-block.php:279 05289 #: serp/serp-content-block.php:2793 5284 5290 msgid "Page not found. We couldn't find similar pages. Try searching above." 5285 5291 msgstr "" 5286 5292 5287 5293 #. translators: %s: the user's search query 5288 #: serp/serp-content-block.php:279 45294 #: serp/serp-content-block.php:2797 5289 5295 #, php-format 5290 5296 msgid "No products matched “%s”." 5291 5297 msgstr "" 5292 5298 5293 #: serp/serp-content-block.php:2 7975299 #: serp/serp-content-block.php:2800 5294 5300 msgid "You’re searching products only. Try product names, styles, sizes, colors, or a budget — for example “red shoes size 7” or “red shoes under $100”." 5295 5301 msgstr "" … … 5297 5303 #. translators: %s: URL to sitewide search without product scope 5298 5304 #. translators: %s: URL to sitewide search without scope 5299 #: serp/serp-content-block.php:28 075300 #: serp/serp-content-block.php:28 295305 #: serp/serp-content-block.php:2810 5306 #: serp/serp-content-block.php:2832 5301 5307 #, php-format 5302 5308 msgid "<a href=\"%s\">Search everything on this site</a>" 5303 5309 msgstr "" 5304 5310 5305 #: serp/serp-content-block.php:281 35311 #: serp/serp-content-block.php:2816 5306 5312 msgid "this scope" 5307 5313 msgstr "" 5308 5314 5309 5315 #. translators: 1: search query, 2: scope phrase e.g. Products 5310 #: serp/serp-content-block.php:281 65316 #: serp/serp-content-block.php:2819 5311 5317 #, php-format 5312 5318 msgid "No results for “%1$s” within %2$s." 5313 5319 msgstr "" 5314 5320 5315 #: serp/serp-content-block.php:283 35321 #: serp/serp-content-block.php:2836 5316 5322 msgid "No results found." 5317 5323 msgstr "" 5318 5324 5319 #: serp/serp-content-block.php:286 35325 #: serp/serp-content-block.php:2866 5320 5326 msgid "Media Preview" 5321 5327 msgstr "" 5322 5328 5323 #: serp/serp-content-block.php:286 55329 #: serp/serp-content-block.php:2868 5324 5330 msgid "Expand to full screen" 5325 5331 msgstr "" 5326 5332 5327 #: serp/serp-content-block.php:28 685333 #: serp/serp-content-block.php:2871 5328 5334 msgid "Close preview" 5329 5335 msgstr "" 5330 5336 5331 #: serp/serp-content-block.php:287 55337 #: serp/serp-content-block.php:2878 5332 5338 msgid "Go to page" 5333 5339 msgstr "" 5334 5340 5335 #: serp/serp-content-block.php:287 65341 #: serp/serp-content-block.php:2879 5336 5342 msgid "Open in new tab" 5337 5343 msgstr "" … … 5380 5386 msgid "Sorry, you are not allowed to do that." 5381 5387 msgstr "" 5388 5389 #: serp/smart-404.php:611 5390 msgid "Page not found" 5391 msgstr "" -
searchus/trunk/readme.txt
r3769819 r3783147 4 4 Requires at least: 5.9 5 5 Tested up to: 7.1 6 Stable tag: 3.1. 36 Stable tag: 3.1.4 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later … … 239 239 240 240 == Changelog == 241 242 = 3.1.4 = 243 * Improved: Search uses less of your server. Search boxes send visitors straight to the results page instead of through a redirect, and the results page loads its results with fewer WordPress database queries. 244 * Improved: Crawlers, monitors and scripts that request a search results page get a short page right away instead of a full results page. Their searches were already not counted. 245 * Improved: Smart 404 no longer searches for missing addresses made only of scanner words (such as /admin/login or /api/config), and more scanner and spam patterns in searches are ignored, so they don't count toward your plan. 241 246 242 247 = 3.1.3 = … … 393 398 == Upgrade Notice == 394 399 400 = 3.1.4 = 401 Search uses less of the WordPress server resources and improved blocking of bot crawlers. No reindex required. 402 395 403 = 3.1.3 = 396 404 Theme compatibility improvements and a WordPress search fallback. -
searchus/trunk/serp/page-template-404-recovery.php
r3601545 r3783147 78 78 $recovery_query_text = ''; 79 79 $hide_recovery_detail = false; 80 if ( function_exists( 'fw_si_get_search_results' ) ) { 81 $payload = fw_si_get_search_results( 82 $fw_si_recovery_query, 83 [ 84 'apply_result_control' => true, 85 'context' => 'smart404', 86 ] 87 ); 80 // Same search the status code was decided from (smart-404.php, template_include). 81 if ( function_exists( 'fw_si_smart404_recovery' ) ) { 82 $fw_si_recovery = fw_si_smart404_recovery(); 83 $fw_si_recovery_query = $fw_si_recovery['query']; 84 $payload = $fw_si_recovery['payload']; 85 $recovery_h1 = $fw_si_recovery['h1']; 88 86 } 89 87 // Analytics: do not call fw_si_analytics_record_search here — fw_si_render_serp_content_block() 90 88 // records smart404 searches once when it renders (avoids double count with preloaded payload). 91 if ( is_array( $payload ) && function_exists( 'fw_si_build_smart404_h1' ) ) {92 $recovery_h1 = fw_si_build_smart404_h1( $fw_si_recovery_query, $payload );93 }94 89 if ( $recovery_h1 !== '' ) { 95 90 $recovery_query_text = $recovery_h1; -
searchus/trunk/serp/search-results.php
r3769819 r3783147 206 206 } 207 207 208 // Crawlers, monitors and scripts: no API call, so nothing is billed or recorded. 209 $fw_si_bot = function_exists( 'fw_si_is_bot_request' ) && fw_si_is_bot_request(); 208 $fw_si_is_smart404 = ( $options['context'] ?? '' ) === 'smart404'; 209 // Crawlers, monitors and scripts: no API call, so nothing is billed or recorded. Search engine and AI 210 // crawlers on a Smart 404 page, or on a results page on the Free plan, are the exception: the service 211 // never counts their searches and returns results when the request's address is the crawler's. 212 $fw_si_bot = function_exists( 'fw_si_is_bot_request' ) && fw_si_is_bot_request(); 213 $fw_si_crawler = ( $fw_si_bot && empty( $options['live_preview'] ) && function_exists( 'fw_si_search_crawler_name' ) 214 && ( $fw_si_is_smart404 || ( function_exists( 'fw_si_crawlers_read_results_pages' ) && fw_si_crawlers_read_results_pages() ) ) ) 215 ? fw_si_search_crawler_name() : ''; 216 if ( $fw_si_crawler !== '' ) { 217 $fw_si_bot = false; 218 } 210 219 // Only stop words: the API would drop every word and still bill the search, so skip it. 211 220 // Live previews are exempt: their last word is a prefix ("and" may complete to "android"). 212 221 $fw_si_stop_only = empty( $options['live_preview'] ) && function_exists( 'fw_si_is_stop_words_only_query' ) && fw_si_is_stop_words_only_query( $api_query ); 213 if ( $fw_si_bot || $fw_si_stop_only ) { 222 // A missing URL made only of scanner path words (/admin/login, /api/config) is a probe, not a visitor. 223 $fw_si_probe = $fw_si_is_smart404 && function_exists( 'fw_si_is_smart404_probe_query' ) && fw_si_is_smart404_probe_query( $api_query ); 224 if ( $fw_si_bot || $fw_si_stop_only || $fw_si_probe ) { 214 225 if ( $fw_si_bot ) { 215 226 // A page cache must not keep this empty page and serve it to visitors. … … 254 265 $api_options[ $live_key ] = $options[ $live_key ]; 255 266 } 267 } 268 if ( $fw_si_is_smart404 ) { 269 $api_options['source'] = 'smart404'; 270 } 271 if ( $fw_si_crawler !== '' ) { 272 $api_options['crawler'] = $fw_si_crawler; 273 // REMOTE_ADDR only: forwarded-for headers are set by whoever sends the request. 274 $remote_addr = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['REMOTE_ADDR'] ) ) : ''; 275 /** 276 * Address sent to the service to verify a crawler's search (default REMOTE_ADDR). 277 * Behind a proxy that does not restore the visitor's address, return the trusted client address here. 278 * 279 * @param string $remote_addr Request address. 280 */ 281 $api_options['crawler_ip'] = (string) apply_filters( 'fw_si_search_crawler_ip', $remote_addr ); 256 282 } 257 283 // Results page reached from live search (Enter / See all): same session, so it is not counted twice. … … 548 574 549 575 /** 576 * Load the posts for a page of results, with their meta, terms and featured images, in a few queries 577 * instead of several per result. 578 * 579 * @param array $ids Result IDs (int or "media:attachment:123"); others are skipped. 580 */ 581 function fw_si_prime_result_post_caches( array $ids ): void { 582 if ( ! function_exists( '_prime_post_caches' ) || ! function_exists( 'fw_si_resolve_post_id' ) ) { 583 return; 584 } 585 $post_ids = []; 586 foreach ( $ids as $rid ) { 587 $pid = ( is_int( $rid ) || is_string( $rid ) ) ? fw_si_resolve_post_id( $rid ) : null; 588 if ( $pid !== null && $pid > 0 ) { 589 $post_ids[ $pid ] = $pid; 590 } 591 } 592 if ( ! $post_ids ) { 593 return; 594 } 595 _prime_post_caches( array_values( $post_ids ), true, true ); 596 $thumbs = []; 597 foreach ( $post_ids as $pid ) { 598 $thumb = (int) get_post_meta( $pid, '_thumbnail_id', true ); 599 if ( $thumb > 0 ) { 600 $thumbs[ $thumb ] = $thumb; 601 } 602 } 603 if ( $thumbs ) { 604 _prime_post_caches( array_values( $thumbs ), false, true ); 605 } 606 } 607 608 /** 550 609 * Convert payload IDs to WP_Post array for the_posts filter. 551 610 * … … 554 613 */ 555 614 function fw_si_payload_ids_to_posts( array $ids ): array { 615 fw_si_prime_result_post_caches( $ids ); 556 616 $posts = []; 557 617 foreach ( $ids as $pid ) { -
searchus/trunk/serp/serp-content-block.php
r3769819 r3783147 1646 1646 } 1647 1647 $qt_build_start = $qt_serp_debug ? microtime( true ) : 0; 1648 if ( function_exists( 'fw_si_prime_result_post_caches' ) ) { 1649 fw_si_prime_result_post_caches( $is_404_recovery ? array_slice( $slice, 0, 12 ) : $slice ); 1650 } 1648 1651 foreach ( $slice as $rid ) { 1649 1652 if ( $is_404_recovery && count( $serp_display ) >= 6 ) { -
searchus/trunk/serp/smart-404.php
r3769819 r3783147 74 74 75 75 /** 76 * True when the missing URL ends in an image, style, script, font or media file extension. 77 * 78 * @return bool 79 */ 80 function fw_si_smart404_path_is_static_file(): bool { 81 $uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['REQUEST_URI'] ) ) : ''; 82 $path = (string) wp_parse_url( $uri, PHP_URL_PATH ); 83 return (bool) preg_match( '/\.(?:jpe?g|png|gif|webp|avif|svg|ico|bmp|tiff?|css|js|mjs|map|json|xml|txt|woff2?|ttf|otf|eot|mp4|webm|mov|mp3|wav|ogg|zip|gz)$/i', $path ); 84 } 85 86 /** 87 * True when the missing URL is being opened as a page by a visitor's browser or a search engine or AI crawler. 88 * Those crawlers see the recovery page with similar results and the attribution. Other crawlers and scripts, 89 * and missing images or scripts, get the theme's 404. 90 * 91 * @return bool 92 */ 93 function fw_si_smart404_request_wants_page(): bool { 94 if ( function_exists( 'fw_si_is_bot_request' ) && fw_si_is_bot_request() ) { 95 // Crawlers often send Accept: */*, so file type comes from the URL. 96 return function_exists( 'fw_si_search_crawler_name' ) && fw_si_search_crawler_name() !== '' && ! fw_si_smart404_path_is_static_file(); 97 } 98 $dest = isset( $_SERVER['HTTP_SEC_FETCH_DEST'] ) ? strtolower( sanitize_text_field( wp_unslash( (string) $_SERVER['HTTP_SEC_FETCH_DEST'] ) ) ) : ''; 99 if ( $dest !== '' ) { 100 return in_array( $dest, array( 'document', 'iframe', 'frame' ), true ); 101 } 102 // Browsers without Sec-Fetch-Dest still list text/html when opening a page. 103 $accept = isset( $_SERVER['HTTP_ACCEPT'] ) ? strtolower( sanitize_text_field( wp_unslash( (string) $_SERVER['HTTP_ACCEPT'] ) ) ) : ''; 104 return strpos( $accept, 'text/html' ) !== false; 105 } 106 107 /** 76 108 * Derive a search query from the 404 URL. 77 109 * … … 301 333 } 302 334 $serving_recovery = ! empty( $GLOBALS['fw_si_serving_404_recovery'] ); 303 $is_404_recovery = $serving_recovery || ( is_404() && (bool) get_option( 'fw_si_smart_404_enabled', true ) && fw_si_ derive_query_from_404_url() !== '' );335 $is_404_recovery = $serving_recovery || ( is_404() && (bool) get_option( 'fw_si_smart_404_enabled', true ) && fw_si_smart404_request_wants_page() && fw_si_derive_query_from_404_url() !== '' ); 304 336 if ( ! $is_404_recovery ) { 305 337 return; … … 380 412 return; 381 413 } 414 if ( ! fw_si_smart404_request_wants_page() ) { 415 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 416 fw_si_smart404_debug_bail( 'not_a_visitor_page_request' ); 417 } 418 return; 419 } 382 420 $query = fw_si_derive_query_from_404_url(); 383 421 if ( $query === '' ) { … … 422 460 423 461 /** 462 * Search, payload and heading for this request's Smart 404 page. The search runs once, before the page 463 * header is sent, so the HTTP status can say whether similar results were found. 464 * 465 * @return array{query:string,payload:array,h1:string,found:bool} 466 */ 467 function fw_si_smart404_recovery(): array { 468 static $recovery = null; 469 if ( $recovery !== null ) { 470 return $recovery; 471 } 472 $query = fw_si_derive_query_from_404_url(); 473 $payload = ( $query !== '' && function_exists( 'fw_si_get_search_results' ) ) 474 ? fw_si_get_search_results( 475 $query, 476 [ 477 'apply_result_control' => true, 478 'context' => 'smart404', 479 ] 480 ) 481 : []; 482 $payload = is_array( $payload ) ? $payload : []; 483 $recovery = [ 484 'query' => $query, 485 'payload' => $payload, 486 'h1' => $payload ? fw_si_build_smart404_h1( $query, $payload ) : '', 487 // Similar means a word from the missing URL is in the shown results' titles, not just any fuzzy match. 488 'found' => fw_si_smart404_url_words_in_titles( $query, $payload ) && fw_si_smart404_payload_has_results( $payload ), 489 ]; 490 return $recovery; 491 } 492 493 /** 494 * True when a word from the missing URL (plural or not) appears in a title of the first 12 results. 495 * 496 * @param string $query Query derived from the URL. 497 * @param array $payload Search payload. 498 * @return bool 499 */ 500 function fw_si_smart404_url_words_in_titles( string $query, array $payload ): bool { 501 // Singular forms a word may stand for: "hoodies" → hoodie, "boxes" → box, "berries" → berry. 502 $forms = static function ( string $word ): array { 503 $out = [ $word ]; 504 $len = strlen( $word ); 505 if ( $len > 3 && substr( $word, -1 ) === 's' ) { 506 $out[] = substr( $word, 0, -1 ); 507 } 508 if ( $len > 4 && substr( $word, -2 ) === 'es' ) { 509 $out[] = substr( $word, 0, -2 ); 510 } 511 if ( $len > 4 && substr( $word, -3 ) === 'ies' ) { 512 $out[] = substr( $word, 0, -3 ) . 'y'; 513 } 514 return $out; 515 }; 516 $filler = array_flip( array( '404', 'www', 'http', 'https', 'html', 'htm', 'php', 'com', 'net', 'org', 'bad', 'url', 'page', 'post', 'search', 'old', 'new', 'the', 'and', 'for', 'with' ) ); 517 $url_words = []; 518 foreach ( preg_split( '/[^a-z0-9]+/', strtolower( $query ), -1, PREG_SPLIT_NO_EMPTY ) ?: [] as $word ) { 519 if ( strlen( $word ) >= 2 && ! isset( $filler[ $word ] ) ) { 520 foreach ( $forms( $word ) as $form ) { 521 $url_words[ $form ] = true; 522 } 523 } 524 } 525 if ( ! $url_words || empty( $payload['all'] ) || ! is_array( $payload['all'] ) ) { 526 return false; 527 } 528 $titles = isset( $payload['titles'] ) && is_array( $payload['titles'] ) ? $payload['titles'] : []; 529 foreach ( array_slice( $payload['all'], 0, 12 ) as $rid ) { 530 $title = isset( $titles[ $rid ] ) ? (string) $titles[ $rid ] : ''; 531 if ( $title === '' && function_exists( 'fw_si_resolve_post_id' ) && ( is_int( $rid ) || is_string( $rid ) ) ) { 532 $post_id = fw_si_resolve_post_id( $rid ); 533 $title = $post_id ? (string) get_the_title( $post_id ) : ''; 534 } 535 foreach ( preg_split( '/[^a-z0-9]+/', strtolower( $title ), -1, PREG_SPLIT_NO_EMPTY ) ?: [] as $word ) { 536 foreach ( $forms( $word ) as $form ) { 537 if ( isset( $url_words[ $form ] ) ) { 538 return true; 539 } 540 } 541 } 542 } 543 return false; 544 } 545 546 /** 547 * True when the payload has at least one result the Smart 404 page shows. 548 * 549 * @param array $payload Search payload. 550 * @return bool 551 */ 552 function fw_si_smart404_payload_has_results( array $payload ): bool { 553 if ( ! empty( $payload['_fw_error'] ) || empty( $payload['all'] ) || ! is_array( $payload['all'] ) ) { 554 return false; 555 } 556 foreach ( array_slice( $payload['all'], 0, 50 ) as $rid ) { 557 if ( function_exists( 'fw_si_result_control_is_synthetic_result_id' ) && fw_si_result_control_is_synthetic_result_id( $rid ) ) { 558 return true; 559 } 560 $post_id = ( function_exists( 'fw_si_resolve_post_id' ) && ( is_int( $rid ) || is_string( $rid ) ) ) ? fw_si_resolve_post_id( $rid ) : null; 561 $post = $post_id ? get_post( $post_id ) : null; 562 if ( $post instanceof WP_Post && ( ! function_exists( 'fw_si_serp_post_allowed_for_404_recovery' ) || fw_si_serp_post_allowed_for_404_recovery( $post ) ) ) { 563 return true; 564 } 565 } 566 return false; 567 } 568 569 /** 570 * This request's URL (no query string), the Smart 404 page's own address. 571 * 572 * @return string 573 */ 574 function fw_si_smart404_page_url(): string { 575 $uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( (string) $_SERVER['REQUEST_URI'] ) ) : '/'; 576 $path = (string) wp_parse_url( $uri, PHP_URL_PATH ); 577 $home = wp_parse_url( home_url( '/' ) ); 578 if ( ! is_array( $home ) || empty( $home['host'] ) ) { 579 return ''; 580 } 581 $port = isset( $home['port'] ) ? ':' . (int) $home['port'] : ''; 582 return esc_url_raw( ( $home['scheme'] ?? 'https' ) . '://' . $home['host'] . $port . ( $path !== '' ? $path : '/' ) ); 583 } 584 585 /** 586 * Head tags for the Smart 404 page. The page is built on the results page with the home page's header, so 587 * WordPress and SEO plugins would otherwise name the home page as canonical and use the site name as title. 588 * With results: canonical is the page's own URL, so search engines can index it. Without: no canonical. 589 * 590 * @param array $recovery fw_si_smart404_recovery(). 591 */ 592 function fw_si_smart404_page_head( array $recovery ): void { 593 remove_action( 'wp_head', 'rel_canonical' ); 594 remove_action( 'wp_head', 'wp_shortlink_wp_head', 10 ); 595 $canonical = ! empty( $recovery['found'] ) ? fw_si_smart404_page_url() : ''; 596 $canonical_filter = static function () use ( $canonical ) { 597 return $canonical !== '' ? $canonical : false; 598 }; 599 add_filter( 'wpseo_canonical', $canonical_filter, 999 ); 600 add_filter( 'rank_math/frontend/canonical', $canonical_filter, 999 ); 601 if ( $canonical !== '' && ! defined( 'WPSEO_VERSION' ) && ! defined( 'RANK_MATH_VERSION' ) ) { 602 add_action( 603 'wp_head', 604 static function () use ( $canonical ) { 605 echo '<link rel="canonical" href="' . esc_url( $canonical ) . '" />' . "\n"; 606 }, 607 10 608 ); 609 } 610 $heading = ! empty( $recovery['found'] ) && isset( $recovery['h1'] ) ? trim( (string) $recovery['h1'] ) : ''; 611 $title = ( $heading !== '' ? $heading : __( 'Page not found', 'searchus' ) ) . ' | ' . get_bloginfo( 'name', 'display' ); 612 $title_filter = static function () use ( $title ) { 613 return esc_html( $title ); 614 }; 615 add_filter( 'pre_get_document_title', $title_filter, 999 ); 616 add_filter( 'wpseo_title', $title_filter, 999 ); 617 add_filter( 'rank_math/frontend/title', $title_filter, 999 ); 618 } 619 620 /** 424 621 * template_include: Serve recovery template when we replaced 404 with Search page (theme header renders). 425 622 * Runs at 5 so we override before search-page.php (25) when serving Smart 404. … … 441 638 $recovery_template = FW_SI_PLUGIN_DIR . '/serp/page-template-404-recovery.php'; 442 639 if ( file_exists( $recovery_template ) ) { 443 status_header( 404 ); 640 // A page of similar results is content search engines may index; a page with none is still a 404. 641 $recovery = fw_si_smart404_recovery(); 642 status_header( $recovery['found'] ? 200 : 404 ); 444 643 nocache_headers(); 644 fw_si_smart404_page_head( $recovery ); 445 645 return $recovery_template; 446 646 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)