The SEO Framework - Version 4.2.4

Version Description

This minor update improves image processing, reducing TSF's load impact by roughly 20% when generating metadata. We also added WordPress 6.0 support for image filesizes, making social sharing even more robust.

Download this release

Release Info

Developer Cybr
Plugin Icon 128x128 The SEO Framework
Version 4.2.4
Comparing to
See all releases

Code changes from version 4.2.3 to 4.2.4

Files changed (46) hide show
  1. autodescription.php +2 -2
  2. bootstrap/upgrade.php +0 -1
  3. inc/classes/admin-init.class.php +3 -3
  4. inc/classes/bridges/feed.class.php +1 -1
  5. inc/classes/bridges/ping.class.php +7 -2
  6. inc/classes/bridges/plugintable.class.php +6 -6
  7. inc/classes/bridges/sitemap.class.php +2 -2
  8. inc/classes/builders/robots/args.class.php +3 -8
  9. inc/classes/builders/robots/query.class.php +7 -16
  10. inc/classes/builders/scripts.class.php +1 -1
  11. inc/classes/builders/sitemap/base.class.php +11 -8
  12. inc/classes/builders/sitemap/main.class.php +4 -4
  13. inc/classes/core.class.php +26 -13
  14. inc/classes/detect.class.php +20 -3
  15. inc/classes/generate-description.class.php +3 -7
  16. inc/classes/generate-image.class.php +86 -46
  17. inc/classes/generate-ldjson.class.php +13 -10
  18. inc/classes/generate-title.class.php +28 -22
  19. inc/classes/generate-url.class.php +5 -5
  20. inc/classes/init.class.php +16 -16
  21. inc/classes/internal/debug.class.php +5 -5
  22. inc/classes/internal/deprecated.class.php +11 -6
  23. inc/classes/interpreters/html.class.php +1 -1
  24. inc/classes/interpreters/markdown.class.php +2 -2
  25. inc/classes/load.class.php +1 -1
  26. inc/classes/query.class.php +22 -8
  27. inc/classes/render.class.php +2 -2
  28. inc/classes/sanitize.class.php +26 -21
  29. inc/classes/site-options.class.php +2 -1
  30. inc/functions/api.php +5 -7
  31. inc/functions/upgrade-suggestion.php +10 -8
  32. inc/views/edit/seo-settings-singular.php +2 -2
  33. inc/views/edit/seo-settings-tt.php +2 -2
  34. inc/views/settings/metaboxes/general.php +9 -1
  35. inc/views/settings/metaboxes/homepage.php +2 -2
  36. inc/views/settings/metaboxes/post-type-archive.php +2 -2
  37. inc/views/settings/metaboxes/schema.php +19 -12
  38. inc/views/settings/metaboxes/sitemaps.php +5 -5
  39. inc/views/settings/metaboxes/title.php +2 -3
  40. inc/views/settings/wrap.php +40 -12
  41. inc/views/sitemap/xml-sitemap.php +8 -8
  42. inc/views/sitemap/xsl-stylesheet.php +1 -1
  43. inc/views/sitemap/xsl/description.php +2 -2
  44. lib/css/settings.css +39 -25
  45. lib/css/settings.min.css +1 -1
  46. readme.txt +7 -3
autodescription.php CHANGED
@@ -3,7 +3,7 @@
3
  * Plugin Name: The SEO Framework
4
  * Plugin URI: https://theseoframework.com/
5
  * Description: An automated, advanced, accessible, unbranded and extremely fast SEO solution for your WordPress website.
6
- * Version: 4.2.3
7
  * Author: The SEO Framework Team
8
  * Author URI: https://theseoframework.com/
9
  * License: GPLv3
@@ -41,7 +41,7 @@ defined( 'ABSPATH' ) or die;
41
  *
42
  * @since 2.3.5
43
  */
44
- define( 'THE_SEO_FRAMEWORK_VERSION', '4.2.3' );
45
 
46
  /**
47
  * The plugin Database version.
3
  * Plugin Name: The SEO Framework
4
  * Plugin URI: https://theseoframework.com/
5
  * Description: An automated, advanced, accessible, unbranded and extremely fast SEO solution for your WordPress website.
6
+ * Version: 4.2.4
7
  * Author: The SEO Framework Team
8
  * Author URI: https://theseoframework.com/
9
  * License: GPLv3
41
  *
42
  * @since 2.3.5
43
  */
44
+ define( 'THE_SEO_FRAMEWORK_VERSION', '4.2.4' );
45
 
46
  /**
47
  * The plugin Database version.
bootstrap/upgrade.php CHANGED
@@ -187,7 +187,6 @@ function _do_upgrade() {
187
  * @return string $current_version The current database version.
188
  */
189
  function _downgrade( $previous_version ) { // phpcs:ignore,VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
190
-
191
  // We aren't (currently) expecting issues where downgrading causes mayem. 4051 did cause some, though. This was added later; just set to current.
192
  return _set_to_current_version();
193
  }
187
  * @return string $current_version The current database version.
188
  */
189
  function _downgrade( $previous_version ) { // phpcs:ignore,VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
 
190
  // We aren't (currently) expecting issues where downgrading causes mayem. 4051 did cause some, though. This was added later; just set to current.
191
  return _set_to_current_version();
192
  }
