| 1 |
<?php |
| 2 |
|
| 3 |
class Meow_MWSEO_Core |
| 4 |
{ |
| 5 |
public $admin = null; |
| 6 |
public $is_rest = false; |
| 7 |
public $site_url = null; |
| 8 |
public $pro = null; // Premium features |
| 9 |
|
| 10 |
// TODO: Migrate to _mwseo_title |
| 11 |
public $meta_key_seo_title = '_kiss_seo_title'; |
| 12 |
// TODO: Migrate to _mwseo_excerpt |
| 13 |
public $meta_key_seo_excerpt = '_kiss_seo_excerpt'; |
| 14 |
// TODO: Migrate to _mwseo_ignore |
| 15 |
public $meta_key_skip = '_kiss_seo_ignore'; |
| 16 |
public $meta_key_seo_keywords = '_mwseo_keywords'; |
| 17 |
|
| 18 |
private $old_option_name = 'seo_kiss_options'; |
| 19 |
private $option_name = 'mwseo_options'; |
| 20 |
|
| 21 |
private $sitemap_module = null; |
| 22 |
private $schema_module = null; |
| 23 |
private $insights_module = null; |
| 24 |
private $analytics_module = null; |
| 25 |
private $googleanalytics_module = null; |
| 26 |
private $plausibleanalytics_module = null; |
| 27 |
private $matomoanalytics_module = null; |
| 28 |
private $parsers = null; |
| 29 |
public $redirects_module = null; |
| 30 |
public $indexnow_module = null; |
| 31 |
|
| 32 |
public function __construct() { |
| 33 |
global $mwseo_core; |
| 34 |
$mwseo_core = $this; |
| 35 |
|
| 36 |
$this->site_url = get_site_url(); |
| 37 |
$this->is_rest = MeowKit_MWSEO_Helpers::is_rest(); |
| 38 |
|
| 39 |
add_action( 'plugins_loaded', array( $this, 'init' ) ); |
| 40 |
|
| 41 |
$analyze_on_update = $this->get_option( 'analyze_on_update', 'quick' ); |
| 42 |
if ( $analyze_on_update !== 'none' && $analyze_on_update !== false ) { |
| 43 |
add_action( 'save_post', array( $this, 'analyze_on_update' ), 10, 3 ); |
| 44 |
} |
| 45 |
// Always registered so events scheduled before a settings change still run. |
| 46 |
add_action( 'mwseo_analyze_post_event', array( $this, 'run_scheduled_analysis' ), 10, 2 ); |
| 47 |
} |
| 48 |
|
| 49 |
function init() { |
| 50 |
global $mwseo; |
| 51 |
|
| 52 |
$mwseo = new Meow_MWSEO_API( $this ); |
| 53 |
|
| 54 |
// Part of the core, settings and stuff |
| 55 |
$this->admin = new Meow_MWSEO_Admin( $this ); |
| 56 |
|
| 57 |
// Only for REST |
| 58 |
if ( $this->is_rest ) { |
| 59 |
new Meow_MWSEO_Rest( $this, $this->admin ); |
| 60 |
} |
| 61 |
|
| 62 |
// Attachment URL redirection feature |
| 63 |
$redirect_attachments = $this->get_option( 'redirect_attachments', false ); |
| 64 |
if ( $redirect_attachments ) { |
| 65 |
add_filter( 'attachment_link', array( $this, 'redirect_attachment_to_parent' ), 10, 2 ); |
| 66 |
add_action( 'template_redirect', array( $this, 'handle_attachment_redirect' ) ); |
| 67 |
} |
| 68 |
|
| 69 |
// Dashboard |
| 70 |
$use_mwseo_sitemap = $this->get_option( 'sitemap', false ); |
| 71 |
if ( $use_mwseo_sitemap ) { |
| 72 |
add_filter( |
| 73 |
'init', |
| 74 |
function() { |
| 75 |
$this->sitemap_module = new Meow_MWSEO_Sitemap( $this ); |
| 76 |
wp_register_sitemap_provider( MWSEO_DOMAIN, $this->sitemap_module ); |
| 77 |
} |
| 78 |
); |
| 79 |
|
| 80 |
} |
| 81 |
|
| 82 |
// Website |
| 83 |
//add_filter( 'wp_title', [ $this, 'render_title' ], 10, 2 ); |
| 84 |
$use_content_seo = $this->get_option( 'content_seo', false ); |
| 85 |
$use_technical_seo = $this->get_option( 'technical_seo', false ); |
| 86 |
$use_seo_metadata = $this->get_option( 'use_seo_metadata', true ); |
| 87 |
$auto_page_title = $this->get_option( 'auto_page_title', true ); |
| 88 |
// When another SEO plugin (Yoast, Rank Math, AIOSEO, SEOPress) is handling the |
| 89 |
// frontend meta tags, SEO Engine steps back so the two don't emit duplicates. |
| 90 |
// It keeps doing everything else (analysis, scoring, AI, MCP) regardless. |
| 91 |
$render_frontend_meta = $this->should_render_frontend_meta(); |
| 92 |
if ( ( $use_content_seo || $use_technical_seo ) && $auto_page_title && $render_frontend_meta ) { |
| 93 |
add_filter( 'pre_get_document_title', [ $this, 'render_title' ], 99, 1 ); |
| 94 |
// Remove WordPress archive prefixes ("Archives: ", "Category: ", etc.) for cleaner SEO titles |
| 95 |
add_filter( 'get_the_archive_title_prefix', '__return_empty_string' ); |
| 96 |
} |
| 97 |
if ( $use_content_seo && $use_seo_metadata && $render_frontend_meta ) { |
| 98 |
add_action( 'wp_head', [ $this, 'render_description' ], 99, 0 ); |
| 99 |
add_action( 'wp_head', [ $this, 'render_canonical' ], 99, 0 ); |
| 100 |
} |
| 101 |
|
| 102 |
add_action( 'init', [ $this, 'reject_gptbot_user_agent' ], 99, 0 ); |
| 103 |
|
| 104 |
// Live content: when enabled, the analyzed content is the actual rendered page |
| 105 |
// (handles all page builders) instead of the raw post_content. |
| 106 |
if ( $this->get_live_content_mode() !== 'disabled' ) { |
| 107 |
add_filter( 'mwseo_post_content', [ $this, 'filter_live_content' ], 10, 3 ); |
| 108 |
} else { |
| 109 |
// If not using Live Content, rely on parsers instead |
| 110 |
$this->parsers = new Meow_MWSEO_Helpers_Parsers( $this ); |
| 111 |
} |
| 112 |
|
| 113 |
// Advanced Core |
| 114 |
if ( class_exists( 'MeowPro_MWSEO_Core' ) ) { |
| 115 |
$this->pro = new MeowPro_MWSEO_Core( $this ); |
| 116 |
} |
| 117 |
|
| 118 |
// Insights |
| 119 |
if ( class_exists( 'Meow_MWSEO_Modules_Insights' ) ) { |
| 120 |
$this->insights_module = new Meow_MWSEO_Modules_Insights( $this ); |
| 121 |
} |
| 122 |
|
| 123 |
// Schema |
| 124 |
if ( class_exists( 'Meow_MWSEO_Modules_Schema' ) ) { |
| 125 |
$this->schema_module = new Meow_MWSEO_Modules_Schema( $this ); |
| 126 |
} |
| 127 |
|
| 128 |
// Analytics |
| 129 |
if ( class_exists( 'Meow_MWSEO_Modules_Analytics' ) ) { |
| 130 |
$this->analytics_module = new Meow_MWSEO_Modules_Analytics( $this ); |
| 131 |
|
| 132 |
// Track visits on frontend |
| 133 |
if ( !is_admin() && !$this->is_rest ) { |
| 134 |
add_action( 'wp', array( $this, 'track_current_visit' ) ); |
| 135 |
} |
| 136 |
} |
| 137 |
|
| 138 |
// Google Analytics |
| 139 |
if ( class_exists( 'Meow_MWSEO_Modules_GoogleAnalytics' ) ) { |
| 140 |
$this->googleanalytics_module = new Meow_MWSEO_Modules_GoogleAnalytics( $this ); |
| 141 |
} |
| 142 |
|
| 143 |
// Plausible Analytics |
| 144 |
if ( class_exists( 'Meow_MWSEO_Modules_PlausibleAnalytics' ) ) { |
| 145 |
$this->plausibleanalytics_module = new Meow_MWSEO_Modules_PlausibleAnalytics( $this ); |
| 146 |
} |
| 147 |
|
| 148 |
// Matomo Analytics |
| 149 |
if ( class_exists( 'Meow_MWSEO_Modules_MatomoAnalytics' ) ) { |
| 150 |
$this->matomoanalytics_module = new Meow_MWSEO_Modules_MatomoAnalytics( $this ); |
| 151 |
} |
| 152 |
|
| 153 |
// Redirects + 404 monitor |
| 154 |
if ( class_exists( 'Meow_MWSEO_Modules_Redirects' ) ) { |
| 155 |
$this->redirects_module = new Meow_MWSEO_Modules_Redirects( $this ); |
| 156 |
} |
| 157 |
|
| 158 |
// IndexNow (instant indexing pings on publish/update) |
| 159 |
if ( class_exists( 'Meow_MWSEO_Modules_IndexNow' ) ) { |
| 160 |
$this->indexnow_module = new Meow_MWSEO_Modules_IndexNow( $this ); |
| 161 |
} |
| 162 |
|
| 163 |
// MCP integration - check both class and global variable |
| 164 |
if ( class_exists( 'Meow_MWAI_Core' ) || isset( $GLOBALS['mwai'] ) ) { |
| 165 |
new Meow_MWSEO_MCP( $this ); |
| 166 |
} |
| 167 |
|
| 168 |
// AI Engine integration: fills the SEO block in AI Engine's Modules tab. |
| 169 |
// The filter is harmless when AI Engine is absent, so no gating needed. |
| 170 |
new Meow_MWSEO_Modules_AIEngine( $this ); |
| 171 |
|
| 172 |
|
| 173 |
} |
| 174 |
|
| 175 |
function get_logs() { |
| 176 |
$log_file_path = $this->get_logs_path(); |
| 177 |
|
| 178 |
if ( !file_exists( $log_file_path ) ) { |
| 179 |
return "Empty log file."; |
| 180 |
} |
| 181 |
|
| 182 |
$content = file_get_contents( $log_file_path ); |
| 183 |
// $lines = explode( "\n", $content ); |
| 184 |
// $lines = array_filter( $lines ); |
| 185 |
// $lines = array_reverse( $lines ); |
| 186 |
// $content = implode( "\n", $lines ); |
| 187 |
return $content; |
| 188 |
} |
| 189 |
|
| 190 |
function clear_logs() { |
| 191 |
$logPath = $this->get_logs_path(); |
| 192 |
if ( file_exists( $logPath ) ) { |
| 193 |
unlink( $logPath ); |
| 194 |
} |
| 195 |
|
| 196 |
$options = $this->get_all_options(); |
| 197 |
$options['logs_path'] = null; |
| 198 |
$this->update_options( $options ); |
| 199 |
} |
| 200 |
|
| 201 |
function get_logs_path() { |
| 202 |
$uploads_dir = wp_upload_dir(); |
| 203 |
$uploads_dir_path = trailingslashit( $uploads_dir['basedir'] ); |
| 204 |
|
| 205 |
$path = $this->get_option( 'logs_path' ); |
| 206 |
|
| 207 |
if ( $path && file_exists( $path ) ) { |
| 208 |
// make sure the path is legal (within the uploads directory with the MWSEO_PREFIX and log extension) |
| 209 |
if ( strpos( $path, $uploads_dir_path ) !== 0 || strpos( $path, MWSEO_PREFIX ) === false || substr( $path, -4 ) !== '.log' ) { |
| 210 |
$path = null; |
| 211 |
} else { |
| 212 |
return $path; |
| 213 |
} |
| 214 |
} |
| 215 |
|
| 216 |
if ( !$path ) { |
| 217 |
$path = $uploads_dir_path . MWSEO_PREFIX . "_" . $this->random_ascii_chars() . ".log"; |
| 218 |
if ( !file_exists( $path ) ) { |
| 219 |
touch( $path ); |
| 220 |
} |
| 221 |
$options = $this->get_all_options(); |
| 222 |
$options['logs_path'] = $path; |
| 223 |
$this->update_options( $options ); |
| 224 |
} |
| 225 |
|
| 226 |
return $path; |
| 227 |
} |
| 228 |
|
| 229 |
private function random_ascii_chars( $length = 10 ) { |
| 230 |
$chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; |
| 231 |
$chars_length = strlen( $chars ); |
| 232 |
$random_string = ''; |
| 233 |
for ( $i = 0; $i < $length; $i++ ) { |
| 234 |
$random_string .= $chars[ rand( 0, $chars_length - 1 ) ]; |
| 235 |
} |
| 236 |
return $random_string; |
| 237 |
} |
| 238 |
|
| 239 |
function log( $data = null ) { |
| 240 |
$enabled = $this->get_option( 'logs', false ); |
| 241 |
if ( !$enabled ) { return; } |
| 242 |
|
| 243 |
$log_file_path = $this->get_logs_path(); |
| 244 |
|
| 245 |
// Check log file size and rotate if too large (1MB limit) |
| 246 |
if ( file_exists( $log_file_path ) ) { |
| 247 |
$file_size = filesize( $log_file_path ); |
| 248 |
$max_size = apply_filters( 'mwseo_max_log_size', 1 * 1024 * 1024 ); // 1MB default |
| 249 |
|
| 250 |
if ( $file_size > $max_size ) { |
| 251 |
@unlink( $log_file_path ); |
| 252 |
// Log the rotation event |
| 253 |
$fh = @fopen( $log_file_path, 'w' ); |
| 254 |
if ( $fh ) { |
| 255 |
$date = date( "Y-m-d H:i:s" ); |
| 256 |
fwrite( $fh, "$date: 🔄 Log file rotated (exceeded " . size_format( $max_size ) . ")\n\n" ); |
| 257 |
fclose( $fh ); |
| 258 |
} |
| 259 |
} |
| 260 |
} |
| 261 |
|
| 262 |
// Make sure the log file exists before opening it to avoid warnings |
| 263 |
if ( !file_exists( $log_file_path ) ) { |
| 264 |
@touch( $log_file_path ); |
| 265 |
} |
| 266 |
|
| 267 |
$fh = @fopen( $log_file_path, 'a' ); |
| 268 |
if ( !$fh ) { return false; } |
| 269 |
$date = date( "Y-m-d H:i:s" ); |
| 270 |
if ( is_null( $data ) ) { |
| 271 |
fwrite( $fh, "\n" ); |
| 272 |
} |
| 273 |
else { |
| 274 |
fwrite( $fh, "$date: {$data}\n" ); |
| 275 |
//error_log( "[SEO ENGINE] $data" ); |
| 276 |
} |
| 277 |
fclose( $fh ); |
| 278 |
return true; |
| 279 |
} |
| 280 |
|
| 281 |
function get_funny_rejection_message() { |
| 282 |
$messages = [ |
| 283 |
"🤖 Human detected! Pretending to be GPTBot? That’s a fun role-reversal!", |
| 284 |
"😂 Nice try, human! Even robots like GPTBot get a day off, courtesy of <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a>.", |
| 285 |
"🎭 Humans playing robots? What a twist! GPTBot access: Denied by <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a>!", |
| 286 |
"👀 Spotted! A human camouflaging as GPTBot. <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a> sees you!", |
| 287 |
"😜 A human mimicking GPTBot? That's some advanced <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a> wizardry!", |
| 288 |
"🤣 Oh, human, GPTBot impressions? You’re fun, but <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a> says no entry!", |
| 289 |
"🤠 Yeehaw! A wild human pretending to be GPTBot? Not on <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a>'s watch!", |
| 290 |
"🕵️♂️ Detective human, trying to sneak in as GPTBot? <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a> caught ya!", |
| 291 |
"🤪 Human, are you trying to robot? Denied access by the watchful <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a>!", |
| 292 |
"🚫 You must be a fun human to mimic GPTBot! But, <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a> keeps the door locked.", |
| 293 |
"� |
| 294 |
Humans doing robot things? Not under <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a>’s cyber-watch!", |
| 295 |
"🤨 A human in robot’s clothing! Nice try, but <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a> is not fooled.", |
| 296 |
"🤔 Hmm, human, your GPTBot disguise needs work. Spotted by <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a>!", |
| 297 |
"😆 The human is a robot? This plot twist brought to you by <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a>!", |
| 298 |
"🧐 An insightful human trying to GPTBot around! But, <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a> is on guard.", |
| 299 |
"🎉 Surprise! Human, you can’t sneak past <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a>, even dressed as GPTBot!", |
| 300 |
"🎲 Rolled the dice, human? GPTBot disguise didn’t work on <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a>.", |
| 301 |
"🤥 Human, pretending to be GPTBot? <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a> knows your secret!", |
| 302 |
"🧛♂️ Ah ah ah, human! Even as GPTBot, you can’t bypass the <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a> barrier!", |
| 303 |
"🤓 Nerdy human alert! GPTBot cosplay isn’t fooling <a href='https://wordpress.org/plugins/seo-engine/'>SEO Engine</a>!" |
| 304 |
]; |
| 305 |
return $messages[ array_rand( $messages ) ]; |
| 306 |
} |
| 307 |
|
| 308 |
|
| 309 |
function reject_gptbot_user_agent() |
| 310 |
{ |
| 311 |
$disallow_gpt_bot = $this->get_option( 'disallow_gpt_bot', false ); |
| 312 |
|
| 313 |
if ( $disallow_gpt_bot && isset($_SERVER[ 'HTTP_USER_AGENT' ]) && strpos( $_SERVER[ 'HTTP_USER_AGENT' ], 'GPTBot' ) !== false ) { |
| 314 |
$this->log( '🤖 GPTBot has been detected and blocked.'); |
| 315 |
header( 'HTTP/1.1 403 Forbidden' ); |
| 316 |
exit( $this->get_funny_rejection_message() ); |
| 317 |
} |
| 318 |
} |
| 319 |
|
| 320 |
function get_post_types() { |
| 321 |
// global $wpdb; |
| 322 |
// $post_types = $wpdb->get_col( "SELECT DISTINCT post_type FROM $wpdb->posts" ); |
| 323 |
$post_types = get_post_types( array( 'public' => true ), 'names', 'and' ); |
| 324 |
|
| 325 |
// Exclude post types that don't need SEO |
| 326 |
$excluded_types = array( 'revision', 'wp_global_styles', 'wp_navigation' ); |
| 327 |
$post_types = array_diff( $post_types, $excluded_types ); |
| 328 |
|
| 329 |
return array_values( $post_types ); |
| 330 |
} |
| 331 |
|
| 332 |
function get_categories() { |
| 333 |
|
| 334 |
$taxonomy = apply_filters( 'mwseo_category_taxonomies', array( 'category', 'product_cat' ) ); |
| 335 |
|
| 336 |
$categories = get_categories( array( |
| 337 |
'taxonomy' => $taxonomy, |
| 338 |
'hide_empty' => false, |
| 339 |
) ); |
| 340 |
|
| 341 |
return $categories; |
| 342 |
} |
| 343 |
|
| 344 |
// Make post type list as [ 'post_type_value' => 'post_type_label' ] |
| 345 |
function make_post_type_list( $post_types ) { |
| 346 |
$list = []; |
| 347 |
foreach ( $post_types as $post_type ) { |
| 348 |
$posttype_obj = get_post_type_object( $post_type ); |
| 349 |
if ( empty( $posttype_obj ) ) {continue;} |
| 350 |
$label = get_post_type_object( $post_type )->label; |
| 351 |
if ( !$label ) {continue;} |
| 352 |
|
| 353 |
$list[$post_type] = $label; |
| 354 |
} |
| 355 |
return $list; |
| 356 |
} |
| 357 |
|
| 358 |
function make_category_list( $categories ) { |
| 359 |
$list = []; |
| 360 |
foreach ( $categories as $category ) { |
| 361 |
$list[$category->term_id] = $category; |
| 362 |
} |
| 363 |
return array_values( $list ); |
| 364 |
} |
| 365 |
|
| 366 |
function get_taxonomies() { |
| 367 |
$taxonomies = get_taxonomies( array( 'public' => true ), 'objects' ); |
| 368 |
$taxonomies = array_filter( $taxonomies, function( $taxonomy ) { |
| 369 |
return !in_array( $taxonomy->name, [ 'nav_menu', 'link_category', 'post_format' ] ); |
| 370 |
} ); |
| 371 |
return $taxonomies; |
| 372 |
} |
| 373 |
|
| 374 |
function make_taxonomy_list( $taxonomies ) { |
| 375 |
$list = []; |
| 376 |
foreach ( $taxonomies as $taxonomy ) { |
| 377 |
$list[$taxonomy->name] = $taxonomy->label; |
| 378 |
} |
| 379 |
return $list; |
| 380 |
} |
| 381 |
|
| 382 |
function check_title_duplicates($title, $id) { |
| 383 |
$duplicate_hashes = get_option( 'mwseo_title_hashes' ); |
| 384 |
if ( empty( $duplicate_hashes ) ) { |
| 385 |
$duplicate_hashes = array(); |
| 386 |
} |
| 387 |
|
| 388 |
$title_hash = md5( $title ); |
| 389 |
|
| 390 |
if ( array_key_exists( $title_hash, $duplicate_hashes ) ) { |
| 391 |
if( $duplicate_hashes[ $title_hash ] == $id ) { |
| 392 |
return false; |
| 393 |
} |
| 394 |
else { |
| 395 |
//verify that the post still exists |
| 396 |
$post = get_post( $duplicate_hashes[ $title_hash ] ); |
| 397 |
if( empty( $post ) || $post->post_status == 'trash') { |
| 398 |
unset( $duplicate_hashes[ $title_hash ] ); |
| 399 |
update_option( 'mwseo_title_hashes', $duplicate_hashes ); |
| 400 |
return false; |
| 401 |
} |
| 402 |
|
| 403 |
return true; |
| 404 |
} |
| 405 |
} |
| 406 |
else { |
| 407 |
$duplicate_hashes[ $title_hash ] = $id; |
| 408 |
update_option( 'mwseo_title_hashes', $duplicate_hashes ); |
| 409 |
return false; |
| 410 |
} |
| 411 |
} |
| 412 |
|
| 413 |
// Keeps scores and issue counts fresh after edits: instead of analyzing inline (which |
| 414 |
// would slow every save, badly so in full mode with AI checks), schedule a one-off cron |
| 415 |
// event a few seconds later. Only re-analyzes posts that already have an analysis, so a |
| 416 |
// brand new draft is never surprise-scanned. |
| 417 |
function analyze_on_update( $post_id, $post, $update ) { |
| 418 |
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) return; |
| 419 |
if ( !$post || $post->post_status !== 'publish' ) return; |
| 420 |
$public_types = get_post_types( array( 'public' => true ) ); |
| 421 |
if ( !in_array( $post->post_type, $public_types, true ) ) return; |
| 422 |
if ( empty( get_post_meta( $post_id, '_mwseo_analysis', true ) ) ) return; |
| 423 |
|
| 424 |
$analyze_on_update = $this->get_option( 'analyze_on_update', 'quick' ); |
| 425 |
$analysis_type = ( $analyze_on_update === 'full' ) ? 'full' : 'quick'; |
| 426 |
if ( !wp_next_scheduled( 'mwseo_analyze_post_event', array( $post_id, $analysis_type ) ) ) { |
| 427 |
wp_schedule_single_event( time() + 10, 'mwseo_analyze_post_event', array( $post_id, $analysis_type ) ); |
| 428 |
} |
| 429 |
} |
| 430 |
|
| 431 |
function run_scheduled_analysis( $post_id, $analysis_type = 'quick' ) { |
| 432 |
$post = get_post( $post_id ); |
| 433 |
if ( !$post || $post->post_status !== 'publish' ) return; |
| 434 |
$this->calculate_seo_score( $post, $analysis_type === 'full' ? 'full' : 'quick' ); |
| 435 |
} |
| 436 |
|
| 437 |
|
| 438 |
// Decides whether or not the post is SEO-friendly or not. |
| 439 |
function calculate_seo_score( $post, $analysis_type = 'full' ) { |
| 440 |
global $mwseo_score; |
| 441 |
if ( !$mwseo_score ) { |
| 442 |
return [ 'status' => 'error', 'message' => 'Score module not initialized.' ]; |
| 443 |
} |
| 444 |
|
| 445 |
if ( get_post_meta( $post->ID, '_mwseo_status', true ) === 'skip' ) { |
| 446 |
$this->log( "🚫 Post (#{$post->ID}) was skipped." ); |
| 447 |
return [ 'status' => 'skip', 'message' => 'Skipped.' ]; |
| 448 |
} |
| 449 |
|
| 450 |
// Clear magic fix markers before re-analysis |
| 451 |
$this->clear_magic_fix_markers( $post->ID ); |
| 452 |
|
| 453 |
// Calculate SEO score with analysis type ('quick' or 'full') |
| 454 |
$result = $mwseo_score->calculate( $post, $analysis_type ); |
| 455 |
|
| 456 |
// Save results |
| 457 |
update_post_meta( $post->ID, '_mwseo_analysis', $result ); |
| 458 |
|
| 459 |
// Also save denormalized meta for sorting/filtering |
| 460 |
update_post_meta( $post->ID, '_mwseo_overall', $result['overall'] ); |
| 461 |
// Note: cq and tech are legacy pillar scores, removed in v3 |
| 462 |
update_post_meta( $post->ID, '_mwseo_updated', time() ); |
| 463 |
|
| 464 |
// Maintain backward compatibility with old meta keys |
| 465 |
$status = $this->get_status_from_score( $result ); |
| 466 |
$message = $this->get_message_from_score( $result ); |
| 467 |
$codes = $this->get_codes_from_score( $result ); |
| 468 |
|
| 469 |
update_post_meta( $post->ID, '_mwseo_score', $result['overall'] ); |
| 470 |
update_post_meta( $post->ID, '_mwseo_status', $status ); |
| 471 |
update_post_meta( $post->ID, '_mwseo_message', $message ); |
| 472 |
update_post_meta( $post->ID, '_mwseo_codes', $codes ); |
| 473 |
|
| 474 |
return [ |
| 475 |
'score' => $result['overall'], |
| 476 |
'status' => $status, |
| 477 |
'message' => $message, |
| 478 |
'errors' => [ 'codes' => $codes ], |
| 479 |
'data' => $result |
| 480 |
]; |
| 481 |
} |
| 482 |
|
| 483 |
// Convert score results to legacy status |
| 484 |
private function get_status_from_score( $result ) { |
| 485 |
$overall = $result['overall']; |
| 486 |
|
| 487 |
if ( $overall >= 70 ) return 'ok'; |
| 488 |
if ( $overall >= 40 ) return 'error'; // Could improve |
| 489 |
return 'error'; // Needs attention |
| 490 |
} |
| 491 |
|
| 492 |
// Convert score results to legacy message |
| 493 |
private function get_message_from_score( $result ) { |
| 494 |
$overall = $result['overall']; |
| 495 |
|
| 496 |
if ( $overall >= 70 ) { |
| 497 |
$emoji = '🟢'; |
| 498 |
$label = __( 'Great', 'seo-engine' ); |
| 499 |
} elseif ( $overall >= 40 ) { |
| 500 |
$emoji = '🟠'; |
| 501 |
$label = __( 'Could improve', 'seo-engine' ); |
| 502 |
} else { |
| 503 |
$emoji = '🔴'; |
| 504 |
$label = __( 'Needs attention', 'seo-engine' ); |
| 505 |
} |
| 506 |
|
| 507 |
$message = sprintf( '%s %s (%d%%)', $emoji, $label, $overall ); |
| 508 |
|
| 509 |
if ( !empty( $result['ai']['summary'] ) ) { |
| 510 |
$message .= "\n" . sprintf( __( 'How AI reads this: "%s"', 'seo-engine' ), $result['ai']['summary'] ); |
| 511 |
} |
| 512 |
|
| 513 |
return $message; |
| 514 |
} |
| 515 |
|
| 516 |
// Extract error codes from score tests |
| 517 |
private function get_codes_from_score( $result ) { |
| 518 |
$codes = []; |
| 519 |
|
| 520 |
foreach ( $result['tests'] as $test_name => $score ) { |
| 521 |
if ( $score === 'NA' ) continue; |
| 522 |
if ( $score < 70 ) { |
| 523 |
$codes[] = $test_name; |
| 524 |
} |
| 525 |
} |
| 526 |
|
| 527 |
return $codes; |
| 528 |
} |
| 529 |
|
| 530 |
function get_all_posts_with_seo_score() { |
| 531 |
|
| 532 |
// $show_unscored_posts = $this->get_option( 'show_unscored_chart', false ); |
| 533 |
// $show_any_post_type = $this->get_option( 'show_any_post_type', false ); |
| 534 |
|
| 535 |
$show_any_post_type = false; |
| 536 |
$show_unscored_posts = true; |
| 537 |
|
| 538 |
$meta_query = $show_unscored_posts ? [] : [ 'key' => '_mwseo_score', 'compare' => 'EXISTS' ]; |
| 539 |
$default_post_type = $this->get_option( 'default_post_type', 'post' ); |
| 540 |
$post_type = $show_any_post_type ? 'any' : $default_post_type; |
| 541 |
|
| 542 |
// When 'any' is selected, use only the enabled post types from settings |
| 543 |
if ( $post_type === 'any' ) { |
| 544 |
$post_type = $this->get_option( 'select_post_types', ['post', 'page'] ); |
| 545 |
} |
| 546 |
|
| 547 |
// PERFORMANCE FIX: Allow limiting posts for very large sites |
| 548 |
// Default to 10000 max to prevent memory issues |
| 549 |
$max_posts = apply_filters( 'seo_engine_max_chart_posts', 10000 ); |
| 550 |
|
| 551 |
$args = array( |
| 552 |
'post_type' => $post_type, |
| 553 |
'posts_per_page' => $max_posts, |
| 554 |
'meta_query' => array( |
| 555 |
$meta_query, |
| 556 |
), |
| 557 |
); |
| 558 |
|
| 559 |
$posts = get_posts( $args ); |
| 560 |
|
| 561 |
// PERFORMANCE FIX: Prime meta cache to avoid N+1 queries |
| 562 |
if (!empty($posts)) { |
| 563 |
$post_ids = wp_list_pluck($posts, 'ID'); |
| 564 |
update_meta_cache('post', $post_ids); |
| 565 |
} |
| 566 |
|
| 567 |
$posts_with_seo_score = array(); |
| 568 |
|
| 569 |
foreach ( $posts as $post ) { |
| 570 |
$seo_score = get_post_meta( $post->ID, '_mwseo_score', true ); |
| 571 |
$seo_status = get_post_meta( $post->ID, '_mwseo_status', true ); |
| 572 |
|
| 573 |
|
| 574 |
$posts_with_seo_score[] = array( |
| 575 |
'id' => $post->ID, |
| 576 |
'title' => $post->post_title, |
| 577 |
'score' => $seo_score, |
| 578 |
'status' => $seo_status, |
| 579 |
'category' => $this->get_seo_category( $seo_score ), |
| 580 |
); |
| 581 |
} |
| 582 |
|
| 583 |
return $posts_with_seo_score; |
| 584 |
} |
| 585 |
|
| 586 |
private function get_seo_category ( $score ) { |
| 587 |
|
| 588 |
if ( $score >= 0 && $score < 25 ) { |
| 589 |
return 'Terrible'; |
| 590 |
} |
| 591 |
else if ( $score >= 25 && $score < 50 ) { |
| 592 |
return 'Poor'; |
| 593 |
} |
| 594 |
else if ( $score >= 50 && $score < 75 ) { |
| 595 |
return 'Acceptable'; |
| 596 |
} |
| 597 |
else if ( $score >= 75 && $score < 90 ) { |
| 598 |
return 'Great'; |
| 599 |
} |
| 600 |
else if ( $score >= 90 && $score <= 100 ) { |
| 601 |
return 'Excellent'; |
| 602 |
} |
| 603 |
else { |
| 604 |
return 'Unknown'; |
| 605 |
} |
| 606 |
} |
| 607 |
|
| 608 |
function get_seo_engine_post_meta( $post ) { |
| 609 |
|
| 610 |
$post_id = $post->ID; |
| 611 |
|
| 612 |
$score = get_post_meta( $post_id, '_mwseo_score', true ); |
| 613 |
$status = get_post_meta( $post_id, '_mwseo_status', true ); |
| 614 |
$message = get_post_meta( $post_id, '_mwseo_message', true ); |
| 615 |
$codes = get_post_meta( $post_id, '_mwseo_codes', true ); |
| 616 |
|
| 617 |
if( !$status ) { $status = 'pending'; } |
| 618 |
if( !$message ) { $message = 'Start an analysis to get a score.'; } |
| 619 |
|
| 620 |
return [ |
| 621 |
'score' => $score, |
| 622 |
'status' => $status, |
| 623 |
'message' => $message, |
| 624 |
'codes' => $codes, |
| 625 |
]; |
| 626 |
} |
| 627 |
|
| 628 |
|
| 629 |
|
| 630 |
function build_excerpt( $post, $excerpt = "" ) { |
| 631 |
if ( is_category() ) { |
| 632 |
$category = get_queried_object(); |
| 633 |
if ( $category && !empty( $category->term_id ) ) { |
| 634 |
$seo_description = get_term_meta( $category->term_id, '_mwseo_description', true ); |
| 635 |
$excerpt = !empty( $seo_description ) ? $seo_description : category_description(); |
| 636 |
} else { |
| 637 |
$excerpt = category_description(); |
| 638 |
} |
| 639 |
} |
| 640 |
else if ( is_tag() ) { |
| 641 |
$excerpt = tag_description(); |
| 642 |
} |
| 643 |
else if ( is_author() ) { |
| 644 |
$excerpt = get_the_author_meta( 'description' ); |
| 645 |
} |
| 646 |
else if ( is_search() ) { |
| 647 |
$excerpt = get_search_query(); |
| 648 |
} |
| 649 |
else if ( is_404() ) { |
| 650 |
$excerpt = '404'; |
| 651 |
} |
| 652 |
else if ( is_home() ) { |
| 653 |
$excerpt = get_the_excerpt( get_option( 'page_for_posts' ) ); |
| 654 |
} |
| 655 |
else if ( is_archive() ) { |
| 656 |
$excerpt = get_the_archive_description(); |
| 657 |
} |
| 658 |
else if ( !empty( $post ) ) { |
| 659 |
$seo_excerpt = get_post_meta( $post->ID, $this->meta_key_seo_excerpt, true ); |
| 660 |
$excerpt = !empty( $seo_excerpt ) ? $seo_excerpt : get_the_excerpt( $post->ID ); |
| 661 |
} |
| 662 |
$excerpt = html_entity_decode( $excerpt ); |
| 663 |
return $excerpt; |
| 664 |
} |
| 665 |
|
| 666 |
function build_title( $post, $title = "" ) { |
| 667 |
$override = false; |
| 668 |
if ( is_category() ) { |
| 669 |
$category = get_queried_object(); |
| 670 |
if ( $category && !empty( $category->term_id ) ) { |
| 671 |
$seo_title = get_term_meta( $category->term_id, '_mwseo_title', true ); |
| 672 |
if ( !empty( $seo_title ) ) { |
| 673 |
$override = true; |
| 674 |
$title = $seo_title; |
| 675 |
} else { |
| 676 |
$title = single_cat_title( '', false ); |
| 677 |
} |
| 678 |
} else { |
| 679 |
$title = single_cat_title( '', false ); |
| 680 |
} |
| 681 |
} |
| 682 |
else if ( is_tag() ) { |
| 683 |
$title = single_tag_title( '', false ); |
| 684 |
} |
| 685 |
else if ( is_author() ) { |
| 686 |
$title = get_the_author(); |
| 687 |
} |
| 688 |
else if ( is_search() ) { |
| 689 |
$title = get_search_query(); |
| 690 |
} |
| 691 |
else if ( is_404() ) { |
| 692 |
$title = '404'; |
| 693 |
} |
| 694 |
else if ( is_home() ) { |
| 695 |
$title = get_the_title( get_option( 'page_for_posts' ) ); |
| 696 |
} |
| 697 |
else if ( is_archive() ) { |
| 698 |
$title = get_the_archive_title(); |
| 699 |
} |
| 700 |
else if ( !empty( $post ) ) { |
| 701 |
$seo_title = get_post_meta( $post->ID, $this->meta_key_seo_title, true ); |
| 702 |
if ( !empty( $seo_title ) ) { |
| 703 |
$override = true; |
| 704 |
$title = $seo_title; |
| 705 |
} |
| 706 |
else { |
| 707 |
$title = get_the_title( $post->ID ); |
| 708 |
} |
| 709 |
} |
| 710 |
$title = trim( strip_tags( $title ) ); |
| 711 |
if ( !$override ) { |
| 712 |
$title = $title . " | " . trim( get_bloginfo( 'name' ) ); |
| 713 |
} |
| 714 |
$title = html_entity_decode( $title ); |
| 715 |
return $title; |
| 716 |
} |
| 717 |
|
| 718 |
// Kept in sync with the detection surfaced to the admin UI in admin.php. |
| 719 |
function is_other_seo_plugin_active() { |
| 720 |
return class_exists( 'WPSEO_Frontend' ) // Yoast SEO |
| 721 |
|| class_exists( 'All_in_One_SEO_Pack' ) // All in One SEO |
| 722 |
|| class_exists( 'RankMath' ) // Rank Math |
| 723 |
|| class_exists( 'SEOPress' ); // SEOPress |
| 724 |
} |
| 725 |
|
| 726 |
// Whether SEO Engine should output its own frontend meta tags (title, description, |
| 727 |
// canonical, Open Graph). In 'auto' it steps back when another SEO plugin is active |
| 728 |
// so the two don't emit duplicate tags; 'always' and 'never' force the behavior. |
| 729 |
// The analysis, scoring, AI and MCP features run regardless of this setting. |
| 730 |
function should_render_frontend_meta() { |
| 731 |
$mode = $this->get_option( 'frontend_meta_mode', 'auto' ); |
| 732 |
if ( $mode === 'always' ) { |
| 733 |
return true; |
| 734 |
} |
| 735 |
if ( $mode === 'never' ) { |
| 736 |
return false; |
| 737 |
} |
| 738 |
return !$this->is_other_seo_plugin_active(); |
| 739 |
} |
| 740 |
|
| 741 |
function render_title( $title ) { |
| 742 |
global $post; |
| 743 |
$title = $this->build_title( $post, $title ); |
| 744 |
return esc_html( $title ); |
| 745 |
} |
| 746 |
|
| 747 |
function render_description() { |
| 748 |
global $post; |
| 749 |
$excerpt = $this->build_excerpt( $post ); |
| 750 |
$excerpt = trim( strip_tags( $excerpt ) ); |
| 751 |
echo '<meta class="mwseo-meta" name="description" content="' . esc_html( $excerpt ) . '" />'; |
| 752 |
} |
| 753 |
|
| 754 |
function render_canonical() { |
| 755 |
global $post; |
| 756 |
if ( ! $post instanceof WP_Post ) { |
| 757 |
return; |
| 758 |
} |
| 759 |
$canonical = get_post_meta( $post->ID, '_mwseo_canonical', true ); |
| 760 |
if ( !empty( $canonical ) ) { |
| 761 |
echo '<link class="mwseo-canonical" rel="canonical" href="' . esc_url( $canonical ) . '" />'; |
| 762 |
} |
| 763 |
} |
| 764 |
|
| 765 |
function get_images_from_content( $content ) { |
| 766 |
if (empty($content)) { |
| 767 |
return array(); |
| 768 |
} |
| 769 |
|
| 770 |
$dom = new DOMDocument; |
| 771 |
$loadHTML = @$dom->loadHTML($content); |
| 772 |
|
| 773 |
if (!$loadHTML) { |
| 774 |
throw new Exception('Failed to load HTML content'); |
| 775 |
} |
| 776 |
|
| 777 |
$images = array(); |
| 778 |
|
| 779 |
$images_html = array(); |
| 780 |
foreach ( $dom->getElementsByTagName( 'img' ) as $img ) { |
| 781 |
$src = $img->getAttribute( 'src' ); |
| 782 |
$alt = $img->getAttribute( 'alt' ); |
| 783 |
|
| 784 |
if ( $src && filter_var( $src, FILTER_VALIDATE_URL ) ) { |
| 785 |
$images_html[] = array( |
| 786 |
'src' => preg_replace( '~-[0-9]+x[0-9]+.~', '.', $src ), |
| 787 |
'alt' => $alt ? $alt : '', |
| 788 |
); |
| 789 |
} |
| 790 |
} |
| 791 |
|
| 792 |
$images_shortcode = array(); |
| 793 |
$pattern = get_shortcode_regex(); |
| 794 |
preg_match_all( '/'. $pattern .'/s', $content, $matches ); |
| 795 |
if ( !empty( $matches[2] ) ) { |
| 796 |
foreach ( $matches[2] as $key => $shortcode ) { |
| 797 |
if ( $shortcode == 'gallery' || $shortcode == 'meow-gallery' ) { |
| 798 |
$atts = shortcode_parse_atts( $matches[3][$key] ); |
| 799 |
if (isset($atts['ids'])) { |
| 800 |
$ids = explode( ',', $atts['ids'] ); |
| 801 |
foreach ( $ids as $id ) { |
| 802 |
if (is_numeric($id)) { |
| 803 |
$src = wp_get_attachment_url( $id ); |
| 804 |
$alt = get_post_meta( $id, '_wp_attachment_image_alt', true ); |
| 805 |
if ($src && filter_var($src, FILTER_VALIDATE_URL)) { |
| 806 |
|
| 807 |
if( in_array( $src, array_column( $images_shortcode, 'src' ) ) ) { |
| 808 |
continue; |
| 809 |
} |
| 810 |
|
| 811 |
$images_shortcode[] = array( |
| 812 |
'src' => $src, |
| 813 |
'alt' => $alt ? $alt : '', |
| 814 |
); |
| 815 |
} |
| 816 |
} |
| 817 |
} |
| 818 |
} |
| 819 |
} |
| 820 |
} |
| 821 |
} |
| 822 |
|
| 823 |
$images = array_merge( $images_html, $images_shortcode ); |
| 824 |
|
| 825 |
return $images; |
| 826 |
} |
| 827 |
|
| 828 |
function get_images_from_content_with_above_paragraph( $img_src_array, $content ) { |
| 829 |
$images = []; |
| 830 |
|
| 831 |
//Get all the text before the img src |
| 832 |
foreach ( $img_src_array as $img_src ) { |
| 833 |
|
| 834 |
$before_img = substr( $content, 0, strpos( $content, $img_src ) ); |
| 835 |
$before_img = substr( $before_img, strrpos( $before_img, '<p>' ) + 3 ); |
| 836 |
$before_img = strip_tags( $before_img ); |
| 837 |
$before_img = preg_replace( '/\s+/', ' ', $before_img ); |
| 838 |
$before_img = trim( $before_img ); |
| 839 |
|
| 840 |
$images[] = array( |
| 841 |
'src' => $img_src, |
| 842 |
'alt' => $before_img, |
| 843 |
'text' => $before_img, |
| 844 |
); |
| 845 |
} |
| 846 |
|
| 847 |
|
| 848 |
return $images; |
| 849 |
} |
| 850 |
|
| 851 |
// Normalizes the live content mode option, accounting for legacy boolean values |
| 852 |
// ('disabled' = off, 'main' = extract main content, 'whole' = use the whole page). |
| 853 |
function get_live_content_mode() { |
| 854 |
$mode = $this->get_option( 'check_live_content', 'disabled' ); |
| 855 |
if ( $mode === true || $mode === 1 || $mode === '1' ) { |
| 856 |
return 'main'; |
| 857 |
} |
| 858 |
if ( in_array( $mode, [ 'main', 'whole' ], true ) ) { |
| 859 |
return $mode; |
| 860 |
} |
| 861 |
return 'disabled'; |
| 862 |
} |
| 863 |
|
| 864 |
function filter_live_content( $content, $post, $strip_html = true ) { |
| 865 |
return $this->get_live_content( $post, $strip_html ); |
| 866 |
} |
| 867 |
|
| 868 |
function get_live_content( $post, $strip_html = true ) { |
| 869 |
// Fetch the actual rendered page (handles all page builders) |
| 870 |
$post_url = get_permalink( $post->ID ); |
| 871 |
if ( !$post_url ) { |
| 872 |
$this->log( "⚠️ Live Content: Could not find an URL to load for post {$post->ID}" ); |
| 873 |
return $strip_html ? wp_strip_all_tags( $post->post_content ) : $post->post_content; |
| 874 |
} |
| 875 |
|
| 876 |
$response = wp_remote_get( $post_url, [ |
| 877 |
'timeout' => 15, |
| 878 |
'user-agent' => 'Mozilla/5.0 (compatible; SEO Engine Content Analyzer; +' . home_url() . ')', |
| 879 |
'sslverify' => false, // Allow self-signed certs on local/staging |
| 880 |
] ); |
| 881 |
|
| 882 |
if ( is_wp_error( $response ) ) { |
| 883 |
$this->log( "⚠️ Live Content: HTTP request failed for {$post_url}. Error: " . $response->get_error_message() ); |
| 884 |
return $strip_html ? wp_strip_all_tags( $post->post_content ) : $post->post_content; |
| 885 |
} |
| 886 |
|
| 887 |
$status_code = wp_remote_retrieve_response_code( $response ); |
| 888 |
if ( $status_code !== 200 ) { |
| 889 |
$this->log( "⚠️ Live Content: HTTP {$status_code} for {$post_url}" ); |
| 890 |
return $strip_html ? wp_strip_all_tags( $post->post_content ) : $post->post_content; |
| 891 |
} |
| 892 |
|
| 893 |
$html = wp_remote_retrieve_body( $response ); |
| 894 |
if ( empty( $html ) ) { |
| 895 |
$this->log( "⚠️ Live Content: Empty response body for {$post_url}" ); |
| 896 |
return $strip_html ? wp_strip_all_tags( $post->post_content ) : $post->post_content; |
| 897 |
} |
| 898 |
|
| 899 |
// Extract main content area, unless the whole page should be analyzed. |
| 900 |
$content = $this->get_live_content_mode() === 'whole' ? $html : $this->extract_main_content( $html ); |
| 901 |
$content_length = strlen( wp_strip_all_tags( $content ) ); |
| 902 |
|
| 903 |
if ( $content_length < 50 ) { |
| 904 |
$this->log( "⚠️ Live Content: Extracted only {$content_length} chars from {$post_url}" ); |
| 905 |
} else { |
| 906 |
$this->log( "� |
| 907 |
Live Content: Extracted {$content_length} chars from {$post_url}" ); |
| 908 |
} |
| 909 |
|
| 910 |
if ( $strip_html ) { |
| 911 |
$content = wp_strip_all_tags( $content ); |
| 912 |
} |
| 913 |
|
| 914 |
return preg_replace( '/\s+/', ' ', trim( $content ) ); |
| 915 |
} |
| 916 |
|
| 917 |
/** |
| 918 |
* Extract main content from rendered HTML page |
| 919 |
*/ |
| 920 |
private function extract_main_content( $html ) { |
| 921 |
// Remove scripts, styles, comments |
| 922 |
$html = preg_replace( '/<script\b[^>]*>.*?<\/script>/is', '', $html ); |
| 923 |
$html = preg_replace( '/<style\b[^>]*>.*?<\/style>/is', '', $html ); |
| 924 |
$html = preg_replace( '/<!--.*?-->/s', '', $html ); |
| 925 |
|
| 926 |
// Try common content selectors |
| 927 |
$selectors = [ |
| 928 |
'/<article[^>]*>(.*?)<\/article>/is', |
| 929 |
'/<main[^>]*>(.*?)<\/main>/is', |
| 930 |
'/<div[^>]*class="[^"]*(?:entry-content|post-content|page-content|content-area)[^"]*"[^>]*>(.*?)<\/div>/is', |
| 931 |
]; |
| 932 |
|
| 933 |
foreach ( $selectors as $pattern ) { |
| 934 |
if ( preg_match( $pattern, $html, $m ) && strlen( wp_strip_all_tags( $m[1] ) ) > 100 ) { |
| 935 |
return $m[1]; |
| 936 |
} |
| 937 |
} |
| 938 |
|
| 939 |
// Fallback: body minus header/footer/nav |
| 940 |
if ( preg_match( '/<body[^>]*>(.*?)<\/body>/is', $html, $m ) ) { |
| 941 |
$body = $m[1]; |
| 942 |
$body = preg_replace( '/<header[^>]*>.*?<\/header>/is', '', $body ); |
| 943 |
$body = preg_replace( '/<footer[^>]*>.*?<\/footer>/is', '', $body ); |
| 944 |
$body = preg_replace( '/<nav[^>]*>.*?<\/nav>/is', '', $body ); |
| 945 |
$body = preg_replace( '/<aside[^>]*>.*?<\/aside>/is', '', $body ); |
| 946 |
return $body; |
| 947 |
} |
| 948 |
|
| 949 |
return $html; |
| 950 |
} |
| 951 |
|
| 952 |
function check_images_alt_text( $post ) { |
| 953 |
|
| 954 |
$content = apply_filters( 'mwseo_post_content', $post->post_content, $post, false ); |
| 955 |
|
| 956 |
$images = $this->get_images_from_content( $content ); |
| 957 |
if ( !is_array( $images ) ) { |
| 958 |
return array(); |
| 959 |
} |
| 960 |
|
| 961 |
$missing_alt_text = array(); |
| 962 |
$image_meta_data = $this->get_all_image_meta_data($images); |
| 963 |
|
| 964 |
foreach ( $images as $image ) { |
| 965 |
if ( empty( $image[ 'alt' ] ) ) { |
| 966 |
$attachment_id = attachment_url_to_postid( $image[ 'src' ] ); |
| 967 |
$meta_alt_text = $image_meta_data[$attachment_id]; |
| 968 |
if ( !empty( $meta_alt_text ) ) { |
| 969 |
$this->log( sprintf( "🖼️ Image (src : %s) has alt text : %s", $image[ 'src' ], $meta_alt_text ) ); |
| 970 |
continue; |
| 971 |
} |
| 972 |
$missing_alt_text[] = $image[ 'src' ]; |
| 973 |
} |
| 974 |
} |
| 975 |
|
| 976 |
return $missing_alt_text; |
| 977 |
} |
| 978 |
|
| 979 |
function get_all_image_meta_data($images) { |
| 980 |
$meta_data = array(); |
| 981 |
foreach ($images as $image) { |
| 982 |
$attachment_id = attachment_url_to_postid( $image[ 'src' ] ); |
| 983 |
$meta_data[$attachment_id] = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ); |
| 984 |
} |
| 985 |
return $meta_data; |
| 986 |
} |
| 987 |
|
| 988 |
|
| 989 |
function check_links( $content ){ |
| 990 |
|
| 991 |
$links = array(); |
| 992 |
$external_links = array(); |
| 993 |
$internal_links = array(); |
| 994 |
|
| 995 |
if ( preg_match_all( '/<a[^>]+>/i', $content, $matches ) ) { |
| 996 |
foreach ( $matches[0] as $link ) { |
| 997 |
if ( preg_match( '/href="http[s]?:\/\/([^"]+)"/i', $link, $href ) ) { |
| 998 |
$external_links[] = $href[ 1 ]; |
| 999 |
}elseif ( preg_match( '/href="([^"]+)"/i', $link, $href ) ) { |
| 1000 |
$internal_links[] = $href[ 1 ]; |
| 1001 |
} |
| 1002 |
} |
| 1003 |
} |
| 1004 |
|
| 1005 |
//TODO: Display the links somewhere, for now just the count |
| 1006 |
|
| 1007 |
$links[ 'external' ] = count( $external_links ); |
| 1008 |
$links[ 'internal' ] = count( $internal_links ); |
| 1009 |
$links[ 'total' ] = $links[ 'external' ] + $links[ 'internal' ]; |
| 1010 |
|
| 1011 |
return $links; |
| 1012 |
} |
| 1013 |
|
| 1014 |
|
| 1015 |
/** |
| 1016 |
* Generate AI solution for a specific issue - Backward compatibility wrapper |
| 1017 |
* |
| 1018 |
* @deprecated Use $core->pro->magic_fix->generate_solution() instead |
| 1019 |
*/ |
| 1020 |
public function magic_fix_generate_solution( $post, $issue_type ) { |
| 1021 |
if ( $this->pro && $this->pro->magic_fix ) { |
| 1022 |
return $this->pro->magic_fix->generate_solution( $post, $issue_type ); |
| 1023 |
} |
| 1024 |
$this->log( '⚠️ Magic Fix is not available. This is a premium feature.' ); |
| 1025 |
return false; |
| 1026 |
} |
| 1027 |
|
| 1028 |
/** |
| 1029 |
* Apply a generated solution to a post - Backward compatibility wrapper |
| 1030 |
* |
| 1031 |
* @deprecated Use $core->pro->magic_fix->apply_solution() instead |
| 1032 |
*/ |
| 1033 |
public function magic_fix_apply_solution( $post, $issue_type, $solution ) { |
| 1034 |
if ( $this->pro && $this->pro->magic_fix ) { |
| 1035 |
return $this->pro->magic_fix->apply_solution( $post, $issue_type, $solution ); |
| 1036 |
} |
| 1037 |
$this->log( '⚠️ Magic Fix is not available. This is a premium feature.' ); |
| 1038 |
return false; |
| 1039 |
} |
| 1040 |
|
| 1041 |
/** |
| 1042 |
* Mark an issue as fixed by Magic Fix - Backward compatibility wrapper |
| 1043 |
* |
| 1044 |
* @deprecated Use $core->pro->magic_fix->mark_as_fixed() instead |
| 1045 |
*/ |
| 1046 |
public function mark_issue_as_magic_fixed( $post_id, $issue_type ) { |
| 1047 |
if ( $this->pro && $this->pro->magic_fix ) { |
| 1048 |
return $this->pro->magic_fix->mark_as_fixed( $post_id, $issue_type ); |
| 1049 |
} |
| 1050 |
} |
| 1051 |
|
| 1052 |
/** |
| 1053 |
* Check if an issue has been marked as fixed - Backward compatibility wrapper |
| 1054 |
* |
| 1055 |
* @deprecated Use $core->pro->magic_fix->is_fixed() instead |
| 1056 |
*/ |
| 1057 |
public function is_issue_magic_fixed( $post_id, $issue_type ) { |
| 1058 |
if ( $this->pro && $this->pro->magic_fix ) { |
| 1059 |
return $this->pro->magic_fix->is_fixed( $post_id, $issue_type ); |
| 1060 |
} |
| 1061 |
return false; |
| 1062 |
} |
| 1063 |
|
| 1064 |
/** |
| 1065 |
* Clear all magic fix markers for a post - Backward compatibility wrapper |
| 1066 |
* |
| 1067 |
* @deprecated Use $core->pro->magic_fix->clear_markers() instead |
| 1068 |
*/ |
| 1069 |
public function clear_magic_fix_markers( $post_id ) { |
| 1070 |
if ( $this->pro && $this->pro->magic_fix ) { |
| 1071 |
return $this->pro->magic_fix->clear_markers( $post_id ); |
| 1072 |
} |
| 1073 |
} |
| 1074 |
|
| 1075 |
/** |
| 1076 |
* Migrate old meta keys to new mwseo prefixed keys |
| 1077 |
* This migration runs once per post when the post is accessed/analyzed |
| 1078 |
*/ |
| 1079 |
public function migrate_post_meta_keys( $post_id ) { |
| 1080 |
// Mapping of old keys to new keys |
| 1081 |
$meta_keys_map = [ |
| 1082 |
'_seo_engine_data' => '_mwseo_analysis', |
| 1083 |
'_seo_engine_ignored_tests' => '_mwseo_ignored_tests', |
| 1084 |
'_seo_engine_overall' => '_mwseo_overall', |
| 1085 |
'_seo_engine_updated' => '_mwseo_updated', |
| 1086 |
'_seo_engine_ai_keywords' => '_mwseo_keywords', |
| 1087 |
'_seo_title' => '_mwseo_title', |
| 1088 |
'_seo_excerpt' => '_mwseo_excerpt', |
| 1089 |
'_seo_robots' => '_mwseo_robots', |
| 1090 |
'_seo_canonical' => '_mwseo_canonical', |
| 1091 |
'_seo_status' => '_mwseo_status', |
| 1092 |
'_seo_message' => '_mwseo_message', |
| 1093 |
'_seo_score' => '_mwseo_score', |
| 1094 |
'_seo_codes' => '_mwseo_codes', |
| 1095 |
]; |
| 1096 |
|
| 1097 |
foreach ( $meta_keys_map as $old_key => $new_key ) { |
| 1098 |
$value = get_post_meta( $post_id, $old_key, true ); |
| 1099 |
if ( $value !== '' && $value !== false ) { |
| 1100 |
// Copy to new key |
| 1101 |
update_post_meta( $post_id, $new_key, $value ); |
| 1102 |
// Delete old key |
| 1103 |
delete_post_meta( $post_id, $old_key ); |
| 1104 |
} |
| 1105 |
} |
| 1106 |
} |
| 1107 |
|
| 1108 |
/** |
| 1109 |
* |
| 1110 |
* Roles & Access Rights |
| 1111 |
* |
| 1112 |
*/ |
| 1113 |
function can_access_settings() { |
| 1114 |
return apply_filters( 'mwseo_allow_setup', current_user_can( 'manage_options' ) ); |
| 1115 |
} |
| 1116 |
|
| 1117 |
function can_access_features() { |
| 1118 |
return apply_filters( 'mwseo_allow_usage', current_user_can( 'administrator' ) ); |
| 1119 |
} |
| 1120 |
|
| 1121 |
#region Options |
| 1122 |
|
| 1123 |
function get_option( $option, $default = null ) { |
| 1124 |
$options = $this->get_all_options(); |
| 1125 |
return $options[$option] ?? $default; |
| 1126 |
} |
| 1127 |
|
| 1128 |
function sanitized_options(){ |
| 1129 |
// This is a READ-ONLY method - it computes derived values but NEVER writes to the database |
| 1130 |
$options = $this->get_all_options(); |
| 1131 |
|
| 1132 |
$options['post_types'] = $this->make_post_type_list( $this->get_post_types() ); |
| 1133 |
$options['categories'] = $this->make_category_list( $this->get_categories() ); |
| 1134 |
|
| 1135 |
if ( $options['language'] == 'auto' || empty( $options['language'] ) ) { |
| 1136 |
$options['language'] = $this->get_language_name( get_locale() ) ?? 'English'; |
| 1137 |
} |
| 1138 |
|
| 1139 |
if ( empty( $options['readability_treshold'] ) ) { |
| 1140 |
$options['readability_treshold'] = 50; |
| 1141 |
} |
| 1142 |
|
| 1143 |
// Always force analyze_buffer to 1 - analyzing multiple posts at once causes performance issues |
| 1144 |
$options['analyze_buffer'] = 1; |
| 1145 |
|
| 1146 |
// AI Engine - check status but DON'T modify stored options |
| 1147 |
global $mwai; |
| 1148 |
|
| 1149 |
$options['mwai_has_ai'] = !empty( $mwai ) && $mwai->hasAI(); |
| 1150 |
$options['mwai_has_mcp'] = !empty( $mwai ) && $mwai->hasMCP(); |
| 1151 |
// Legacy |
| 1152 |
$options['ai_engine_status'] = $options['mwai_has_ai']; |
| 1153 |
|
| 1154 |
// AI Features - Only disable in the returned array for UI purposes |
| 1155 |
// DO NOT modify the stored user preferences |
| 1156 |
if ( !$options['mwai_has_ai'] ) { |
| 1157 |
$options['ai_magic_fix'] = false; |
| 1158 |
$options['ai_auto_correct'] = false; |
| 1159 |
$options['ai_magic_wand'] = false; |
| 1160 |
$options['ai_keywords'] = false; |
| 1161 |
$options['woocommerce_assistant'] = false; |
| 1162 |
} |
| 1163 |
|
| 1164 |
// Module hierarchy - disable child features when parent module is disabled |
| 1165 |
// Technical SEO children |
| 1166 |
if ( empty( $options['technical_seo'] ) ) { |
| 1167 |
$options['auto_schema_enabled'] = false; |
| 1168 |
$options['redirect_attachments'] = false; |
| 1169 |
$options['disallow_gpt_bot'] = false; |
| 1170 |
$options['sitemap'] = false; |
| 1171 |
$options['social_networks'] = false; |
| 1172 |
$options['robot_editor'] = false; |
| 1173 |
$options['llms_editor'] = false; |
| 1174 |
$options['indexnow'] = false; |
| 1175 |
} |
| 1176 |
|
| 1177 |
// Return sanitized options WITHOUT persisting to database |
| 1178 |
return $options; |
| 1179 |
} |
| 1180 |
|
| 1181 |
// TODO: Review which options are still in use after meta key refactoring - many may be obsolete |
| 1182 |
function list_options() { |
| 1183 |
return array( |
| 1184 |
// Module Toggles |
| 1185 |
'content_seo' => true, |
| 1186 |
'technical_seo' => true, |
| 1187 |
|
| 1188 |
'speed_and_vitals' => false, |
| 1189 |
'analytics' => true, |
| 1190 |
|
| 1191 |
// Content SEO |
| 1192 |
'default_post_type' => 'post', |
| 1193 |
'posts_limit' => 10, |
| 1194 |
'posts_sort' => array( 'accessor' => 'score', 'by' => 'asc' ), |
| 1195 |
'posts_filter' => 'all', |
| 1196 |
'post_types' => $this->make_post_type_list( $this->get_post_types() ), |
| 1197 |
|
| 1198 |
'select_post_types' => ['post', 'page'], |
| 1199 |
'readability_treshold' => 50, |
| 1200 |
'language' => $this->get_language_name( get_locale() ) ?? 'English', |
| 1201 |
// 'quick' keeps scores fresh after edits (non-AI baseline, scheduled off-request); |
| 1202 |
// 'full' also re-runs the AI checks; 'none' lets counts go stale until a manual scan. |
| 1203 |
'analyze_on_update' => 'quick', |
| 1204 |
'analyze_buffer' => 1, |
| 1205 |
'analyze_buffer_interval' => 0, |
| 1206 |
|
| 1207 |
// Technical SEO |
| 1208 |
'indexnow' => true, |
| 1209 |
'indexnow_key' => '', |
| 1210 |
'sitemap' => false, |
| 1211 |
'disable_wp_sitemap' => false, |
| 1212 |
'sitemap_custom' => false, |
| 1213 |
'sitemap_style' => 'default', |
| 1214 |
'sitemap_path' => '', |
| 1215 |
'sitemap_exclude_users_provider' => false, |
| 1216 |
'sitemap_exclude_posts_provider' => false, |
| 1217 |
'sitemap_exclude_taxonomies_provider' => false, |
| 1218 |
'sitemap_excluded_post_types' => [], |
| 1219 |
'sitemap_excluded_taxonomies' => [], |
| 1220 |
'sitemap_excluded_post_ids' => [], |
| 1221 |
'sitemap_max_urls' => 100, |
| 1222 |
'sitemap_post_max_pages' => 100, |
| 1223 |
'taxonomies' => $this->make_taxonomy_list( $this->get_taxonomies() ), |
| 1224 |
'categories' => $this->make_category_list( $this->get_categories() ), |
| 1225 |
'robot_editor' => false, |
| 1226 |
'disallow_gpt_bot' => false, |
| 1227 |
|
| 1228 |
// Social Networks |
| 1229 |
'social_networks' => false, |
| 1230 |
'social_networks_twitter' => '', |
| 1231 |
'social_networks_facebook_app_id' => '', |
| 1232 |
|
| 1233 |
// Google Cloud / API (shared: used by Speed & Vitals / PageSpeed Insights) |
| 1234 |
'google_api_key' => '', |
| 1235 |
|
| 1236 |
// Analytics Configuration |
| 1237 |
'analytics_method' => 'none', // 'none', 'private', 'google', 'plausible', 'matomo' |
| 1238 |
|
| 1239 |
// Analytics Options |
| 1240 |
'bots_track' => true, |
| 1241 |
'bots_instructions' => false, |
| 1242 |
'bots_instructions_content' => '', |
| 1243 |
// Private Analytics |
| 1244 |
'general_analytics' => false, |
| 1245 |
'analytics_track_logged_users' => false, |
| 1246 |
'analytics_track_power_users' => false, |
| 1247 |
'analytics_privacy' => false, |
| 1248 |
|
| 1249 |
// Google Analytics |
| 1250 |
// The Settings toggle and the GA module both use 'analytics_cache'; this default |
| 1251 |
// was named 'google_analytics_cache' for a while, so caching silently never engaged. |
| 1252 |
'analytics_cache' => true, |
| 1253 |
'google_analytics' => false, |
| 1254 |
'google_analytics_property_ids' => [], |
| 1255 |
'google_analytics_property_id' => '', |
| 1256 |
'google_analytics_client_secret' => '', |
| 1257 |
'google_analytics_client_id' => '', |
| 1258 |
'google_analytics_tracking_ids' => [], |
| 1259 |
'google_analytics_track_logged_users' => false, |
| 1260 |
'google_analytics_track_power_users' => false, |
| 1261 |
'google_analytics_tracking_disabled' => false, |
| 1262 |
|
| 1263 |
// Plausible Analytics |
| 1264 |
'plausible_analytics' => false, |
| 1265 |
'plausible_site_id' => '', |
| 1266 |
'plausible_api_key' => '', |
| 1267 |
'plausible_server_url' => 'https://plausible.io', |
| 1268 |
'plausible_track_logged_users' => false, |
| 1269 |
'plausible_track_power_users' => false, |
| 1270 |
'plausible_analytics_tracking_disabled' => false, |
| 1271 |
|
| 1272 |
// Matomo Analytics |
| 1273 |
'matomo_analytics' => false, |
| 1274 |
'matomo_site_id' => '', |
| 1275 |
'matomo_api_key' => '', |
| 1276 |
'matomo_server_url' => '', |
| 1277 |
'matomo_track_logged_users' => false, |
| 1278 |
'matomo_track_power_users' => false, |
| 1279 |
'matomo_analytics_tracking_disabled' => false, |
| 1280 |
|
| 1281 |
// Search Console module (Pro). When enabled, surfaces its own tab in |
| 1282 |
// the admin with quick wins, top queries/pages, and post pulse — pulling |
| 1283 |
// real Google search data. Reuses google_analytics_client_id/secret since |
| 1284 |
// a single GCP OAuth client can grant tokens for multiple Google APIs. |
| 1285 |
// `property` is the default for site-wide tools; `properties` is the set |
| 1286 |
// of all GSC properties belonging to this WP install (Polylang/WPML |
| 1287 |
// multi-domain). Per-post tools auto-pick from `properties` by URL match. |
| 1288 |
'search_console' => false, |
| 1289 |
'google_search_console_property' => '', |
| 1290 |
'google_search_console_properties' => [], |
| 1291 |
|
| 1292 |
// AI Features |
| 1293 |
'mwai_has_ai' => false, |
| 1294 |
'mwai_has_mcp' => false, |
| 1295 |
'ai_engine_status' => false, |
| 1296 |
'ai_magic_fix' => false, |
| 1297 |
'magic_fix_instructions' => '', |
| 1298 |
'ai_auto_correct' => false, |
| 1299 |
'ai_magic_wand' => false, |
| 1300 |
'ai_keywords' => false, |
| 1301 |
'ai_semantic_analysis' => true, // Enable AI semantic analysis (requires AI Engine) |
| 1302 |
'full_analysis' => true, // Enable Full Analysis with Intelligence checks |
| 1303 |
'daily_insights' => true, |
| 1304 |
'woocommerce_assistant' => false, |
| 1305 |
'woo_assistant_instructions' => '', |
| 1306 |
'select_magic_fix' => [ |
| 1307 |
'readability_score', |
| 1308 |
], |
| 1309 |
|
| 1310 |
// Scoring Preferences |
| 1311 |
'pillar_weight_cq' => 50, // Content Quality weight (0-100) |
| 1312 |
'pillar_weight_tech' => 50, // Technical weight (0-100) |
| 1313 |
'quality_safeguards_enabled' => true, |
| 1314 |
'use_seo_metadata' => true, |
| 1315 |
// Frontend meta output: 'auto' defers to another active SEO plugin (Yoast, |
| 1316 |
// Rank Math, AIOSEO, SEOPress), 'always' outputs regardless, 'never' stays silent. |
| 1317 |
'frontend_meta_mode' => 'auto', |
| 1318 |
'show_ai_summary' => true, |
| 1319 |
'row_density' => 'cozy', // compact | cozy |
| 1320 |
|
| 1321 |
// Content Quality - Basic |
| 1322 |
'use_excerpt_as_meta' => true, |
| 1323 |
'aim_easy_reading' => true, |
| 1324 |
'encourage_image_alt' => true, |
| 1325 |
'ai_detect_intent' => true, |
| 1326 |
'short_strong_mode' => true, |
| 1327 |
|
| 1328 |
// Content Quality - Advanced |
| 1329 |
'readability_flesch_min' => 50, |
| 1330 |
'readability_flesch_max' => 70, |
| 1331 |
'readability_grade_min' => 7, |
| 1332 |
'readability_grade_max' => 10, |
| 1333 |
'short_strong_sensitivity' => 'normal', // normal | strict | off |
| 1334 |
'show_author_visible' => true, |
| 1335 |
'link_to_author_page' => true, |
| 1336 |
'intent_schema_coherence' => true, |
| 1337 |
|
| 1338 |
// Technical - Basic |
| 1339 |
'require_unique_title' => true, |
| 1340 |
'encourage_short_slug' => true, |
| 1341 |
'encourage_internal_links' => true, |
| 1342 |
'check_internal_links' => true, |
| 1343 |
'check_missing_alt_text' => true, |
| 1344 |
'check_orphaned_content' => true, |
| 1345 |
'auto_schema_enabled' => false, |
| 1346 |
|
| 1347 |
// Length Guidelines |
| 1348 |
'title_length_min' => 30, |
| 1349 |
'title_length_max' => 75, |
| 1350 |
'excerpt_length_min' => 80, |
| 1351 |
'excerpt_length_max' => 160, |
| 1352 |
'content_length_min' => 300, |
| 1353 |
'content_length_max' => 1600, |
| 1354 |
|
| 1355 |
// Technical - Advanced |
| 1356 |
'slug_length_max' => 60, |
| 1357 |
'slug_words_min' => 2, |
| 1358 |
'slug_words_max' => 6, |
| 1359 |
'smart_internal_links' => true, |
| 1360 |
'check_external_links' => false, |
| 1361 |
'check_live_content' => 'disabled', |
| 1362 |
'schema_by_post_type' => [ |
| 1363 |
'post' => 'Article', |
| 1364 |
'page' => 'WebPage', |
| 1365 |
'product' => 'Product', |
| 1366 |
], |
| 1367 |
|
| 1368 |
// Content Comfort Zones |
| 1369 |
'comfort_zone_quick_min' => 150, |
| 1370 |
'comfort_zone_quick_max' => 500, |
| 1371 |
'comfort_zone_guide_min' => 700, |
| 1372 |
'comfort_zone_guide_max' => 1600, |
| 1373 |
'comfort_zone_product_min' => 300, |
| 1374 |
'comfort_zone_product_max' => 900, |
| 1375 |
|
| 1376 |
// Legacy Score Factors (kept for compatibility) |
| 1377 |
'score_check_title_exists' => true, |
| 1378 |
'score_check_title_unique' => true, |
| 1379 |
'score_check_title_length' => true, |
| 1380 |
'score_check_excerpt_exists' => true, |
| 1381 |
'score_check_excerpt_length' => true, |
| 1382 |
'score_check_slug_length' => true, |
| 1383 |
'score_check_slug_words' => true, |
| 1384 |
'score_check_content_length' => true, |
| 1385 |
'score_check_images_alt' => true, |
| 1386 |
'score_check_links' => true, |
| 1387 |
'score_check_readability' => true, |
| 1388 |
|
| 1389 |
// Redirections + 404 |
| 1390 |
'redirects_enabled' => false, |
| 1391 |
'redirects_track_404' => true, |
| 1392 |
'redirects_auto_slug' => false, |
| 1393 |
'redirects_default_status' => 301, |
| 1394 |
'redirects_404_retention_days' => 30, |
| 1395 |
'redirects_404_exclude' => "/wp-admin/*\n/feed*\n/xmlrpc.php", |
| 1396 |
'redirects_skip_bots' => true, |
| 1397 |
|
| 1398 |
// AI Visibility |
| 1399 |
'ai_visibility' => false, |
| 1400 |
'ai_visibility_surfaces' => [], |
| 1401 |
|
| 1402 |
// Maintenance & Others |
| 1403 |
'logs' => false, |
| 1404 |
'hide_dashboard_message' => false, |
| 1405 |
'mcp_support' => false, |
| 1406 |
); |
| 1407 |
} |
| 1408 |
|
| 1409 |
// TODO: Delete after January 2026 |
| 1410 |
function migrate_option_names( $options ) { |
| 1411 |
// Create a new array with migrated keys |
| 1412 |
$migrated_options = array(); |
| 1413 |
|
| 1414 |
foreach ( $options as $key => $value ) { |
| 1415 |
// Check if the key starts with 'seo_engine_' |
| 1416 |
if ( strpos( $key, 'seo_engine_' ) === 0 ) { |
| 1417 |
// Remove 'seo_engine_' prefix |
| 1418 |
$new_key = substr( $key, 11 ); |
| 1419 |
$migrated_options[$new_key] = $value; |
| 1420 |
} else { |
| 1421 |
// Keep the key as is if it doesn't have the prefix |
| 1422 |
$migrated_options[$key] = $value; |
| 1423 |
} |
| 1424 |
} |
| 1425 |
|
| 1426 |
// Save the migrated options |
| 1427 |
update_option( $this->option_name, $migrated_options, false ); |
| 1428 |
|
| 1429 |
// Also migrate WordPress native options |
| 1430 |
$this->migrate_native_options(); |
| 1431 |
|
| 1432 |
return $migrated_options; |
| 1433 |
} |
| 1434 |
|
| 1435 |
// TODO: Delete after January 2026 |
| 1436 |
function migrate_native_options() { |
| 1437 |
// Migrate title hashes |
| 1438 |
$old_title_hashes = get_option( 'seo_engine_title_hashes' ); |
| 1439 |
if ( $old_title_hashes !== false ) { |
| 1440 |
update_option( 'mwseo_title_hashes', $old_title_hashes ); |
| 1441 |
delete_option( 'seo_engine_title_hashes' ); |
| 1442 |
} |
| 1443 |
|
| 1444 |
// Add any other native options that need migration here |
| 1445 |
} |
| 1446 |
|
| 1447 |
function get_all_options() { |
| 1448 |
|
| 1449 |
// TODO: Delete after January 2026 |
| 1450 |
$old_options = get_option( $this->old_option_name, null ); |
| 1451 |
if ( $old_options ) { |
| 1452 |
// Migrate old options to new option name |
| 1453 |
update_option( $this->option_name, $old_options, false ); |
| 1454 |
delete_option( $this->old_option_name ); |
| 1455 |
|
| 1456 |
$options = $old_options; |
| 1457 |
} else { |
| 1458 |
$options = get_option( $this->option_name, null ); |
| 1459 |
} |
| 1460 |
|
| 1461 |
// Check if we need to migrate from old option names |
| 1462 |
if ( $options && isset( $options['default_post_type'] ) ) { |
| 1463 |
$options = $this->migrate_option_names( $options ); |
| 1464 |
} |
| 1465 |
|
| 1466 |
// Make sure every option is set |
| 1467 |
$options = wp_parse_args( $options, $this->list_options() ); |
| 1468 |
|
| 1469 |
if( $this->parsers ) { |
| 1470 |
$options = $this->parsers->refresh_parsers( $options ); |
| 1471 |
} |
| 1472 |
|
| 1473 |
return $options; |
| 1474 |
} |
| 1475 |
|
| 1476 |
function update_options( $options ) { |
| 1477 |
if ( $options == get_option( $this->option_name ) ) { |
| 1478 |
return $options; |
| 1479 |
} |
| 1480 |
|
| 1481 |
if ( !update_option( $this->option_name, $options, false ) ) { |
| 1482 |
return false; |
| 1483 |
} |
| 1484 |
|
| 1485 |
$options = $this->sanitized_options(); |
| 1486 |
|
| 1487 |
return $options; |
| 1488 |
} |
| 1489 |
|
| 1490 |
function update_option( $option, $value ) { |
| 1491 |
$options = $this->get_all_options(); |
| 1492 |
$options[$option] = $value; |
| 1493 |
return $this->update_options( $options ); |
| 1494 |
} |
| 1495 |
|
| 1496 |
function reset_options() { |
| 1497 |
return $this->update_options( $this->list_options() ); |
| 1498 |
} |
| 1499 |
|
| 1500 |
|
| 1501 |
|
| 1502 |
#endregion |
| 1503 |
|
| 1504 |
#region WooCommerce |
| 1505 |
|
| 1506 |
function generate_woocommerce_fields( $params ) |
| 1507 |
{ |
| 1508 |
global $mwai; |
| 1509 |
|
| 1510 |
if ( is_null( $mwai ) || !isset( $mwai ) ) { |
| 1511 |
throw new Exception( 'AI Engine is not available.' ); |
| 1512 |
} |
| 1513 |
|
| 1514 |
$product_id = intval( $params['post_id'] ?? 0 ); |
| 1515 |
$product = get_post( $product_id ); |
| 1516 |
if ( !$product || $product->post_type !== 'product' ) { |
| 1517 |
throw new Exception( 'Product not found.' ); |
| 1518 |
} |
| 1519 |
|
| 1520 |
// Which fields to generate. Defaults to all of them. |
| 1521 |
$allowed_fields = [ 'seo_title', 'description', 'short_description', 'tags' ]; |
| 1522 |
$fields = !empty( $params['fields'] ) && is_array( $params['fields'] ) |
| 1523 |
? array_values( array_intersect( $allowed_fields, $params['fields'] ) ) |
| 1524 |
: $allowed_fields; |
| 1525 |
if ( empty( $fields ) ) { |
| 1526 |
throw new Exception( 'No fields requested.' ); |
| 1527 |
} |
| 1528 |
|
| 1529 |
// In bulk mode we only rely on the product title (and the image via Vision), |
| 1530 |
// not on the existing descriptions/attributes. |
| 1531 |
$title_only = !empty( $params['title_only'] ); |
| 1532 |
if ( $title_only ) { |
| 1533 |
$product_data = [ |
| 1534 |
'id' => $product_id, |
| 1535 |
'title' => $product->post_title, |
| 1536 |
]; |
| 1537 |
} |
| 1538 |
else { |
| 1539 |
$wc_product = function_exists( 'wc_get_product' ) ? wc_get_product( $product_id ) : null; |
| 1540 |
$product_data = [ |
| 1541 |
'id' => $product_id, |
| 1542 |
'title' => $product->post_title, |
| 1543 |
'description' => $product->post_content, |
| 1544 |
'short_description' => $product->post_excerpt, |
| 1545 |
'categories' => wp_get_post_terms( $product_id, 'product_cat', array( 'fields' => 'names' ) ), |
| 1546 |
'tags' => wp_get_post_terms( $product_id, 'product_tag', array( 'fields' => 'names' ) ), |
| 1547 |
'attributes' => $wc_product ? $wc_product->get_attributes() : [], |
| 1548 |
'price' => $wc_product ? $wc_product->get_price() : null, |
| 1549 |
]; |
| 1550 |
} |
| 1551 |
|
| 1552 |
// Use Vision if enabled |
| 1553 |
$use_vision = !empty( $params['vision'] ); |
| 1554 |
$vision_result = null; |
| 1555 |
if ( $use_vision ) { |
| 1556 |
$product_image = get_post_meta( $product_id, '_thumbnail_id', true ); |
| 1557 |
if ( $product_image ) { |
| 1558 |
|
| 1559 |
$prompt = "Describe the main elements in the image, the colors, the style, the context, and any text that is visible. Write it in " . $params['language'] . "."; |
| 1560 |
|
| 1561 |
$intermediate = image_get_intermediate_size( $product_image, 'medium' ); |
| 1562 |
$path = $intermediate |
| 1563 |
? path_join( dirname( get_attached_file( $product_image ) ), $intermediate['file'] ) |
| 1564 |
: get_attached_file( $product_image ); |
| 1565 |
$url = $intermediate |
| 1566 |
? wp_get_attachment_image_url( $product_image, 'medium' ) |
| 1567 |
: wp_get_attachment_url( $product_image ); |
| 1568 |
|
| 1569 |
if ( !file_exists( $path ) ) { |
| 1570 |
$this->log( "⚠️ WooCommerce Assistant: Product image file not found at {$path}" ); |
| 1571 |
} else { |
| 1572 |
// Cache vision results by file hash so the same image is only described once. |
| 1573 |
$file_hash = md5_file( $path ); |
| 1574 |
$transient_key = 'mwseo_vision_' . $file_hash; |
| 1575 |
$cached = get_transient( $transient_key ); |
| 1576 |
|
| 1577 |
if ( $cached !== false ) { |
| 1578 |
$this->log( "🔍 WooCommerce Assistant: Using cached vision result for image hash {$file_hash}." ); |
| 1579 |
$vision_result = $cached; |
| 1580 |
} else { |
| 1581 |
$vision_result = $mwai->simpleVisionQuery( $prompt, $url, $path, [ 'scope' => 'seo' ] ); |
| 1582 |
if ( $vision_result ) { |
| 1583 |
set_transient( $transient_key, $vision_result, WEEK_IN_SECONDS ); |
| 1584 |
} |
| 1585 |
} |
| 1586 |
} |
| 1587 |
|
| 1588 |
} |
| 1589 |
} |
| 1590 |
|
| 1591 |
$field_specs = [ |
| 1592 |
'description' => 'a description of this product (between 120 and 240 words)', |
| 1593 |
'short_description' => 'a short description (between 20 and 49 words)', |
| 1594 |
'seo_title' => 'a SEO-friendly title', |
| 1595 |
'tags' => 'tags (separated by commas)', |
| 1596 |
]; |
| 1597 |
$requested_specs = array_map( function( $field ) use ( $field_specs ) { |
| 1598 |
return $field_specs[ $field ]; |
| 1599 |
}, $fields ); |
| 1600 |
|
| 1601 |
$prompt = "Based on the product, write " . implode( ', ', $requested_specs ) . ". SEO and sales focused. Write it in {LANGUAGE}."; |
| 1602 |
|
| 1603 |
if ( !empty( $params['product'] ) ) { |
| 1604 |
$prompt .= "\n\nHere are the user provided details about the product: {PRODUCT}."; |
| 1605 |
} |
| 1606 |
|
| 1607 |
// The custom instructions are persistent (Content SEO settings), so single-product |
| 1608 |
// generation gets them too. The bulk modal sends its own value to override them for |
| 1609 |
// that run (an explicit empty string means "none"). |
| 1610 |
$instructions = isset( $params['instructions'] ) |
| 1611 |
? trim( (string) $params['instructions'] ) |
| 1612 |
: trim( (string) $this->get_option( 'woo_assistant_instructions', '' ) ); |
| 1613 |
|
| 1614 |
if ( !empty( $instructions ) ) { |
| 1615 |
$prompt .= "\n\nHere are general instructions from the user: " . $instructions; |
| 1616 |
} |
| 1617 |
|
| 1618 |
$prompt .= "\n\nHere are the details extracted from the product data: " . json_encode( $product_data ); |
| 1619 |
|
| 1620 |
if ( $vision_result ) { |
| 1621 |
$prompt .= "\n\nAlso, here is what is seen in the product image: " . $vision_result; |
| 1622 |
} |
| 1623 |
|
| 1624 |
$prompt = apply_filters( 'mwseo_woo_product_prompt', $prompt ); |
| 1625 |
$prompt .= "Your reply must be a formatted JSON only. Use these keys: " . implode( ', ', $fields ) . "."; |
| 1626 |
|
| 1627 |
$prompt = str_replace( '{PRODUCT}', $params['product'] ?? '', $prompt ); |
| 1628 |
$prompt = str_replace( '{LANGUAGE}', $params['language'], $prompt ); |
| 1629 |
|
| 1630 |
$response = $mwai->simpleTextQuery( $prompt ); |
| 1631 |
|
| 1632 |
// Remove any "```json" or "```" from the response |
| 1633 |
$response = preg_replace( '/^```json\s*/', '', $response ); |
| 1634 |
$response = preg_replace( '/\s*```$/', '', $response ); |
| 1635 |
|
| 1636 |
// Ensure the response is valid JSON |
| 1637 |
$json = json_decode( $response, true ); |
| 1638 |
if ( json_last_error() !== JSON_ERROR_NONE ) { |
| 1639 |
$this->log( '⚠️ WooCommerce Assistant: Invalid JSON response from AI: ' . json_last_error_msg() ); |
| 1640 |
$this->log( 'Response: ' .$response ); |
| 1641 |
return false; |
| 1642 |
} |
| 1643 |
|
| 1644 |
// Only return the requested fields. |
| 1645 |
return array_intersect_key( $json, array_flip( $fields ) ); |
| 1646 |
} |
| 1647 |
|
| 1648 |
function apply_woocommerce_fields( $params ) |
| 1649 |
{ |
| 1650 |
$product_id = intval( $params['post_id'] ?? 0 ); |
| 1651 |
$product = get_post( $product_id ); |
| 1652 |
if ( !$product || $product->post_type !== 'product' ) { |
| 1653 |
throw new Exception( 'Product not found.' ); |
| 1654 |
} |
| 1655 |
|
| 1656 |
$update = [ 'ID' => $product_id ]; |
| 1657 |
if ( isset( $params['seo_title'] ) && trim( $params['seo_title'] ) !== '' ) { |
| 1658 |
$update['post_title'] = sanitize_text_field( $params['seo_title'] ); |
| 1659 |
} |
| 1660 |
if ( isset( $params['description'] ) && trim( $params['description'] ) !== '' ) { |
| 1661 |
$update['post_content'] = wp_kses_post( $params['description'] ); |
| 1662 |
} |
| 1663 |
if ( isset( $params['short_description'] ) && trim( $params['short_description'] ) !== '' ) { |
| 1664 |
$update['post_excerpt'] = wp_kses_post( $params['short_description'] ); |
| 1665 |
} |
| 1666 |
|
| 1667 |
if ( count( $update ) > 1 ) { |
| 1668 |
$result = wp_update_post( $update, true ); |
| 1669 |
if ( is_wp_error( $result ) ) { |
| 1670 |
throw new Exception( $result->get_error_message() ); |
| 1671 |
} |
| 1672 |
} |
| 1673 |
|
| 1674 |
if ( !empty( $params['tags'] ) ) { |
| 1675 |
$tags = is_array( $params['tags'] ) ? $params['tags'] : explode( ',', $params['tags'] ); |
| 1676 |
$tags = array_filter( array_map( function( $tag ) { |
| 1677 |
return sanitize_text_field( trim( $tag ) ); |
| 1678 |
}, $tags ) ); |
| 1679 |
if ( !empty( $tags ) ) { |
| 1680 |
// Append: existing tags are kept, already-present tags are not duplicated. |
| 1681 |
$result = wp_set_object_terms( $product_id, $tags, 'product_tag', true ); |
| 1682 |
if ( is_wp_error( $result ) ) { |
| 1683 |
throw new Exception( $result->get_error_message() ); |
| 1684 |
} |
| 1685 |
} |
| 1686 |
} |
| 1687 |
|
| 1688 |
return true; |
| 1689 |
} |
| 1690 |
|
| 1691 |
#endregion |
| 1692 |
|
| 1693 |
#region Utilities |
| 1694 |
|
| 1695 |
function import_yoast() { |
| 1696 |
// For all the selected post types, get the _yoast_wpseo_title & _yoast_wpseo_metadesc and import them to _kiss_seo_title & _kiss_seo_excerpt |
| 1697 |
// TODO: Migrate to _mwseo_title & _mwseo_excerpt |
| 1698 |
$post_types = $this->get_option( 'select_post_types', array( 'post ') ); |
| 1699 |
$posts = get_posts( array( |
| 1700 |
'post_type' => $post_types, |
| 1701 |
'posts_per_page' => -1, |
| 1702 |
'meta_query' => array( |
| 1703 |
array( |
| 1704 |
'key' => '_yoast_wpseo_title', |
| 1705 |
'compare' => 'EXISTS', |
| 1706 |
), |
| 1707 |
array( |
| 1708 |
'key' => '_yoast_wpseo_metadesc', |
| 1709 |
'compare' => 'EXISTS', |
| 1710 |
), |
| 1711 |
), |
| 1712 |
) ); |
| 1713 |
|
| 1714 |
foreach ( $posts as $post ) { |
| 1715 |
$yoast_title = get_post_meta( $post->ID, '_yoast_wpseo_title', true ); |
| 1716 |
$yoast_excerpt = get_post_meta( $post->ID, '_yoast_wpseo_metadesc', true ); |
| 1717 |
$yoast_keywords = get_post_meta( $post->ID, '_yoast_wpseo_focuskw', true ); |
| 1718 |
|
| 1719 |
if ( !empty( $yoast_title ) ) { |
| 1720 |
update_post_meta( $post->ID, $this->meta_key_seo_title, $yoast_title ); |
| 1721 |
} |
| 1722 |
if ( !empty( $yoast_excerpt ) ) { |
| 1723 |
update_post_meta( $post->ID, $this->meta_key_seo_excerpt, $yoast_excerpt ); |
| 1724 |
} |
| 1725 |
if ( !empty( $yoast_keywords ) ) { |
| 1726 |
$keywords = explode( ',', $yoast_keywords ); |
| 1727 |
$keywords = array_slice( $keywords, 0, 5 ); |
| 1728 |
|
| 1729 |
update_post_meta( $post->ID, $this->meta_key_seo_keywords, $keywords ); |
| 1730 |
} |
| 1731 |
} |
| 1732 |
|
| 1733 |
return array( |
| 1734 |
'posts' => count( $posts ), |
| 1735 |
'redirects' => 0, |
| 1736 |
); |
| 1737 |
} |
| 1738 |
|
| 1739 |
function get_seo_title( $post ) { |
| 1740 |
$seo_title = get_post_meta( $post->ID, $this->meta_key_seo_title, true ); |
| 1741 |
if ( empty( $seo_title ) ) { |
| 1742 |
$seo_title = $this->build_title( $post ); |
| 1743 |
} |
| 1744 |
return $seo_title; |
| 1745 |
} |
| 1746 |
|
| 1747 |
function set_seo_title( $post, $title ) { |
| 1748 |
if ( empty( $title ) ) { |
| 1749 |
return false; |
| 1750 |
} |
| 1751 |
$title = trim( strip_tags( $title ) ); |
| 1752 |
if ( empty( $title ) ) { |
| 1753 |
return false; |
| 1754 |
} |
| 1755 |
update_post_meta( $post->ID, $this->meta_key_seo_title, $title ); |
| 1756 |
return true; |
| 1757 |
} |
| 1758 |
|
| 1759 |
function get_seo_excerpt( $post ) { |
| 1760 |
$seo_excerpt = get_post_meta( $post->ID, $this->meta_key_seo_excerpt, true ); |
| 1761 |
if ( empty( $seo_excerpt ) ) { |
| 1762 |
$seo_excerpt = $this->build_excerpt( $post ); |
| 1763 |
} |
| 1764 |
return $seo_excerpt; |
| 1765 |
} |
| 1766 |
|
| 1767 |
function set_seo_excerpt( $post, $excerpt ) { |
| 1768 |
if ( empty( $excerpt ) ) { |
| 1769 |
return false; |
| 1770 |
} |
| 1771 |
$excerpt = trim( strip_tags( $excerpt ) ); |
| 1772 |
if ( empty( $excerpt ) ) { |
| 1773 |
return false; |
| 1774 |
} |
| 1775 |
update_post_meta( $post->ID, $this->meta_key_seo_excerpt, $excerpt ); |
| 1776 |
return true; |
| 1777 |
} |
| 1778 |
|
| 1779 |
/** |
| 1780 |
* Calculate effective display width of text for SERP. |
| 1781 |
* CJK characters (Chinese, Japanese, Korean) are roughly twice as wide as Latin characters |
| 1782 |
* in Google's search results, so they count as 2 units. This approximates the pixel-based |
| 1783 |
* measurement that Google actually uses for title/description truncation. |
| 1784 |
* |
| 1785 |
* @param string $text The text to measure |
| 1786 |
* @return int Display width in units (Latin chars = 1, CJK chars = 2) |
| 1787 |
*/ |
| 1788 |
function get_display_width( $text ) { |
| 1789 |
if ( empty( $text ) ) { |
| 1790 |
return 0; |
| 1791 |
} |
| 1792 |
$width = 0; |
| 1793 |
// Split into individual characters (UTF-8 safe) |
| 1794 |
$chars = preg_split( '//u', $text, -1, PREG_SPLIT_NO_EMPTY ); |
| 1795 |
foreach ( $chars as $char ) { |
| 1796 |
// CJK Unified Ideographs, Extension A, Hiragana, Katakana, Hangul Syllables |
| 1797 |
if ( preg_match( '/[\x{4E00}-\x{9FFF}\x{3400}-\x{4DBF}\x{3040}-\x{309F}\x{30A0}-\x{30FF}\x{AC00}-\x{D7AF}]/u', $char ) ) { |
| 1798 |
$width += 2; |
| 1799 |
} else { |
| 1800 |
$width += 1; |
| 1801 |
} |
| 1802 |
} |
| 1803 |
return $width; |
| 1804 |
} |
| 1805 |
|
| 1806 |
/** |
| 1807 |
* Check if text is predominantly CJK characters. |
| 1808 |
* |
| 1809 |
* @param string $text The text to check |
| 1810 |
* @param float $threshold Ratio threshold (default 0.5 = 50% CJK) |
| 1811 |
* @return bool True if text meets or exceeds CJK threshold |
| 1812 |
*/ |
| 1813 |
function is_mostly_cjk( $text, $threshold = 0.5 ) { |
| 1814 |
if ( empty( $text ) ) { |
| 1815 |
return false; |
| 1816 |
} |
| 1817 |
$text_length = mb_strlen( $text, 'UTF-8' ); |
| 1818 |
if ( $text_length === 0 ) { |
| 1819 |
return false; |
| 1820 |
} |
| 1821 |
// Count CJK characters |
| 1822 |
preg_match_all( '/[\x{4E00}-\x{9FFF}\x{3400}-\x{4DBF}\x{3040}-\x{309F}\x{30A0}-\x{30FF}\x{AC00}-\x{D7AF}]/u', $text, $matches ); |
| 1823 |
$cjk_count = count( $matches[0] ); |
| 1824 |
return ( $cjk_count / $text_length ) >= $threshold; |
| 1825 |
} |
| 1826 |
|
| 1827 |
function import_rank_math() { |
| 1828 |
// For all the selected post types, get the _rank_math_title & _rank_math_description and import them to _kiss_seo_title & _kiss_seo_excerpt |
| 1829 |
// TODO: Migrate to _mwseo_title & _mwseo_excerpt |
| 1830 |
$post_types = $this->get_option( 'select_post_types', array( 'post ') ); |
| 1831 |
$posts = get_posts( array( |
| 1832 |
'post_type' => $post_types, |
| 1833 |
'posts_per_page' => -1, |
| 1834 |
'meta_query' => array( |
| 1835 |
array( |
| 1836 |
'key' => 'rank_math_title', |
| 1837 |
'compare' => 'EXISTS', |
| 1838 |
), |
| 1839 |
array( |
| 1840 |
'key' => 'rank_math_description', |
| 1841 |
'compare' => 'EXISTS', |
| 1842 |
), |
| 1843 |
), |
| 1844 |
) ); |
| 1845 |
|
| 1846 |
foreach ( $posts as $post ) { |
| 1847 |
|
| 1848 |
$rank_math_title = get_post_meta( $post->ID, 'rank_math_title', true ); |
| 1849 |
$rank_math_excerpt = get_post_meta( $post->ID, 'rank_math_description', true ); |
| 1850 |
$rank_math_keywords = get_post_meta( $post->ID, 'rank_math_focus_keyword', true ); |
| 1851 |
|
| 1852 |
if ( !empty( $rank_math_title ) ) { |
| 1853 |
update_post_meta( $post->ID, $this->meta_key_seo_title, $rank_math_title ); |
| 1854 |
} |
| 1855 |
if ( !empty( $rank_math_excerpt ) ) { |
| 1856 |
update_post_meta( $post->ID, $this->meta_key_seo_excerpt, $rank_math_excerpt ); |
| 1857 |
} |
| 1858 |
if ( !empty( $rank_math_keywords ) ) { |
| 1859 |
$keywords = explode( ',', $rank_math_keywords ); |
| 1860 |
$keywords = array_slice( $keywords, 0, 5 ); |
| 1861 |
|
| 1862 |
update_post_meta( $post->ID, $this->meta_key_seo_keywords, $keywords ); |
| 1863 |
} |
| 1864 |
} |
| 1865 |
|
| 1866 |
// Also import Rank Math redirections that aren't already declared in SEO Engine. |
| 1867 |
$redirects = 0; |
| 1868 |
if ( $this->redirects_module ) { |
| 1869 |
$redirects = (int) $this->redirects_module->import_from_rank_math(); |
| 1870 |
} |
| 1871 |
|
| 1872 |
return array( |
| 1873 |
'posts' => count( $posts ), |
| 1874 |
'redirects' => $redirects, |
| 1875 |
); |
| 1876 |
} |
| 1877 |
|
| 1878 |
function get_language_name( $locale ) { |
| 1879 |
$locale_name_array = [ |
| 1880 |
'en_US' => 'English (US)', |
| 1881 |
'en_GB' => 'English (UK)', |
| 1882 |
'en_AU' => 'English (Australia)', |
| 1883 |
'en_CA' => 'English (Canada)', |
| 1884 |
'fr_FR' => 'French', |
| 1885 |
'fr_BE' => 'French (Belgium)', |
| 1886 |
'fr_CA' => 'French (Canada)', |
| 1887 |
'de_DE' => 'German', |
| 1888 |
'de_CH' => 'German (Switzerland)', |
| 1889 |
'de_AT' => 'German (Austria)', |
| 1890 |
'it_IT' => 'Italian', |
| 1891 |
'pt_PT' => 'Portuguese', |
| 1892 |
'pt_BR' => 'Portuguese (Brazil)', |
| 1893 |
'es_ES' => 'Spanish', |
| 1894 |
'es_MX' => 'Spanish (Mexico)', |
| 1895 |
'es_GT' => 'Spanish (Guatemala)', |
| 1896 |
'es_CL' => 'Spanish (Chile)', |
| 1897 |
'es_AR' => 'Spanish (Argentina)', |
| 1898 |
'es_CO' => 'Spanish (Colombia)', |
| 1899 |
'es_PE' => 'Spanish (Peru)', |
| 1900 |
'es_VE' => 'Spanish (Venezuela)', |
| 1901 |
'nl_NL' => 'Dutch', |
| 1902 |
'nl_BE' => 'Dutch (Belgium)', |
| 1903 |
'ru_RU' => 'Russian', |
| 1904 |
'pl_PL' => 'Polish', |
| 1905 |
'ja' => 'Japanese', |
| 1906 |
'zh_CN' => 'Chinese (China)', |
| 1907 |
'zh_TW' => 'Chinese (Taiwan)', |
| 1908 |
'ko_KR' => 'Korean', |
| 1909 |
'ar' => 'Arabic', |
| 1910 |
'he_IL' => 'Hebrew', |
| 1911 |
'id_ID' => 'Indonesian', |
| 1912 |
'ms_MY' => 'Malay', |
| 1913 |
'th' => 'Thai', |
| 1914 |
'vi' => 'Vietnamese', |
| 1915 |
'tr_TR' => 'Turkish', |
| 1916 |
'bg_BG' => 'Bulgarian', |
| 1917 |
'el' => 'Greek', |
| 1918 |
'da_DK' => 'Danish', |
| 1919 |
'fa_IR' => 'Persian', |
| 1920 |
'fi' => 'Finnish', |
| 1921 |
'hi_IN' => 'Hindi', |
| 1922 |
'hr' => 'Croatian', |
| 1923 |
'hu_HU' => 'Hungarian', |
| 1924 |
'nb_NO' => 'Norwegian (Bokmål)', |
| 1925 |
'ro_RO' => 'Romanian', |
| 1926 |
'sl_SI' => 'Slovenian', |
| 1927 |
'sv_SE' => 'Swedish', |
| 1928 |
'uk' => 'Ukrainian', |
| 1929 |
'cs_CZ' => 'Czech', |
| 1930 |
'sk_SK' => 'Slovak', |
| 1931 |
'lt_LT' => 'Lithuanian', |
| 1932 |
'mk_MK' => 'Macedonian', |
| 1933 |
'mg_MG' => 'Malagasy', |
| 1934 |
'ta_IN' => 'Tamil', |
| 1935 |
'tl' => 'Tagalog', |
| 1936 |
'az_AZ' => 'Azerbaijani', |
| 1937 |
'az_TR' => 'Azerbaijani (Turkey)' |
| 1938 |
]; |
| 1939 |
|
| 1940 |
if ( isset( $locale_name_array[ $locale ] ) ) { |
| 1941 |
return $locale_name_array[ $locale ]; |
| 1942 |
} |
| 1943 |
else { |
| 1944 |
return null; |
| 1945 |
} |
| 1946 |
} |
| 1947 |
|
| 1948 |
// Returns a human-readable language name for AI prompts. |
| 1949 |
// Falls back to the global 'language' option when no multilingual plugin is |
| 1950 |
// active or the post has no language assigned. |
| 1951 |
function get_post_language_name( $post_id ) { |
| 1952 |
$post_locale = null; |
| 1953 |
if ( function_exists( 'pll_get_post_language' ) ) { |
| 1954 |
$post_locale = pll_get_post_language( $post_id, 'locale' ); |
| 1955 |
} |
| 1956 |
elseif ( function_exists( 'wpml_get_language_information' ) ) { |
| 1957 |
$info = wpml_get_language_information( null, $post_id ); |
| 1958 |
if ( ! is_wp_error( $info ) && ! empty( $info['locale'] ) ) { |
| 1959 |
$post_locale = $info['locale']; |
| 1960 |
} |
| 1961 |
} |
| 1962 |
elseif ( function_exists( 'bogo_get_post_locale' ) ) { |
| 1963 |
// Bogo stores the locale in the _locale meta and falls back to the |
| 1964 |
// site default itself, so this is always a usable locale. |
| 1965 |
$post_locale = bogo_get_post_locale( $post_id ); |
| 1966 |
} |
| 1967 |
$name = null; |
| 1968 |
if ( $post_locale ) { |
| 1969 |
$name = $this->get_language_name( $post_locale ); |
| 1970 |
} |
| 1971 |
if ( !$name ) { |
| 1972 |
$name = $this->get_option( 'language', 'English' ); |
| 1973 |
} |
| 1974 |
// Escape hatch for setups we don't detect (custom or unsupported |
| 1975 |
// multilingual plugins): return the language AI should write in. |
| 1976 |
return apply_filters( 'mwseo_post_language_name', $name, $post_id, $post_locale ); |
| 1977 |
} |
| 1978 |
|
| 1979 |
/** |
| 1980 |
* The post's language as an identifier used to group/filter posts, or null |
| 1981 |
* when no multilingual plugin is active. Polylang and WPML return a short |
| 1982 |
* slug ("fr"); Bogo returns the full locale ("fr_FR") because two Bogo |
| 1983 |
* locales can share a slug and we need them to stay distinct. |
| 1984 |
*/ |
| 1985 |
function get_post_language_slug( $post_id ) { |
| 1986 |
if ( function_exists( 'pll_get_post_language' ) ) { |
| 1987 |
$slug = pll_get_post_language( $post_id, 'slug' ); |
| 1988 |
return $slug ? $slug : null; |
| 1989 |
} |
| 1990 |
if ( function_exists( 'wpml_get_language_information' ) ) { |
| 1991 |
$info = wpml_get_language_information( null, $post_id ); |
| 1992 |
return ( !is_wp_error( $info ) && !empty( $info['language_code'] ) ) ? $info['language_code'] : null; |
| 1993 |
} |
| 1994 |
if ( function_exists( 'bogo_get_post_locale' ) ) { |
| 1995 |
$locale = bogo_get_post_locale( $post_id ); |
| 1996 |
return $locale ? $locale : null; |
| 1997 |
} |
| 1998 |
return null; |
| 1999 |
} |
| 2000 |
|
| 2001 |
/** |
| 2002 |
* IDs of the post's translations. May include the post itself (callers merge |
| 2003 |
* it into an exclusion list anyway). |
| 2004 |
*/ |
| 2005 |
function get_post_translation_ids( $post_id ) { |
| 2006 |
if ( function_exists( 'pll_get_post_translations' ) ) { |
| 2007 |
$ids = pll_get_post_translations( $post_id ); |
| 2008 |
return is_array( $ids ) ? array_values( array_map( 'intval', $ids ) ) : array(); |
| 2009 |
} |
| 2010 |
if ( function_exists( 'wpml_get_language_information' ) ) { |
| 2011 |
global $sitepress; |
| 2012 |
$post = get_post( $post_id ); |
| 2013 |
if ( $sitepress && $post ) { |
| 2014 |
$trid = $sitepress->get_element_trid( $post_id, 'post_' . $post->post_type ); |
| 2015 |
$translations = $sitepress->get_element_translations( $trid, 'post_' . $post->post_type ); |
| 2016 |
if ( is_array( $translations ) ) { |
| 2017 |
return array_values( array_map( 'intval', wp_list_pluck( $translations, 'element_id' ) ) ); |
| 2018 |
} |
| 2019 |
} |
| 2020 |
return array(); |
| 2021 |
} |
| 2022 |
if ( function_exists( 'bogo_get_post_translations' ) ) { |
| 2023 |
// Bogo returns WP_Post objects keyed by locale, and excludes the post itself. |
| 2024 |
$posts = bogo_get_post_translations( $post_id ); |
| 2025 |
if ( is_array( $posts ) ) { |
| 2026 |
return array_values( array_map( function ( $p ) { |
| 2027 |
return isset( $p->ID ) ? (int) $p->ID : (int) $p; |
| 2028 |
}, $posts ) ); |
| 2029 |
} |
| 2030 |
return array(); |
| 2031 |
} |
| 2032 |
return array(); |
| 2033 |
} |
| 2034 |
|
| 2035 |
/** |
| 2036 |
* Restrict WP_Query args to one language. $language is an identifier from |
| 2037 |
* get_available_languages(); 'all' (or empty) means every language. |
| 2038 |
* |
| 2039 |
* Every query we run outside the admin has to go through this, even when no |
| 2040 |
* language is selected: Bogo hooks parse_query and narrows ANY query it sees |
| 2041 |
* down to get_locale() unless bogo_suppress_locale_query is set. REST requests |
| 2042 |
* are not is_admin(), so without that opt-out our lists, scans and sitemaps |
| 2043 |
* silently returned the site language only, and asking for another language |
| 2044 |
* could never match (Bogo's own WHERE was already ANDed in). |
| 2045 |
*/ |
| 2046 |
function apply_language_filter( $args, $language ) { |
| 2047 |
$is_bogo = function_exists( 'bogo_get_post_locale' ); |
| 2048 |
if ( $is_bogo ) { |
| 2049 |
$args['bogo_suppress_locale_query'] = true; |
| 2050 |
} |
| 2051 |
if ( empty( $language ) || $language === 'all' ) { |
| 2052 |
return $args; |
| 2053 |
} |
| 2054 |
if ( function_exists( 'pll_get_post_language' ) ) { |
| 2055 |
$args['tax_query'][] = array( |
| 2056 |
'taxonomy' => 'language', |
| 2057 |
'field' => 'slug', |
| 2058 |
'terms' => $language, |
| 2059 |
); |
| 2060 |
return $args; |
| 2061 |
} |
| 2062 |
if ( $is_bogo ) { |
| 2063 |
// Filter on Bogo's _locale meta rather than its own 'lang' query var: |
| 2064 |
// get_posts() defaults to suppress_filters, so the WHERE clause Bogo |
| 2065 |
// builds from 'lang' would never run for half of our queries. |
| 2066 |
$clause = array( |
| 2067 |
'relation' => 'OR', |
| 2068 |
array( 'key' => '_locale', 'value' => $language ), |
| 2069 |
); |
| 2070 |
// Posts in the site's default language often have no _locale row at |
| 2071 |
// all (Bogo only writes it for translations), so treat "missing" as |
| 2072 |
// the default locale. |
| 2073 |
$default = function_exists( 'bogo_get_default_locale' ) ? bogo_get_default_locale() : null; |
| 2074 |
if ( $default && $language === $default ) { |
| 2075 |
$clause[] = array( 'key' => '_locale', 'compare' => 'NOT EXISTS' ); |
| 2076 |
} |
| 2077 |
$args['meta_query'][] = $clause; |
| 2078 |
return $args; |
| 2079 |
} |
| 2080 |
return $args; |
| 2081 |
} |
| 2082 |
|
| 2083 |
/** |
| 2084 |
* Languages available on the site, as [ [ 'slug' => ..., 'name' => ... ] ]. |
| 2085 |
* Empty when no supported multilingual plugin is active. |
| 2086 |
*/ |
| 2087 |
function get_available_languages() { |
| 2088 |
$languages = array(); |
| 2089 |
if ( function_exists( 'pll_languages_list' ) ) { |
| 2090 |
$slugs = pll_languages_list(); |
| 2091 |
$names = pll_languages_list( array( 'fields' => 'name' ) ); |
| 2092 |
foreach ( (array) $slugs as $i => $slug ) { |
| 2093 |
$languages[] = array( 'slug' => $slug, 'name' => $names[ $i ] ?? $slug ); |
| 2094 |
} |
| 2095 |
return $languages; |
| 2096 |
} |
| 2097 |
if ( function_exists( 'bogo_available_locales' ) ) { |
| 2098 |
foreach ( (array) bogo_available_locales() as $locale ) { |
| 2099 |
$name = function_exists( 'bogo_get_language_native_name' ) |
| 2100 |
? bogo_get_language_native_name( $locale ) : ''; |
| 2101 |
$languages[] = array( 'slug' => $locale, 'name' => $name ? $name : $locale ); |
| 2102 |
} |
| 2103 |
return $languages; |
| 2104 |
} |
| 2105 |
return $languages; |
| 2106 |
} |
| 2107 |
|
| 2108 |
#endregion |
| 2109 |
|
| 2110 |
/** |
| 2111 |
* Redirect attachment URLs to their parent post URLs |
| 2112 |
*/ |
| 2113 |
public function redirect_attachment_to_parent( $link, $post_id ) { |
| 2114 |
$attachment = get_post( $post_id ); |
| 2115 |
if ( $attachment && $attachment->post_type === 'attachment' && $attachment->post_parent ) { |
| 2116 |
return get_permalink( $attachment->post_parent ); |
| 2117 |
} |
| 2118 |
return $link; |
| 2119 |
} |
| 2120 |
|
| 2121 |
/** |
| 2122 |
* Handle attachment page redirects to parent post |
| 2123 |
*/ |
| 2124 |
public function handle_attachment_redirect() |
| 2125 |
{ |
| 2126 |
if ( is_attachment() ) { |
| 2127 |
global $post; |
| 2128 |
|
| 2129 |
if ( $post && $post->post_parent ) { |
| 2130 |
wp_safe_redirect( esc_url( get_permalink( $post->post_parent ) ), 301 ) ; |
| 2131 |
} else { |
| 2132 |
wp_safe_redirect ( esc_url( home_url( '/' ) ), 301 ); |
| 2133 |
} |
| 2134 |
} |
| 2135 |
} |
| 2136 |
|
| 2137 |
#region Sitemap |
| 2138 |
|
| 2139 |
function generate_sitemap() { |
| 2140 |
if ( !isset( $this->sitemap_module ) ) { |
| 2141 |
return false; |
| 2142 |
} |
| 2143 |
|
| 2144 |
return $this->sitemap_module->create_sitemap(); |
| 2145 |
} |
| 2146 |
|
| 2147 |
|
| 2148 |
#endregion |
| 2149 |
|
| 2150 |
#region Robots.txt |
| 2151 |
|
| 2152 |
function get_robots_txt() { |
| 2153 |
if ( ! function_exists( 'get_home_path' ) ) { |
| 2154 |
require_once ABSPATH . 'wp-admin/includes/file.php'; |
| 2155 |
} |
| 2156 |
$home_path = get_home_path(); |
| 2157 |
|
| 2158 |
if ( ! is_writable( $home_path ) && ! empty( $_SERVER['DOCUMENT_ROOT'] ) ) { |
| 2159 |
$home_path = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR; |
| 2160 |
} |
| 2161 |
|
| 2162 |
$robots_path = $home_path . 'robots.txt'; |
| 2163 |
$source = 'none'; |
| 2164 |
|
| 2165 |
|
| 2166 |
if ( file_exists( $robots_path ) ) { |
| 2167 |
$robotsTxt = file_get_contents( $robots_path ); |
| 2168 |
$source = 'file'; |
| 2169 |
} else { |
| 2170 |
// Get the default WordPress robots.txt content |
| 2171 |
ob_start(); |
| 2172 |
do_robots(); |
| 2173 |
$robotsTxt = ob_get_clean(); |
| 2174 |
$source = 'default'; |
| 2175 |
} |
| 2176 |
|
| 2177 |
return [ |
| 2178 |
'content' => $robotsTxt, |
| 2179 |
'source' => $source, |
| 2180 |
'path' => $robots_path, |
| 2181 |
]; |
| 2182 |
} |
| 2183 |
|
| 2184 |
function set_robots_txt( $content ) { |
| 2185 |
if ( ! function_exists( 'get_home_path' ) ) { |
| 2186 |
require_once ABSPATH . 'wp-admin/includes/file.php'; |
| 2187 |
} |
| 2188 |
$home_path = get_home_path(); |
| 2189 |
|
| 2190 |
if ( ! is_writable( $home_path ) && ! empty( $_SERVER['DOCUMENT_ROOT'] ) ) { |
| 2191 |
$home_path = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR; |
| 2192 |
} |
| 2193 |
|
| 2194 |
$robots_file = $home_path . 'robots.txt'; |
| 2195 |
|
| 2196 |
if( !is_writable( $home_path ) ) { |
| 2197 |
return false; |
| 2198 |
} |
| 2199 |
|
| 2200 |
if( !file_exists( $robots_file ) ) { |
| 2201 |
ob_start(); |
| 2202 |
error_reporting( 0 ); |
| 2203 |
do_robots(); |
| 2204 |
$robots_content = ob_get_clean(); |
| 2205 |
|
| 2206 |
$f = fopen( $robots_file, 'x' ); |
| 2207 |
fwrite( $f, $robots_content ); |
| 2208 |
fclose( $f ); |
| 2209 |
} |
| 2210 |
|
| 2211 |
if ( !empty( $content ) ) { |
| 2212 |
$content = sanitize_textarea_field( wp_unslash( $content ) ); |
| 2213 |
$f = fopen( $robots_file, 'w+' ); |
| 2214 |
fwrite( $f, $content ); |
| 2215 |
fclose( $f ); |
| 2216 |
} |
| 2217 |
|
| 2218 |
return true; |
| 2219 |
} |
| 2220 |
|
| 2221 |
#endregion |
| 2222 |
|
| 2223 |
#region LLMs.txt |
| 2224 |
|
| 2225 |
function get_llms_txt() { |
| 2226 |
if ( ! function_exists( 'get_home_path' ) ) { |
| 2227 |
require_once ABSPATH . 'wp-admin/includes/file.php'; |
| 2228 |
} |
| 2229 |
$home_path = get_home_path(); |
| 2230 |
|
| 2231 |
if ( ! is_writable( $home_path ) && ! empty( $_SERVER['DOCUMENT_ROOT'] ) ) { |
| 2232 |
$home_path = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR; |
| 2233 |
} |
| 2234 |
|
| 2235 |
$llms_path = $home_path . 'llms.txt'; |
| 2236 |
$source = 'none'; |
| 2237 |
|
| 2238 |
if ( file_exists( $llms_path ) ) { |
| 2239 |
$llmsTxt = file_get_contents( $llms_path ); |
| 2240 |
$source = 'file'; |
| 2241 |
} else { |
| 2242 |
$llmsTxt = $this->get_template_llms(); |
| 2243 |
$source = 'template'; |
| 2244 |
} |
| 2245 |
|
| 2246 |
return [ |
| 2247 |
'content' => $llmsTxt, |
| 2248 |
'source' => $source, |
| 2249 |
'path' => $llms_path, |
| 2250 |
]; |
| 2251 |
} |
| 2252 |
|
| 2253 |
function get_template_llms() { |
| 2254 |
|
| 2255 |
$site_url = get_site_url(); |
| 2256 |
$site_name = get_bloginfo( 'name' ); |
| 2257 |
$site_desc = get_bloginfo( 'description' ); |
| 2258 |
|
| 2259 |
$summary = !empty( $site_desc ) |
| 2260 |
? $site_desc |
| 2261 |
: 'Short summary describing what this site is about.'; |
| 2262 |
|
| 2263 |
$lines = []; |
| 2264 |
$lines[] = '# ' . $site_name; |
| 2265 |
$lines[] = ''; |
| 2266 |
$lines[] = '> ' . $summary; |
| 2267 |
$lines[] = ''; |
| 2268 |
|
| 2269 |
// Pages section: front/blog page + top-level pages. |
| 2270 |
$page_entries = []; |
| 2271 |
$front_id = (int) get_option( 'page_on_front' ); |
| 2272 |
$blog_id = (int) get_option( 'page_for_posts' ); |
| 2273 |
$seen = []; |
| 2274 |
$push_page = function( $p ) use ( &$page_entries, &$seen ) { |
| 2275 |
if ( !$p || $p->post_status !== 'publish' ) return; |
| 2276 |
$url = get_permalink( $p ); |
| 2277 |
if ( isset( $seen[ $url ] ) ) return; |
| 2278 |
$seen[ $url ] = true; |
| 2279 |
$title = !empty( $p->post_title ) ? $p->post_title : $url; |
| 2280 |
$page_entries[] = '* [' . $title . '](' . $url . '): Add a one-line description for this page.'; |
| 2281 |
}; |
| 2282 |
if ( $front_id ) $push_page( get_post( $front_id ) ); |
| 2283 |
if ( $blog_id && $blog_id !== $front_id ) $push_page( get_post( $blog_id ) ); |
| 2284 |
$top_pages = get_posts( [ |
| 2285 |
'post_type' => 'page', |
| 2286 |
'post_status' => 'publish', |
| 2287 |
'posts_per_page' => 5, |
| 2288 |
'orderby' => 'menu_order title', |
| 2289 |
'order' => 'ASC', |
| 2290 |
'post_parent' => 0, |
| 2291 |
] ); |
| 2292 |
foreach ( $top_pages as $p ) $push_page( $p ); |
| 2293 |
|
| 2294 |
if ( !empty( $page_entries ) ) { |
| 2295 |
$lines[] = '## Pages'; |
| 2296 |
foreach ( $page_entries as $e ) $lines[] = $e; |
| 2297 |
$lines[] = ''; |
| 2298 |
} |
| 2299 |
|
| 2300 |
// Articles section: most recent published posts. |
| 2301 |
$recent = wp_get_recent_posts( [ |
| 2302 |
'numberposts' => 5, |
| 2303 |
'post_status' => 'publish', |
| 2304 |
], OBJECT ); |
| 2305 |
if ( !empty( $recent ) ) { |
| 2306 |
$lines[] = '## Articles'; |
| 2307 |
foreach ( $recent as $p ) { |
| 2308 |
$url = get_permalink( $p ); |
| 2309 |
if ( isset( $seen[ $url ] ) ) continue; |
| 2310 |
$seen[ $url ] = true; |
| 2311 |
$title = !empty( $p->post_title ) ? $p->post_title : $url; |
| 2312 |
$lines[] = '* [' . $title . '](' . $url . '): Add a one-line description for this article.'; |
| 2313 |
} |
| 2314 |
$lines[] = ''; |
| 2315 |
} |
| 2316 |
|
| 2317 |
// Always include an Optional section so users see the spec pattern. |
| 2318 |
if ( empty( $page_entries ) && empty( $recent ) ) { |
| 2319 |
$lines[] = '## Pages'; |
| 2320 |
$lines[] = '* [Page Title](' . $site_url . '/page-url/): Add a one-line description for this page.'; |
| 2321 |
$lines[] = ''; |
| 2322 |
$lines[] = '## Articles'; |
| 2323 |
$lines[] = '* [Article Title](' . $site_url . '/post-url/): Add a one-line description for this article.'; |
| 2324 |
$lines[] = ''; |
| 2325 |
} |
| 2326 |
|
| 2327 |
$lines[] = '## Optional'; |
| 2328 |
$lines[] = '* [Lower-priority resource](' . $site_url . '/optional-url/): Content that can be skipped when context is limited.'; |
| 2329 |
$lines[] = ''; |
| 2330 |
|
| 2331 |
return implode( "\n", $lines ); |
| 2332 |
|
| 2333 |
} |
| 2334 |
|
| 2335 |
function set_llms_txt( $content ) { |
| 2336 |
if ( ! function_exists( 'get_home_path' ) ) { |
| 2337 |
require_once ABSPATH . 'wp-admin/includes/file.php'; |
| 2338 |
} |
| 2339 |
$home_path = get_home_path(); |
| 2340 |
|
| 2341 |
if ( ! is_writable( $home_path ) && ! empty( $_SERVER['DOCUMENT_ROOT'] ) ) { |
| 2342 |
$home_path = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR; |
| 2343 |
} |
| 2344 |
|
| 2345 |
$llms_path = $home_path . 'llms.txt'; |
| 2346 |
|
| 2347 |
if( !is_writable( $home_path ) ) { |
| 2348 |
return false; |
| 2349 |
} |
| 2350 |
|
| 2351 |
if ( empty( $content ) ) { |
| 2352 |
$content = $this->get_template_llms(); |
| 2353 |
} |
| 2354 |
|
| 2355 |
$site_url = get_site_url(); |
| 2356 |
$site_name = get_bloginfo( 'name' ); |
| 2357 |
|
| 2358 |
$content = str_replace( '{SITE_URL}', $site_url, $content ); |
| 2359 |
$content = str_replace( '{SITE_NAME}', $site_name, $content ); |
| 2360 |
|
| 2361 |
// Handle UTF-8 BOM for proper browser encoding detection |
| 2362 |
$utf8_bom = "\xEF\xBB\xBF"; |
| 2363 |
if ( substr( $content, 0, 3 ) === $utf8_bom ) { |
| 2364 |
$content = substr( $content, 3 ); |
| 2365 |
} |
| 2366 |
|
| 2367 |
$content = $utf8_bom . $content; |
| 2368 |
|
| 2369 |
if ( !empty( $content ) ) { |
| 2370 |
$content = sanitize_textarea_field( wp_unslash( $content ) ); |
| 2371 |
$f = fopen( $llms_path, 'w+' ); |
| 2372 |
fwrite( $f, $content ); |
| 2373 |
fclose( $f ); |
| 2374 |
} |
| 2375 |
|
| 2376 |
return true; |
| 2377 |
} |
| 2378 |
|
| 2379 |
function delete_llms_txt() { |
| 2380 |
if ( ! function_exists( 'get_home_path' ) ) { |
| 2381 |
require_once ABSPATH . 'wp-admin/includes/file.php'; |
| 2382 |
} |
| 2383 |
$home_path = get_home_path(); |
| 2384 |
|
| 2385 |
if ( ! is_writable( $home_path ) && ! empty( $_SERVER['DOCUMENT_ROOT'] ) ) { |
| 2386 |
$home_path = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR; |
| 2387 |
} |
| 2388 |
|
| 2389 |
$llms_path = $home_path . 'llms.txt'; |
| 2390 |
|
| 2391 |
if ( ! file_exists( $llms_path ) ) { |
| 2392 |
return false; |
| 2393 |
} |
| 2394 |
|
| 2395 |
if ( ! is_writable( $llms_path ) ) { |
| 2396 |
return false; |
| 2397 |
} |
| 2398 |
|
| 2399 |
return @unlink( $llms_path ); |
| 2400 |
} |
| 2401 |
|
| 2402 |
#endregion |
| 2403 |
|
| 2404 |
#region Performance Insights |
| 2405 |
|
| 2406 |
function get_speed_and_vitals( $post_id ) { |
| 2407 |
$result = $this->insights_module->get_insights_for_post( $post_id ); |
| 2408 |
|
| 2409 |
return $result; |
| 2410 |
} |
| 2411 |
|
| 2412 |
function get_last_insights() { |
| 2413 |
$last_insights = $this->insights_module->get_last_insights(); |
| 2414 |
if ( !is_array( $last_insights ) ) { |
| 2415 |
return []; |
| 2416 |
} |
| 2417 |
|
| 2418 |
return $last_insights; |
| 2419 |
} |
| 2420 |
|
| 2421 |
#endregion |
| 2422 |
|
| 2423 |
#region Analytics |
| 2424 |
|
| 2425 |
function track_current_visit() { |
| 2426 |
if ( !isset( $this->analytics_module ) ) { |
| 2427 |
return; |
| 2428 |
} |
| 2429 |
|
| 2430 |
// Only track single posts and pages |
| 2431 |
if ( is_singular() ) { |
| 2432 |
global $post; |
| 2433 |
if ( $post && $post->ID ) { |
| 2434 |
$this->analytics_module->track_visit( $post->ID ); |
| 2435 |
} |
| 2436 |
} |
| 2437 |
} |
| 2438 |
|
| 2439 |
#region AI Visibility delegators |
| 2440 |
|
| 2441 |
// AI Visibility is a Pro module (premium/aivisibility.php); it simply does not |
| 2442 |
// exist in the free build, so this returns null there. |
| 2443 |
function ai_visibility() { |
| 2444 |
return ( $this->pro && isset( $this->pro->ai_visibility ) ) ? $this->pro->ai_visibility : null; |
| 2445 |
} |
| 2446 |
|
| 2447 |
#endregion |
| 2448 |
|
| 2449 |
// TODO [2025]: Refactor to unified analytics provider interface |
| 2450 |
function get_analytics_data( $args = array() ) { |
| 2451 |
if ( !isset( $this->analytics_module ) ) { |
| 2452 |
return array(); |
| 2453 |
} |
| 2454 |
|
| 2455 |
return $this->analytics_module->get_analytics_data( $args ); |
| 2456 |
} |
| 2457 |
|
| 2458 |
// TODO [2025]: Refactor to unified analytics provider interface |
| 2459 |
function get_analytics_summary( $start_date = null, $end_date = null ) { |
| 2460 |
if ( !isset( $this->analytics_module ) ) { |
| 2461 |
return array(); |
| 2462 |
} |
| 2463 |
|
| 2464 |
return $this->analytics_module->get_analytics_summary( $start_date, $end_date ); |
| 2465 |
} |
| 2466 |
|
| 2467 |
function get_analytics_realtime_data() { |
| 2468 |
if ( !isset( $this->analytics_module ) ) { |
| 2469 |
return array(); |
| 2470 |
} |
| 2471 |
|
| 2472 |
return $this->analytics_module->get_realtime_data(); |
| 2473 |
} |
| 2474 |
|
| 2475 |
// TODO [2025]: Refactor to unified analytics provider interface |
| 2476 |
function get_top_posts( $args = array() ) { |
| 2477 |
if ( !isset( $this->analytics_module ) ) { |
| 2478 |
return array(); |
| 2479 |
} |
| 2480 |
|
| 2481 |
return $this->analytics_module->get_top_posts( $args ); |
| 2482 |
} |
| 2483 |
|
| 2484 |
function get_ai_agents_summary( $start_date = null, $end_date = null ) { |
| 2485 |
if ( !isset( $this->analytics_module ) ) { |
| 2486 |
return array(); |
| 2487 |
} |
| 2488 |
|
| 2489 |
return $this->analytics_module->get_ai_agents_summary( $start_date, $end_date ); |
| 2490 |
} |
| 2491 |
|
| 2492 |
function get_ai_agent_details( $bot_name, $start_date = null, $end_date = null ) { |
| 2493 |
if ( !isset( $this->analytics_module ) ) { |
| 2494 |
return array(); |
| 2495 |
} |
| 2496 |
|
| 2497 |
return $this->analytics_module->get_ai_agent_details( $bot_name, $start_date, $end_date ); |
| 2498 |
} |
| 2499 |
|
| 2500 |
function get_ai_agents_by_post( $post_id, $days = 30 ) { |
| 2501 |
if ( !isset( $this->analytics_module ) ) { |
| 2502 |
return array( |
| 2503 |
'openai' => 0, |
| 2504 |
'anthropic' => 0, |
| 2505 |
'google' => 0, |
| 2506 |
'others' => 0 |
| 2507 |
); |
| 2508 |
} |
| 2509 |
|
| 2510 |
return $this->analytics_module->get_ai_agents_by_post( $post_id, $days ); |
| 2511 |
} |
| 2512 |
|
| 2513 |
function get_ai_bots_totals_by_posts( $post_ids, $days = 30 ) { |
| 2514 |
if ( !isset( $this->analytics_module ) ) { |
| 2515 |
return array(); |
| 2516 |
} |
| 2517 |
return $this->analytics_module->get_ai_bots_totals_by_posts( $post_ids, $days ); |
| 2518 |
} |
| 2519 |
|
| 2520 |
function query_bot_traffic( $args = array() ) { |
| 2521 |
if ( !isset( $this->analytics_module ) ) { |
| 2522 |
return array(); |
| 2523 |
} |
| 2524 |
|
| 2525 |
return $this->analytics_module->query_bot_traffic( $args ); |
| 2526 |
} |
| 2527 |
|
| 2528 |
function get_bots_by_type( $type ) { |
| 2529 |
if ( !isset( $this->analytics_module ) ) { |
| 2530 |
return array(); |
| 2531 |
} |
| 2532 |
|
| 2533 |
return $this->analytics_module->get_bots_by_type( $type ); |
| 2534 |
} |
| 2535 |
|
| 2536 |
function rank_posts_for_bots( $args = array() ) { |
| 2537 |
if ( !isset( $this->analytics_module ) ) { |
| 2538 |
return array(); |
| 2539 |
} |
| 2540 |
|
| 2541 |
return $this->analytics_module->rank_posts_for_bots( $args ); |
| 2542 |
} |
| 2543 |
|
| 2544 |
function get_bot_profile( $bot_name, $start_date = null, $end_date = null ) { |
| 2545 |
if ( !isset( $this->analytics_module ) ) { |
| 2546 |
return array(); |
| 2547 |
} |
| 2548 |
|
| 2549 |
return $this->analytics_module->get_bot_profile( $bot_name, $start_date, $end_date ); |
| 2550 |
} |
| 2551 |
|
| 2552 |
function compare_bot_periods( $args = array() ) { |
| 2553 |
if ( !isset( $this->analytics_module ) ) { |
| 2554 |
return array(); |
| 2555 |
} |
| 2556 |
|
| 2557 |
return $this->analytics_module->compare_bot_periods( $args ); |
| 2558 |
} |
| 2559 |
|
| 2560 |
function get_bot_mix( $args = array() ) { |
| 2561 |
if ( !isset( $this->analytics_module ) ) { |
| 2562 |
return array(); |
| 2563 |
} |
| 2564 |
|
| 2565 |
return $this->analytics_module->get_bot_mix( $args ); |
| 2566 |
} |
| 2567 |
|
| 2568 |
#endregion |
| 2569 |
|
| 2570 |
#region Google Analytics |
| 2571 |
function get_google_analytics_state() { |
| 2572 |
if ( !class_exists( 'Meow_MWSEO_Modules_GoogleAnalytics' ) ) { |
| 2573 |
return []; |
| 2574 |
} |
| 2575 |
|
| 2576 |
// Get property_id with fallback to first element of property_ids array |
| 2577 |
$property_id = $this->get_option( 'google_analytics_property_id', '' ); |
| 2578 |
if ( empty( $property_id ) ) { |
| 2579 |
$property_ids = $this->get_option( 'google_analytics_property_ids', [] ); |
| 2580 |
if ( !empty( $property_ids ) && is_array( $property_ids ) ) { |
| 2581 |
$property_id = $property_ids[0]; |
| 2582 |
$this->update_option( 'google_analytics_property_id', $property_id ); |
| 2583 |
} |
| 2584 |
} |
| 2585 |
|
| 2586 |
return [ |
| 2587 |
'redirect_url' => $this->get_google_redirect_url(), |
| 2588 |
'property_id' => $property_id, |
| 2589 |
]; |
| 2590 |
} |
| 2591 |
|
| 2592 |
|
| 2593 |
function unlink_google_analytics() { |
| 2594 |
if ( !isset( $this->googleanalytics_module ) ) { |
| 2595 |
return false; |
| 2596 |
} |
| 2597 |
|
| 2598 |
return $this->googleanalytics_module->unlink(); |
| 2599 |
} |
| 2600 |
|
| 2601 |
function get_is_authenticated() { |
| 2602 |
if ( !isset( $this->googleanalytics_module ) ) { |
| 2603 |
return false; |
| 2604 |
} |
| 2605 |
|
| 2606 |
// Refresh the options |
| 2607 |
$this->googleanalytics_module->init(); |
| 2608 |
|
| 2609 |
return $this->googleanalytics_module->is_authenticated(); |
| 2610 |
} |
| 2611 |
|
| 2612 |
function get_google_auth_url() { |
| 2613 |
if ( !isset( $this->googleanalytics_module ) ) { |
| 2614 |
return ''; |
| 2615 |
} |
| 2616 |
|
| 2617 |
// Refresh the options |
| 2618 |
$this->googleanalytics_module->init(); |
| 2619 |
|
| 2620 |
return $this->googleanalytics_module->get_auth_url(); |
| 2621 |
} |
| 2622 |
|
| 2623 |
function get_google_redirect_url() { |
| 2624 |
if ( !isset( $this->googleanalytics_module ) ) { |
| 2625 |
return ''; |
| 2626 |
} |
| 2627 |
|
| 2628 |
return $this->googleanalytics_module->get_redirect_url(); |
| 2629 |
} |
| 2630 |
|
| 2631 |
#endregion |
| 2632 |
|
| 2633 |
#region Google Search Console |
| 2634 |
|
| 2635 |
function get_google_search_console_state() { |
| 2636 |
if ( !$this->pro || !$this->pro->search_console ) { |
| 2637 |
return []; |
| 2638 |
} |
| 2639 |
$gsc = $this->pro->search_console; |
| 2640 |
return [ |
| 2641 |
'redirect_url' => $gsc->get_redirect_url(), |
| 2642 |
'property' => $gsc->get_property(), |
| 2643 |
'tracked' => $gsc->get_tracked_properties(), |
| 2644 |
'is_authenticated' => $gsc->is_authenticated(), |
| 2645 |
]; |
| 2646 |
} |
| 2647 |
|
| 2648 |
function toggle_gsc_tracked_property( $property, $tracked ) { |
| 2649 |
if ( !$this->pro || !$this->pro->search_console ) { |
| 2650 |
return false; |
| 2651 |
} |
| 2652 |
return $this->pro->search_console->toggle_tracked_property( $property, $tracked ); |
| 2653 |
} |
| 2654 |
|
| 2655 |
function get_gsc_summary( $args = [] ) { |
| 2656 |
if ( !$this->pro || !$this->pro->search_console ) return []; |
| 2657 |
return $this->pro->search_console->get_summary( $args ); |
| 2658 |
} |
| 2659 |
|
| 2660 |
function get_gsc_timeseries( $args = [] ) { |
| 2661 |
if ( !$this->pro || !$this->pro->search_console ) return []; |
| 2662 |
return $this->pro->search_console->get_timeseries( $args ); |
| 2663 |
} |
| 2664 |
|
| 2665 |
function get_gsc_page_movers( $args = [] ) { |
| 2666 |
if ( !$this->pro || !$this->pro->search_console ) return []; |
| 2667 |
return $this->pro->search_console->get_page_movers( $args ); |
| 2668 |
} |
| 2669 |
|
| 2670 |
function get_gsc_quick_wins( $args = [] ) { |
| 2671 |
if ( !$this->pro || !$this->pro->search_console ) return []; |
| 2672 |
return $this->pro->search_console->get_quick_wins( $args ); |
| 2673 |
} |
| 2674 |
|
| 2675 |
function get_gsc_top_pages( $args = [] ) { |
| 2676 |
if ( !$this->pro || !$this->pro->search_console ) return []; |
| 2677 |
return $this->pro->search_console->get_top_pages( $args ); |
| 2678 |
} |
| 2679 |
|
| 2680 |
function get_gsc_top_queries( $args = [] ) { |
| 2681 |
if ( !$this->pro || !$this->pro->search_console ) return []; |
| 2682 |
return $this->pro->search_console->get_top_queries( $args ); |
| 2683 |
} |
| 2684 |
|
| 2685 |
function get_gsc_post_metrics_map( $args = [] ) { |
| 2686 |
if ( !$this->pro || !$this->pro->search_console ) return []; |
| 2687 |
return $this->pro->search_console->get_post_metrics_map( $args ); |
| 2688 |
} |
| 2689 |
|
| 2690 |
function estimate_gsc_quick_win_potential( $impressions, $position ) { |
| 2691 |
if ( !$this->pro || !$this->pro->search_console ) return 0; |
| 2692 |
return $this->pro->search_console->estimate_quick_win_potential( $impressions, $position ); |
| 2693 |
} |
| 2694 |
|
| 2695 |
function get_gsc_search_breakdown( $args = [] ) { |
| 2696 |
if ( !$this->pro || !$this->pro->search_console ) return [ 'top_queries' => [], 'top_pages' => [] ]; |
| 2697 |
return $this->pro->search_console->get_search_breakdown( $args ); |
| 2698 |
} |
| 2699 |
|
| 2700 |
function get_gsc_pages_with_issues( $args = [] ) { |
| 2701 |
if ( !$this->pro || !$this->pro->search_console ) return []; |
| 2702 |
return $this->pro->search_console->get_pages_with_issues( $args ); |
| 2703 |
} |
| 2704 |
|
| 2705 |
function get_gsc_post_pulse( $post_id, $days = 28 ) { |
| 2706 |
if ( !$this->pro || !$this->pro->search_console ) return null; |
| 2707 |
return $this->pro->search_console->get_post_pulse( (int) $post_id, $days ); |
| 2708 |
} |
| 2709 |
|
| 2710 |
function get_gsc_auth_url() { |
| 2711 |
if ( !$this->pro || !$this->pro->search_console ) { |
| 2712 |
return ''; |
| 2713 |
} |
| 2714 |
$this->pro->search_console->init(); |
| 2715 |
return $this->pro->search_console->get_auth_url(); |
| 2716 |
} |
| 2717 |
|
| 2718 |
function is_gsc_authenticated() { |
| 2719 |
if ( !$this->pro || !$this->pro->search_console ) { |
| 2720 |
return false; |
| 2721 |
} |
| 2722 |
$this->pro->search_console->init(); |
| 2723 |
return $this->pro->search_console->is_authenticated(); |
| 2724 |
} |
| 2725 |
|
| 2726 |
function unlink_gsc() { |
| 2727 |
if ( !$this->pro || !$this->pro->search_console ) { |
| 2728 |
return false; |
| 2729 |
} |
| 2730 |
return $this->pro->search_console->unlink(); |
| 2731 |
} |
| 2732 |
|
| 2733 |
function list_gsc_properties() { |
| 2734 |
if ( !$this->pro || !$this->pro->search_console ) { |
| 2735 |
return []; |
| 2736 |
} |
| 2737 |
$properties = $this->pro->search_console->list_properties(); |
| 2738 |
return is_array( $properties ) ? $properties : []; |
| 2739 |
} |
| 2740 |
|
| 2741 |
function set_gsc_property( $property ) { |
| 2742 |
if ( !$this->pro || !$this->pro->search_console ) { |
| 2743 |
return false; |
| 2744 |
} |
| 2745 |
return $this->pro->search_console->set_property( $property ); |
| 2746 |
} |
| 2747 |
|
| 2748 |
#endregion |
| 2749 |
|
| 2750 |
// TODO [2025]: Refactor to unified analytics provider interface |
| 2751 |
function get_google_analytics_data( $args = array() ) { |
| 2752 |
if ( !isset( $this->googleanalytics_module ) ) { |
| 2753 |
return array(); |
| 2754 |
} |
| 2755 |
|
| 2756 |
return $this->googleanalytics_module->get_analytics_data( $args ); |
| 2757 |
} |
| 2758 |
|
| 2759 |
function get_post_analytics( $post_id, $page_path, $start_date = null, $end_date = null ) { |
| 2760 |
if ( !isset( $this->analytics_module ) ) { |
| 2761 |
return array(); |
| 2762 |
} |
| 2763 |
|
| 2764 |
return $this->analytics_module->get_post_analytics( $post_id, $page_path, $start_date, $end_date ); |
| 2765 |
} |
| 2766 |
|
| 2767 |
function get_posts_visitor_series( $post_ids, $days = 30 ) { |
| 2768 |
if ( !isset( $this->analytics_module ) ) { |
| 2769 |
return array(); |
| 2770 |
} |
| 2771 |
return $this->analytics_module->get_posts_visitor_series( $post_ids, $days ); |
| 2772 |
} |
| 2773 |
|
| 2774 |
function get_posts_visitor_totals( $post_ids, $days = 30 ) { |
| 2775 |
if ( !isset( $this->analytics_module ) ) { |
| 2776 |
return array(); |
| 2777 |
} |
| 2778 |
return $this->analytics_module->get_posts_visitor_totals( $post_ids, $days ); |
| 2779 |
} |
| 2780 |
|
| 2781 |
function get_google_analytics_pages_daily( $start_date = null, $end_date = null, $paths = null ) { |
| 2782 |
if ( !isset( $this->googleanalytics_module ) ) { |
| 2783 |
return array(); |
| 2784 |
} |
| 2785 |
$this->googleanalytics_module->init(); |
| 2786 |
return $this->googleanalytics_module->get_pages_daily_visitors( $start_date, $end_date, $paths ); |
| 2787 |
} |
| 2788 |
|
| 2789 |
function get_google_analytics_pages_totals( $start_date = null, $end_date = null ) { |
| 2790 |
if ( !isset( $this->googleanalytics_module ) ) { |
| 2791 |
return array(); |
| 2792 |
} |
| 2793 |
$this->googleanalytics_module->init(); |
| 2794 |
return $this->googleanalytics_module->get_pages_total_visitors( $start_date, $end_date ); |
| 2795 |
} |
| 2796 |
|
| 2797 |
// TODO [2025]: Refactor to unified analytics provider interface |
| 2798 |
function get_google_analytics_summary( $start_date = null, $end_date = null ) { |
| 2799 |
if ( !isset( $this->googleanalytics_module ) ) { |
| 2800 |
return array(); |
| 2801 |
} |
| 2802 |
|
| 2803 |
return $this->googleanalytics_module->get_analytics_summary( $start_date, $end_date ); |
| 2804 |
} |
| 2805 |
|
| 2806 |
function get_google_analytics_post_analytics( $page_path, $start_date = null, $end_date = null ) { |
| 2807 |
if ( !isset( $this->googleanalytics_module ) ) { |
| 2808 |
return array(); |
| 2809 |
} |
| 2810 |
|
| 2811 |
return $this->googleanalytics_module->get_post_analytics( $page_path, $start_date, $end_date ); |
| 2812 |
} |
| 2813 |
|
| 2814 |
// TODO [2025]: Refactor to unified analytics provider interface |
| 2815 |
function get_google_analytics_top_posts( $args = array() ) { |
| 2816 |
if ( !isset( $this->googleanalytics_module ) ) { |
| 2817 |
return array(); |
| 2818 |
} |
| 2819 |
|
| 2820 |
return $this->googleanalytics_module->get_top_posts( $args ); |
| 2821 |
} |
| 2822 |
|
| 2823 |
// TODO [2025]: Refactor to unified analytics provider interface |
| 2824 |
function get_google_analytics_realtime_data() { |
| 2825 |
if ( !isset( $this->googleanalytics_module ) ) { |
| 2826 |
return array(); |
| 2827 |
} |
| 2828 |
|
| 2829 |
return $this->googleanalytics_module->get_realtime_data(); |
| 2830 |
} |
| 2831 |
|
| 2832 |
// Plausible Analytics |
| 2833 |
|
| 2834 |
// TODO [2025]: Refactor to unified analytics provider interface |
| 2835 |
function get_plausible_analytics_data( $args = array() ) { |
| 2836 |
if ( !isset( $this->plausibleanalytics_module ) ) { |
| 2837 |
return array(); |
| 2838 |
} |
| 2839 |
|
| 2840 |
return $this->plausibleanalytics_module->get_data( $args ); |
| 2841 |
} |
| 2842 |
|
| 2843 |
// TODO [2025]: Refactor to unified analytics provider interface |
| 2844 |
function get_plausible_analytics_summary( $start_date = null, $end_date = null ) { |
| 2845 |
if ( !isset( $this->plausibleanalytics_module ) ) { |
| 2846 |
return array(); |
| 2847 |
} |
| 2848 |
|
| 2849 |
return $this->plausibleanalytics_module->get_summary( $start_date, $end_date ); |
| 2850 |
} |
| 2851 |
|
| 2852 |
function get_plausible_analytics_post_analytics( $page_path, $start_date = null, $end_date = null ) { |
| 2853 |
if ( !isset( $this->plausibleanalytics_module ) ) { |
| 2854 |
return array(); |
| 2855 |
} |
| 2856 |
|
| 2857 |
return $this->plausibleanalytics_module->get_post_analytics( $page_path, $start_date, $end_date ); |
| 2858 |
} |
| 2859 |
|
| 2860 |
function get_plausible_analytics_realtime_data() { |
| 2861 |
if ( !isset( $this->plausibleanalytics_module ) ) { |
| 2862 |
return array(); |
| 2863 |
} |
| 2864 |
|
| 2865 |
return $this->plausibleanalytics_module->get_realtime_data(); |
| 2866 |
} |
| 2867 |
|
| 2868 |
// TODO [2025]: Refactor to unified analytics provider interface |
| 2869 |
function get_plausible_analytics_top_posts( $start_date = null, $end_date = null, $limit = 10 ) { |
| 2870 |
if ( !isset( $this->plausibleanalytics_module ) ) { |
| 2871 |
return array(); |
| 2872 |
} |
| 2873 |
|
| 2874 |
return $this->plausibleanalytics_module->get_top_posts( $start_date, $end_date, $limit ); |
| 2875 |
} |
| 2876 |
|
| 2877 |
// Matomo Analytics |
| 2878 |
|
| 2879 |
function get_matomo_analytics_data( $args = array() ) { |
| 2880 |
if ( !isset( $this->matomoanalytics_module ) ) { |
| 2881 |
return array(); |
| 2882 |
} |
| 2883 |
|
| 2884 |
return $this->matomoanalytics_module->get_data( $args ); |
| 2885 |
} |
| 2886 |
|
| 2887 |
function get_matomo_analytics_summary( $start_date = null, $end_date = null ) { |
| 2888 |
if ( !isset( $this->matomoanalytics_module ) ) { |
| 2889 |
return array(); |
| 2890 |
} |
| 2891 |
|
| 2892 |
return $this->matomoanalytics_module->get_summary( $start_date, $end_date ); |
| 2893 |
} |
| 2894 |
|
| 2895 |
function get_matomo_analytics_post_analytics( $page_path, $start_date = null, $end_date = null ) { |
| 2896 |
if ( !isset( $this->matomoanalytics_module ) ) { |
| 2897 |
return array(); |
| 2898 |
} |
| 2899 |
|
| 2900 |
return $this->matomoanalytics_module->get_post_analytics( $page_path, $start_date, $end_date ); |
| 2901 |
} |
| 2902 |
|
| 2903 |
function get_matomo_analytics_top_posts( $args = array() ) { |
| 2904 |
if ( !isset( $this->matomoanalytics_module ) ) { |
| 2905 |
return array(); |
| 2906 |
} |
| 2907 |
|
| 2908 |
return $this->matomoanalytics_module->get_top_posts( $args ); |
| 2909 |
} |
| 2910 |
|
| 2911 |
function get_matomo_analytics_realtime_data() { |
| 2912 |
if ( !isset( $this->matomoanalytics_module ) ) { |
| 2913 |
return array(); |
| 2914 |
} |
| 2915 |
|
| 2916 |
return $this->matomoanalytics_module->get_realtime_data(); |
| 2917 |
} |
| 2918 |
|
| 2919 |
// Matomo exposes a real per-day, per-page report, so unlike Plausible it can feed |
| 2920 |
// the Content SEO sparklines and per-post totals. |
| 2921 |
function get_matomo_analytics_pages_daily( $start_date = null, $end_date = null, $paths = null ) { |
| 2922 |
if ( !isset( $this->matomoanalytics_module ) ) { |
| 2923 |
return array(); |
| 2924 |
} |
| 2925 |
|
| 2926 |
return $this->matomoanalytics_module->get_pages_daily( $start_date, $end_date, $paths ); |
| 2927 |
} |
| 2928 |
|
| 2929 |
function get_matomo_analytics_pages_totals( $start_date = null, $end_date = null ) { |
| 2930 |
if ( !isset( $this->matomoanalytics_module ) ) { |
| 2931 |
return array(); |
| 2932 |
} |
| 2933 |
|
| 2934 |
return $this->matomoanalytics_module->get_pages_totals( $start_date, $end_date ); |
| 2935 |
} |
| 2936 |
|
| 2937 |
#endregion |
| 2938 |
} |
| 2939 |
|
| 2940 |
?> |