inc/classes/admin-init.class.php CHANGED
@@ -369,6 +369,8 @@ class Admin_Init extends Init {
369
  * @since 2.9.3 1. Query arguments work again (regression 2.9.2).
370
  * 2. Now only accepts http and https protocols.
371
  * @since 4.2.0 Now allows query arguments with value 0|'0'.
 
 
372
  *
373
  * @param string $page Menu slug. This slug must exist, or the redirect will loop back to the current page.
374
  * @param array $query_args Optional. Associative array of query string arguments
@@ -398,8 +400,6 @@ class Admin_Init extends Init {
398
 
399
  // White screen of death for non-debugging users. Let's make it friendlier.
400
  if ( $headers_sent && $target ) {
401
- $this->handle_admin_redirect_error( $target );
402
-
403
  $headers_list = headers_list();
404
  $location = sprintf( 'Location: %s', \wp_sanitize_redirect( $target ) );
405
 
@@ -539,7 +539,7 @@ class Admin_Init extends Init {
539
  * @return bool True on success, false on failure.
540
  */
541
  public function clear_persistent_notice( $key ) {
542
-
543
  $notices = $this->get_static_cache( 'persistent_notices', [] );
544
  unset( $notices[ $key ] );
545
 
369
  * @since 2.9.3 1. Query arguments work again (regression 2.9.2).
370
  * 2. Now only accepts http and https protocols.
371
  * @since 4.2.0 Now allows query arguments with value 0|'0'.
372
+ * @TODO Remove failsafe? WP 5.2/5.4 broke it (this method can never run on failure...)
373
+ * Maybe we should investigate the cause and remove WP's blockade. This is a corner-case, however.
374
  *
375
  * @param string $page Menu slug. This slug must exist, or the redirect will loop back to the current page.
376
  * @param array $query_args Optional. Associative array of query string arguments
400
 
401
  // White screen of death for non-debugging users. Let's make it friendlier.
402
  if ( $headers_sent && $target ) {
 
 
403
  $headers_list = headers_list();
404
  $location = sprintf( 'Location: %s', \wp_sanitize_redirect( $target ) );
405
 
539
  * @return bool True on success, false on failure.
540
  */
541
  public function clear_persistent_notice( $key ) {
542
+ // TODO We could make a oneliner using array_diff_key?: array_diff_key( cache, [ $key ] )
543
  $notices = $this->get_static_cache( 'persistent_notices', [] );
544
  unset( $notices[ $key ] );
545
 
inc/classes/bridges/feed.class.php CHANGED
@@ -128,7 +128,7 @@ final class Feed {
128
  }
129
 
130
  if ( static::$tsf->get_option( 'source_the_feed' ) ) {
131
- $content .= PHP_EOL . $this->get_feed_entry_source_link();
132
  }
133
 
134
  return $content;
128
  }
129
 
130
  if ( static::$tsf->get_option( 'source_the_feed' ) ) {
131
+ $content .= "\n" . $this->get_feed_entry_source_link();
132
  }
133
 
134
  return $content;
inc/classes/bridges/ping.class.php CHANGED
@@ -80,7 +80,12 @@ final class Ping {
80
  }
81
 
82
  /**
83
- * Retries pinging the search engines.
 
 
 
 
 
84
  *
85
  * @since 4.1.2
86
  * @see static::engage_pinging_retry_cron()
@@ -96,7 +101,7 @@ final class Ping {
96
  }
97
 
98
  /**
99
- * Pings search engines.
100
  *
101
  * @since 2.2.9
102
  * @since 2.8.0 Only worked when the blog was not public...
80
  }
81
 
82
  /**
83
+ * Retries pinging the search engines for the base sitemap only.
84
+ *
85
+ * Devs: If you need to retry pinging another sitemap, please engage a custom scheduler,
86
+ * first call `engage_pinging_retry_cron( [ 'id' => 'my_sitemap_id' ] )`, then hook
87
+ * into action `tsf_sitemap_cron_hook_retry` and test `$args['id']` like below.
88
+ * Alternatively, hitch with `the_seo_framework_ping_search_engines`.
89
  *
90
  * @since 4.1.2
91
  * @see static::engage_pinging_retry_cron()
101
  }
102
 
103
  /**
104
+ * Pings search engines the base sitemap.
105
  *
106
  * @since 2.2.9
107
  * @since 2.8.0 Only worked when the blog was not public...
inc/classes/bridges/plugintable.class.php CHANGED
@@ -59,12 +59,12 @@ final class PluginTable {
59
  }
60
 
61
  $tsf_links['tsfem'] = sprintf(
62
- '<a href="%s" rel="noreferrer noopener" target="_blank">%s</a>',
63
  'https://theseoframework.com/extensions/',
64
  \esc_html_x( 'Extensions', 'Plugin extensions', 'autodescription' )
65
  );
66
  $tsf_links['pricing'] = sprintf(
67
- '<a href="%s" rel="noreferrer noopener" target="_blank">%s</a>',
68
  'https://theseoframework.com/pricing/',
69
  \esc_html_x( 'Pricing', 'Plugin pricing', 'autodescription' )
70
  );
@@ -97,28 +97,28 @@ final class PluginTable {
97
  $plugin_meta,
98
  [
99
  'support' => vsprintf(
100
- '<a href="%s" rel="noreferrer noopener nofollow" target="_blank">%s</a>',
101
  [
102
  'https://tsf.fyi/support',
103
  \esc_html__( 'Get support', 'autodescription' ),
104
  ]
105
  ),
106
  'docs' => vsprintf(
107
- '<a href="%s" rel="noreferrer noopener nofollow" target="_blank">%s</a>',
108
  [
109
  'https://tsf.fyi/docs',
110
  \esc_html__( 'View documentation', 'autodescription' ),
111
  ]
112
  ),
113
  'API' => vsprintf(
114
- '<a href="%s" rel="noreferrer noopener nofollow" target="_blank">%s</a>',
115
  [
116
  'https://tsf.fyi/docs/api',
117
  \esc_html__( 'View API docs', 'autodescription' ),
118
  ]
119
  ),
120
  'EM' => vsprintf(
121
- '<a href="%s" rel="noreferrer noopener nofollow" target="_blank">%s</a>',
122
  [
123
  'https://tsf.fyi/extension-manager',
124
  $_get_em
59
  }
60
 
61
  $tsf_links['tsfem'] = sprintf(
62
+ '<a href="%s" rel="noreferrer noopener" target=_blank>%s</a>',
63
  'https://theseoframework.com/extensions/',
64
  \esc_html_x( 'Extensions', 'Plugin extensions', 'autodescription' )
65
  );
66
  $tsf_links['pricing'] = sprintf(
67
+ '<a href="%s" rel="noreferrer noopener" target=_blank>%s</a>',
68
  'https://theseoframework.com/pricing/',
69
  \esc_html_x( 'Pricing', 'Plugin pricing', 'autodescription' )
70
  );
97
  $plugin_meta,
98
  [
99
  'support' => vsprintf(
100
+ '<a href="%s" rel="noreferrer noopener nofollow" target=_blank>%s</a>',
101
  [
102
  'https://tsf.fyi/support',
103
  \esc_html__( 'Get support', 'autodescription' ),
104
  ]
105
  ),
106
  'docs' => vsprintf(
107
+ '<a href="%s" rel="noreferrer noopener nofollow" target=_blank>%s</a>',
108
  [
109
  'https://tsf.fyi/docs',
110
  \esc_html__( 'View documentation', 'autodescription' ),
111
  ]
112
  ),
113
  'API' => vsprintf(
114
+ '<a href="%s" rel="noreferrer noopener nofollow" target=_blank>%s</a>',
115
  [
116
  'https://tsf.fyi/docs/api',
117
  \esc_html__( 'View API docs', 'autodescription' ),
118
  ]
119
  ),
120
  'EM' => vsprintf(
121
+ '<a href="%s" rel="noreferrer noopener nofollow" target=_blank>%s</a>',
122
  [
123
  'https://tsf.fyi/extension-manager',
124
  $_get_em
inc/classes/bridges/sitemap.class.php CHANGED
@@ -267,7 +267,7 @@ final class Sitemap {
267
  echo 'Sitemap is locked temporarily. Try again later.';
268
  }
269
 
270
- echo PHP_EOL;
271
  exit;
272
  }
273
 
@@ -425,7 +425,7 @@ final class Sitemap {
425
  */
426
  public function output_sitemap_header() {
427
 
428
- echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
429
 
430
  if ( static::$tsf->get_option( 'sitemap_styles' ) ) {
431
  printf(
267
  echo 'Sitemap is locked temporarily. Try again later.';
268
  }
269
 
270
+ echo "\n";
271
  exit;
272
  }
273
 
425
  */
426
  public function output_sitemap_header() {
427
 
428
+ echo '<?xml version="1.0" encoding="UTF-8"?>', "\n";
429
 
430
  if ( static::$tsf->get_option( 'sitemap_styles' ) ) {
431
  printf(
inc/classes/builders/robots/args.class.php CHANGED
@@ -56,11 +56,8 @@ final class Args extends Factory {
56
 
57
  $asserting_noindex = 'noindex' === $type;
58
 
59
- meta_settings: {
60
- // We assert options here for a jump to meta_settings might be unaware.
61
- if ( static::$options & \The_SEO_Framework\ROBOTS_IGNORE_SETTINGS )
62
- goto after_meta_settings;
63
-
64
  $qubit = null;
65
 
66
  if ( $args['taxonomy'] ) {
@@ -90,9 +87,8 @@ final class Args extends Factory {
90
  yield 'meta_qubit_default' => false;
91
  endswitch;
92
  }
93
- after_meta_settings:;
94
 
95
- globals: {
96
  yield 'globals_site' => (bool) $tsf->get_option( "site_$type" );
97
 
98
  if ( $args['taxonomy'] ) {
@@ -116,7 +112,6 @@ final class Args extends Factory {
116
  if ( $args['id'] )
117
  yield 'globals_post_type' => $tsf->is_post_type_robots_set( $type, \get_post_type( $args['id'] ) );
118
  }
119
- }
120
 
121
  index_protection: if ( $asserting_noindex ) {
122
  // We assert options here for a jump to index_protection might be unaware.
56
 
57
  $asserting_noindex = 'noindex' === $type;
58
 
59
+ // We assert options here for a jump to meta_settings might be unaware.
60
+ meta_settings: if ( ! ( static::$options & \The_SEO_Framework\ROBOTS_IGNORE_SETTINGS ) ) {
 
 
 
61
  $qubit = null;
62
 
63
  if ( $args['taxonomy'] ) {
87
  yield 'meta_qubit_default' => false;
88
  endswitch;
89
  }
 
90
 
91
+ globals:
92
  yield 'globals_site' => (bool) $tsf->get_option( "site_$type" );
93
 
94
  if ( $args['taxonomy'] ) {
112
  if ( $args['id'] )
113
  yield 'globals_post_type' => $tsf->is_post_type_robots_set( $type, \get_post_type( $args['id'] ) );
114
  }
 
115
 
116
  index_protection: if ( $asserting_noindex ) {
117
  // We assert options here for a jump to index_protection might be unaware.
inc/classes/builders/robots/query.class.php CHANGED
@@ -55,10 +55,8 @@ final class Query extends Factory {
55
 
56
  $asserting_noindex = 'noindex' === $type;
57
 
58
- meta_settings: {
59
- // We assert options here for a jump to meta_settings might be unaware.
60
- if ( static::$options & \The_SEO_Framework\ROBOTS_IGNORE_SETTINGS )
61
- goto after_meta_settings;
62
 
63
  $qubit = null;
64
 
@@ -89,9 +87,8 @@ final class Query extends Factory {
89
  yield 'meta_qubit_default' => false;
90
  endswitch;
91
  }
92
- after_meta_settings:;
93
 
94
- globals: {
95
  yield 'globals_site' => (bool) $tsf->get_option( "site_$type" );
96
 
97
  if ( $tsf->is_real_front_page() ) {
@@ -109,7 +106,7 @@ final class Query extends Factory {
109
  if ( $tsf->is_archive() ) {
110
  if ( $tsf->is_author() ) {
111
  yield 'globals_author' => (bool) $tsf->get_option( "author_$type" );
112
- } elseif ( $tsf->is_date() ) {
113
  yield 'globals_date' => (bool) $tsf->get_option( "date_$type" );
114
  }
115
  } elseif ( $tsf->is_search() ) {
@@ -134,13 +131,9 @@ final class Query extends Factory {
134
  } elseif ( $tsf->is_singular() ) {
135
  yield 'globals_post_type' => $tsf->is_post_type_robots_set( $type, $tsf->get_current_post_type() );
136
  }
137
- }
138
-
139
- index_protection: if ( $asserting_noindex ) {
140
- // We assert options here for a jump to index_protection might be unaware.
141
- if ( static::$options & \The_SEO_Framework\ROBOTS_IGNORE_PROTECTION )
142
- goto after_index_protection;
143
 
 
 
144
  if ( $tsf->is_singular() ) {
145
  // A reiteration of the very same code as above... but, homepage may not always be singular.
146
  // The conditions below MUST overwrite this, too. So, this is the perfect placement.
@@ -158,13 +151,11 @@ final class Query extends Factory {
158
  yield from static::assert_noindex_query_pass( 'cpage' );
159
  }
160
  }
161
- after_index_protection:;
162
 
163
  exploit_protection: if ( $tsf->is_query_exploited() ) {
164
  if ( \in_array( $type, [ 'noindex', 'nofollow' ], true ) )
165
  yield 'query_protection' => true;
166
  }
167
- after_exploit_protection:;
168
 
169
  end:;
170
  }
@@ -201,7 +192,7 @@ final class Query extends Factory {
201
  * the first page indexable via user-intent only. Concordingly, too
202
  * because we cannot assert this via the administrative dashboard.
203
  */
204
- yield '404' => $tsf->is_404();
205
  else :
206
  /**
207
  * Check for 404, or if archive is empty: set noindex for those.
55
 
56
  $asserting_noindex = 'noindex' === $type;
57
 
58
+ // We assert options here for a jump to meta_settings might be unaware.
59
+ meta_settings: if ( ! ( static::$options & \The_SEO_Framework\ROBOTS_IGNORE_SETTINGS ) ) {
 
 
60
 
61
  $qubit = null;
62
 
87
  yield 'meta_qubit_default' => false;
88
  endswitch;
89
  }
 
90
 
91
+ globals:
92
  yield 'globals_site' => (bool) $tsf->get_option( "site_$type" );
93
 
94
  if ( $tsf->is_real_front_page() ) {
106
  if ( $tsf->is_archive() ) {
107
  if ( $tsf->is_author() ) {
108
  yield 'globals_author' => (bool) $tsf->get_option( "author_$type" );
109
+ } elseif ( \is_date() ) {
110
  yield 'globals_date' => (bool) $tsf->get_option( "date_$type" );
111
  }
112
  } elseif ( $tsf->is_search() ) {
131
  } elseif ( $tsf->is_singular() ) {
132
  yield 'globals_post_type' => $tsf->is_post_type_robots_set( $type, $tsf->get_current_post_type() );
133
  }
 
 
 
 
 
 
134
 
135
+ // We assert options here for a jump to index_protection might be unaware.
136
+ index_protection: if ( $asserting_noindex && ! ( static::$options & \The_SEO_Framework\ROBOTS_IGNORE_PROTECTION ) ) {
137
  if ( $tsf->is_singular() ) {
138
  // A reiteration of the very same code as above... but, homepage may not always be singular.
139
  // The conditions below MUST overwrite this, too. So, this is the perfect placement.
151
  yield from static::assert_noindex_query_pass( 'cpage' );
152
  }
153
  }
 
154
 
155
  exploit_protection: if ( $tsf->is_query_exploited() ) {
156
  if ( \in_array( $type, [ 'noindex', 'nofollow' ], true ) )
157
  yield 'query_protection' => true;
158
  }
 
159
 
160
  end:;
161
  }
192
  * the first page indexable via user-intent only. Concordingly, too
193
  * because we cannot assert this via the administrative dashboard.
194
  */
195
+ yield '404' => \is_404();
196
  else :
197
  /**
198
  * Check for 404, or if archive is empty: set noindex for those.
inc/classes/builders/scripts.class.php CHANGED
@@ -114,7 +114,7 @@ final class Scripts {
114
  \add_filter( 'admin_body_class', [ $this, '_add_body_class' ] );
115
  \add_action( 'in_admin_header', [ $this, '_print_tsfjs_script' ] );
116
 
117
- \add_action( 'admin_enqueue_scripts', [ $this, '_prepare_admin_scripts' ], 1 ); // Magic number: likely 1 after this is called.
118
  \add_action( 'admin_footer', [ $this, '_output_templates' ], 999 ); // Magic number: later is less likely to collide?
119
  }
120
 
114
  \add_filter( 'admin_body_class', [ $this, '_add_body_class' ] );
115
  \add_action( 'in_admin_header', [ $this, '_print_tsfjs_script' ] );
116
 
117
+ \add_action( 'admin_enqueue_scripts', [ $this, '_prepare_admin_scripts' ], 1 ); // Magic number: we likely run at priority 0. Add 1.
118
  \add_action( 'admin_footer', [ $this, '_output_templates' ], 999 ); // Magic number: later is less likely to collide?
119
  }
120
 
inc/classes/builders/sitemap/base.class.php CHANGED
@@ -312,6 +312,10 @@ class Base extends Main {
312
  $content .= $this->build_url_item( $_values );
313
  }
314
 
 
 
 
 
315
  if ( \has_filter( 'the_seo_framework_sitemap_additional_urls' ) ) {
316
  foreach ( $this->generate_additional_base_urls(
317
  compact( 'show_modified', 'count' ),
@@ -322,8 +326,7 @@ class Base extends Main {
322
  }
323
 
324
  /**
325
- * NOTE: This filter is slower than `the_seo_framework_sitemap_additional_urls`, because it's not a generator.
326
- * If you only need to add a few URLs (fewer than 500), then you can safely use this.
327
  *
328
  * @since 2.5.2
329
  * @since 4.0.0 Added $args parameter.
@@ -331,7 +334,7 @@ class Base extends Main {
331
  * @param string $extend Custom sitemap extension. Must be escaped.
332
  * @param array $args : {
333
  * bool $show_modified : Whether to display modified date.
334
- * int $total_itemns : Estimate: The total sitemap items before adding additional URLs.
335
  * }
336
  */
337
  $extend = (string) \apply_filters_ref_array(
@@ -463,9 +466,9 @@ class Base extends Main {
463
  * @generator
464
  * @iterator
465
  *
466
- * @param iterable $post_ids The post IDs to go over.
467
- * @param array $args The generator arguments.
468
- * @param int $count The iteration count. Passed by reference.
469
  * @yield array|void : {
470
  * string loc
471
  * string lastmod
@@ -490,8 +493,8 @@ class Base extends Main {
490
  yield $_values;
491
  }
492
 
493
- // Only clean post cache when NOT using an external object caching plugin.
494
- \wp_using_ext_object_cache() or \clean_post_cache( $post );
495
  }
496
  }
497
 
312
  $content .= $this->build_url_item( $_values );
313
  }
314
 
315
+ /**
316
+ * NOTE to devs: Use this filter if you want to let the generator build the string (lower memory usage).
317
+ * This filter also keeps track toward the sitemap limit via $count.
318
+ */
319
  if ( \has_filter( 'the_seo_framework_sitemap_additional_urls' ) ) {
320
  foreach ( $this->generate_additional_base_urls(
321
  compact( 'show_modified', 'count' ),
326
  }
327
 
328
  /**
329
+ * This filter accepts a simple string, which may strain the memory usage if not generated (via co-routine).
 
330
  *
331
  * @since 2.5.2
332
  * @since 4.0.0 Added $args parameter.
334
  * @param string $extend Custom sitemap extension. Must be escaped.
335
  * @param array $args : {
336
  * bool $show_modified : Whether to display modified date.
337
+ * int $count : The total sitemap items before adding additional URLs.
338
  * }
339
  */
340
  $extend = (string) \apply_filters_ref_array(
466
  * @generator
467
  * @iterator
468
  *
469
+ * @param int[] $post_ids The post IDs to go over.
470
+ * @param array $args The generator arguments.
471
+ * @param int $count The iteration count. Passed by reference.
472
  * @yield array|void : {
473
  * string loc
474
  * string lastmod
493
  yield $_values;
494
  }
495
 
496
+ // Only clean post cache when NOT using a caching plugin.
497
+ WP_CACHE or \clean_post_cache( $post );
498
  }
499
  }
500
 
inc/classes/builders/sitemap/main.class.php CHANGED
@@ -162,8 +162,8 @@ abstract class Main {
162
  */
163
  final public function is_post_included_in_sitemap( $post_id ) {
164
 
165
- static $excluded = null;
166
- if ( null === $excluded ) {
167
  /**
168
  * @since 2.5.2
169
  * @since 2.8.0 No longer accepts '0' as entry.
@@ -214,8 +214,8 @@ abstract class Main {
214
  */
215
  final public function is_term_included_in_sitemap( $term_id, $taxonomy ) {
216
 
217
- static $excluded = null;
218
- if ( null === $excluded ) {
219
  /**
220
  * @since 4.0.0
221
  * @param int[] $excluded Sequential list of excluded IDs: [ int ...term_id ]
162
  */
163
  final public function is_post_included_in_sitemap( $post_id ) {
164
 
165
+ static $excluded;
166
+ if ( ! isset( $excluded ) ) {
167
  /**
168
  * @since 2.5.2
169
  * @since 2.8.0 No longer accepts '0' as entry.
214
  */
215
  final public function is_term_included_in_sitemap( $term_id, $taxonomy ) {
216
 
217
+ static $excluded;
218
+ if ( ! isset( $excluded ) ) {
219
  /**
220
  * @since 4.0.0
221
  * @param int[] $excluded Sequential list of excluded IDs: [ int ...term_id ]
inc/classes/core.class.php CHANGED
@@ -579,7 +579,7 @@ class Core {
579
  }
580
 
581
  /**
582
- * Calculates the relative font color according to the background.
583
  *
584
  * @since 2.8.0
585
  * @since 2.9.0 Now adds a little more relative softness based on rel_lum.
@@ -609,7 +609,7 @@ class Core {
609
  );
610
 
611
  $get_relative_luminance = static function( $v ) {
612
- // Convert to 0~1 value.
613
  $v /= 0xFF;
614
 
615
  if ( $v > .03928 ) {
@@ -620,19 +620,20 @@ class Core {
620
  return $lum;
621
  };
622
 
623
- // Create Relative Luminance via sRGB.
624
- $rl = ( 0.2126 * $get_relative_luminance( $r ) )
625
- + ( 0.7152 * $get_relative_luminance( $g ) )
626
- + ( 0.0722 * $get_relative_luminance( $b ) );
627
 
628
- // Build light greyscale. Rounding is required for bitwise operation (PHP8.1+).
629
- $gr = round( ( $r * 0.2989 / 8 ) * $rl );
630
- $gg = round( ( $g * 0.5870 / 8 ) * $rl );
631
- $gb = round( ( $b * 0.1140 / 8 ) * $rl );
 
 
632
 
633
- // Invert colors if they hit this luminance boundary.
634
- if ( $rl < 0.5 ) {
635
- // Build dark greyscale. bitwise operators...
636
  $gr ^= 0xFF;
637
  $gg ^= 0xFF;
638
  $gb ^= 0xFF;
@@ -696,4 +697,16 @@ class Core {
696
  public function convert_markdown( $text, $convert = [], $args = [] ) {
697
  return Interpreters\Markdown::convert( $text, $convert, $args );
698
  }
 
 
 
 
 
 
 
 
 
 
 
 
699
  }
579
  }
580
 
581
  /**
582
+ * Calculates the relative font color according to the background, grayscale.
583
  *
584
  * @since 2.8.0
585
  * @since 2.9.0 Now adds a little more relative softness based on rel_lum.
609
  );
610
 
611
  $get_relative_luminance = static function( $v ) {
612
+ // Convert hex to 0~1 float.
613
  $v /= 0xFF;
614
 
615
  if ( $v > .03928 ) {
620
  return $lum;
621
  };
622
 
623
+ // Calc relative Luminance using sRGB.
624
+ $rl = .2126 * $get_relative_luminance( $r )
625
+ + .7152 * $get_relative_luminance( $g )
626
+ + .0722 * $get_relative_luminance( $b );
627
 
628
+ // Build light greyscale using relative constrast.
629
+ // Rounding is required for bitwise operation (PHP8.1+).
630
+ // printf will round anyway when floats are detected. Diff in #opcodes should be minimal.
631
+ $gr = round( $r * .2989 / 8 * $rl );
632
+ $gg = round( $g * .5870 / 8 * $rl );
633
+ $gb = round( $b * .1140 / 8 * $rl );
634
 
635
+ // Invert grayscela if they hit the relative luminance middle.
636
+ if ( $rl < .5 ) {
 
637
  $gr ^= 0xFF;
638
  $gg ^= 0xFF;
639
  $gb ^= 0xFF;
697
  public function convert_markdown( $text, $convert = [], $args = [] ) {
698
  return Interpreters\Markdown::convert( $text, $convert, $args );
699
  }
700
+
701
+ /**
702
+ * Whether to display Extension Manager suggestions to the user based on several conditions.
703
+ *
704
+ * @since 4.2.4
705
+ * @uses TSF_DISABLE_SUGGESTIONS Set that to true if you don't like us.
706
+ *
707
+ * @return bool
708
+ */
709
+ public function _display_extension_suggestions() {
710
+ return \current_user_can( 'install_plugins' ) && ! ( \defined( 'TSF_DISABLE_SUGGESTIONS' ) && TSF_DISABLE_SUGGESTIONS );
711
+ }
712
  }
inc/classes/detect.class.php CHANGED
@@ -180,7 +180,7 @@ class Detect extends Render {
180
  * @uses $this->detect_plugin_multi()
181
  *
182
  * @param array[] $plugins Array of array for globals, constants, classes
183
- * and/or functions to check for plugin existence.
184
  * @param bool $use_cache Bypasses cache if false
185
  */
186
  public function can_i_use( $plugins = [], $use_cache = true ) {
@@ -576,6 +576,7 @@ class Detect extends Render {
576
  * @since 4.0.0
577
  * @since 4.0.2 Now tests for an existing post/term ID when on singular/term pages.
578
  * @since 4.0.3 Can now assert empty categories again by checking for taxonomy support.
 
579
  *
580
  * @return bool
581
  */
@@ -585,7 +586,11 @@ class Detect extends Render {
585
  if ( null !== $memo = memo() ) return $memo;
586
 
587
  switch ( true ) :
588
- case $this->is_feed():
 
 
 
 
589
  $supported = false;
590
  break;
591
 
@@ -604,10 +609,22 @@ class Detect extends Render {
604
  $supported = $this->is_taxonomy_supported() && $this->get_the_real_ID();
605
  break;
606
 
607
- // Including 404.
608
  default:
609
  $supported = true;
610
  break;
 
 
 
 
 
 
 
 
 
 
 
 
611
  endswitch;
612
 
613
  /**
180
  * @uses $this->detect_plugin_multi()
181
  *
182
  * @param array[] $plugins Array of array for globals, constants, classes
183
+ * and/or functions to check for plugin existence.
184
  * @param bool $use_cache Bypasses cache if false
185
  */
186
  public function can_i_use( $plugins = [], $use_cache = true ) {
576
  * @since 4.0.0
577
  * @since 4.0.2 Now tests for an existing post/term ID when on singular/term pages.
578
  * @since 4.0.3 Can now assert empty categories again by checking for taxonomy support.
579
+ * @since 4.2.4 Added detection for AJAX, Cron, JSON, and REST queries (they're not supported as SEO-able queries).
580
  *
581
  * @return bool
582
  */
586
  if ( null !== $memo = memo() ) return $memo;
587
 
588
  switch ( true ) :
589
+ case \is_feed():
590
+ case \wp_doing_ajax():
591
+ case \wp_doing_cron():
592
+ case \wp_is_json_request():
593
+ case \defined( 'REST_REQUEST' ) && REST_REQUEST:
594
  $supported = false;
595
  break;
596
 
609
  $supported = $this->is_taxonomy_supported() && $this->get_the_real_ID();
610
  break;
611
 
612
+ // This includes 404.
613
  default:
614
  $supported = true;
615
  break;
616
+
617
+ // TODO consider this instead of the current default? (it'd make the AJAX through REST test obsolete)
618
+ // Every recognized query (aside from $this->is_singular()/is_post_type_archive for we already covered those in full)
619
+ // case \is_404():
620
+ // case $this->is_search():
621
+ // case $this->is_real_front_page():
622
+ // case $this->is_archive():
623
+ // $supported = true;
624
+ // break;
625
+ // default:
626
+ // $supported = false;
627
+ // break;
628
  endswitch;
629
 
630
  /**
inc/classes/generate-description.class.php CHANGED
@@ -655,10 +655,8 @@ class Generate_Description extends Generate {
655
 
656
  $id = $this->get_the_front_page_ID();
657
 
658
- $excerpt = ( $id ? $this->get_singular_description_excerpt( $id ) : '' )
659
- ?: $this->get_description_additions( [ 'id' => $id ] );
660
-
661
- return $excerpt;
662
  }
663
 
664
  /**
@@ -851,11 +849,9 @@ class Generate_Description extends Generate {
851
  $excerpt = $this->strip_newline_urls( $excerpt );
852
  $excerpt = $this->strip_paragraph_urls( $excerpt );
853
  }
854
- } else {
855
- $excerpt = '';
856
  }
857
 
858
- return $excerpt;
859
  }
860
 
861
  /**
655
 
656
  $id = $this->get_the_front_page_ID();
657
 
658
+ return ( $id ? $this->get_singular_description_excerpt( $id ) : '' )
659
+ ?: $this->get_description_additions( [ 'id' => $id ] );
 
 
660
  }
661
 
662
  /**
849
  $excerpt = $this->strip_newline_urls( $excerpt );
850
  $excerpt = $this->strip_paragraph_urls( $excerpt );
851
  }
 
 
852
  }
853
 
854
+ return $excerpt ?? '';
855
  }
856
 
857
  /**
inc/classes/generate-image.class.php CHANGED
@@ -188,13 +188,15 @@ class Generate_Image extends Generate_Url {
188
  *
189
  * @since 4.0.0
190
  * @since 4.2.0 Can now return post type archive images from settings.
 
191
  *
192
  * @return array The image details array, sequential: int => {
193
- * string url: The image URL,
194
- * int id: The image ID,
195
- * int width: The image width in pixels,
196
- * int height: The image height in pixels,
197
- * string alt: The image alt tag,
 
198
  * }
199
  */
200
  protected function get_custom_field_image_details_from_query() {
@@ -256,14 +258,16 @@ class Generate_Image extends Generate_Url {
256
  *
257
  * @since 4.0.0
258
  * @since 4.2.0 Now supports the `$args['pta']` index.
 
259
  *
260
  * @param array $args The query arguments. Must have 'id' and 'taxonomy'.
261
  * @return array The image details array, sequential: int => {
262
- * string url: The image URL,
263
- * int id: The image ID,
264
- * int width: The image width in pixels,
265
- * int height: The image height in pixels,
266
- * string alt: The image alt tag,
 
267
  * }
268
  */
269
  protected function get_custom_field_image_details_from_args( $args ) {
@@ -327,7 +331,7 @@ class Generate_Image extends Generate_Url {
327
  * @param string $context The filter context. Default 'social'.
328
  * May be (for example) 'breadcrumb' or 'article' for structured data.
329
  * @return array The image generation parameters, associative: {
330
- * string size: The image size,
331
  * boolean multi: Whether multiple images may be returned,
332
  * array cbs: An array of image generation callbacks, in order of most important to least.
333
  * When 'multi' (or $single input) parameter is "false", it will use the first found.
@@ -424,17 +428,19 @@ class Generate_Image extends Generate_Url {
424
  * Generates image details.
425
  *
426
  * @since 4.0.0
 
427
  *
428
  * @param array|null $args The query arguments. Accepts 'id', 'taxonomy', and 'pta'.
429
  * Leave null to autodetermine query.
430
  * @param bool $single Whether to fetch one image, or multiple.
431
  * @param string $context The context of the image generation, albeit 'social', 'schema', etc.
432
  * @return array The image details array, sequential: int => {
433
- * string url: The image URL,
434
- * int id: The image ID,
435
- * int width: The image width in pixels,
436
- * int height: The image height in pixels,
437
- * string alt: The image alt tag,
 
438
  * }
439
  */
440
  protected function generate_image_details( $args, $single = true, $context = 'social' ) {
@@ -452,18 +458,20 @@ class Generate_Image extends Generate_Url {
452
  * Processes image detail callbacks.
453
  *
454
  * @since 4.0.0
 
455
  *
456
- * @param array $cbs The callbacks to parse. Ideally be generators, so we can halt early.
457
  * @param array|null $args The query arguments. Accepts 'id', 'taxonomy', and 'pta'.
458
  * Leave null to autodetermine query.
459
  * @param string $size The image size to use.
460
  * @param bool $single Whether to fetch one image, or multiple.
461
  * @return array The image details array, sequential: int => {
462
- * string url: The image URL,
463
- * int id: The image ID,
464
- * int width: The image width in pixels,
465
- * int height: The image height in pixels,
466
- * string alt: The image alt tag,
 
467
  * }
468
  */
469
  protected function process_image_cbs( $cbs, $args, $size, $single ) {
@@ -489,6 +497,8 @@ class Generate_Image extends Generate_Url {
489
  * Adds image dimension and alt parameters to the input details, if any.
490
  *
491
  * @since 4.0.0
 
 
492
  *
493
  * @param array $details The image details array, associative: {
494
  * string url: The image URL,
@@ -501,57 +511,66 @@ class Generate_Image extends Generate_Url {
501
  * int width: The image width in pixels,
502
  * int height: The image height in pixels,
503
  * string alt: The image alt tag,
 
504
  * }
505
  */
506
  public function merge_extra_image_details( $details, $size = 'full' ) {
507
 
508
- $details += $this->get_image_dimensions( $details['id'], $details['url'], $size );
509
- $details += [ 'alt' => $this->get_image_alt_tag( $details['id'] ) ];
 
 
 
 
 
 
 
 
 
 
510
 
511
  return $details;
512
  }
513
 
514
  /**
515
- * Generates image dimensions.
516
  *
 
517
  * @since 4.0.0
 
 
518
  *
519
  * @param int $src_id The source ID of the image.
520
- * @param string $url The source URL of the image. Ideally related to the $src_id.
521
  * @param string $size The size of the image used.
522
  * @return array The image dimensions, associative: {
523
  * int width: The image width in pixels,
524
  * int height: The image height in pixels,
525
  * }
526
  */
527
- public function get_image_dimensions( $src_id, $url, $size ) {
528
 
529
- $image = \wp_get_attachment_image_src( $src_id, $size );
 
 
 
 
530
 
531
  $dimensions = [
532
  'width' => 0,
533
  'height' => 0,
534
  ];
535
 
536
- if ( $image ) {
537
- [ $src, $width, $height ] = $image;
538
-
539
- $test_src = \esc_url_raw( $this->set_url_scheme( $src, 'https' ), [ 'https', 'http' ] );
540
- $test_url = \esc_url_raw( $this->set_url_scheme( $url, 'https' ), [ 'https', 'http' ] );
541
-
542
- if ( $test_src === $test_url ) {
543
- $dimensions = [
544
- 'width' => $width,
545
- 'height' => $height,
546
- ];
547
- }
548
  }
549
 
550
  return $dimensions;
551
  }
552
 
553
  /**
554
- * Generates image dimensions.
555
  *
556
  * @since 4.0.0
557
  *
@@ -563,26 +582,47 @@ class Generate_Image extends Generate_Url {
563
  return $src_id ? trim( strip_tags( \get_post_meta( $src_id, '_wp_attachment_image_alt', true ) ) ) : '';
564
  }
565
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
  /**
567
  * Returns the largest acceptable image size's details.
 
568
  *
569
  * @since 4.0.2
 
570
  *
571
- * @param int $id The image ID.
572
- * @param int $max_size The largest acceptable size in pixels. Accounts for both width and height.
 
573
  * @return false|array Returns an array (url, width, height, is_intermediate), or false, if no image is available.
574
  */
575
- public function get_largest_acceptable_image_src( $id, $max_size = 4096 ) {
576
 
577
  // Imply there's a correct ID set. When there's not, the loop won't run.
578
- $meta = \wp_get_attachment_metadata( $id );
579
- $sizes = ! empty( $meta['sizes'] ) && \is_array( $meta['sizes'] ) ? $meta['sizes'] : [];
580
 
581
  // law = largest accepted width.
582
  $law = 0;
583
  $size = '';
584
 
585
  foreach ( $sizes as $_s => $_d ) {
 
 
 
586
  if ( isset( $_d['width'], $_d['height'] ) ) {
587
  if ( $_d['width'] <= $max_size && $_d['height'] <= $max_size && $_d['width'] > $law ) {
588
  $law = $_d['width'];
188
  *
189
  * @since 4.0.0
190
  * @since 4.2.0 Can now return post type archive images from settings.
191
+ * @since 4.2.4 Now returns filesizes under index `filesize`.
192
  *
193
  * @return array The image details array, sequential: int => {
194
+ * string url: The image URL,
195
+ * int id: The image ID,
196
+ * int width: The image width in pixels,
197
+ * int height: The image height in pixels,
198
+ * string alt: The image alt tag,
199
+ * int filesize: The image filesize in bytes,
200
  * }
201
  */
202
  protected function get_custom_field_image_details_from_query() {
258
  *
259
  * @since 4.0.0
260
  * @since 4.2.0 Now supports the `$args['pta']` index.
261
+ * @since 4.2.4 Now returns filesizes under index `filesize`.
262
  *
263
  * @param array $args The query arguments. Must have 'id' and 'taxonomy'.
264
  * @return array The image details array, sequential: int => {
265
+ * string url: The image URL,
266
+ * int id: The image ID,
267
+ * int width: The image width in pixels,
268
+ * int height: The image height in pixels,
269
+ * string alt: The image alt tag,
270
+ * int filesize: The image filesize in bytes,
271
  * }
272
  */
273
  protected function get_custom_field_image_details_from_args( $args ) {
331
  * @param string $context The filter context. Default 'social'.
332
  * May be (for example) 'breadcrumb' or 'article' for structured data.
333
  * @return array The image generation parameters, associative: {
334
+ * string size: The image size by name,
335
  * boolean multi: Whether multiple images may be returned,
336
  * array cbs: An array of image generation callbacks, in order of most important to least.
337
  * When 'multi' (or $single input) parameter is "false", it will use the first found.
428
  * Generates image details.
429
  *
430
  * @since 4.0.0
431
+ * @since 4.2.4 Now returns filesizes under index `filesize`.
432
  *
433
  * @param array|null $args The query arguments. Accepts 'id', 'taxonomy', and 'pta'.
434
  * Leave null to autodetermine query.
435
  * @param bool $single Whether to fetch one image, or multiple.
436
  * @param string $context The context of the image generation, albeit 'social', 'schema', etc.
437
  * @return array The image details array, sequential: int => {
438
+ * string url: The image URL,
439
+ * int id: The image ID,
440
+ * int width: The image width in pixels,
441
+ * int height: The image height in pixels,
442
+ * string alt: The image alt tag,
443
+ * int filesize: The image filesize in bytes,
444
  * }
445
  */
446
  protected function generate_image_details( $args, $single = true, $context = 'social' ) {
458
  * Processes image detail callbacks.
459
  *
460
  * @since 4.0.0
461
+ * @since 4.2.4 Now returns filesizes under index `filesize`.
462
  *
463
+ * @param callable[] $cbs The callbacks to parse. Ideally be generators, so we can halt early.
464
  * @param array|null $args The query arguments. Accepts 'id', 'taxonomy', and 'pta'.
465
  * Leave null to autodetermine query.
466
  * @param string $size The image size to use.
467
  * @param bool $single Whether to fetch one image, or multiple.
468
  * @return array The image details array, sequential: int => {
469
+ * string url: The image URL,
470
+ * int id: The image ID,
471
+ * int width: The image width in pixels,
472
+ * int height: The image height in pixels,
473
+ * string alt: The image alt tag,
474
+ * int filesize: The image filesize in bytes,
475
  * }
476
  */
477
  protected function process_image_cbs( $cbs, $args, $size, $single ) {
497
  * Adds image dimension and alt parameters to the input details, if any.
498
  *
499
  * @since 4.0.0
500
+ * @since 4.2.4 1. Now returns filesizes under index `filesize`.
501
+ * 2. No longer processes details when no `id` is given in `$details`.
502
  *
503
  * @param array $details The image details array, associative: {
504
  * string url: The image URL,
511
  * int width: The image width in pixels,
512
  * int height: The image height in pixels,
513
  * string alt: The image alt tag,
514
+ * int filesize: The image filesize in bytes,
515
  * }
516
  */
517
  public function merge_extra_image_details( $details, $size = 'full' ) {
518
 
519
+ if ( $details['id'] ) {
520
+ $details += $this->get_image_dimensions( $details['id'], $details['url'], $size );
521
+ $details += [ 'alt' => $this->get_image_alt_tag( $details['id'] ) ];
522
+ $details += [ 'filesize' => $this->get_image_filesize( $details['id'], $details['url'], $size ) ];
523
+ } else {
524
+ $details += [
525
+ 'width' => 0,
526
+ 'height' => 0,
527
+ 'url' => '',
528
+ 'filesize' => 0,
529
+ ];
530
+ }
531
 
532
  return $details;
533
  }
534
 
535
  /**
536
+ * Fetches image dimensions.
537
  *
538
+ * @TODO shift parametes and deprecate using the third one.
539
  * @since 4.0.0
540
+ * @since 4.2.4 1. No longer relies on `$url` to fetch the correct dimensions, improving performance significantly.
541
+ * 2. Renamed `$url` to `$depr`, without a deprecation notice added.
542
  *
543
  * @param int $src_id The source ID of the image.
544
+ * @param string $depr Deprecated. Used to be the source URL of the image.
545
  * @param string $size The size of the image used.
546
  * @return array The image dimensions, associative: {
547
  * int width: The image width in pixels,
548
  * int height: The image height in pixels,
549
  * }
550
  */
551
+ public function get_image_dimensions( $src_id, $depr, $size ) {
552
 
553
+ // PHP 7.4+, major version API change.
554
+ // $size ??= $depr;
555
+
556
+ $data = \wp_get_attachment_metadata( $src_id ) ?? [];
557
+ $data = $data['sizes'][ $size ] ?? $data;
558
 
559
  $dimensions = [
560
  'width' => 0,
561
  'height' => 0,
562
  ];
563
 
564
+ if ( isset( $data['width'], $data['height'] ) ) {
565
+ $dimensions['width'] = $data['width'];
566
+ $dimensions['height'] = $data['height'];
 
 
 
 
 
 
 
 
 
567
  }
568
 
569
  return $dimensions;
570
  }
571
 
572
  /**
573
+ * Fetches image dimensions.
574
  *
575
  * @since 4.0.0
576
  *
582
  return $src_id ? trim( strip_tags( \get_post_meta( $src_id, '_wp_attachment_image_alt', true ) ) ) : '';
583
  }
584
 
585
+ /**
586
+ * Fetches image filesize in bytes. Requires an image (re)generated in WP 6.0 or later.
587
+ *
588
+ * @since 4.2.4
589
+ *
590
+ * @param int $src_id The source ID of the image.
591
+ * @param string $size The size of the image used.
592
+ * @return int The image filesize in bytes. Returns 0 for unprocessed/unprocessable image.
593
+ */
594
+ public function get_image_filesize( $src_id, $size ) {
595
+
596
+ $data = \wp_get_attachment_metadata( $src_id ) ?? [];
597
+
598
+ return ( $data['sizes'][ $size ]['filesize'] ?? $data['filesize'] ?? 0 ) ?: 0;
599
+ }
600
+
601
  /**
602
  * Returns the largest acceptable image size's details.
603
+ * Skips the original image, which may also be acceptable.
604
  *
605
  * @since 4.0.2
606
+ * @since 4.2.4 Added parameter `$max_filesize` that filters images larger than it.
607
  *
608
+ * @param int $id The image ID.
609
+ * @param int $max_size The largest acceptable dimension in pixels. Accounts for both width and height.
610
+ * @param int $max_filesize The largest acceptable filesize in bytes. Default 5MB (5242880).
611
  * @return false|array Returns an array (url, width, height, is_intermediate), or false, if no image is available.
612
  */
613
+ public function get_largest_acceptable_image_src( $id, $max_size = 4096, $max_filesize = 5242880 ) {
614
 
615
  // Imply there's a correct ID set. When there's not, the loop won't run.
616
+ $sizes = \wp_get_attachment_metadata( $id )['sizes'] ?? [];
 
617
 
618
  // law = largest accepted width.
619
  $law = 0;
620
  $size = '';
621
 
622
  foreach ( $sizes as $_s => $_d ) {
623
+ if ( ( $_d['filesize'] ?? 0 ) > $max_filesize )
624
+ continue;
625
+
626
  if ( isset( $_d['width'], $_d['height'] ) ) {
627
  if ( $_d['width'] <= $max_size && $_d['height'] <= $max_size && $_d['width'] > $law ) {
628
  $law = $_d['width'];
inc/classes/generate-ldjson.class.php CHANGED
@@ -166,6 +166,7 @@ class Generate_Ldjson extends Generate_Image {
166
  * @since 2.9.3
167
  * @since 3.0.0 This whole functions now only listens to the searchbox option.
168
  * @since 4.1.2 Now properly slashes the search URL.
 
169
  *
170
  * @return string escaped LD+JSON Search and Sitename script.
171
  */
@@ -210,7 +211,10 @@ class Generate_Ldjson extends Generate_Image {
210
  'potentialAction' => [
211
  '@type' => 'SearchAction',
212
  // not properly sanitized; however, search_term_string is inert.
213
- 'target' => sprintf( $pattern, \esc_url( $search_url ), $action_name ),
 
 
 
214
  'query-input' => sprintf( 'required name=%s', $action_name ),
215
  ],
216
  ];
@@ -221,7 +225,7 @@ class Generate_Ldjson extends Generate_Image {
221
  $json = $this->receive_json_data( $key );
222
 
223
  if ( $json )
224
- return '<script type="application/ld+json">' . $json . '</script>' . "\r\n";
225
 
226
  return '';
227
  }
@@ -294,7 +298,7 @@ class Generate_Ldjson extends Generate_Image {
294
  $json = $this->receive_json_data( $key );
295
 
296
  if ( $json )
297
- return '<script type="application/ld+json">' . $json . '</script>' . "\r\n";
298
 
299
  return '';
300
  }
@@ -329,6 +333,7 @@ class Generate_Ldjson extends Generate_Image {
329
  * Generates LD+JSON Breadcrumbs script.
330
  *
331
  * @since 2.9.3
 
332
  *
333
  * @return string LD+JSON Breadcrumbs script.
334
  */
@@ -340,11 +345,10 @@ class Generate_Ldjson extends Generate_Image {
340
  $output = '';
341
 
342
  if ( $this->is_singular() && ! $this->is_real_front_page() ) {
343
- // TODO Shouldn't this be is_post_type_hierarchical()?
344
- if ( $this->is_single() ) {
345
- $output = $this->get_ld_json_breadcrumbs_post();
346
- } else {
347
  $output = $this->get_ld_json_breadcrumbs_page();
 
 
348
  }
349
  }
350
 
@@ -415,7 +419,7 @@ class Generate_Ldjson extends Generate_Image {
415
 
416
  $post_id = $this->get_the_real_ID();
417
  $post_type = \get_post_type( $post_id );
418
- $taxonomies = $this->get_hierarchical_taxonomies_as( 'names', \get_post_type( $post_id ) );
419
 
420
  /**
421
  * @since 3.0.0
@@ -543,7 +547,6 @@ class Generate_Ldjson extends Generate_Image {
543
  ?: $this->get_static_untitled_title();
544
  }
545
 
546
- // Store in cache.
547
  $items[] = [
548
  '@type' => 'ListItem',
549
  'position' => $position,
@@ -757,7 +760,7 @@ class Generate_Ldjson extends Generate_Image {
757
  $it++;
758
 
759
  if ( $json )
760
- return '<script type="application/ld+json">' . $json . '</script>' . "\r\n";
761
 
762
  return '';
763
  }
166
  * @since 2.9.3
167
  * @since 3.0.0 This whole functions now only listens to the searchbox option.
168
  * @since 4.1.2 Now properly slashes the search URL.
169
+ * @since 4.2.4 Added EntryPoint definition (from implied to implicit)
170
  *
171
  * @return string escaped LD+JSON Search and Sitename script.
172
  */
211
  'potentialAction' => [
212
  '@type' => 'SearchAction',
213
  // not properly sanitized; however, search_term_string is inert.
214
+ 'target' => [
215
+ '@type' => 'EntryPoint',
216
+ 'urlTemplate' => sprintf( $pattern, \esc_url( $search_url ), $action_name ),
217
+ ],
218
  'query-input' => sprintf( 'required name=%s', $action_name ),
219
  ],
220
  ];
225
  $json = $this->receive_json_data( $key );
226
 
227
  if ( $json )
228
+ return '<script type="application/ld+json">' . $json . '</script>' . "\n";
229
 
230
  return '';
231
  }
298
  $json = $this->receive_json_data( $key );
299
 
300
  if ( $json )
301
+ return '<script type="application/ld+json">' . $json . '</script>' . "\n";
302
 
303
  return '';
304
  }
333
  * Generates LD+JSON Breadcrumbs script.
334
  *
335
  * @since 2.9.3
336
+ * @since 4.2.4 Exchanged "is_single" test for "is_post_type_hierarchical".
337
  *
338
  * @return string LD+JSON Breadcrumbs script.
339
  */
345
  $output = '';
346
 
347
  if ( $this->is_singular() && ! $this->is_real_front_page() ) {
348
+ if ( \is_post_type_hierarchical( $this->get_post_type_real_ID() ) ) {
 
 
 
349
  $output = $this->get_ld_json_breadcrumbs_page();
350
+ } else {
351
+ $output = $this->get_ld_json_breadcrumbs_post();
352
  }
353
  }
354
 
419
 
420
  $post_id = $this->get_the_real_ID();
421
  $post_type = \get_post_type( $post_id );
422
+ $taxonomies = $this->get_hierarchical_taxonomies_as( 'names', $post_type );
423
 
424
  /**
425
  * @since 3.0.0
547
  ?: $this->get_static_untitled_title();
548
  }
549
 
 
550
  $items[] = [
551
  '@type' => 'ListItem',
552
  'position' => $position,
760
  $it++;
761
 
762
  if ( $json )
763
+ return '<script type="application/ld+json">' . $json . '</script>' . "\n";
764
 
765
  return '';
766
  }
inc/classes/generate-title.class.php CHANGED
@@ -713,6 +713,7 @@ class Generate_Title extends Generate_Description {
713
  *
714
  * @since 3.1.0
715
  * @since 4.2.0 Flipped order of query tests.
 
716
  * @internal
717
  * @see $this->get_raw_generated_title()
718
  *
@@ -720,9 +721,7 @@ class Generate_Title extends Generate_Description {
720
  */
721
  protected function generate_title_from_query() {
722
 
723
- $title = '';
724
-
725
- if ( $this->is_404() ) {
726
  $title = $this->get_static_404_title();
727
  } elseif ( $this->is_search() ) {
728
  $title = $this->get_generated_search_query_title();
@@ -734,7 +733,7 @@ class Generate_Title extends Generate_Description {
734
  $title = $this->get_generated_archive_title();
735
  }
736
 
737
- return $title;
738
  }
739
 
740
  /**
@@ -750,8 +749,6 @@ class Generate_Title extends Generate_Description {
750
  */
751
  protected function generate_title_from_args( $args ) {
752
 
753
- $title = '';
754
-
755
  if ( $args['taxonomy'] ) {
756
  $title = $this->get_generated_archive_title( \get_term( $args['id'], $args['taxonomy'] ) );
757
  } elseif ( $args['pta'] ) {
@@ -930,14 +927,14 @@ class Generate_Title extends Generate_Description {
930
  } elseif ( $this->is_author() ) {
931
  $title = \get_queried_object()->display_name ?? '';
932
  $prefix = \_x( 'Author:', 'author archive title prefix', 'default' );
933
- } elseif ( $this->is_date() ) {
934
- if ( $this->is_year() ) {
935
  $title = \get_the_date( \_x( 'Y', 'yearly archives date format', 'default' ) );
936
  $prefix = \_x( 'Year:', 'date archive title prefix', 'default' );
937
- } elseif ( $this->is_month() ) {
938
  $title = \get_the_date( \_x( 'F Y', 'monthly archives date format', 'default' ) );
939
  $prefix = \_x( 'Month:', 'date archive title prefix', 'default' );
940
- } elseif ( $this->is_day() ) {
941
  $title = \get_the_date( \_x( 'F j, Y', 'daily archives date format', 'default' ) );
942
  $prefix = \_x( 'Day:', 'date archive title prefix', 'default' );
943
  }
@@ -1165,6 +1162,7 @@ class Generate_Title extends Generate_Description {
1165
  * @return string The untitled title.
1166
  */
1167
  public function get_static_untitled_title() {
 
1168
  return \__( 'Untitled', 'default' );
1169
  }
1170
 
@@ -1293,7 +1291,7 @@ class Generate_Title extends Generate_Description {
1293
  if ( $paged >= 2 || $page >= 2 ) {
1294
  $sep = $this->get_title_separator();
1295
 
1296
- // phpcs:ignore, WordPress.WP.I18n -- WP didn't add translator code either.
1297
  $paging = sprintf( \__( 'Page %s', 'default' ), max( $paged, $page ) );
1298
 
1299
  if ( \is_rtl() ) {
@@ -1311,6 +1309,7 @@ class Generate_Title extends Generate_Description {
1311
  * @since 3.1.2 Added strict taxonomical checks for title protection.
1312
  * @since 3.1.3 Fixed conditional logic.
1313
  * @since 4.2.0 Now supports the `$args['pta']` index.
 
1314
  * @see $this->merge_title_prefixes()
1315
  *
1316
  * @param string $title The title. Passed by reference.
@@ -1321,19 +1320,23 @@ class Generate_Title extends Generate_Description {
1321
  public function merge_title_protection( &$title, $args = null ) {
1322
 
1323
  if ( null === $args ) {
1324
- $id = $this->get_the_real_ID();
1325
- $run = $this->is_singular();
1326
  } else {
1327
  $this->fix_generation_args( $args );
1328
- $id = $args['id'];
1329
- $run = ! $args['taxonomy'] && ! $args['pta'];
1330
  }
1331
 
1332
- if ( $run ) return;
1333
 
1334
  $post = $id ? \get_post( $id ) : null;
1335
 
1336
- if ( isset( $post->post_password ) && '' !== $post->post_password ) {
 
 
 
 
1337
  /**
1338
  * Filters the text prepended to the post title of private posts.
1339
  *
@@ -1345,10 +1348,13 @@ class Generate_Title extends Generate_Description {
1345
  * Default 'Private: %s'.
1346
  * @param WP_Post $post Current post object.
1347
  */
1348
- // phpcs:ignore, WordPress.WP.I18n -- WordPress doesn't have a comment, either.
1349
- $protected_title_format = (string) \apply_filters( 'protected_title_format', \__( 'Protected: %s', 'default' ), $post );
1350
  $title = sprintf( $protected_title_format, $title );
1351
  } elseif ( isset( $post->post_status ) && 'private' === $post->post_status ) {
 
 
 
 
1352
  /**
1353
  * Filters the text prepended to the post title of private posts.
1354
  *
@@ -1360,8 +1366,8 @@ class Generate_Title extends Generate_Description {
1360
  * Default 'Private: %s'.
1361
  * @param WP_Post $post Current post object.
1362
  */
1363
- // phpcs:ignore, WordPress.WP.I18n -- WordPress doesn't have a comment, either.
1364
- $private_title_format = (string) \apply_filters( 'private_title_format', \__( 'Private: %s', 'default' ), $post );
1365
  $title = sprintf( $private_title_format, $title );
1366
  }
1367
  }
@@ -1454,7 +1460,7 @@ class Generate_Title extends Generate_Description {
1454
 
1455
  //? Only add pagination if the query is autodetermined, and on a real page.
1456
  if ( null === $args ) {
1457
- if ( $this->is_404() || \is_admin() ) {
1458
  $use = false;
1459
  } else {
1460
  $use = true;
713
  *
714
  * @since 3.1.0
715
  * @since 4.2.0 Flipped order of query tests.
716
+ * @since 4.3.0 Added failover filter for failed queries.
717
  * @internal
718
  * @see $this->get_raw_generated_title()
719
  *
721
  */
722
  protected function generate_title_from_query() {
723
 
724
+ if ( \is_404() ) {
 
 
725
  $title = $this->get_static_404_title();
726
  } elseif ( $this->is_search() ) {
727
  $title = $this->get_generated_search_query_title();
733
  $title = $this->get_generated_archive_title();
734
  }
735
 
736
+ return $title ?? '';
737
  }
738
 
739
  /**
749
  */
750
  protected function generate_title_from_args( $args ) {
751
 
 
 
752
  if ( $args['taxonomy'] ) {
753
  $title = $this->get_generated_archive_title( \get_term( $args['id'], $args['taxonomy'] ) );
754
  } elseif ( $args['pta'] ) {
927
  } elseif ( $this->is_author() ) {
928
  $title = \get_queried_object()->display_name ?? '';
929
  $prefix = \_x( 'Author:', 'author archive title prefix', 'default' );
930
+ } elseif ( \is_date() ) {
931
+ if ( \is_year() ) {
932
  $title = \get_the_date( \_x( 'Y', 'yearly archives date format', 'default' ) );
933
  $prefix = \_x( 'Year:', 'date archive title prefix', 'default' );
934
+ } elseif ( \is_month() ) {
935
  $title = \get_the_date( \_x( 'F Y', 'monthly archives date format', 'default' ) );
936
  $prefix = \_x( 'Month:', 'date archive title prefix', 'default' );
937
+ } elseif ( \is_day() ) {
938
  $title = \get_the_date( \_x( 'F j, Y', 'daily archives date format', 'default' ) );
939
  $prefix = \_x( 'Day:', 'date archive title prefix', 'default' );
940
  }
1162
  * @return string The untitled title.
1163
  */
1164
  public function get_static_untitled_title() {
1165
+ // FIXME: WordPress no longer outputs 'Untitled' for the title. It still actively holds this translation, but other context (Widget).
1166
  return \__( 'Untitled', 'default' );
1167
  }
1168
 
1291
  if ( $paged >= 2 || $page >= 2 ) {
1292
  $sep = $this->get_title_separator();
1293
 
1294
+ /* translators: %s: Page number. */
1295
  $paging = sprintf( \__( 'Page %s', 'default' ), max( $paged, $page ) );
1296
 
1297
  if ( \is_rtl() ) {
1309
  * @since 3.1.2 Added strict taxonomical checks for title protection.
1310
  * @since 3.1.3 Fixed conditional logic.
1311
  * @since 4.2.0 Now supports the `$args['pta']` index.
1312
+ * @since 4.2.4 Resolved regression where $run-test was reversed (renamed to $merge).
1313
  * @see $this->merge_title_prefixes()
1314
  *
1315
  * @param string $title The title. Passed by reference.
1320
  public function merge_title_protection( &$title, $args = null ) {
1321
 
1322
  if ( null === $args ) {
1323
+ $id = $this->get_the_real_ID();
1324
+ $merge = $this->is_singular();
1325
  } else {
1326
  $this->fix_generation_args( $args );
1327
+ $id = $args['id'];
1328
+ $merge = ! $args['taxonomy'] && ! $args['pta'];
1329
  }
1330
 
1331
+ if ( ! $merge ) return;
1332
 
1333
  $post = $id ? \get_post( $id ) : null;
1334
 
1335
+ if ( ! empty( $post->post_password ) ) {
1336
+
1337
+ /* translators: %s: Protected post title. */
1338
+ $prepend = \__( 'Protected: %s', 'default' );
1339
+
1340
  /**
1341
  * Filters the text prepended to the post title of private posts.
1342
  *
1348
  * Default 'Private: %s'.
1349
  * @param WP_Post $post Current post object.
1350
  */
1351
+ $protected_title_format = (string) \apply_filters( 'protected_title_format', $prepend, $post );
 
1352
  $title = sprintf( $protected_title_format, $title );
1353
  } elseif ( isset( $post->post_status ) && 'private' === $post->post_status ) {
1354
+
1355
+ /* translators: %s: Private post title. */
1356
+ $prepend = \__( 'Private: %s', 'default' );
1357
+
1358
  /**
1359
  * Filters the text prepended to the post title of private posts.
1360
  *
1366
  * Default 'Private: %s'.
1367
  * @param WP_Post $post Current post object.
1368
  */
1369
+ /* translators: %s: Private post title. */
1370
+ $private_title_format = (string) \apply_filters( 'private_title_format', $prepend, $post );
1371
  $title = sprintf( $private_title_format, $title );
1372
  }
1373
  }
1460
 
1461
  //? Only add pagination if the query is autodetermined, and on a real page.
1462
  if ( null === $args ) {
1463
+ if ( \is_404() || \is_admin() ) {
1464
  $use = false;
1465
  } else {
1466
  $use = true;
inc/classes/generate-url.class.php CHANGED
@@ -269,12 +269,12 @@ class Generate_Url extends Generate_Title {
269
  ?: $this->get_post_type_archive_canonical_url();
270
  } elseif ( $this->is_author() ) {
271
  $url = $this->get_author_canonical_url();
272
- } elseif ( $this->is_date() ) {
273
- if ( $this->is_day() ) {
274
  $url = $this->get_date_canonical_url( \get_query_var( 'year' ), \get_query_var( 'monthnum' ), \get_query_var( 'day' ) );
275
- } elseif ( $this->is_month() ) {
276
  $url = $this->get_date_canonical_url( \get_query_var( 'year' ), \get_query_var( 'monthnum' ) );
277
- } elseif ( $this->is_year() ) {
278
  $url = $this->get_date_canonical_url( \get_query_var( 'year' ) );
279
  }
280
  }
@@ -866,7 +866,7 @@ class Generate_Url extends Generate_Title {
866
  $url = \add_query_arg( [ 'cat' => $id ], $home );
867
  } elseif ( $this->is_tag() ) {
868
  $url = \add_query_arg( [ 'post_tag' => $id ], $home );
869
- } elseif ( $this->is_date() && isset( $GLOBALS['wp_query']->query ) ) {
870
  // FIXME: Core Report: WP doesn't accept paged parameters w/ date parameters. It'll lead to the homepage.
871
  $_query = $GLOBALS['wp_query']->query;
872
  $_date = [
269
  ?: $this->get_post_type_archive_canonical_url();
270
  } elseif ( $this->is_author() ) {
271
  $url = $this->get_author_canonical_url();
272
+ } elseif ( \is_date() ) {
273
+ if ( \is_day() ) {
274
  $url = $this->get_date_canonical_url( \get_query_var( 'year' ), \get_query_var( 'monthnum' ), \get_query_var( 'day' ) );
275
+ } elseif ( \is_month() ) {
276
  $url = $this->get_date_canonical_url( \get_query_var( 'year' ), \get_query_var( 'monthnum' ) );
277
+ } elseif ( \is_year() ) {
278
  $url = $this->get_date_canonical_url( \get_query_var( 'year' ) );
279
  }
280
  }
866
  $url = \add_query_arg( [ 'cat' => $id ], $home );
867
  } elseif ( $this->is_tag() ) {
868
  $url = \add_query_arg( [ 'post_tag' => $id ], $home );
869
+ } elseif ( \is_date() && isset( $GLOBALS['wp_query']->query ) ) {
870
  // FIXME: Core Report: WP doesn't accept paged parameters w/ date parameters. It'll lead to the homepage.
871
  $_query = $GLOBALS['wp_query']->query;
872
  $_date = [
inc/classes/init.class.php CHANGED
@@ -329,7 +329,7 @@ class Init extends Query {
329
  * @since 2.4.1
330
  * @param bool $overwrite_titles Whether to enable legacy title overwriting.
331
  *
332
- * TODO remove this block? -- it's been 6 years...
333
  * <https://make.wordpress.org/core/2015/10/20/document-title-in-4-4/>
334
  */
335
  if ( \apply_filters( 'the_seo_framework_manipulate_title', true ) ) {
@@ -483,7 +483,7 @@ class Init extends Query {
483
  */
484
  public function html_output() {
485
 
486
- if ( $this->is_preview() || $this->is_customize_preview() || ! $this->query_supports_seo() ) return;
487
 
488
  /**
489
  * @since 2.6.0
@@ -503,7 +503,7 @@ class Init extends Query {
503
  $init_start = microtime( true );
504
 
505
  // phpcs:disable, WordPress.Security.EscapeOutput -- Output is escaped.
506
- echo PHP_EOL, $this->get_plugin_indicator( 'before' );
507
 
508
  $this->do_meta_output();
509
 
@@ -511,7 +511,7 @@ class Init extends Query {
511
  'after',
512
  microtime( true ) - $init_start,
513
  $bootstrap_timer
514
- ), PHP_EOL;
515
  // phpcs:enable, WordPress.Security.EscapeOutput
516
 
517
  /**
@@ -557,7 +557,7 @@ class Init extends Query {
557
  'pint_site_output',
558
  ]
559
  );
560
- elseif ( $this->is_404() ) :
561
  array_push(
562
  $get,
563
  ...[
@@ -635,7 +635,7 @@ class Init extends Query {
635
  */
636
  public function _init_custom_field_redirect() {
637
 
638
- if ( $this->is_preview() || $this->is_customize_preview() || ! $this->query_supports_seo() ) return;
639
 
640
  $url = $this->get_redirect_url();
641
 
@@ -767,26 +767,26 @@ class Init extends Query {
767
  /**
768
  * @since 2.5.0
769
  * @param string $pre The output before this plugin's output.
770
- * Don't forget to add line breaks ( "\r\n" || PHP_EOL )!
771
  */
772
  $output = (string) \apply_filters( 'the_seo_framework_robots_txt_pre', '' );
773
 
774
  // Output defaults
775
- $output .= "User-agent: *\r\n";
776
- $output .= "Disallow: $site_path/wp-admin/\r\n";
777
- $output .= "Allow: $site_path/wp-admin/admin-ajax.php\r\n";
778
 
779
  /**
780
  * @since 2.5.0
781
  * @param bool $disallow Whether to disallow robots queries.
782
  */
783
  if ( \apply_filters( 'the_seo_framework_robots_disallow_queries', false ) )
784
- $output .= "Disallow: /*?*\r\n";
785
 
786
  /**
787
  * @since 2.5.0
788
  * @param string $pro The output after this plugin's output.
789
- * Don't forget to add line breaks ( "\r\n" || PHP_EOL )!
790
  */
791
  $output .= (string) \apply_filters( 'the_seo_framework_robots_txt_pro', '' );
792
 
@@ -797,9 +797,9 @@ class Init extends Query {
797
 
798
  foreach ( $sitemaps->get_sitemap_endpoint_list() as $id => $data )
799
  if ( ! empty( $data['robots'] ) )
800
- $output .= sprintf( "\r\nSitemap: %s", \esc_url( $sitemaps->get_expected_sitemap_endpoint_url( $id ) ) );
801
 
802
- $output .= "\r\n";
803
  } elseif ( ! $this->detect_sitemap_plugin() ) { // detect_sitemap_plugin() temp backward compat.
804
  if ( $this->use_core_sitemaps() ) {
805
  $wp_sitemaps_server = \wp_sitemaps_get_server();
@@ -820,7 +820,7 @@ class Init extends Query {
820
  // Simple test for invalid directory depth. Even //robots.txt is an invalid location.
821
  if ( strrpos( $raw_uri, '/' ) > 0 ) {
822
  $error = sprintf(
823
- "%s\r\n%s\r\n\r\n",
824
  '# This is an invalid robots.txt location.',
825
  '# Please visit: ' . \esc_url( \trailingslashit( $this->set_preferred_url_scheme( $this->get_home_host() ) ) . 'robots.txt' )
826
  );
@@ -860,7 +860,7 @@ class Init extends Query {
860
  */
861
  public function _init_robots_headers() {
862
 
863
- $noindex = $this->is_robots() || ( ! $this->get_option( 'index_the_feed' ) && $this->is_feed() );
864
 
865
  /**
866
  * @since 4.0.5
329
  * @since 2.4.1
330
  * @param bool $overwrite_titles Whether to enable legacy title overwriting.
331
  *
332
+ * TODO remove this block? -- it's been 7 years...
333
  * <https://make.wordpress.org/core/2015/10/20/document-title-in-4-4/>
334
  */
335
  if ( \apply_filters( 'the_seo_framework_manipulate_title', true ) ) {
483
  */
484
  public function html_output() {
485
 
486
+ if ( $this->is_preview() || \is_customize_preview() || ! $this->query_supports_seo() ) return;
487
 
488
  /**
489
  * @since 2.6.0
503
  $init_start = microtime( true );
504
 
505
  // phpcs:disable, WordPress.Security.EscapeOutput -- Output is escaped.
506
+ echo "\n", $this->get_plugin_indicator( 'before' );
507
 
508
  $this->do_meta_output();
509
 
511
  'after',
512
  microtime( true ) - $init_start,
513
  $bootstrap_timer
514
+ ), "\n";
515
  // phpcs:enable, WordPress.Security.EscapeOutput
516
 
517
  /**
557
  'pint_site_output',
558
  ]
559
  );
560
+ elseif ( \is_404() ) :
561
  array_push(
562
  $get,
563
  ...[
635
  */
636
  public function _init_custom_field_redirect() {
637
 
638
+ if ( $this->is_preview() || \is_customize_preview() || ! $this->query_supports_seo() ) return;
639
 
640
  $url = $this->get_redirect_url();
641
 
767
  /**
768
  * @since 2.5.0
769
  * @param string $pre The output before this plugin's output.
770
+ * Don't forget to add line breaks ( "\n" )!
771
  */
772
  $output = (string) \apply_filters( 'the_seo_framework_robots_txt_pre', '' );
773
 
774
  // Output defaults
775
+ $output .= "User-agent: *\n";
776
+ $output .= "Disallow: $site_path/wp-admin/\n";
777
+ $output .= "Allow: $site_path/wp-admin/admin-ajax.php\n";
778
 
779
  /**
780
  * @since 2.5.0
781
  * @param bool $disallow Whether to disallow robots queries.
782
  */
783
  if ( \apply_filters( 'the_seo_framework_robots_disallow_queries', false ) )
784
+ $output .= "Disallow: /*?*\n";
785
 
786
  /**
787
  * @since 2.5.0
788
  * @param string $pro The output after this plugin's output.
789
+ * Don't forget to add line breaks ( "\n" )!
790
  */
791
  $output .= (string) \apply_filters( 'the_seo_framework_robots_txt_pro', '' );
792
 
797
 
798
  foreach ( $sitemaps->get_sitemap_endpoint_list() as $id => $data )
799
  if ( ! empty( $data['robots'] ) )
800
+ $output .= sprintf( "\nSitemap: %s", \esc_url( $sitemaps->get_expected_sitemap_endpoint_url( $id ) ) );
801
 
802
+ $output .= "\n";
803
  } elseif ( ! $this->detect_sitemap_plugin() ) { // detect_sitemap_plugin() temp backward compat.
804
  if ( $this->use_core_sitemaps() ) {
805
  $wp_sitemaps_server = \wp_sitemaps_get_server();
820
  // Simple test for invalid directory depth. Even //robots.txt is an invalid location.
821
  if ( strrpos( $raw_uri, '/' ) > 0 ) {
822
  $error = sprintf(
823
+ "%s\n%s\n\n",
824
  '# This is an invalid robots.txt location.',
825
  '# Please visit: ' . \esc_url( \trailingslashit( $this->set_preferred_url_scheme( $this->get_home_host() ) ) . 'robots.txt' )
826
  );
860
  */
861
  public function _init_robots_headers() {
862
 
863
+ $noindex = \is_robots() || ( ! $this->get_option( 'index_the_feed' ) && \is_feed() );
864
 
865
  /**
866
  * @since 4.0.5
inc/classes/internal/debug.class.php CHANGED
@@ -359,7 +359,7 @@ final class Debug {
359
  $_message = "'<span><strong>$type:</strong> $message";
360
  $_message .= $file ? " In $file" : '';
361
  $_message .= $line ? " on line $line" : '';
362
- $_message .= '</span><br>' . PHP_EOL;
363
 
364
  return $_message;
365
  }
@@ -419,7 +419,7 @@ final class Debug {
419
  $title = '<div style="display:inline-block;width:100%;padding:20px;margin:0 auto;border-bottom:1px solid #ccc;"><h2 style="font-family:unset;color:#ddd;font-size:22px;padding:0;margin:0">' . $title . '</h2></div>';
420
 
421
  // Escape it, replace EOL with breaks, and style everything between quotes (which are ending with space).
422
- $output = str_replace( PHP_EOL, '<br>' . PHP_EOL, \esc_html( str_replace( str_repeat( ' ', 4 ), str_repeat( '&nbsp;', 4 ), $output ) ) );
423
  $output = preg_replace( '/(&quot;.*?&quot;)(\s|&nbps;)/', '<font color="arnoldschwarzenegger">$1</font> ', $output );
424
 
425
  $output = '<div style="display:inline-block;width:100%;padding:20px;font-family:Consolas,Monaco,monospace;font-size:14px;">' . $output . '</div>';
@@ -569,7 +569,7 @@ final class Debug {
569
 
570
  $value = '<font color="harrisonford">' . "$type $value" . '</font>';
571
  $out = \esc_html( $name ) . ' => ' . $value;
572
- $output .= '<span style="background:#dadada">' . $out . '</span>' . PHP_EOL;
573
  }
574
 
575
  foreach ( $not_current as $name => $value ) {
@@ -584,7 +584,7 @@ final class Debug {
584
  $value = '<font color="harrisonford">' . "$type $value" . '</font>';
585
  $out = \esc_html( $name ) . ' => ' . $value;
586
 
587
- $output .= $out . PHP_EOL;
588
  }
589
 
590
  if ( 'yup' === $cache_version ) {
@@ -593,7 +593,7 @@ final class Debug {
593
  $title = \is_admin() ? 'Expected Front-end WordPress Query' : 'Current WordPress Query';
594
  }
595
 
596
- $output = str_replace( PHP_EOL, '<br>' . PHP_EOL, $output );
597
  $output = sprintf(
598
  '<div style="display:block;width:100%%;background:#fafafa;color:#333;border-bottom:1px solid #666">%s%s%s</div>',
599
  sprintf(
359
  $_message = "'<span><strong>$type:</strong> $message";
360
  $_message .= $file ? " In $file" : '';
361
  $_message .= $line ? " on line $line" : '';
362
+ $_message .= "</span><br>\n";
363
 
364
  return $_message;
365
  }
419
  $title = '<div style="display:inline-block;width:100%;padding:20px;margin:0 auto;border-bottom:1px solid #ccc;"><h2 style="font-family:unset;color:#ddd;font-size:22px;padding:0;margin:0">' . $title . '</h2></div>';
420
 
421
  // Escape it, replace EOL with breaks, and style everything between quotes (which are ending with space).
422
+ $output = str_replace( [ "\r\n", "\r", "\n" ], "<br>\n", \esc_html( str_replace( str_repeat( ' ', 4 ), str_repeat( '&nbsp;', 4 ), $output ) ) );
423
  $output = preg_replace( '/(&quot;.*?&quot;)(\s|&nbps;)/', '<font color="arnoldschwarzenegger">$1</font> ', $output );
424
 
425
  $output = '<div style="display:inline-block;width:100%;padding:20px;font-family:Consolas,Monaco,monospace;font-size:14px;">' . $output . '</div>';
569
 
570
  $value = '<font color="harrisonford">' . "$type $value" . '</font>';
571
  $out = \esc_html( $name ) . ' => ' . $value;
572
+ $output .= "<span style=background:#dadada>$out</span>\n";
573
  }
574
 
575
  foreach ( $not_current as $name => $value ) {
584
  $value = '<font color="harrisonford">' . "$type $value" . '</font>';
585
  $out = \esc_html( $name ) . ' => ' . $value;
586
 
587
+ $output .= "$out\n";
588
  }
589
 
590
  if ( 'yup' === $cache_version ) {
593
  $title = \is_admin() ? 'Expected Front-end WordPress Query' : 'Current WordPress Query';
594
  }
595
 
596
+ $output = str_replace( [ "\r\n", "\r", "\n" ], "<br>\n", $output );
597
  $output = sprintf(
598
  '<div style="display:block;width:100%%;background:#fafafa;color:#333;border-bottom:1px solid #666">%s%s%s</div>',
599
  sprintf(
inc/classes/internal/deprecated.class.php CHANGED
@@ -144,7 +144,7 @@ final class Deprecated {
144
  . $tsf->yandex_site_output()
145
  . $tsf->baidu_site_output()
146
  . $tsf->pint_site_output();
147
- elseif ( $tsf->is_404() ) :
148
  $output = $tsf->theme_color()
149
  . $tsf->google_site_output()
150
  . $tsf->bing_site_output()
@@ -153,7 +153,7 @@ final class Deprecated {
153
  . $tsf->pint_site_output();
154
  elseif ( $tsf->is_query_exploited() ) :
155
  // aqp = advanced query protection
156
- $output = '<meta name="tsf:aqp" value="1" />' . PHP_EOL;
157
  else :
158
  // Inefficient concatenation is inefficient. Improve this?
159
  $output = $tsf->the_description()
@@ -1504,13 +1504,17 @@ final class Deprecated {
1504
  * @since 3.2.2 Removed SEO settings page check. This now returns false on that page.
1505
  * @since 4.2.0 1. No longer casts input $id to integer.
1506
  * 2. Deprecated.
 
1507
  * @deprecated
1508
  *
1509
  * @param int $id The page ID, required. Can be 0.
1510
  * @return bool True if ID if for the homepage.
1511
  */
1512
  public function is_front_page_by_id( $id ) {
1513
- \tsf()->_deprecated_function( 'tsf()->is_front_page_by_id()', '4.2.0', 'tsf()->is_real_front_page_by_id()' );
 
 
 
1514
 
1515
  $pof = (int) \get_option( 'page_on_front' );
1516
 
@@ -1521,13 +1525,13 @@ final class Deprecated {
1521
 
1522
  case 'posts':
1523
  $is_front_page =
1524
- ( 0 === $pof && $this->is_home() )
1525
  || $pof === $id;
1526
  break;
1527
 
1528
  default:
1529
  // Elegant Themes's Extra support
1530
- $is_front_page = 0 === $id && $this->is_home();
1531
  break;
1532
  endswitch;
1533
 
@@ -1578,6 +1582,7 @@ final class Deprecated {
1578
  * @since 4.0.5 1. The shop ID is now handled via the filter.
1579
  * 2. The question ID (AnsPress) is no longer called. This should work out-of-the-box since AnsPress 4.1.
1580
  * @since 4.2.0 Deprecated
 
1581
  * @deprecated
1582
  *
1583
  * @return int The admin ID.
@@ -1592,7 +1597,7 @@ final class Deprecated {
1592
  */
1593
  return (int) \apply_filters(
1594
  'the_seo_framework_real_id',
1595
- $this->is_feed() ? \get_the_ID() : 0
1596
  );
1597
  }
1598
 
144
  . $tsf->yandex_site_output()
145
  . $tsf->baidu_site_output()
146
  . $tsf->pint_site_output();
147
+ elseif ( \is_404() ) :
148
  $output = $tsf->theme_color()
149
  . $tsf->google_site_output()
150
  . $tsf->bing_site_output()
153
  . $tsf->pint_site_output();
154
  elseif ( $tsf->is_query_exploited() ) :
155
  // aqp = advanced query protection
156
+ $output = '<meta name="tsf:aqp" value="1" />' . "\n";
157
  else :
158
  // Inefficient concatenation is inefficient. Improve this?
159
  $output = $tsf->the_description()
1504
  * @since 3.2.2 Removed SEO settings page check. This now returns false on that page.
1505
  * @since 4.2.0 1. No longer casts input $id to integer.
1506
  * 2. Deprecated.
1507
+ * @since 4.2.4 No longer causes a fatal error.
1508
  * @deprecated
1509
  *
1510
  * @param int $id The page ID, required. Can be 0.
1511
  * @return bool True if ID if for the homepage.
1512
  */
1513
  public function is_front_page_by_id( $id ) {
1514
+
1515
+ $tsf = \tsf();
1516
+
1517
+ $tsf->_deprecated_function( 'tsf()->is_front_page_by_id()', '4.2.0', 'tsf()->is_real_front_page_by_id()' );
1518
 
1519
  $pof = (int) \get_option( 'page_on_front' );
1520
 
1525
 
1526
  case 'posts':
1527
  $is_front_page =
1528
+ ( 0 === $pof && $tsf->is_home() )
1529
  || $pof === $id;
1530
  break;
1531
 
1532
  default:
1533
  // Elegant Themes's Extra support
1534
+ $is_front_page = 0 === $id && $tsf->is_home();
1535
  break;
1536
  endswitch;
1537
 
1582
  * @since 4.0.5 1. The shop ID is now handled via the filter.
1583
  * 2. The question ID (AnsPress) is no longer called. This should work out-of-the-box since AnsPress 4.1.
1584
  * @since 4.2.0 Deprecated
1585
+ * @since 4.2.4 No longer causes a fatal error.
1586
  * @deprecated
1587
  *
1588
  * @return int The admin ID.
1597
  */
1598
  return (int) \apply_filters(
1599
  'the_seo_framework_real_id',
1600
+ \is_feed() ? \get_the_ID() : 0
1601
  );
1602
  }
1603
 
inc/classes/interpreters/html.class.php CHANGED
@@ -188,7 +188,7 @@ final class HTML {
188
  public static function wrap_fields( $input = '', $echo = false ) {
189
 
190
  if ( \is_array( $input ) )
191
- $input = implode( PHP_EOL, $input );
192
 
193
  $output = "<div class=tsf-fields>$input</div>";
194
 
188
  public static function wrap_fields( $input = '', $echo = false ) {
189
 
190
  if ( \is_array( $input ) )
191
+ $input = implode( "\n", $input );
192
 
193
  $output = "<div class=tsf-fields>$input</div>";
194
 
inc/classes/interpreters/markdown.class.php CHANGED
@@ -58,11 +58,11 @@ final class Markdown {
58
  public static function convert( $text, $convert = [], $args = [] ) {
59
 
60
  // preprocess
61
- $text = str_replace( "\r\n", "\n", $text );
62
  $text = str_replace( "\t", ' ', $text );
63
  $text = trim( $text );
64
 
65
- // You need 3 chars to make a markdown: *m*
66
  if ( \strlen( $text ) < 3 )
67
  return '';
68
 
58
  public static function convert( $text, $convert = [], $args = [] ) {
59
 
60
  // preprocess
61
+ $text = str_replace( [ "\r\n", "\r" ], "\n", $text );
62
  $text = str_replace( "\t", ' ', $text );
63
  $text = trim( $text );
64
 
65
+ // You need at least 3 chars to make a markdown: *m*
66
  if ( \strlen( $text ) < 3 )
67
  return '';
68
 
inc/classes/load.class.php CHANGED
@@ -61,7 +61,7 @@ final class Load extends Cache {
61
  /**
62
  * @since 4.1.4
63
  * @access protected
64
- * DO NOT OVERWRITE.
65
  * Feel free to read.
66
  * Use constant `THE_SEO_FRAMEWORK_HEADLESS` instead.
67
  * @var bool|array $is_headless Whether headless TSF is enabled.
61
  /**
62
  * @since 4.1.4
63
  * @access protected
64
+ * DO NOT OVERWRITE, it should be 'immutable'. <https://wiki.php.net/rfc/immutability>
65
  * Feel free to read.
66
  * Use constant `THE_SEO_FRAMEWORK_HEADLESS` instead.
67
  * @var bool|array $is_headless Whether headless TSF is enabled.
inc/classes/query.class.php CHANGED
@@ -195,7 +195,7 @@ class Query extends Core {
195
  */
196
  $id = \apply_filters(
197
  'the_seo_framework_real_id',
198
- $this->is_feed() ? \get_the_ID() : 0
199
  );
200
  }
201
 
@@ -309,6 +309,8 @@ class Query extends Core {
309
  * Detects 404.
310
  *
311
  * @since 2.6.0
 
 
312
  *
313
  * @return bool
314
  */
@@ -320,6 +322,8 @@ class Query extends Core {
320
  * Detects admin screen.
321
  *
322
  * @since 2.6.0
 
 
323
  *
324
  * @return bool
325
  */
@@ -590,6 +594,8 @@ class Query extends Core {
590
  * in `\WP_Customize_Manager::setup_theme()`.
591
  *
592
  * @since 4.0.0
 
 
593
  *
594
  * @return bool
595
  */
@@ -601,6 +607,8 @@ class Query extends Core {
601
  * Detects date archives.
602
  *
603
  * @since 2.6.0
 
 
604
  *
605
  * @return bool
606
  */
@@ -612,7 +620,8 @@ class Query extends Core {
612
  * Detects day archives.
613
  *
614
  * @since 2.6.0
615
- * @uses $this->is_date()
 
616
  *
617
  * @return bool
618
  */
@@ -624,6 +633,8 @@ class Query extends Core {
624
  * Detects feed.
625
  *
626
  * @since 2.6.0
 
 
627
  *
628
  * @param string|array $feeds Optional feed types to check.
629
  * @return bool
@@ -679,6 +690,8 @@ class Query extends Core {
679
  * Detects month archives.
680
  *
681
  * @since 2.6.0
 
 
682
  *
683
  * @return bool
684
  */
@@ -815,18 +828,14 @@ class Query extends Core {
815
  * @since 2.5.2
816
  * @since 3.1.0 Now passes $post_types parameter in admin screens, only when it's an integer.
817
  * @since 4.0.0 No longer processes integers as input.
 
818
  * @uses $this->is_singular_admin()
819
  *
820
- * @param string|array $post_types Optional. Post type or array of post types. Default empty string.
821
  * @return bool Post Type is singular
822
  */
823
  public function is_singular( $post_types = '' ) {
824
 
825
- if ( \is_int( $post_types ) ) {
826
- // Integers are no longer accepted.
827
- $post_types = '';
828
- }
829
-
830
  // WP_Query functions require loop, do alternative check.
831
  if ( \is_admin() )
832
  return $this->is_singular_admin();
@@ -863,6 +872,7 @@ class Query extends Core {
863
  */
864
  public function is_static_frontpage( $id = 0 ) {
865
 
 
866
  $front_id = umemo( __METHOD__ )
867
  ?? umemo(
868
  __METHOD__,
@@ -993,6 +1003,8 @@ class Query extends Core {
993
  * Detects year archives.
994
  *
995
  * @since 2.6.0
 
 
996
  *
997
  * @return bool
998
  */
@@ -1235,6 +1247,8 @@ class Query extends Core {
1235
  * Determines whether we're on the robots.txt file output.
1236
  *
1237
  * @since 2.9.2
 
 
1238
  *
1239
  * @return bool
1240
  */
195
  */
196
  $id = \apply_filters(
197
  'the_seo_framework_real_id',
198
+ \is_feed() ? \get_the_ID() : 0
199
  );
200
  }
201
 
309
  * Detects 404.
310
  *
311
  * @since 2.6.0
312
+ * @ignore unused.
313
+ * @todo deprecate
314
  *
315
  * @return bool
316
  */
322
  * Detects admin screen.
323
  *
324
  * @since 2.6.0
325
+ * @ignore unused.
326
+ * @todo deprecate
327
  *
328
  * @return bool
329
  */
594
  * in `\WP_Customize_Manager::setup_theme()`.
595
  *
596
  * @since 4.0.0
597
+ * @ignore unused.
598
+ * @todo deprecate
599
  *
600
  * @return bool
601
  */
607
  * Detects date archives.
608
  *
609
  * @since 2.6.0
610
+ * @ignore unused.
611
+ * @todo deprecate
612
  *
613
  * @return bool
614
  */
620
  * Detects day archives.
621
  *
622
  * @since 2.6.0
623
+ * @ignore unused.
624
+ * @todo deprecate
625
  *
626
  * @return bool
627
  */
633
  * Detects feed.
634
  *
635
  * @since 2.6.0
636
+ * @ignore unused.
637
+ * @todo deprecate
638
  *
639
  * @param string|array $feeds Optional feed types to check.
640
  * @return bool
690
  * Detects month archives.
691
  *
692
  * @since 2.6.0
693
+ * @ignore unused.
694
+ * @todo deprecate
695
  *
696
  * @return bool
697
  */
828
  * @since 2.5.2
829
  * @since 3.1.0 Now passes $post_types parameter in admin screens, only when it's an integer.
830
  * @since 4.0.0 No longer processes integers as input.
831
+ * @since 4.2.4 No longer tests type of $post_types.
832
  * @uses $this->is_singular_admin()
833
  *
834
+ * @param string|string[] $post_types Optional. Post type or array of post types. Default empty string.
835
  * @return bool Post Type is singular
836
  */
837
  public function is_singular( $post_types = '' ) {
838
 
 
 
 
 
 
839
  // WP_Query functions require loop, do alternative check.
840
  if ( \is_admin() )
841
  return $this->is_singular_admin();
872
  */
873
  public function is_static_frontpage( $id = 0 ) {
874
 
875
+ // Memo this slow part seperately; memo_query() would cache the whole method, which isn't necessary.
876
  $front_id = umemo( __METHOD__ )
877
  ?? umemo(
878
  __METHOD__,
1003
  * Detects year archives.
1004
  *
1005
  * @since 2.6.0
1006
+ * @ignore unused.
1007
+ * @todo deprecate
1008
  *
1009
  * @return bool
1010
  */
1247
  * Determines whether we're on the robots.txt file output.
1248
  *
1249
  * @since 2.9.2
1250
+ * @ignore unused.
1251
+ * @todo deprecate
1252
  *
1253
  * @return bool
1254
  */
inc/classes/render.class.php CHANGED
@@ -75,7 +75,7 @@ class Render extends Admin_Init {
75
  * @since 4.0.0 Removed extraneous, unused parameters.
76
  * @see $this->get_title()
77
  *
78
- * @param string $title The filterable title.
79
  * @return string $title
80
  */
81
  public function get_wp_title( $title = '' ) {
@@ -212,7 +212,7 @@ class Render extends Admin_Init {
212
  );
213
  }
214
 
215
- return $el . ( $new_line ? PHP_EOL : '' );
216
  }
217
 
218
  /**
75
  * @since 4.0.0 Removed extraneous, unused parameters.
76
  * @see $this->get_title()
77
  *
78
+ * @param string $title The filterable title.
79
  * @return string $title
80
  */
81
  public function get_wp_title( $title = '' ) {
212
  );
213
  }
214
 
215
+ return $el . ( $new_line ? "\n" : '' );
216
  }
217
 
218
  /**
inc/classes/sanitize.class.php CHANGED
@@ -1640,7 +1640,7 @@ class Sanitize extends Admin_Pages {
1640
  * @since 4.0.0 1. Removed rudimentary relative URL testing.
1641
  * 2. Removed input transformation filters, and with that, removed redundant multisite spam protection.
1642
  * 3. Now allows all protocols. Enjoy!
1643
- * 4. Now no longer lets through double-absolute URLs (e.g. `https://google.com/https://google.com/path/to/file/`)
1644
  * when filter `the_seo_framework_allow_external_redirect` is set to false.
1645
  *
1646
  * @param string $new_value String with potentially unwanted redirect URL.
@@ -1994,15 +1994,17 @@ class Sanitize extends Admin_Pages {
1994
  * @since 4.0.5 Now faults images with filename extensions APNG, BMP, ICO, TIFF, or SVG.
1995
  * @since 4.1.4 Fixed theoretical issue where a different image could be set when width
1996
  * and height are supplied and either over 4K, but no ID is given.
 
1997
  * @NOTE If the input details are in an associative array, they'll be converted to sequential.
1998
  *
1999
  * @param array $details The image details, either associative (see $defaults) or sequential.
2000
  * @return array The image details array, sequential: int => {
2001
- * string url: The image URL,
2002
- * int id: The image ID,
2003
- * int width: The image width in pixels,
2004
- * int height: The image height in pixels,
2005
- * string alt: The image alt tag,
 
2006
  * }
2007
  */
2008
  public function s_image_details( $details ) {
@@ -2011,14 +2013,15 @@ class Sanitize extends Admin_Pages {
2011
  return $this->s_image_details_deep( $details );
2012
 
2013
  $defaults = [
2014
- 'url' => '',
2015
- 'id' => 0,
2016
- 'width' => 0,
2017
- 'height' => 0,
2018
- 'alt' => '',
 
2019
  ];
2020
 
2021
- [ $url, $id, $width, $height, $alt ] = array_values( array_merge( $defaults, $details ) );
2022
 
2023
  if ( ! $url ) return $defaults;
2024
 
@@ -2036,7 +2039,7 @@ class Sanitize extends Admin_Pages {
2036
  *
2037
  * Tested with Facebook; they ignore them too. There's no documentation available.
2038
  * TODO Should we even test for this here, or at the image generators' type?
2039
- * It seems, however, that all services we want to communicate with ignore these types, anyway.
2040
  */
2041
  if ( \in_array(
2042
  strtolower( strtok( pathinfo( $url, PATHINFO_EXTENSION ), '?' ) ),
@@ -2050,8 +2053,8 @@ class Sanitize extends Admin_Pages {
2050
  if ( ! $width || ! $height )
2051
  $width = $height = 0;
2052
 
2053
- if ( $id && ( $width > 4096 || $height > 4096 ) ) {
2054
- $new_image = $this->get_largest_acceptable_image_src( $id, 4096 );
2055
  $url = $new_image ? $this->s_url_relative_to_current_scheme( $new_image[0] ) : '';
2056
 
2057
  if ( ! $url ) return $defaults;
@@ -2068,21 +2071,23 @@ class Sanitize extends Admin_Pages {
2068
  $alt = \strlen( $alt ) > 420 ? $this->trim_excerpt( $alt, 0, 420 ) : $alt;
2069
  }
2070
 
2071
- return compact( 'url', 'id', 'width', 'height', 'alt' );
2072
  }
2073
 
2074
  /**
2075
  * Iterates over and cleans known parameters from image details. Also strips out duplicates.
2076
  *
2077
  * @since 4.0.0
 
2078
  *
2079
  * @param array $details_array The image details, preferably sequential.
2080
  * @return array The image details array, sequential: int => {
2081
- * string url: The image URL,
2082
- * int id: The image ID,
2083
- * int width: The image width in pixels,
2084
- * int height: The image height in pixels,
2085
- * string alt: The image alt tag,
 
2086
  * }
2087
  */
2088
  public function s_image_details_deep( $details_array ) {
1640
  * @since 4.0.0 1. Removed rudimentary relative URL testing.
1641
  * 2. Removed input transformation filters, and with that, removed redundant multisite spam protection.
1642
  * 3. Now allows all protocols. Enjoy!
1643
+ * 4. Now no longer lets through double-absolute URLs (e.g. `https://example.com/https://example.com/path/to/file/`)
1644
  * when filter `the_seo_framework_allow_external_redirect` is set to false.
1645
  *
1646
  * @param string $new_value String with potentially unwanted redirect URL.
1994
  * @since 4.0.5 Now faults images with filename extensions APNG, BMP, ICO, TIFF, or SVG.
1995
  * @since 4.1.4 Fixed theoretical issue where a different image could be set when width
1996
  * and height are supplied and either over 4K, but no ID is given.
1997
+ * @since 4.2.4 Now accepts, processes, and returns filesizes under index `filesize`.
1998
  * @NOTE If the input details are in an associative array, they'll be converted to sequential.
1999
  *
2000
  * @param array $details The image details, either associative (see $defaults) or sequential.
2001
  * @return array The image details array, sequential: int => {
2002
+ * string url: The image URL,
2003
+ * int id: The image ID,
2004
+ * int width: The image width in pixels,
2005
+ * int height: The image height in pixels,
2006
+ * string alt: The image alt tag,
2007
+ * int filesize: The image filesize in bytes,
2008
  * }
2009
  */
2010
  public function s_image_details( $details ) {
2013
  return $this->s_image_details_deep( $details );
2014
 
2015
  $defaults = [
2016
+ 'url' => '',
2017
+ 'id' => 0,
2018
+ 'width' => 0,
2019
+ 'height' => 0,
2020
+ 'alt' => '',
2021
+ 'filesize' => 0,
2022
  ];
2023
 
2024
+ [ $url, $id, $width, $height, $alt, $filesize ] = array_values( array_merge( $defaults, $details ) );
2025
 
2026
  if ( ! $url ) return $defaults;
2027
 
2039
  *
2040
  * Tested with Facebook; they ignore them too. There's no documentation available.
2041
  * TODO Should we even test for this here, or at the image generators' type?
2042
+ * It seems, however, that _all_ services we want to communicate with ignore these types, anyway.
2043
  */
2044
  if ( \in_array(
2045
  strtolower( strtok( pathinfo( $url, PATHINFO_EXTENSION ), '?' ) ),
2053
  if ( ! $width || ! $height )
2054
  $width = $height = 0;
2055
 
2056
+ if ( $id && ( $width > 4096 || $height > 4096 || $filesize > 5 * MB_IN_BYTES ) ) {
2057
+ $new_image = $this->get_largest_acceptable_image_src( $id, 4096, 5 * MB_IN_BYTES );
2058
  $url = $new_image ? $this->s_url_relative_to_current_scheme( $new_image[0] ) : '';
2059
 
2060
  if ( ! $url ) return $defaults;
2071
  $alt = \strlen( $alt ) > 420 ? $this->trim_excerpt( $alt, 0, 420 ) : $alt;
2072
  }
2073
 
2074
+ return compact( 'url', 'id', 'width', 'height', 'alt', 'filesize' );
2075
  }
2076
 
2077
  /**
2078
  * Iterates over and cleans known parameters from image details. Also strips out duplicates.
2079
  *
2080
  * @since 4.0.0
2081
+ * @since 4.2.4 Now accepts, processes, and returns filesizes under index `filesize`.
2082
  *
2083
  * @param array $details_array The image details, preferably sequential.
2084
  * @return array The image details array, sequential: int => {
2085
+ * string url: The image URL,
2086
+ * int id: The image ID,
2087
+ * int width: The image width in pixels,
2088
+ * int height: The image height in pixels,
2089
+ * string alt: The image alt tag,
2090
+ * int filesize: The image filesize in bytes,
2091
  * }
2092
  */
2093
  public function s_image_details_deep( $details_array ) {
inc/classes/site-options.class.php CHANGED
@@ -50,6 +50,7 @@ class Site_Options extends Sanitize {
50
  * @since 2.6.0
51
  * @since 3.1.0 Now applies filters 'the_seo_framework_default_site_options'
52
  * @since 4.0.0 `home_title_location` is now switched from right to left, or vice-versa.
 
53
  *
54
  * @return array Default site options.
55
  */
@@ -147,7 +148,7 @@ class Site_Options extends Sanitize {
147
  // Robots copyright.
148
  'set_copyright_directives' => 1, // Allow copyright directive settings.
149
  'max_snippet_length' => -1, // Max text-snippet length. -1 = unlimited, 0 = disabled, R>0 = characters.
150
- 'max_image_preview' => 'standard', // Max image-preview size. 'none', 'standard', 'large'.
151
  'max_video_preview' => -1, // Max video-preview size. -1 = unlimited, 0 = disabled, R>0 = seconds.
152
 
153
  // Robots home.
50
  * @since 2.6.0
51
  * @since 3.1.0 Now applies filters 'the_seo_framework_default_site_options'
52
  * @since 4.0.0 `home_title_location` is now switched from right to left, or vice-versa.
53
+ * @since 4.2.4 `max_image_preview` now defaults to `large`, from `standard`, matching WordPress's default.
54
  *
55
  * @return array Default site options.
56
  */
148
  // Robots copyright.
149
  'set_copyright_directives' => 1, // Allow copyright directive settings.
150
  'max_snippet_length' => -1, // Max text-snippet length. -1 = unlimited, 0 = disabled, R>0 = characters.
151
+ 'max_image_preview' => 'large', // Max image-preview size. 'none', 'standard', 'large'.
152
  'max_video_preview' => -1, // Max video-preview size. -1 = unlimited, 0 = disabled, R>0 = seconds.
153
 
154
  // Robots home.
inc/functions/api.php CHANGED
@@ -282,14 +282,12 @@ namespace The_SEO_Framework {
282
  + debug_backtrace( 0, 2 )[1]
283
  );
284
 
285
- // If the hash is stored, return null back to the caller.
286
- if ( isset( $memo[ $hash ] ) )
287
- return $memo[ $hash ] === $hash ? null : $memo[ $hash ];
 
 
288
 
289
- // Store the result of the function. If that's null/void, store hash.
290
- $memo[ $hash ] = \call_user_func( $fn ) ?? $hash;
291
-
292
- // If the hash is stored, return null back to the caller.
293
  return $memo[ $hash ] === $hash ? null : $memo[ $hash ];
294
  }
295
  }
282
  + debug_backtrace( 0, 2 )[1]
283
  );
284
 
285
+ // Normally, I try to avoid NOTs for they add (tiny) overhead. Here, I chose readability over performance.
286
+ if ( ! isset( $memo[ $hash ] ) ) {
287
+ // Store the result of the function. If that's null/void, store hash.
288
+ $memo[ $hash ] = \call_user_func( $fn ) ?? $hash;
289
+ }
290
 
 
 
 
 
291
  return $memo[ $hash ] === $hash ? null : $memo[ $hash ];
292
  }
293
  }
inc/functions/upgrade-suggestion.php CHANGED
@@ -72,14 +72,16 @@ function _prepare( $previous_version, $current_version ) {
72
  //? 2
73
  if ( ! \is_main_site() ) return;
74
 
75
- $show_sale = true;
76
- if ( \function_exists( '\\tsf_extension_manager' ) && method_exists( \tsf_extension_manager(), 'is_connected_user' ) ) {
77
- $show_sale = ! \tsf_extension_manager()->is_connected_user();
78
- }
79
- if ( $show_sale ) {
80
- // phpcs:ignore, TSF.Performance.Opcodes.ShouldHaveNamespaceEscape
81
- _suggest_temp_sale( $previous_version, $current_version );
82
- }
 
 
83
 
84
  //? 3a
85
  if ( \defined( 'TSF_EXTENSION_MANAGER_VERSION' ) ) return;
72
  //? 2
73
  if ( ! \is_main_site() ) return;
74
 
75
+ // phpcs:disable -- There is no sale, leftover code.
76
+ // $show_sale = true;
77
+ // if ( \function_exists( '\\tsf_extension_manager' ) && method_exists( \tsf_extension_manager(), 'is_connected_user' ) ) {
78
+ // $show_sale = ! \tsf_extension_manager()->is_connected_user();
79
+ // }
80
+ // if ( $show_sale ) {
81
+ // // phpcs:ignore, TSF.Performance.Opcodes.ShouldHaveNamespaceEscape
82
+ // _suggest_temp_sale( $previous_version, $current_version );
83
+ // }
84
+ // phpcs:enable
85
 
86
  //? 3a
87
  if ( \defined( 'TSF_EXTENSION_MANAGER_VERSION' ) ) return;
inc/views/edit/seo-settings-singular.php CHANGED
@@ -119,7 +119,7 @@ switch ( $this->get_view_instance( 'inpost', $instance ) ) :
119
  <?php
120
  HTML::make_info(
121
  __( 'The meta title can be used to determine the title used on search engine result pages.', 'autodescription' ),
122
- 'https://developers.google.com/search/docs/advanced/appearance/good-titles-snippets#page-titles'
123
  );
124
  ?>
125
  </div>
@@ -189,7 +189,7 @@ switch ( $this->get_view_instance( 'inpost', $instance ) ) :
189
  <?php
190
  HTML::make_info(
191
  __( 'The meta description can be used to determine the text used under the title on search engine results pages.', 'autodescription' ),
192
- 'https://developers.google.com/search/docs/advanced/appearance/good-titles-snippets#meta-descriptions'
193
  );
194
  ?>
195
  </div>
119
  <?php
120
  HTML::make_info(
121
  __( 'The meta title can be used to determine the title used on search engine result pages.', 'autodescription' ),
122
+ 'https://developers.google.com/search/docs/advanced/appearance/title-link'
123
  );
124
  ?>
125
  </div>
189
  <?php
190
  HTML::make_info(
191
  __( 'The meta description can be used to determine the text used under the title on search engine results pages.', 'autodescription' ),
192
+ 'https://developers.google.com/search/docs/advanced/appearance/snippet'
193
  );
194
  ?>
195
  </div>
inc/views/edit/seo-settings-tt.php CHANGED
@@ -120,7 +120,7 @@ $robots_settings = [
120
  echo ' ';
121
  HTML::make_info(
122
  __( 'The meta title can be used to determine the title used on search engine result pages.', 'autodescription' ),
123
- 'https://developers.google.com/search/docs/advanced/appearance/good-titles-snippets#page-titles'
124
  );
125
  ?>
126
  </label>
@@ -171,7 +171,7 @@ $robots_settings = [
171
  echo ' ';
172
  HTML::make_info(
173
  __( 'The meta description can be used to determine the text used under the title on search engine results pages.', 'autodescription' ),
174
- 'https://developers.google.com/search/docs/advanced/appearance/good-titles-snippets#meta-descriptions'
175
  );
176
  ?>
177
  </label>
120
  echo ' ';
121
  HTML::make_info(
122
  __( 'The meta title can be used to determine the title used on search engine result pages.', 'autodescription' ),
123
+ 'https://developers.google.com/search/docs/advanced/appearance/title-link'
124
  );
125
  ?>
126
  </label>
171
  echo ' ';
172
  HTML::make_info(
173
  __( 'The meta description can be used to determine the text used under the title on search engine results pages.', 'autodescription' ),
174
+ 'https://developers.google.com/search/docs/advanced/appearance/snippet'
175
  );
176
  ?>
177
  </label>
inc/views/settings/metaboxes/general.php CHANGED
@@ -267,8 +267,16 @@ switch ( $this->get_view_instance( 'general', $instance ) ) :
267
  'https' => 'HTTPS',
268
  ]
269
  );
 
270
  foreach ( $scheme_types as $value => $name )
271
- echo '<option value="' . esc_attr( $value ) . '"' . selected( $this->get_option( 'canonical_scheme' ), esc_attr( $value ), false ) . '>' . esc_html( $name ) . '</option>' . "\n";
 
 
 
 
 
 
 
272
  ?>
273
  </select>
274
 
267
  'https' => 'HTTPS',
268
  ]
269
  );
270
+ $_current = $this->get_option( 'canonical_scheme' );
271
  foreach ( $scheme_types as $value => $name )
272
+ vprintf(
273
+ '<option value="%s" %s>%s</option>',
274
+ [
275
+ esc_attr( $value ),
276
+ selected( $_current, esc_attr( $value ), false ),
277
+ esc_html( $name ),
278
+ ]
279
+ );
280
  ?>
281
  </select>
282
 
inc/views/settings/metaboxes/homepage.php CHANGED
@@ -68,7 +68,7 @@ switch ( $this->get_view_instance( 'homepage', $instance ) ) :
68
  echo ' ';
69
  HTML::make_info(
70
  __( 'The meta title can be used to determine the title used on search engine result pages.', 'autodescription' ),
71
- 'https://developers.google.com/search/docs/advanced/appearance/good-titles-snippets#page-titles'
72
  );
73
  ?>
74
  </label>
@@ -135,7 +135,7 @@ switch ( $this->get_view_instance( 'homepage', $instance ) ) :
135
  echo ' ';
136
  HTML::make_info(
137
  __( 'The meta description can be used to determine the text used under the title on search engine results pages.', 'autodescription' ),
138
- 'https://developers.google.com/search/docs/advanced/appearance/good-titles-snippets#meta-descriptions'
139
  );
140
  ?>
141
  </label>
68
  echo ' ';
69
  HTML::make_info(
70
  __( 'The meta title can be used to determine the title used on search engine result pages.', 'autodescription' ),
71
+ 'https://developers.google.com/search/docs/advanced/appearance/title-link'
72
  );
73
  ?>
74
  </label>
135
  echo ' ';
136
  HTML::make_info(
137
  __( 'The meta description can be used to determine the text used under the title on search engine results pages.', 'autodescription' ),
138
+ 'https://developers.google.com/search/docs/advanced/appearance/snippet'
139
  );
140
  ?>
141
  </label>
inc/views/settings/metaboxes/post-type-archive.php CHANGED
@@ -154,7 +154,7 @@ switch ( $this->get_view_instance( 'post_type_archive', $instance ) ) :
154
  echo ' ';
155
  HTML::make_info(
156
  __( 'The meta title can be used to determine the title used on search engine result pages.', 'autodescription' ),
157
- 'https://developers.google.com/search/docs/advanced/appearance/good-titles-snippets#page-titles'
158
  );
159
  ?>
160
  </label>
@@ -223,7 +223,7 @@ switch ( $this->get_view_instance( 'post_type_archive', $instance ) ) :
223
  echo ' ';
224
  HTML::make_info(
225
  __( 'The meta description can be used to determine the text used under the title on search engine results pages.', 'autodescription' ),
226
- 'https://developers.google.com/search/docs/advanced/appearance/good-titles-snippets#meta-descriptions'
227
  );
228
  ?>
229
  </label>
154
  echo ' ';
155
  HTML::make_info(
156
  __( 'The meta title can be used to determine the title used on search engine result pages.', 'autodescription' ),
157
+ 'https://developers.google.com/search/docs/advanced/appearance/title-link'
158
  );
159
  ?>
160
  </label>
223
  echo ' ';
224
  HTML::make_info(
225
  __( 'The meta description can be used to determine the text used under the title on search engine results pages.', 'autodescription' ),
226
+ 'https://developers.google.com/search/docs/advanced/appearance/snippet'
227
  );
228
  ?>
229
  </label>
inc/views/settings/metaboxes/schema.php CHANGED
@@ -62,7 +62,7 @@ switch ( $this->get_view_instance( 'schema', $instance ) ) :
62
 
63
  $info = HTML::make_info(
64
  __( 'Learn how this data is used.', 'autodescription' ),
65
- 'https://developers.google.com/search/docs/data-types/breadcrumb',
66
  false
67
  );
68
  HTML::wrap_fields(
@@ -82,7 +82,7 @@ switch ( $this->get_view_instance( 'schema', $instance ) ) :
82
 
83
  $info = HTML::make_info(
84
  __( 'Learn how this data is used.', 'autodescription' ),
85
- 'https://developers.google.com/search/docs/data-types/sitelinks-searchbox',
86
  false
87
  );
88
  HTML::wrap_fields(
@@ -101,7 +101,7 @@ switch ( $this->get_view_instance( 'schema', $instance ) ) :
101
 
102
  $info = HTML::make_info(
103
  __( 'Learn how this data is used.', 'autodescription' ),
104
- 'https://developers.google.com/search/docs/guides/enhance-site#add-your-sites-name-logo-and-social-links',
105
  false
106
  );
107
  HTML::wrap_fields(
@@ -127,9 +127,16 @@ switch ( $this->get_view_instance( 'schema', $instance ) ) :
127
  'person' => __( 'A Person', 'autodescription' ),
128
  ]
129
  );
130
- foreach ( $knowledge_type as $value => $name ) {
131
- echo '<option value="' . esc_attr( $value ) . '"' . selected( $this->get_option( 'knowledge_type' ), esc_attr( $value ), false ) . '>' . esc_html( $name ) . '</option>' . "\n";
132
- }
 
 
 
 
 
 
 
133
  ?>
134
  </select>
135
  </p>
@@ -148,7 +155,7 @@ switch ( $this->get_view_instance( 'schema', $instance ) ) :
148
  HTML::description( esc_html__( 'These options are used when this site represents an organization. When no logo is outputted, search engine will look elsewhere.', 'autodescription' ) );
149
  $info = HTML::make_info(
150
  __( 'Learn how this data is used.', 'autodescription' ),
151
- 'https://developers.google.com/search/docs/data-types/logo',
152
  false
153
  );
154
  HTML::wrap_fields(
@@ -178,10 +185,10 @@ switch ( $this->get_view_instance( 'schema', $instance ) ) :
178
  'id' => 'knowledge_logo',
179
  'data' => [
180
  'inputType' => 'logo',
181
- 'width' => 512,
182
- 'height' => 512,
183
- 'minWidth' => 112,
184
- 'minHeight' => 112,
185
  'flex' => true,
186
  ],
187
  'i18n' => [
@@ -289,7 +296,7 @@ switch ( $this->get_view_instance( 'schema', $instance ) ) :
289
  sprintf(
290
  /* translators: %s = Learn more URL. Markdown! */
291
  esc_html__( 'These settings are marked for removal. When you clear a field, it will be hidden forever. [Learn more](%s).', 'autodescription' ),
292
- 'https://developers.google.com/search/docs/data-types/social-profile'
293
  ),
294
  [ 'a' ],
295
  [ 'a_internal' => false ]
62
 
63
  $info = HTML::make_info(
64
  __( 'Learn how this data is used.', 'autodescription' ),
65
+ 'https://developers.google.com/search/docs/advanced/structured-data/breadcrumb',
66
  false
67
  );
68
  HTML::wrap_fields(
82
 
83
  $info = HTML::make_info(
84
  __( 'Learn how this data is used.', 'autodescription' ),
85
+ 'https://developers.google.com/search/docs/advanced/structured-data/sitelinks-searchbox',
86
  false
87
  );
88
  HTML::wrap_fields(
101
 
102
  $info = HTML::make_info(
103
  __( 'Learn how this data is used.', 'autodescription' ),
104
+ 'https://developers.google.com/search/docs/beginner/establish-business-details',
105
  false
106
  );
107
  HTML::wrap_fields(
127
  'person' => __( 'A Person', 'autodescription' ),
128
  ]
129
  );
130
+ $_current = $this->get_option( 'knowledge_type' );
131
+ foreach ( $knowledge_type as $value => $name )
132
+ vprintf(
133
+ '<option value="%s" %s>%s</option>',
134
+ [
135
+ esc_attr( $value ),
136
+ selected( $_current, esc_attr( $value ), false ),
137
+ esc_html( $name ),
138
+ ]
139
+ );
140
  ?>
141
  </select>
142
  </p>
155
  HTML::description( esc_html__( 'These options are used when this site represents an organization. When no logo is outputted, search engine will look elsewhere.', 'autodescription' ) );
156
  $info = HTML::make_info(
157
  __( 'Learn how this data is used.', 'autodescription' ),
158
+ 'https://developers.google.com/search/docs/advanced/structured-data/logo',
159
  false
160
  );
161
  HTML::wrap_fields(
185
  'id' => 'knowledge_logo',
186
  'data' => [
187
  'inputType' => 'logo',
188
+ 'width' => 512, // Magic number -> Google requirement? "MAGIC::GOOGLE->LOGO_MAX"?
189
+ 'height' => 512, // Magic number
190
+ 'minWidth' => 112, // Magic number -> Google requirement? "MAGIC::GOOGLE->LOGO_MIN"?
191
+ 'minHeight' => 112, // Magic number
192
  'flex' => true,
193
  ],
194
  'i18n' => [
296
  sprintf(
297
  /* translators: %s = Learn more URL. Markdown! */
298
  esc_html__( 'These settings are marked for removal. When you clear a field, it will be hidden forever. [Learn more](%s).', 'autodescription' ),
299
+ 'https://support.google.com/knowledgepanel/answer/7534842'
300
  ),
301
  [ 'a' ],
302
  [ 'a_internal' => false ]
inc/views/settings/metaboxes/sitemaps.php CHANGED
@@ -361,7 +361,7 @@ switch ( $this->get_view_instance( 'sitemaps', $instance ) ) :
361
  );
362
 
363
  $ph_id = get_theme_mod( 'custom_logo' ) ?: 0;
364
- $ph_src = $ph_id ? wp_get_attachment_image_src( $ph_id, [ 29, 29 ] ) : [];
365
 
366
  $logo_placeholder = ! empty( $ph_src[0] ) ? $ph_src[0] : '';
367
  ?>
@@ -383,10 +383,10 @@ switch ( $this->get_view_instance( 'sitemaps', $instance ) ) :
383
  'id' => 'sitemap_logo',
384
  'data' => [
385
  'inputType' => 'logo',
386
- 'width' => 512,
387
- 'height' => 512,
388
- 'minWidth' => 64,
389
- 'minHeight' => 64,
390
  'flex' => true,
391
  ],
392
  'i18n' => [
361
  );
362
 
363
  $ph_id = get_theme_mod( 'custom_logo' ) ?: 0;
364
+ $ph_src = $ph_id ? wp_get_attachment_image_src( $ph_id, [ 29, 29 ] ) : []; // TODO magic number "SITEMAP_LOGO_PX"
365
 
366
  $logo_placeholder = ! empty( $ph_src[0] ) ? $ph_src[0] : '';
367
  ?>
383
  'id' => 'sitemap_logo',
384
  'data' => [
385
  'inputType' => 'logo',
386
+ 'width' => 512, // Magic number "CUSTOMIZER_LOGO_MAX" (should be defined in WP?)
387
+ 'height' => 512, // Magic number
388
+ 'minWidth' => 64, // Magic number "CUSTOMIZER_LOGO_MIN" (should be defined in WP?)
389
+ 'minHeight' => 64, // Magic number
390
  'flex' => true,
391
  ],
392
  'i18n' => [
inc/views/settings/metaboxes/title.php CHANGED
@@ -104,10 +104,9 @@ switch ( $this->get_view_instance( 'title', $instance ) ) :
104
  <hr>
105
  <?php
106
  if (
107
- ! ( defined( 'TSF_DISABLE_SUGGESTIONS' ) && TSF_DISABLE_SUGGESTIONS )
108
  && ! current_theme_supports( 'title-tag' )
109
  && ! defined( 'TSFEM_E_TITLE_FIX' )
110
- && current_user_can( 'install_plugins' )
111
  ) {
112
  ?>
113
  <h4>
@@ -283,7 +282,7 @@ switch ( $this->get_view_instance( 'title', $instance ) ) :
283
  <?php
284
  $info = HTML::make_info(
285
  __( 'Always brand your titles. Search engines may ignore your titles with this feature enabled.', 'autodescription' ),
286
- 'https://developers.google.com/search/docs/advanced/appearance/good-titles-snippets#page-titles',
287
  false
288
  );
289
 
104
  <hr>
105
  <?php
106
  if (
107
+ $this->_display_extension_suggestions()
108
  && ! current_theme_supports( 'title-tag' )
109
  && ! defined( 'TSFEM_E_TITLE_FIX' )
 
110
  ) {
111
  ?>
112
  <h4>
282
  <?php
283
  $info = HTML::make_info(
284
  __( 'Always brand your titles. Search engines may ignore your titles with this feature enabled.', 'autodescription' ),
285
+ 'https://developers.google.com/search/docs/advanced/appearance/title-link',
286
  false
287
  );
288
 
inc/views/settings/wrap.php CHANGED
@@ -12,18 +12,38 @@ use The_SEO_Framework\Interpreters\HTML,
12
 
13
  defined( 'THE_SEO_FRAMEWORK_PRESENT' ) and tsf()->_verify_include_secret( $_secret ) or die;
14
 
15
- $_ays_reset = esc_js( __( 'Are you sure you want to reset all SEO settings to their defaults?', 'autodescription' ) );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
- $_save_button = get_submit_button(
18
  __( 'Save Settings', 'autodescription' ),
19
- 'primary',
20
  'submit',
21
  false,
22
  [ 'id' => '' ] // we ouput this twice, don't set ID.
23
  );
 
 
24
  $_reset_button = get_submit_button(
25
  __( 'Reset Settings', 'autodescription' ),
26
- 'secondary',
27
  Input::get_field_name( 'tsf-settings-reset' ),
28
  false,
29
  [
@@ -41,12 +61,12 @@ $_reset_button = get_submit_button(
41
 
42
  <div class=tsf-top-wrap>
43
  <h1><?= esc_html( get_admin_page_title() ) ?></h1>
44
- <p class=tsf-top-buttons>
45
  <?php
46
  // phpcs:ignore, WordPress.Security.EscapeOutput.OutputNotEscaped -- submit_button() escapes (mostly...)
47
- echo $_save_button, $_reset_button;
48
  ?>
49
- </p>
50
  </div>
51
 
52
  <hr class=wp-header-end>
@@ -61,11 +81,19 @@ $_reset_button = get_submit_button(
61
  do_action( "{$this->seo_settings_page_hook}_settings_page_boxes", $this->seo_settings_page_hook );
62
  ?>
63
 
64
- <div class=tsf-bottom-buttons>
65
- <?php
66
- // phpcs:ignore, WordPress.Security.EscapeOutput.OutputNotEscaped -- submit_button() escapes (mostly...)
67
- echo $_save_button, $_reset_button;
68
- ?>
 
 
 
 
 
 
 
 
69
  </div>
70
  </form>
71
  </div>
12
 
13
  defined( 'THE_SEO_FRAMEWORK_PRESENT' ) and tsf()->_verify_include_secret( $_secret ) or die;
14
 
15
+ if ( function_exists( 'tsf_extension_manager' )
16
+ && in_array(
17
+ tsf_extension_manager()->seo_extensions_page_slug ?? null,
18
+ array_column( $GLOBALS['submenu'][ $this->seo_settings_page_slug ] ?? [], 2 ),
19
+ true
20
+ )
21
+ ) {
22
+ $_extensions_button = sprintf(
23
+ '<a href="%s" class="button">%s</a>',
24
+ menu_page_url( tsf_extension_manager()->seo_extensions_page_slug, false ),
25
+ esc_html_x( 'Extensions', 'Plugin extensions', 'autodescription' )
26
+ );
27
+ } else {
28
+ $_extensions_button = $this->_display_extension_suggestions() ? sprintf(
29
+ '<a href="%s" class="button" rel="noreferrer noopener" target=_blank>%s</a>',
30
+ 'https://theseoframework.com/?p=3599',
31
+ esc_html_x( 'Extensions', 'Plugin extensions', 'autodescription' )
32
+ ) : '';
33
+ }
34
 
35
+ $_save_button = get_submit_button(
36
  __( 'Save Settings', 'autodescription' ),
37
+ [ 'primary' ],
38
  'submit',
39
  false,
40
  [ 'id' => '' ] // we ouput this twice, don't set ID.
41
  );
42
+
43
+ $_ays_reset = esc_js( __( 'Are you sure you want to reset all SEO settings to their defaults?', 'autodescription' ) );
44
  $_reset_button = get_submit_button(
45
  __( 'Reset Settings', 'autodescription' ),
46
+ [ 'secondary' ],
47
  Input::get_field_name( 'tsf-settings-reset' ),
48
  false,
49
  [
61
 
62
  <div class=tsf-top-wrap>
63
  <h1><?= esc_html( get_admin_page_title() ) ?></h1>
64
+ <div class="tsf-top-buttons tsf-end">
65
  <?php
66
  // phpcs:ignore, WordPress.Security.EscapeOutput.OutputNotEscaped -- submit_button() escapes (mostly...)
67
+ echo $_save_button, $_reset_button, $_extensions_button;
68
  ?>
69
+ </div>
70
  </div>
71
 
72
  <hr class=wp-header-end>
81
  do_action( "{$this->seo_settings_page_hook}_settings_page_boxes", $this->seo_settings_page_hook );
82
  ?>
83
 
84
+ <div class=tsf-bottom-wrap>
85
+ <div class="tsf-bottom-buttons tsf-start">
86
+ <?php
87
+ // phpcs:ignore, WordPress.Security.EscapeOutput.OutputNotEscaped -- submit_button() escapes (mostly...)
88
+ echo $_extensions_button;
89
+ ?>
90
+ </div>
91
+ <div class="tsf-bottom-buttons tsf-end">
92
+ <?php
93
+ // phpcs:ignore, WordPress.Security.EscapeOutput.OutputNotEscaped -- submit_button() escapes (mostly...)
94
+ echo $_save_button;
95
+ ?>
96
+ </div>
97
  </div>
98
  </form>
99
  </div>
inc/views/sitemap/xml-sitemap.php CHANGED
@@ -16,8 +16,8 @@ $sitemap_bridge = The_SEO_Framework\Bridges\Sitemap::get_instance();
16
  $sitemap_bridge->output_sitemap_header();
17
 
18
  if ( $this->the_seo_framework_debug ) {
19
- echo '<!-- Site estimated peak usage prior to generation: ' . number_format( memory_get_peak_usage() / 1024 / 1024, 3 ) . ' MB -->' . "\n";
20
- echo '<!-- System estimated peak usage prior to generation: ' . number_format( memory_get_peak_usage( true ) / 1024 / 1024, 3 ) . ' MB -->' . "\n";
21
  }
22
 
23
  $sitemap_bridge->output_sitemap_urlset_open_tag();
@@ -34,15 +34,15 @@ if ( $sitemap_base->base_is_regenerated ) {
34
  echo "\n" . '<!-- ' . esc_html__( 'Sitemap is served from cache', 'autodescription' ) . ' -->';
35
  }
36
 
37
- // Destroy class.
38
  $sitemap_base = null;
39
 
40
  if ( $this->the_seo_framework_debug ) {
41
- echo "\n" . '<!-- Site estimated current usage: ' . number_format( memory_get_usage() / 1024 / 1024, 3 ) . ' MB -->';
42
- echo "\n" . '<!-- System estimated current usage: ' . number_format( memory_get_usage( true ) / 1024 / 1024, 3 ) . ' MB -->';
43
- echo "\n" . '<!-- Site estimated peak usage: ' . number_format( memory_get_peak_usage() / 1024 / 1024, 3 ) . ' MB -->';
44
- echo "\n" . '<!-- System estimated peak usage: ' . number_format( memory_get_peak_usage( true ) / 1024 / 1024, 3 ) . ' MB -->';
45
- echo "\n" . '<!-- Freed memory prior to generation: ' . number_format( $sitemap_bridge->get_freed_memory( true ) / 1024, 3 ) . ' kB -->';
46
  echo "\n" . '<!-- Sitemap generation time: ' . number_format( microtime( true ) - $timer_start, 6 ) . ' seconds -->';
47
  echo "\n" . '<!-- Sitemap caching enabled: ' . ( $this->get_option( 'cache_sitemap' ) ? 'yes' : 'no' ) . ' -->';
48
  echo "\n" . '<!-- Sitemap transient key: ' . esc_html( $this->get_sitemap_transient_name() ) . ' -->';
16
  $sitemap_bridge->output_sitemap_header();
17
 
18
  if ( $this->the_seo_framework_debug ) {
19
+ echo '<!-- Site estimated peak usage prior to generation: ' . number_format( memory_get_peak_usage() / MB_IN_BYTES, 3 ) . ' MB -->' . "\n";
20
+ echo '<!-- System estimated peak usage prior to generation: ' . number_format( memory_get_peak_usage( true ) / MB_IN_BYTES, 3 ) . ' MB -->' . "\n";
21
  }
22
 
23
  $sitemap_bridge->output_sitemap_urlset_open_tag();
34
  echo "\n" . '<!-- ' . esc_html__( 'Sitemap is served from cache', 'autodescription' ) . ' -->';
35
  }
36
 
37
+ // Destruct class.
38
  $sitemap_base = null;
39
 
40
  if ( $this->the_seo_framework_debug ) {
41
+ echo "\n" . '<!-- Site estimated current usage: ' . number_format( memory_get_usage() / MB_IN_BYTES, 3 ) . ' MB -->';
42
+ echo "\n" . '<!-- System estimated current usage: ' . number_format( memory_get_usage( true ) / MB_IN_BYTES, 3 ) . ' MB -->';
43
+ echo "\n" . '<!-- Site estimated peak usage: ' . number_format( memory_get_peak_usage() / MB_IN_BYTES, 3 ) . ' MB -->';
44
+ echo "\n" . '<!-- System estimated peak usage: ' . number_format( memory_get_peak_usage( true ) / MB_IN_BYTES, 3 ) . ' MB -->';
45
+ echo "\n" . '<!-- Freed memory prior to generation: ' . number_format( $sitemap_bridge->get_freed_memory( true ) / KB_IN_BYTES, 3 ) . ' kB -->';
46
  echo "\n" . '<!-- Sitemap generation time: ' . number_format( microtime( true ) - $timer_start, 6 ) . ' seconds -->';
47
  echo "\n" . '<!-- Sitemap caching enabled: ' . ( $this->get_option( 'cache_sitemap' ) ? 'yes' : 'no' ) . ' -->';
48
  echo "\n" . '<!-- Sitemap transient key: ' . esc_html( $this->get_sitemap_transient_name() ) . ' -->';
inc/views/sitemap/xsl-stylesheet.php CHANGED
@@ -10,7 +10,7 @@
10
  defined( 'THE_SEO_FRAMEWORK_PRESENT' ) and tsf()->_verify_include_secret( $_secret ) or die;
11
 
12
  // echo here, otherwise XML closes PHP...
13
- echo '<?xml version="1.0" encoding="UTF-8"?>', PHP_EOL;
14
 
15
  ?>
16
  <xsl:stylesheet version="2.0"
10
  defined( 'THE_SEO_FRAMEWORK_PRESENT' ) and tsf()->_verify_include_secret( $_secret ) or die;
11
 
12
  // echo here, otherwise XML closes PHP...
13
+ echo '<?xml version="1.0" encoding="UTF-8"?>', "\n";
14
 
15
  ?>
16
  <xsl:stylesheet version="2.0"
inc/views/sitemap/xsl/description.php CHANGED
@@ -13,12 +13,12 @@ $logo = '';
13
  if ( $this->get_option( 'sitemap_logo' ) ) {
14
 
15
  $id = $this->get_option( 'sitemap_logo_id' ) ?: 0;
16
- $_src = $id ? wp_get_attachment_image_src( $id, [ 29, 29 ] ) : [];
17
 
18
  // Fallback to theme mod.
19
  if ( ! $_src ) {
20
  $id = get_theme_mod( 'custom_logo' ) ?: 0;
21
- $_src = $id ? wp_get_attachment_image_src( $id, [ 29, 29 ] ) : [];
22
  }
23
 
24
  /**
13
  if ( $this->get_option( 'sitemap_logo' ) ) {
14
 
15
  $id = $this->get_option( 'sitemap_logo_id' ) ?: 0;
16
+ $_src = $id ? wp_get_attachment_image_src( $id, [ 29, 29 ] ) : []; // Magic number "SITEMAP_LOGO_PX"
17
 
18
  // Fallback to theme mod.
19
  if ( ! $_src ) {
20
  $id = get_theme_mod( 'custom_logo' ) ?: 0;
21
+ $_src = $id ? wp_get_attachment_image_src( $id, [ 29, 29 ] ) : []; // Magic number "SITEMAP_LOGO_PX"
22
  }
23
 
24
  /**
lib/css/settings.css CHANGED
@@ -5,42 +5,57 @@
5
  }
6
 
7
  .tsf-top-wrap {
8
- width: 100%;
9
- display: inline-block;
10
- vertical-align: top;
11
- }
12
-
13
- .tsf-top-wrap > h1 {
14
- float: left;
15
- }
16
-
17
- body.rtl .tsf-top-wrap > h1 {
18
- float: right;
19
  }
20
 
21
- .tsf-metaboxes .metabox-holder {
22
- clear: both;
 
 
 
 
 
 
23
  }
24
 
 
25
  .tsf-top-buttons {
26
- float: right;
27
  }
28
-
29
- body.rtl .tsf-top-buttons {
30
- float: left;
 
 
 
 
 
 
 
 
31
  }
32
 
33
- .tsf-bottom-buttons {
34
- text-align: right;
 
 
 
 
 
 
 
 
 
35
  }
36
 
37
- body.rtl .tsf-bottom-buttons {
38
- text-align: left;
 
39
  }
40
 
41
- .tsf-metaboxes .tsf-top-buttons input,
42
- .tsf-metaboxes .tsf-bottom-buttons input {
43
- margin-left: 10px;
44
  }
45
 
46
  #tsf-title-separator {
@@ -182,7 +197,6 @@ body.rtl .tsf-bottom-buttons {
182
  font-weight: 600;
183
  border-radius: 3px 3px 0 0;
184
  }
185
-
186
  body.rtl .tsf-nav-tab-label {
187
  float: right;
188
  margin-left: 0;
5
  }
6
 
7
  .tsf-top-wrap {
8
+ /* About half of notification height */
9
+ margin-bottom: 8px;
 
 
 
 
 
 
 
 
 
10
  }
11
 
12
+ .tsf-top-wrap,
13
+ .tsf-bottom-wrap {
14
+ width: 100%;
15
+ display: flex;
16
+ justify-content: space-between;
17
+ flex: 1 1 100%;
18
+ flex-flow: row wrap;
19
+ gap: 10px;
20
  }
21
 
22
+ /* Matches .wrap h1 */
23
  .tsf-top-buttons {
24
+ padding: 9px 0 4px;
25
  }
26
+ .tsf-top-buttons,
27
+ .tsf-bottom-buttons {
28
+ display: flex;
29
+ flex: 1 1 auto;
30
+ justify-content: start;
31
+ flex-flow: row wrap;
32
+ gap: 10px;
33
+ }
34
+ .tsf-top-buttons.tsf-end,
35
+ .tsf-bottom-buttons.tsf-end {
36
+ justify-content: flex-end;
37
  }
38
 
39
+ @media screen and (max-width: 782px) {
40
+ .tsf-top-wrap,
41
+ .tsf-bottom-wrap,
42
+ .tsf-top-buttons.tsf-end,
43
+ .tsf-bottom-buttons.tsf-end {
44
+ justify-content: start;
45
+ }
46
+ .tsf-top-buttons,
47
+ .tsf-bottom-buttons {
48
+ width: 100%;
49
+ }
50
  }
51
 
52
+ .tsf-metaboxes .tsf-top-buttons > *,
53
+ .tsf-metaboxes .tsf-bottom-buttons > * {
54
+ text-align: center;
55
  }
56
 
57
+ .tsf-metaboxes .metabox-holder {
58
+ clear: both;
 
59
  }
60
 
61
  #tsf-title-separator {
197
  font-weight: 600;
198
  border-radius: 3px 3px 0 0;
199
  }
 
200
  body.rtl .tsf-nav-tab-label {
201
  float: right;
202
  margin-left: 0;
lib/css/settings.min.css CHANGED
@@ -1 +1 @@
1
- .tsf-metaboxes{box-sizing:border-box;max-width:740px;padding-bottom:20px}.tsf-top-wrap{width:100%;display:inline-block;vertical-align:top}.tsf-top-wrap>h1{float:left}body.rtl .tsf-top-wrap>h1{float:right}.tsf-metaboxes .metabox-holder{clear:both}.tsf-top-buttons{float:right}body.rtl .tsf-top-buttons{float:left}.tsf-bottom-buttons{text-align:right}body.rtl .tsf-bottom-buttons{text-align:left}.tsf-metaboxes .tsf-bottom-buttons input,.tsf-metaboxes .tsf-top-buttons input{margin-left:10px}#tsf-title-separator{display:table;width:100%;border-collapse:collapse;border-spacing:0}#tsf-title-separator input{position:absolute;width:0;height:0;opacity:0;margin:0;padding:0;border:0;z-index:-1;-webkit-appearance:none;-moz-appearance:none;appearance:none}#tsf-title-separator label{display:inline-block;width:auto;min-width:28px;min-height:28px;margin:3px;-moz-margin-end:1.5px;-moz-margin-start:1.5px;padding:0 4px;border:1px solid #ccc;line-height:28px;text-align:center;cursor:pointer;box-shadow:-1px -1px 1px #aaa inset;font-size:16px}#tsf-title-separator label:hover{box-shadow:1px 1px 1px #aaa inset;background-color:#fff}#tsf-title-separator input:checked+label{box-shadow:1px 1px 1px #333 inset;background-color:#fff}#tsf-title-separator input:focus+label:not(.tsf-no-focus-ring){box-shadow:0 0 1px 1px #333 inset}#tsf-home-title-location,#tsf-title-location{display:block}#tsf-home-title-location label>span,#tsf-title-location label>span{display:inline-block;min-width:60px;vertical-align:baseline}.tsf-title-additions-example-left code,.tsf-title-additions-example-right code,.tsf-title-additions-js{white-space:pre-wrap}.tsf-title-additions-location-hidden,.tsf-title-tax-prefix-hidden{display:none}#tsf-post-type-archive-header-wrap{display:flex;flex-flow:row wrap;justify-content:space-between;align-items:center;vertical-align:top;gap:1.625em 4ch}#tsf-post-type-archive-select-wrap{display:flex;flex-flow:row wrap;gap:1em;align-items:center}#tsf-post-type-archive-select-wrap label{font-weight:600}#tsf-post-type-archive-header-wrap h4{margin:0}.tsf-post-type-archive-link{display:inline-block}.tsf-post-type-archive-if-excluded p:last-of-type{margin-bottom:0}.tsf-select-block>select:last-of-type{margin-bottom:14px}.tsf-nav-tab-no-js,.tsf-nav-tab-wrapper{position:relative;clear:both;width:100%;display:inline-block;border-bottom:1px solid #ccc;line-height:inherit;padding:8px 12px 0;margin:-4px -12px}.tsf-nav-tab-label{float:left;border:1px solid #ccc;margin-left:.5em;margin-bottom:-1px;padding:1ch 2ch;font-size:12px;background:#f1f1f1;color:#555;font-weight:600;border-radius:3px 3px 0 0}body.rtl .tsf-nav-tab-label{float:right;margin-left:0;margin-right:.5em}.tsf-nav-desktop{margin-left:7px}.tsf-dashicons-tabs{font-size:initial;display:inline;vertical-align:text-bottom}input.tsf-nav-tab-radio{position:absolute;width:0;height:0;opacity:0;margin:0;padding:0;border:0;z-index:-1;-webkit-appearance:none;-moz-appearance:none;appearance:none}.tsf-nav-tab-active,.tsf-nav-tab-radio:checked+label{background-color:inherit;border-bottom-color:#fff;color:#000}.tsf-nav-tab-radio:focus+label:not(.tsf-no-focus-ring){box-shadow:0 0 0 1px #5b9dd9,0 0 2px 1px rgba(30,140,190,.8);border-color:#fff}.tsf-nav-tab-content{margin:1.33em auto 0}.tsf-nav-tab-content select{width:100%;max-width:max-content;text-overflow:ellipsis}.tsf-nav-tab-content-no-js{margin:1.33em auto}body.js .tsf-nav-tab-content{display:none}body.js .tsf-nav-tab-content.tsf-nav-tab-content-active{display:block}
1
+ .tsf-metaboxes{box-sizing:border-box;max-width:740px;padding-bottom:20px}.tsf-top-wrap{margin-bottom:8px}.tsf-bottom-wrap,.tsf-top-wrap{width:100%;display:flex;justify-content:space-between;flex:1 1 100%;flex-flow:row wrap;gap:10px}.tsf-top-buttons{padding:9px 0 4px}.tsf-bottom-buttons,.tsf-top-buttons{display:flex;flex:1 1 auto;justify-content:start;flex-flow:row wrap;gap:10px}.tsf-bottom-buttons.tsf-end,.tsf-top-buttons.tsf-end{justify-content:flex-end}@media screen and (max-width:782px){.tsf-bottom-buttons.tsf-end,.tsf-bottom-wrap,.tsf-top-buttons.tsf-end,.tsf-top-wrap{justify-content:start}.tsf-bottom-buttons,.tsf-top-buttons{width:100%}}.tsf-metaboxes .tsf-bottom-buttons>*,.tsf-metaboxes .tsf-top-buttons>*{text-align:center}.tsf-metaboxes .metabox-holder{clear:both}#tsf-title-separator{display:table;width:100%;border-collapse:collapse;border-spacing:0}#tsf-title-separator input{position:absolute;width:0;height:0;opacity:0;margin:0;padding:0;border:0;z-index:-1;-webkit-appearance:none;-moz-appearance:none;appearance:none}#tsf-title-separator label{display:inline-block;width:auto;min-width:28px;min-height:28px;margin:3px;-moz-margin-end:1.5px;-moz-margin-start:1.5px;padding:0 4px;border:1px solid #ccc;line-height:28px;text-align:center;cursor:pointer;box-shadow:-1px -1px 1px #aaa inset;font-size:16px}#tsf-title-separator label:hover{box-shadow:1px 1px 1px #aaa inset;background-color:#fff}#tsf-title-separator input:checked+label{box-shadow:1px 1px 1px #333 inset;background-color:#fff}#tsf-title-separator input:focus+label:not(.tsf-no-focus-ring){box-shadow:0 0 1px 1px #333 inset}#tsf-home-title-location,#tsf-title-location{display:block}#tsf-home-title-location label>span,#tsf-title-location label>span{display:inline-block;min-width:60px;vertical-align:baseline}.tsf-title-additions-example-left code,.tsf-title-additions-example-right code,.tsf-title-additions-js{white-space:pre-wrap}.tsf-title-additions-location-hidden,.tsf-title-tax-prefix-hidden{display:none}#tsf-post-type-archive-header-wrap{display:flex;flex-flow:row wrap;justify-content:space-between;align-items:center;vertical-align:top;gap:1.625em 4ch}#tsf-post-type-archive-select-wrap{display:flex;flex-flow:row wrap;gap:1em;align-items:center}#tsf-post-type-archive-select-wrap label{font-weight:600}#tsf-post-type-archive-header-wrap h4{margin:0}.tsf-post-type-archive-link{display:inline-block}.tsf-post-type-archive-if-excluded p:last-of-type{margin-bottom:0}.tsf-select-block>select:last-of-type{margin-bottom:14px}.tsf-nav-tab-no-js,.tsf-nav-tab-wrapper{position:relative;clear:both;width:100%;display:inline-block;border-bottom:1px solid #ccc;line-height:inherit;padding:8px 12px 0;margin:-4px -12px}.tsf-nav-tab-label{float:left;border:1px solid #ccc;margin-left:.5em;margin-bottom:-1px;padding:1ch 2ch;font-size:12px;background:#f1f1f1;color:#555;font-weight:600;border-radius:3px 3px 0 0}body.rtl .tsf-nav-tab-label{float:right;margin-left:0;margin-right:.5em}.tsf-nav-desktop{margin-left:7px}.tsf-dashicons-tabs{font-size:initial;display:inline;vertical-align:text-bottom}input.tsf-nav-tab-radio{position:absolute;width:0;height:0;opacity:0;margin:0;padding:0;border:0;z-index:-1;-webkit-appearance:none;-moz-appearance:none;appearance:none}.tsf-nav-tab-active,.tsf-nav-tab-radio:checked+label{background-color:inherit;border-bottom-color:#fff;color:#000}.tsf-nav-tab-radio:focus+label:not(.tsf-no-focus-ring){box-shadow:0 0 0 1px #5b9dd9,0 0 2px 1px rgba(30,140,190,.8);border-color:#fff}.tsf-nav-tab-content{margin:1.33em auto 0}.tsf-nav-tab-content select{width:100%;max-width:max-content;text-overflow:ellipsis}.tsf-nav-tab-content-no-js{margin:1.33em auto}body.js .tsf-nav-tab-content{display:none}body.js .tsf-nav-tab-content.tsf-nav-tab-content-active{display:block}
readme.txt CHANGED
@@ -1,11 +1,11 @@
1
- === The SEO Framework – Automated, Effortless, Fast. ===
2
  Contributors: Cybr
3
  Donate link: https://github.com/sponsors/sybrew
4
  Tags: seo, xml sitemap, google search, open graph, schema.org, twitter card, performance, headless
5
  Requires at least: 5.5.0
6
- Tested up to: 5.9
7
  Requires PHP: 7.2.0
8
- Stable tag: 4.2.3
9
  License: GPLv3
10
  License URI: http://www.gnu.org/licenses/gpl-3.0.html
11
 
@@ -247,6 +247,10 @@ If you wish to display breadcrumbs, then your theme should provide this. Alterna
247
 
248
  == Changelog ==
249
 
 
 
 
 
250
  = 4.2.3 =
251
 
252
  This minor update addresses a regression where the singular-archive canonical URLs always [pointed to the first page](https://theseoframework.com/?p=3858).
1
+ === The SEO Framework – Fast, Automated, Effortless. ===
2
  Contributors: Cybr
3
  Donate link: https://github.com/sponsors/sybrew
4
  Tags: seo, xml sitemap, google search, open graph, schema.org, twitter card, performance, headless
5
  Requires at least: 5.5.0
6
+ Tested up to: 6.0
7
  Requires PHP: 7.2.0
8
+ Stable tag: 4.2.4
9
  License: GPLv3
10
  License URI: http://www.gnu.org/licenses/gpl-3.0.html
11
 
247
 
248
  == Changelog ==
249
 
250
+ = 4.2.4 =
251
+
252
+ This minor update improves image processing, reducing TSF's load impact by roughly 20% when generating metadata. We also added WordPress 6.0 support for image filesizes, making social sharing [even more robust](https://theseoframework.com/?p=3903).
253
+
254
  = 4.2.3 =
255
 
256
  This minor update addresses a regression where the singular-archive canonical URLs always [pointed to the first page](https://theseoframework.com/?p=3858).