Changeset 3364780
- Timestamp:
- 09/19/2025 10:45:07 PM (5 months ago)
- Location:
- seo-ai-audit-tool
- Files:
-
- 26 edited
- 1 copied
-
tags/1.1.6 (copied) (copied from seo-ai-audit-tool/trunk)
-
tags/1.1.6/assets/css/seoaudp-style.css (modified) (6 diffs)
-
tags/1.1.6/assets/js/seoaudp-script.js (modified) (3 diffs)
-
tags/1.1.6/changelog.txt (modified) (1 diff)
-
tags/1.1.6/includes/class-seo-audit-data-import.php (modified) (17 diffs)
-
tags/1.1.6/includes/class-seo-audit-db.php (modified) (4 diffs)
-
tags/1.1.6/includes/class-seo-audit-settings-page.php (modified) (2 diffs)
-
tags/1.1.6/includes/models/class-seo-audit-model.php (modified) (13 diffs)
-
tags/1.1.6/includes/views/class-seo-audit-view.php (modified) (1 diff)
-
tags/1.1.6/includes/views/partials/default-category-rows-partial.php (modified) (1 diff)
-
tags/1.1.6/includes/views/partials/table-header-partial.php (modified) (1 diff)
-
tags/1.1.6/readme.md (modified) (1 diff)
-
tags/1.1.6/seo-ai-audit-tool.code-workspace (modified) (1 diff)
-
tags/1.1.6/seo-ai-audit-tool.php (modified) (3 diffs)
-
trunk/assets/css/seoaudp-style.css (modified) (6 diffs)
-
trunk/assets/js/seoaudp-script.js (modified) (3 diffs)
-
trunk/changelog.txt (modified) (1 diff)
-
trunk/includes/class-seo-audit-data-import.php (modified) (17 diffs)
-
trunk/includes/class-seo-audit-db.php (modified) (4 diffs)
-
trunk/includes/class-seo-audit-settings-page.php (modified) (2 diffs)
-
trunk/includes/models/class-seo-audit-model.php (modified) (13 diffs)
-
trunk/includes/views/class-seo-audit-view.php (modified) (1 diff)
-
trunk/includes/views/partials/default-category-rows-partial.php (modified) (1 diff)
-
trunk/includes/views/partials/table-header-partial.php (modified) (1 diff)
-
trunk/readme.md (modified) (1 diff)
-
trunk/seo-ai-audit-tool.code-workspace (modified) (1 diff)
-
trunk/seo-ai-audit-tool.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
seo-ai-audit-tool/tags/1.1.6/assets/css/seoaudp-style.css
r3359560 r3364780 803 803 } 804 804 805 .seoaudp-grid-two-columns { 806 display: grid; 807 grid-template-columns: 1fr 1fr; 808 gap: 20px; 809 } 810 811 .seoaudp-grid-two-columns .seoaudp-column { display: contents; } 812 813 .seoaudp-gsc-sub-columns { display: contents; } 814 815 .seoaudp-ahrefs-sub-columns { display: contents; } 816 817 /* Responsive design for smaller screens */ 818 @media (max-width: 768px) { 819 .seoaudp-grid-two-columns { 820 grid-template-columns: 1fr; 821 } 822 } 823 805 824 .seoaudp-form-group { 806 825 margin-bottom: 20px; … … 1402 1421 color: rgba(26, 41, 72, 1); 1403 1422 font-weight: 500; 1423 } 1424 .seoaudp-page-title-text{ 1425 display: inline-block; 1426 max-width: 220px; 1427 white-space: nowrap; 1428 overflow: hidden; 1429 text-overflow: ellipsis; 1430 vertical-align: bottom; 1404 1431 } 1405 1432 .seoaudp-page-title-container a small{ … … 1907 1934 1908 1935 .seoaudp-sticky-title { 1909 position: relative; 1936 position: relative; /* allow JS transform without native sticky gaps */ 1910 1937 transition: transform 0.02s ease-out; 1911 1938 background: white; … … 1923 1950 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); 1924 1951 } 1952 .seoaudp-is-scrolled .seoaudp-sticky-title .seoaudp-checkbox { display: none; } 1953 .seoaudp-is-scrolled .seoaudp-sticky-title br { display: none; } 1954 /* Show only the slug on sticky */ 1955 .seoaudp-is-scrolled .seoaudp-sticky-title .seoaudp-page-title-text { display: none; } 1956 .seoaudp-is-scrolled .seoaudp-sticky-title a small { display: inline-block; font-size: 13px; } 1925 1957 .seoaudp-page-name-column { 1926 1958 position: relative; … … 1957 1989 /* Responsive adjustments */ 1958 1990 @media screen and (max-width: 782px) { 1959 .seoaudp-sticky-header { 1960 top: 46px; /* Adjusted for mobile WP admin bar */ 1961 } 1962 1963 .seoaudp-sticky-title { 1964 padding: 10px; 1965 } 1991 .seoaudp-sticky-title { padding: 10px; } 1966 1992 } 1967 1993 … … 3613 3639 } 3614 3640 } 3641 3642 /* Banner Styles */ 3643 .seoaudp-banner { 3644 margin: 20px 0; 3645 padding: 15px 20px; 3646 border-radius: 6px; 3647 box-shadow: 0 2px 4px rgba(0,0,0,0.1); 3648 position: relative; 3649 display: flex; 3650 align-items: flex-start; 3651 gap: 12px; 3652 } 3653 3654 .seoaudp-banner--warning { 3655 border: 2px solid #ffb900; 3656 background: #fff8e5; 3657 color: #8a6914; 3658 } 3659 3660 .seoaudp-banner--info { 3661 border: 2px solid #0073aa; 3662 background: #e7f3ff; 3663 color: #0073aa; 3664 } 3665 3666 .seoaudp-banner--success { 3667 border: 2px solid #00a32a; 3668 background: #e7f7e7; 3669 color: #00a32a; 3670 } 3671 3672 .seoaudp-banner--error { 3673 border: 2px solid #d63638; 3674 background: #fce8e8; 3675 color: #d63638; 3676 } 3677 3678 .seoaudp-banner__icon { 3679 flex-shrink: 0; 3680 margin-top: 2px; 3681 font-size: 20px; 3682 } 3683 3684 .seoaudp-banner__content { 3685 flex: 1; 3686 padding-right: 25px; 3687 } 3688 3689 .seoaudp-banner__title { 3690 margin: 0 0 8px 0; 3691 font-weight: 600; 3692 font-size: 16px; 3693 } 3694 3695 .seoaudp-banner__message { 3696 margin: 0 0 12px 0; 3697 font-size: 14px; 3698 line-height: 1.5; 3699 } 3700 3701 .seoaudp-banner__actions { 3702 margin: 0; 3703 } 3704 3705 .seoaudp-banner__close { 3706 position: absolute; 3707 top: 8px; 3708 right: 8px; 3709 background: none; 3710 border: none; 3711 font-size: 18px; 3712 cursor: pointer; 3713 padding: 4px; 3714 line-height: 1; 3715 opacity: 0.7; 3716 transition: opacity 0.2s; 3717 } 3718 3719 .seoaudp-banner__close:hover { 3720 opacity: 1; 3721 } -
seo-ai-audit-tool/tags/1.1.6/assets/js/seoaudp-script.js
r3359560 r3364780 849 849 }); 850 850 851 // Handler to open the Searchers Intention modal 852 jQuery( '.seoaudp-open-intention-modal').on('click', function(e) {851 // Handler to open the Searchers Intention modal (custom modal) 852 jQuery(document).on('click', '.seoaudp-open-intention-modal', function(e) { 853 853 e.preventDefault(); 854 854 var pageId = jQuery(this).data('page-id'); 855 855 var keywords = window.seoaudp_keywordIntentions[pageId] || []; 856 856 857 // Generate the modal content 858 var modalContent = jQuery('<div id="seoaudp-search-intention-modal" class="seoaudp-modal"></div>'); 859 var table = jQuery('<table class="wp-list-table widefat fixed striped seoaudp-intention-table">').appendTo(modalContent); 860 861 // Add table header 862 var thead = $('<thead>').appendTo(table); 863 var headerRow = $('<tr>').appendTo(thead); 864 [ 865 'Keyword', 866 'Search Volume', 867 'Search Intent', 868 'SERP Features' 869 ].forEach(function(headerText) { 870 $('<th>').text(headerText).appendTo(headerRow); 871 }); 872 873 // Add table body 874 var tbody = $('<tbody>').appendTo(table); 875 857 var $modal = jQuery('#intention-modal-' + pageId); 858 if (!$modal.length) { 859 return; 860 } 861 862 var $contentContainer = $modal.find('.intention-details-content'); 863 $contentContainer.empty(); 864 865 var $table = jQuery('<table class="wp-list-table widefat fixed striped seoaudp-intention-table">'); 866 var $thead = jQuery('<thead>').appendTo($table); 867 var $headerRow = jQuery('<tr>').appendTo($thead); 868 ['Keyword', 'Search Volume', 'Search Intent', 'SERP Features'].forEach(function(headerText) { 869 jQuery('<th>').text(headerText).appendTo($headerRow); 870 }); 871 872 var $tbody = jQuery('<tbody>').appendTo($table); 876 873 if (keywords.length > 0) { 877 874 keywords.forEach(function(item) { 878 var row = $('<tr>').appendTo(tbody); 879 880 // Keyword column 881 $('<td>').text(item.keyword || 'N/A').appendTo(row); 882 883 // Search Volume column 884 $('<td>').text(item.volume ? Number(item.volume).toLocaleString() : 'N/A').appendTo(row); 885 886 // Search Intent column (from database flags) 875 var $row = jQuery('<tr>').appendTo($tbody); 876 jQuery('<td>').text(item.keyword || 'N/A').appendTo($row); 877 jQuery('<td>').text(item.volume ? Number(item.volume).toLocaleString() : 'N/A').appendTo($row); 878 887 879 var intentV2 = []; 888 880 if (item.intent_flags) { … … 894 886 if (item.intent_flags.transactional) intentV2.push('Transactional'); 895 887 } 896 $('<td>').html(intentV2.length ? intentV2.join('<br>') : 'N/A').appendTo(row); 897 898 // SERP Features column 899 $('<td>').text(item.serp_features || 'N/A').appendTo(row); 888 jQuery('<td>').html(intentV2.length ? intentV2.join('<br>') : 'N/A').appendTo($row); 889 jQuery('<td>').text(item.serp_features || 'N/A').appendTo($row); 900 890 }); 901 891 } else { 902 $('<tr>').append( 903 $('<td colspan="4">').text('No keywords available.') 904 ).appendTo(tbody); 905 } 906 907 // Append modal to body and initialize 908 $('body').append(modalContent); 909 $('#seoaudp-search-intention-modal').dialog({ 910 modal: true, 911 width: 'auto', 912 maxWidth: '90%', 913 minWidth: 800, 914 height: 'auto', 915 maxHeight: $(window).height() * 0.9, 916 title: 'Searchers Intention Details', 917 classes: { 918 "ui-dialog": "seoaudp-modal-dialog" 919 }, 920 close: function() { 921 $(this).dialog('destroy').remove(); 922 } 923 }); 892 jQuery('<tr>').append( 893 jQuery('<td colspan="4">').text('No keywords available.') 894 ).appendTo($tbody); 895 } 896 897 $contentContainer.append($table); 898 if (typeof window.seoaudp_openModal === 'function') { 899 window.seoaudp_openModal('intention-modal-' + pageId); 900 } else { 901 $modal.show(); 902 } 903 }); 904 905 // Close handlers for the custom modal 906 jQuery(document).on('click', '.seoaudp-modal .seoaudp-close', function() { 907 jQuery(this).closest('.seoaudp-modal').hide(); 908 }); 909 jQuery(document).on('click', '.seoaudp-modal', function(e) { 910 if (jQuery(e.target).is('.seoaudp-modal')) { 911 jQuery(this).hide(); 912 } 924 913 }); 925 914 … … 1011 1000 function seoaudp_handleScroll() { 1012 1001 const stickyHeaders = document.querySelectorAll('.seoaudp-sticky-header'); 1013 const stickyTitles = document.querySelectorAll('.seoaudp-sticky-title'); 1014 const headerOffset = 32; // WP admin bar height 1002 const headerOffset = 0; // No extra gap above sticky slug 1015 1003 stickyHeaders.forEach(header => { 1016 const headerRect = header.getBoundingClientRect();1017 1004 const parentTable = header.closest('table'); 1018 1005 const tableRect = parentTable.getBoundingClientRect(); 1019 1006 const stickyTitles = header.querySelectorAll('.seoaudp-sticky-title'); 1020 1007 if (tableRect.top < headerOffset) { 1021 1008 header.classList.add('seoaudp-is-scrolled'); 1009 const translateY = Math.abs(tableRect.top - headerOffset); 1022 1010 stickyTitles.forEach(title => { 1023 title.style.transform = `translateY(${ Math.abs(tableRect.top - headerOffset)}px)`;1011 title.style.transform = `translateY(${translateY}px)`; 1024 1012 }); 1025 1013 } else { -
seo-ai-audit-tool/tags/1.1.6/changelog.txt
r3359560 r3364780 1 = 1.1.6 2025-09-19 2 3 Improved: Check message consistency on Page auditor 4 Added: Import GSC & Ahrefs checklist 5 Improved: GUI improvements 6 Fixed: Broken pop-up modal when clicking 'Searchers Intention' 7 Added: GPT-5 settings support for respective pro Addon version 8 Added: Outdated Pro Addon message when manual update is necessary 9 1 10 = 1.1.5 2025-09-11 2 11 -
seo-ai-audit-tool/tags/1.1.6/includes/class-seo-audit-data-import.php
r3359560 r3364780 82 82 <div class="wrap seoaudp-container"> 83 83 <h1 class="seoaudp-title">Import SEO Metrics</h1> 84 <div class="seoaudp-grid"> 85 <div class="seoaudp-card"> 86 <div class="seoaudp-card-header"> 87 <h2>Google Search Console</h2> 84 <div class="seoaudp-grid-two-columns"> 85 <div class="seoaudp-column"> 86 <div class="seoaudp-gsc-sub-columns"> 87 <div class="seoaudp-card"> 88 <div class="seoaudp-card-header"> 89 <h2>Google Search Console (Pages)</h2> 88 90 <?php if ($has_gsc_data): ?> 89 91 <span class="seoaudp-checkmark" title="Data exists in database">✓ <span class="seoaudp-checkmark-tooltip">Data exists in database</span></span> … … 145 147 </div> 146 148 147 <div class="seoaudp-card">148 <div class="seoaudp-card-header">149 <h2>Google Search Console</h2>150 <?php if ($has_gsc_queries_data): ?>151 <span class="seoaudp-checkmark" title="Data exists in database">✓ <span class="seoaudp-checkmark-tooltip">Data exists in database</span></span>152 <?php endif; ?>153 </div>154 <h4>Queries.csv Import</h4>149 <div class="seoaudp-card"> 150 <div class="seoaudp-card-header"> 151 <h2>Google Search Console (Queries)</h2> 152 <?php if ($has_gsc_queries_data): ?> 153 <span class="seoaudp-checkmark" title="Data exists in database">✓ <span class="seoaudp-checkmark-tooltip">Data exists in database</span></span> 154 <?php endif; ?> 155 </div> 156 <h4>Queries.csv Import</h4> 155 157 <div class="seoaudp-steps"> 156 158 <h3>How to export GSC Queries data:</h3> … … 205 207 </form> 206 208 </div> 209 </div> 210 </div> 207 211 </div> 208 212 209 <div class="seoaudp-card"> 210 <div class="seoaudp-card-header"> 211 <h2>Ahrefs Keywords</h2> 213 <div class="seoaudp-column"> 214 <div class="seoaudp-ahrefs-sub-columns"> 215 <div class="seoaudp-card"> 216 <div class="seoaudp-card-header"> 217 <h2>Ahrefs Keywords</h2> 212 218 <?php if ($has_ahrefs_data): ?> 213 219 <span class="seoaudp-checkmark" title="Data exists in database">✓ <span class="seoaudp-checkmark-tooltip">Data exists in database</span></span> … … 221 227 <li>Enter your domain</li> 222 228 <li>Click <b>Organic Keywords</b> (set the target country)</li> 229 <li>Click <b>By location</b> under Organic Keywords so the Keyword Intent columns are included</li> 223 230 <li>Click Export</li> 224 231 <li>Select all, select CSV <span style="color: red;">(UTF-8)</span></li> … … 269 276 </div> 270 277 271 <div class="seoaudp-card">272 <div class="seoaudp-card-header">273 <h2>Ahrefs Backlinks</h2>278 <div class="seoaudp-card"> 279 <div class="seoaudp-card-header"> 280 <h2>Ahrefs Backlinks</h2> 274 281 <?php if ($has_ahrefs_backlinks_data): ?> 275 282 <span class="seoaudp-checkmark" title="Data exists in database">✓ <span class="seoaudp-checkmark-tooltip">Data exists in database</span></span> … … 327 334 </form> 328 335 </div> 336 </div> 337 </div> 329 338 </div> 330 339 </div> … … 377 386 <p>You are about to import <?php echo intval($page_count); ?> pages of GSC data.</p> 378 387 <p><strong>Warning:</strong> This will overwrite any existing GSC data in the database.</p> 388 <?php 389 // Build a lightweight checklist summary based on columns present in first row 390 $first = $processed_data[0]; 391 $has_page = isset($first['page']); 392 $has_clicks = isset($first['clicks']); 393 $has_impr = isset($first['impressions']); 394 $has_ctr = isset($first['ctr']); 395 $has_pos = isset($first['position']); 396 ?> 397 <div style="margin:12px 0; padding:12px; border:1px solid #ddd; background:#fff;"> 398 <h2 style="margin:0 0 8px;">Import Checklist</h2> 399 <ul style="margin:0 0 8px 18px; list-style:disc;"> 400 <li style="color:<?php echo $has_page ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_page ? '✓' : '✗'; ?> Page URL</li> 401 <li style="color:<?php echo $has_clicks ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_clicks ? '✓' : '✗'; ?> Clicks</li> 402 <li style="color:<?php echo $has_impr ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_impr ? '✓' : '✗'; ?> Impressions</li> 403 <li style="color:<?php echo $has_ctr ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_ctr ? '✓' : '✗'; ?> CTR</li> 404 <li style="color:<?php echo $has_pos ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_pos ? '✓' : '✗'; ?> Position</li> 405 </ul> 406 <div style="font-size:12px;color:#555;">Rows detected: <?php echo intval($page_count); ?></div> 407 </div> 379 408 380 409 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"> … … 389 418 390 419 private function seoaudp_render_ahrefs_confirmation_page() { 391 $processed_data = get_transient('seoaudp_ahrefs_processed_data'); 420 $import_key = isset($_GET['import_key']) ? sanitize_text_field($_GET['import_key']) : get_transient('seoaudp_ahrefs_import_key'); 421 $processed_data = $import_key ? get_transient('seoaudp_ahrefs_processed_' . $import_key) : get_transient('seoaudp_ahrefs_processed_data'); 392 422 if (!$processed_data) { 393 423 wp_die('No data to import. Please upload a CSV file first.'); … … 400 430 <p><strong>Warning:</strong> This will overwrite any existing Ahrefs data in the database.</p> 401 431 432 <?php 433 $import_key = isset($_GET['import_key']) ? sanitize_text_field($_GET['import_key']) : get_transient('seoaudp_ahrefs_import_key'); 434 $meta = $import_key ? get_transient('seoaudp_ahrefs_import_meta_' . $import_key) : get_transient('seoaudp_ahrefs_import_meta'); 435 $validation_errors = $import_key ? get_transient('seoaudp_ahrefs_validation_errors_' . $import_key) : get_transient('seoaudp_ahrefs_validation_errors'); 436 if (is_array($meta)) { 437 $missing = array_map('esc_html', $meta['missing_optional'] ?? array()); 438 $present_required = array_map('esc_html', $meta['present_required'] ?? array()); 439 $intent_stats = $meta['intent_stats'] ?? array(); 440 ?> 441 <div style="margin:12px 0; padding:12px; border:1px solid #ddd; background:#fff;"> 442 <h2 style="margin:0 0 8px;">Import Checklist</h2> 443 <ul style="margin:0 0 8px 18px; list-style:disc;"> 444 <?php foreach ($present_required as $req): ?> 445 <li style="color:#2e7d32;">✓ Required present: <?php echo $req; ?></li> 446 <?php endforeach; ?> 447 <?php if (!empty($missing)): ?> 448 <?php foreach ($missing as $miss): ?> 449 <li style="color:#b71c1c;">✗ Optional missing: <?php echo $miss; ?></li> 450 <?php endforeach; ?> 451 <?php else: ?> 452 <li style="color:#2e7d32;">✓ All optional columns detected</li> 453 <?php endif; ?> 454 </ul> 455 456 <?php if (!empty($intent_stats)): ?> 457 <div style="margin:8px 0; padding:8px; background:#f8f9fa; border-left:3px solid #007cba;"> 458 <strong>Keyword Intent Data Summary:</strong><br> 459 <small> 460 • Rows with intent data: <?php echo intval($intent_stats['rows_with_intent_data'] ?? 0); ?><br> 461 • Rows missing intent data: <?php echo intval($intent_stats['rows_missing_intent_data'] ?? 0); ?><br> 462 <?php if (($intent_stats['invalid_intent_values'] ?? 0) > 0): ?> 463 • <span style="color:#d63384;">Invalid intent values found: <?php echo intval($intent_stats['invalid_intent_values']); ?></span><br> 464 <?php endif; ?> 465 </small> 466 </div> 467 <?php endif; ?> 468 469 <?php if (!empty($validation_errors) && is_array($validation_errors)): ?> 470 <div style="margin:8px 0; padding:8px; background:#fff3cd; border-left:3px solid #ffc107; max-height:150px; overflow-y:auto;"> 471 <strong style="color:#856404;">⚠️ Intent Data Validation Warnings:</strong><br> 472 <small style="color:#856404;"> 473 <?php foreach (array_slice($validation_errors, 0, 10) as $error): ?> 474 • <?php echo esc_html($error); ?><br> 475 <?php endforeach; ?> 476 <?php if (count($validation_errors) > 10): ?> 477 <em>... and <?php echo count($validation_errors) - 10; ?> more warnings.</em><br> 478 <?php endif; ?> 479 <strong>These rows will use default intent values (FALSE).</strong> 480 </small> 481 </div> 482 <?php endif; ?> 483 484 <div style="font-size:12px;color:#555;">Valid rows: <?php echo intval($meta['valid_rows'] ?? 0); ?> / Total rows: <?php echo intval($meta['total_rows'] ?? 0); ?></div> 485 </div> 486 <?php } ?> 487 402 488 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"> 403 489 <input type="hidden" name="action" value="seoaudp_confirm_ahrefs_import"> 490 <input type="hidden" name="import_key" value="<?php echo isset($import_key) ? esc_attr($import_key) : ''; ?>"> 404 491 <?php wp_nonce_field('seoaudp_confirm_ahrefs_import', 'seoaudp_ahrefs_confirm_nonce'); ?> 405 492 <?php submit_button('Confirm Import'); ?> … … 421 508 <p>You are about to import <?php echo intval($query_count); ?> queries of GSC data.</p> 422 509 <p><strong>Warning:</strong> This will overwrite any existing GSC queries data in the database.</p> 510 <?php 511 $first = $processed_data[0]; 512 $has_query = isset($first['query']); 513 $has_clicks = isset($first['clicks']); 514 $has_impr = isset($first['impressions']); 515 $has_ctr = isset($first['ctr']); 516 $has_pos = isset($first['position']); 517 ?> 518 <div style="margin:12px 0; padding:12px; border:1px solid #ddd; background:#fff;"> 519 <h2 style="margin:0 0 8px;">Import Checklist</h2> 520 <ul style="margin:0 0 8px 18px; list-style:disc;"> 521 <li style="color:<?php echo $has_query ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_query ? '✓' : '✗'; ?> Query</li> 522 <li style="color:<?php echo $has_clicks ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_clicks ? '✓' : '✗'; ?> Clicks</li> 523 <li style="color:<?php echo $has_impr ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_impr ? '✓' : '✗'; ?> Impressions</li> 524 <li style="color:<?php echo $has_ctr ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_ctr ? '✓' : '✗'; ?> CTR</li> 525 <li style="color:<?php echo $has_pos ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_pos ? '✓' : '✗'; ?> Position</li> 526 </ul> 527 <div style="font-size:12px;color:#555;">Rows detected: <?php echo intval($query_count); ?></div> 528 </div> 423 529 424 530 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"> … … 651 757 // Remove header row if it exists 652 758 $header = array_shift($csv_data); 653 654 // Check if the header contains expected columns 655 $required_columns = ['Keyword', 'Volume', 'KD', 'CPC', 'Current position', 'Current URL', 'SERP features']; 656 $traffic_column = in_array('Organic traffic', $header) ? 'Organic traffic' : 'Current organic traffic'; 657 $required_columns[] = $traffic_column; 658 659 $missing_columns = array_diff($required_columns, $header); 660 661 if (!empty($missing_columns)) { 662 wp_die('The CSV file is missing the following required columns: ' . implode(', ', array_map('esc_html', $missing_columns))); 663 } 664 665 // Get column indexes 759 if (!$header || !is_array($header)) { 760 wp_die('Invalid CSV: missing header row.'); 761 } 762 $header = array_map('trim', $header); 666 763 $column_indexes = array_flip($header); 764 765 // Required columns (super mandatory) 766 $required_columns = ['Keyword', 'Current URL']; 767 $missing_required = array_values(array_filter($required_columns, function($c) use ($column_indexes){ return !isset($column_indexes[$c]); })); 768 769 // Optional columns (nice-to-haves) 770 $optional_columns = ['Volume','KD','CPC','Current position','SERP features','Organic traffic','Current organic traffic','Branded','Local','Navigational','Informational','Commercial','Transactional']; 771 $missing_optional = array_values(array_filter($optional_columns, function($c) use ($column_indexes){ return !isset($column_indexes[$c]); })); 772 773 if (!empty($missing_required)) { 774 wp_die('The CSV file is missing required columns: ' . implode(', ', array_map('esc_html', $missing_required))); 775 } 776 777 // Traffic column can be either of these 778 $traffic_column = isset($column_indexes['Organic traffic']) ? 'Organic traffic' : (isset($column_indexes['Current organic traffic']) ? 'Current organic traffic' : null); 667 779 668 780 // Define the boolean columns we expect … … 675 787 'Transactional' 676 788 ]; 677 foreach ($csv_data as $row) { 678 // Check if the row has the expected number of columns 679 if (count($row) !== count($header)) { 680 continue; 681 } 789 790 $validation_errors = array(); 791 $intent_validation_stats = array( 792 'rows_with_intent_data' => 0, 793 'rows_missing_intent_data' => 0, 794 'invalid_intent_values' => 0 795 ); 796 797 foreach ($csv_data as $row_index => $row) { 798 if (empty($row)) { continue; } 799 // Skip if required fields are empty 800 if (!isset($row[$column_indexes['Keyword']]) || !isset($row[$column_indexes['Current URL']])) { continue; } 682 801 683 802 $url = trim($row[$column_indexes['Current URL']]); 684 if (!empty($url)) { 685 $data = array( 686 'keyword' => trim($row[$column_indexes['Keyword']]), 687 'volume' => intval($row[$column_indexes['Volume']]), 688 'kd' => intval($row[$column_indexes['KD']]), 689 'cpc' => floatval($row[$column_indexes['CPC']]), 690 'traffic' => intval($row[$column_indexes[$traffic_column]]), 691 'position' => intval($row[$column_indexes['Current position']]), 692 'page_url' => $url, 693 'serp_features' => trim($row[$column_indexes['SERP features']]), 694 ); 695 696 // Process boolean columns 697 foreach ($boolean_columns as $column) { 698 if (isset($column_indexes[$column])) { 699 $value = trim($row[$column_indexes[$column]]); 700 // Convert various TRUE values to 1, everything else to 0 701 $data[strtolower($column)] = in_array(strtoupper($value), ['TRUE', '1', 'YES', 'Y'], true) ? 1 : 0; 803 if ($url === '') { continue; } 804 805 $data = array( 806 'keyword' => trim($row[$column_indexes['Keyword']]), 807 'volume' => isset($column_indexes['Volume']) ? intval($row[$column_indexes['Volume']] ?? 0) : 0, 808 'kd' => isset($column_indexes['KD']) ? intval($row[$column_indexes['KD']] ?? 0) : 0, 809 'cpc' => isset($column_indexes['CPC']) ? floatval($row[$column_indexes['CPC']] ?? 0) : 0.0, 810 'traffic' => $traffic_column ? intval($row[$column_indexes[$traffic_column]] ?? 0) : 0, 811 'position' => isset($column_indexes['Current position']) ? intval($row[$column_indexes['Current position']] ?? 0) : 0, 812 'page_url' => $url, 813 'serp_features' => isset($column_indexes['SERP features']) ? trim($row[$column_indexes['SERP features']] ?? '') : '', 814 ); 815 816 // Validate and process intent boolean columns 817 $has_intent_data = false; 818 $intent_validation_errors = array(); 819 820 foreach ($boolean_columns as $column) { 821 $key = strtolower($column); 822 if (isset($column_indexes[$column])) { 823 $value = trim($row[$column_indexes[$column]] ?? ''); 824 825 // Check if this row has any intent data 826 if ($value !== '' && $value !== '0' && strtoupper($value) !== 'FALSE') { 827 $has_intent_data = true; 702 828 } 829 830 // Validate intent column values 831 $valid_values = ['TRUE', 'FALSE', '1', '0', 'YES', 'NO', 'Y', 'N', '']; 832 if ($value !== '' && !in_array(strtoupper($value), $valid_values, true)) { 833 $intent_validation_errors[] = "Row " . ($row_index + 2) . ": Invalid value '{$value}' in column '{$column}'. Expected TRUE/FALSE, 1/0, YES/NO, or empty."; 834 $intent_validation_stats['invalid_intent_values']++; 835 } 836 837 // Set boolean value 838 $data[$key] = in_array(strtoupper($value), ['TRUE','1','YES','Y'], true) ? 1 : 0; 839 } else { 840 $data[$key] = 0; 703 841 } 704 705 $processed_data[] = $data; 706 } 842 } 843 844 // Track intent data statistics 845 if ($has_intent_data) { 846 $intent_validation_stats['rows_with_intent_data']++; 847 } else { 848 $intent_validation_stats['rows_missing_intent_data']++; 849 } 850 851 // Add any validation errors for this row 852 if (!empty($intent_validation_errors)) { 853 $validation_errors = array_merge($validation_errors, $intent_validation_errors); 854 } 855 856 $processed_data[] = $data; 707 857 } 708 858 … … 711 861 } 712 862 863 // Handle validation errors - show warnings but don't stop import 864 if (!empty($validation_errors)) { 865 $error_message = '<strong>Intent Data Validation Warnings:</strong><br>' . implode('<br>', array_slice($validation_errors, 0, 10)); 866 if (count($validation_errors) > 10) { 867 $error_message .= '<br><em>... and ' . (count($validation_errors) - 10) . ' more warnings.</em>'; 868 } 869 $error_message .= '<br><br><strong>These rows will be imported with default intent values (FALSE). Continue?</strong>'; 870 871 // Store validation errors in transient for confirmation page 872 set_transient('seoaudp_ahrefs_validation_errors', $validation_errors, HOUR_IN_SECONDS); 873 } 874 875 // Save import meta for checklist UI on confirmation 876 $import_meta = array( 877 'total_rows' => count($csv_data), 878 'valid_rows' => count($processed_data), 879 'missing_optional' => $missing_optional, 880 'present_required' => $required_columns, 881 'intent_stats' => $intent_validation_stats, 882 'has_validation_errors' => !empty($validation_errors), 883 'validation_error_count' => count($validation_errors), 884 ); 885 set_transient('seoaudp_ahrefs_import_meta', $import_meta, HOUR_IN_SECONDS); 886 887 // Generate a unique import key so multiple imports won't collide and to reduce cache misses 888 $import_key = wp_generate_password(12, false); 889 set_transient('seoaudp_ahrefs_import_key', $import_key, HOUR_IN_SECONDS); 890 set_transient('seoaudp_ahrefs_import_meta_' . $import_key, $import_meta, HOUR_IN_SECONDS); 891 // Also store validation errors with the keyed name if present 892 $existing_validation_errors = get_transient('seoaudp_ahrefs_validation_errors'); 893 if (!empty($existing_validation_errors)) { 894 set_transient('seoaudp_ahrefs_validation_errors_' . $import_key, $existing_validation_errors, HOUR_IN_SECONDS); 895 } 896 713 897 // Check if existing data is present 714 898 $existing_data = $this->db->get_ahrefs_data_count(); … … 716 900 if ($existing_data && $existing_data > 0) { 717 901 // Store the processed data in a transient for later use 718 set_transient('seoaudp_ahrefs_processed_data', $processed_data, 60 * 60); // 1 hour expiration 902 set_transient('seoaudp_ahrefs_processed_data', $processed_data, 60 * 60); // legacy key for backward compatibility 903 set_transient('seoaudp_ahrefs_processed_' . $import_key, $processed_data, 60 * 60); 719 904 720 905 // Redirect to confirmation page 721 wp_redirect(add_query_arg(['page' => 'seo-audit-gsc', 'step' => 'confirm_ahrefs' ], admin_url('admin.php')));906 wp_redirect(add_query_arg(['page' => 'seo-audit-gsc', 'step' => 'confirm_ahrefs', 'import_key' => $import_key], admin_url('admin.php'))); 722 907 exit; 723 908 } else { … … 743 928 check_admin_referer('seoaudp_confirm_ahrefs_import', 'seoaudp_ahrefs_confirm_nonce'); 744 929 745 $processed_data = get_transient('seoaudp_ahrefs_processed_data'); 930 $import_key = isset($_POST['import_key']) ? sanitize_text_field($_POST['import_key']) : get_transient('seoaudp_ahrefs_import_key'); 931 $processed_data = $import_key ? get_transient('seoaudp_ahrefs_processed_' . $import_key) : get_transient('seoaudp_ahrefs_processed_data'); 746 932 747 933 if (!$processed_data) { … … 756 942 757 943 delete_transient('seoaudp_ahrefs_processed_data'); 944 if ($import_key) { 945 delete_transient('seoaudp_ahrefs_processed_' . $import_key); 946 delete_transient('seoaudp_ahrefs_import_meta_' . $import_key); 947 delete_transient('seoaudp_ahrefs_validation_errors_' . $import_key); 948 } 949 delete_transient('seoaudp_ahrefs_import_meta'); 950 delete_transient('seoaudp_ahrefs_validation_errors'); 758 951 if ($result) { 759 952 update_option('seoaudp_ahrefs_last_import', current_time('mysql')); … … 868 1061 // Import new data 869 1062 $result = $this->db->seoaudp_store_gsc_queries_data($processed_data); 870 1063 871 1064 delete_transient('seoaudp_gsc_queries_processed_data'); 872 1065 -
seo-ai-audit-tool/tags/1.1.6/includes/class-seo-audit-db.php
r3356502 r3364780 738 738 global $wpdb; 739 739 $this->ensure_table_exists($this->ahrefs_keywords_table); 740 // Safety guard: ensure page_id is nullable to allow external URLs 741 $col_info = $wpdb->get_row("SHOW COLUMNS FROM {$this->ahrefs_keywords_table} LIKE 'page_id'"); 742 if ($col_info && strtoupper($col_info->Null) === 'NO') { 743 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared 744 $wpdb->query("ALTER TABLE {$this->ahrefs_keywords_table} MODIFY page_id bigint(20) DEFAULT NULL"); 745 } 740 746 741 747 $success = true; … … 909 915 $sql = "CREATE TABLE IF NOT EXISTS $table_name ( 910 916 id mediumint(9) NOT NULL AUTO_INCREMENT, 911 page_id bigint(20) NOT NULL,917 page_id bigint(20) DEFAULT NULL, 912 918 keyword varchar(255) NOT NULL, 913 919 volume int(11) NOT NULL, … … 932 938 dbDelta($sql); 933 939 940 // Ensure page_id allows NULL in case older installs have NOT NULL 941 $col_info = $wpdb->get_row("SHOW COLUMNS FROM $table_name LIKE 'page_id'"); 942 if ($col_info && strtoupper($col_info->Null) === 'NO') { 943 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared 944 $wpdb->query("ALTER TABLE $table_name MODIFY page_id bigint(20) DEFAULT NULL"); 945 } 946 934 947 // Add new columns if they don't exist 935 948 $new_columns = array( … … 1187 1200 } 1188 1201 } else { 1189 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Base query is escaped 1190 $query = $wpdb->prepare($base_query, []); 1202 // No dynamic placeholders; use the base query directly 1203 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Base query is built safely above 1204 $query = $base_query; 1191 1205 } 1192 1206 -
seo-ai-audit-tool/tags/1.1.6/includes/class-seo-audit-settings-page.php
r3359560 r3364780 31 31 add_action('wp_ajax_seoaudp_clear_ai_logs', array($this, 'seoaudp_clear_ai_logs')); 32 32 $this->db = new \SEO_Audit_Tool\SEO_Audit_DB(); 33 } 34 35 /** 36 * Check if GPT-5 support is enabled (pro version 1.1.4 or greater) 37 */ 38 private function is_gpt_5_support_enabled() { 39 if (!seoaudp_pro_active()) { 40 return false; 41 } 42 43 if (defined('SEO_AUDIT_PRO_ADDON_VERSION')) { 44 $pro_version = SEO_AUDIT_PRO_ADDON_VERSION; 45 return version_compare($pro_version, '1.1.4', '>='); 46 } 47 48 return false; 33 49 } 34 50 … … 182 198 title="Select the AI model that best fits your needs - newer models generally provide better results but may cost more" 183 199 <?php echo seoaudp_pro_active() ? '' : 'disabled="disabled"'; ?>> 184 <!-- <option value="gpt-5" <!?php selected(get_option('seoaudp_openai_model'), 'gpt-5'); ?>>GPT-5 (latest)</option> 185 <option value="gpt-5-mini" <!?php selected(get_option('seoaudp_openai_model'), 'gpt-5-mini'); ?>>GPT-5 Mini (recommended)</option> --> 186 <option value="gpt-4.1-mini" <?php selected(get_option('seoaudp_openai_model'), 'gpt-4.1-mini'); ?>>GPT-4.1 Mini (recommended)</option> 187 <option value="gpt-4.1" <?php selected(get_option('seoaudp_openai_model'), 'gpt-4.1'); ?>>GPT-4.1</option> 188 <option value="gpt-4.1-nano" <?php selected(get_option('seoaudp_openai_model'), 'gpt-4.1-nano'); ?>>GPT-4.1 Nano</option> 189 <option value="o4-mini" <?php selected(get_option('seoaudp_openai_model'), 'o4-mini'); ?>>O4 Mini</option> 190 <option value="gpt-4o" <?php selected(get_option('seoaudp_openai_model'), 'gpt-4o'); ?>>GPT-4o</option> 200 <?php 201 $is_gpt_5_support_enabled = $this->is_gpt_5_support_enabled(); 202 203 if ($is_gpt_5_support_enabled) { 204 // Show only GPT-5 models for version 1.1.4 or greater 205 ?> 206 <option value="gpt-5-mini" <?php selected(get_option('seoaudp_openai_model'), 'gpt-5-mini'); ?>>GPT-5 Mini (recommended)</option> 207 <option value="gpt-5" <?php selected(get_option('seoaudp_openai_model'), 'gpt-5'); ?>>GPT-5 (latest)</option> 208 <?php 209 } else { 210 // Show all models except GPT-5 for older versions 211 ?> 212 <option value="gpt-4.1-mini" <?php selected(get_option('seoaudp_openai_model'), 'gpt-4.1-mini'); ?>>GPT-4.1 Mini (recommended)</option> 213 <option value="gpt-4.1" <?php selected(get_option('seoaudp_openai_model'), 'gpt-4.1'); ?>>GPT-4.1</option> 214 <option value="gpt-4.1-nano" <?php selected(get_option('seoaudp_openai_model'), 'gpt-4.1-nano'); ?>>GPT-4.1 Nano</option> 215 <option value="o4-mini" <?php selected(get_option('seoaudp_openai_model'), 'o4-mini'); ?>>O4 Mini</option> 216 <option value="gpt-4o" <?php selected(get_option('seoaudp_openai_model'), 'gpt-4o'); ?>>GPT-4o</option> 217 <?php 218 } 219 ?> 191 220 </select> 192 221 </div> -
seo-ai-audit-tool/tags/1.1.6/includes/models/class-seo-audit-model.php
r3352841 r3364780 275 275 return array( 276 276 'status' => $status, 277 'message' => implode(". ", $message),277 'message' => $status === 'pass' ? "Pass" : "Fail", 278 278 'details' => "<strong>Title:</strong> {$title}<br>" . 279 279 "<strong>Keyword:</strong> {$keyword}<br>" . … … 367 367 return array( 368 368 'status' => $descLength > 50 && $descLength <= 160 && $hasKeyword ? 'pass' : 'fail', 369 'message' => sprintf( 370 "Length: %d chars. Keyword match: %.1f%%", 371 $descLength, 372 $matchPercentage 373 ), 369 'message' => $descLength > 50 && $descLength <= 160 && $hasKeyword ? "Pass" : "Fail", 374 370 'details' => "<strong>Description:</strong> " . $highlightedDesc . "<br>" . 375 371 "<strong>Source:</strong> " . esc_html($description_source) . "<br>" . … … 457 453 return array( 458 454 'status' => !empty($h1_content) && $hasKeyword ? 'pass' : 'fail', 459 'message' => sprintf( 460 "H1 %s. Keyword match: %.1f%%", 461 empty($h1_content) ? "not found" : "found", 462 $matchPercentage 463 ), 455 'message' => !empty($h1_content) && $hasKeyword ? "Pass" : "Fail", 464 456 'details' => sprintf( 465 457 "<strong>H1 Tag (%s):</strong> %s<br><strong>Keyword:</strong> %s", … … 521 513 return array( 522 514 'status' => $hasKeyword ? 'pass' : 'fail', 523 'message' => sprintf( 524 "Keyword in first H2: %.1f%%", 525 $matchPercentage 526 ), 515 'message' => $hasKeyword ? "Pass" : "Fail", 527 516 'details' => "<strong>First H2:</strong> {$highlightedH2}<br><strong>Keyword:</strong> " . esc_html($keyword) 528 517 ); … … 573 562 return array( 574 563 'status' => $hasKeyword ? 'pass' : 'fail', 575 'message' => sprintf( 576 "Keyword in first 100 words: %.1f%%", 577 $matchPercentage 578 ), 564 'message' => $hasKeyword ? "Pass" : "Fail", 579 565 'details' => "<strong>First 100 words:</strong><br>" . $highlightedContent . "<br><br><strong>Keyword:</strong> " . esc_html($keyword) 580 566 ); … … 586 572 return array( 587 573 'status' => $wordCount >= 300 ? 'pass' : 'fail', 588 'message' => "Word count: {$wordCount}",574 'message' => $wordCount >= 300 ? "Pass" : "Fail", 589 575 'details' => "<strong>Word count:</strong> {$wordCount}<br><strong>Recommended minimum:</strong> 300 words" 590 576 ); … … 599 585 return array( 600 586 'status' => $days <= 90 ? 'pass' : 'fail', 601 'message' => "Last modified: {$lastModified} ({$days} days ago)",587 'message' => $days <= 90 ? "Pass" : "Fail", 602 588 'details' => "<strong>Last modified:</strong> {$lastModified}<br><strong>Days since last update:</strong> {$days}<br><strong>Recommendation:</strong> " . ($days <= 90 ? "Content is up to date" : "Content may need updating") 603 589 ); … … 633 619 return array( 634 620 'status' => $bodyLinks > 0 ? 'pass' : 'fail', 635 'message' => $bodyLinks > 0 ? "Found {$bodyLinks} internal link(s)" : "No internal links found - Add internal links to improve SEO and user navigation",621 'message' => $bodyLinks > 0 ? $bodyLinks : "", 636 622 'details' => $details 637 623 ); … … 667 653 return array( 668 654 'status' => $uniqueExternalLinks > 0 ? 'pass' : 'fail', 669 'message' => $uniqueExternalLinks > 0 ? "Found {$uniqueExternalLinks} external link(s)" : "No external links found - Consider adding links to authoritative sources",655 'message' => $uniqueExternalLinks > 0 ? $uniqueExternalLinks : "", 670 656 'details' => $details 671 657 ); … … 743 729 744 730 $status = ($imagesWithAlt === $totalImages && $imagesWithKeyword > 0) ? 'pass' : 'fail'; 745 $message = "Images with alt text: {$imagesWithAlt}/{$totalImages}";731 $message = $status === 'pass' ? "Pass" : "Fail"; 746 732 $details = "<strong>Total images:</strong> {$totalImages}<br><strong>With alt:</strong> {$imagesWithAlt}<br><strong>Without alt:</strong> {$imagesWithoutAlt}<br>" 747 733 . "<strong>With keyword:</strong> {$imagesWithKeyword}<br><strong>WebP format:</strong> {$webpImages}<br><br>" … … 786 772 return array( 787 773 'status' => count($keywordRichImages) > 0 ? 'pass' : 'fail', 788 'message' => "Images with keyword in filename: " . count($keywordRichImages) . "/" . count($images),774 'message' => count($keywordRichImages) > 0 ? "Pass" : "Fail", 789 775 'details' => $details 790 776 ); … … 825 811 826 812 $status = $allWebP ? 'pass' : 'fail'; 827 $message = $ allWebP ? "All images are in WebP format" : "Not all images are in WebP format";813 $message = $status === 'pass' ? "Pass" : "Fail"; 828 814 829 815 $details = "<strong>WebP images:</strong> {$webpCount}<br>" … … 925 911 return array( 926 912 'status' => $hasKeyword ? 'pass' : 'fail', 927 'message' => sprintf( 928 "%s (%.1f%% match)", 929 $hasKeyword ? "Keyword found in URL" : "Keyword not sufficiently represented in URL", 930 $matchPercentage 931 ), 913 'message' => $hasKeyword ? "Pass" : "Fail", 932 914 'details' => "<strong>URL:</strong> {$highlightedUrl}<br>" . 933 915 "<strong>Keyword:</strong> {$keyword}<br>" . -
seo-ai-audit-tool/tags/1.1.6/includes/views/class-seo-audit-view.php
r3242953 r3364780 110 110 <div class="wrap seoaudp-wrapper"> 111 111 <?php include(plugin_dir_path(__FILE__) . 'partials/header-partial.php'); ?> 112 113 <?php if (seoaudp_is_pro_version_outdated()): ?> 114 <div class="seoaudp-banner seoaudp-banner--warning"> 115 <div class="seoaudp-banner__icon"> 116 <span class="dashicons dashicons-warning"></span> 117 </div> 118 <div class="seoaudp-banner__content"> 119 <h4 class="seoaudp-banner__title"> 120 SEO AI Audit Tool Pro Addon Update Required 121 </h4> 122 <p class="seoaudp-banner__message"> 123 Your SEO AI Audit Tool <strong>Pro Addon</strong> is outdated. If you're having trouble updating from the plugins page, 124 download the new version from your <a href="https://seo-ai-audit-tool.designful.ca/my-account/" target="_blank"><strong>Members portal</strong></a>. 125 </p> 126 <p class="seoaudp-banner__actions"> 127 <a href="https://seo-ai-audit-tool.designful.ca/my-account/" 128 target="_blank" 129 class="button button-primary"> 130 <span class="dashicons dashicons-external" style="margin-right: 5px; font-size: 14px; vertical-align: middle;"></span> 131 Go to Members Portal 132 </a> 133 </p> 134 </div> 135 </div> 136 <?php endif; ?> 137 112 138 <div class="seoaudp-wrapper-body"> 113 139 <input type="hidden" id="seoaudp-page-filter" value="<?php echo esc_attr(sanitize_text_field($_GET['post_type'] ?? 'all')); ?>"> -
seo-ai-audit-tool/tags/1.1.6/includes/views/partials/default-category-rows-partial.php
r3216569 r3364780 29 29 <?php 30 30 if (in_array($item, ['Internal Link Count', 'External Link Count'])) { 31 echo esc_html(ucfirst($result['status']) . ' (' . $result['message'] . ')'); 31 if (!empty($result['message'])) { 32 echo esc_html(ucfirst($result['status']) . ' (' . $result['message'] . ')'); 33 } else { 34 echo esc_html(ucfirst($result['status'])); 35 } 32 36 } else { 33 37 echo esc_html(ucfirst($result['status'])); -
seo-ai-audit-tool/tags/1.1.6/includes/views/partials/table-header-partial.php
r3216569 r3364780 18 18 </label> 19 19 <?php } ?> 20 <a href="<?php echo esc_url(get_permalink($page->ID)); ?>" target="_blank" >21 < ?php echo esc_html($page->post_title); ?><br>20 <a href="<?php echo esc_url(get_permalink($page->ID)); ?>" target="_blank" title="<?php echo esc_attr($page->post_title); ?>"> 21 <span class="seoaudp-page-title-text" title="<?php echo esc_attr($page->post_title); ?>"><?php echo esc_html($page->post_title); ?></span><br> 22 22 <small><?php echo esc_html($page->post_name); ?> <span class="dashicons dashicons-admin-links"></span></small> 23 23 </a> -
seo-ai-audit-tool/tags/1.1.6/readme.md
r3359560 r3364780 2 2 Contributors: Designful 3 3 Plugin URL: https://seo-ai-audit-tool.designful.ca/ 4 Version: 1.1. 54 Version: 1.1.6 5 5 Tags: seo audit, ai seo, conversion optimization, content analysis, search intent 6 6 Requires at least: 4.0 7 7 Tested up to: 6.8 8 Stable tag: 1.1. 58 Stable tag: 1.1.6 9 9 Requires PHP: 7.4 10 10 License: GPLv2 or later -
seo-ai-audit-tool/tags/1.1.6/seo-ai-audit-tool.code-workspace
r3333926 r3364780 8 8 } 9 9 ], 10 "settings": {} 10 "settings": { 11 "workbench.colorCustomizations": { 12 "titleBar.activeBackground": "#8f6cff", 13 "titleBar.activeForeground": "#ffffff", 14 "titleBar.inactiveBackground": "#8f6cff", 15 "titleBar.inactiveForeground": "#ffffff", 16 "statusBar.background": "#5a1d5a", 17 "statusBar.foreground": "#e7e7e7" 18 } 19 } 11 20 } -
seo-ai-audit-tool/tags/1.1.6/seo-ai-audit-tool.php
r3359560 r3364780 4 4 * Plugin URI: https://designful.ca/apps/seo-ai-audit-tool/ 5 5 * Description: A WordPress plugin to audit SEO elements of pages and perform AI-powered content analysis 6 * Version: 1.1. 56 * Version: 1.1.6 7 7 * Author: Designful 8 8 * License: GPL2 … … 20 20 define('SEOAUDP_PLUGIN_DIR', plugin_dir_path(__FILE__)); 21 21 define('SEOAUDP_PLUGIN_URL', plugin_dir_url(__FILE__)); 22 define('SEOAUDP_PLUGIN_VERSION', '1.1. 5');22 define('SEOAUDP_PLUGIN_VERSION', '1.1.6'); 23 23 define('SEOAUDP_PLUGIN_BETA_VERSION', false); 24 define('SEOAUDP_DB_VERSION', '1. 8');24 define('SEOAUDP_DB_VERSION', '1.9'); 25 25 define('SEOAUDP_EDD_ITEM_ID', 307); 26 26 define('SEOAUDP_STORE_URL', 'https://seo-ai-audit-tool.designful.ca'); … … 390 390 } 391 391 392 function seoaudp_is_pro_version_outdated() { 393 if (!seoaudp_pro_active()) { 394 return false; 395 } 396 397 if (defined('SEO_AUDIT_PRO_ADDON_VERSION')) { 398 $pro_version = SEO_AUDIT_PRO_ADDON_VERSION; 399 return version_compare($pro_version, '1.1.2', '<'); 400 } 401 402 return false; 403 } 404 392 405 function seoaudp_init_hooks() { 393 406 add_action('admin_print_scripts', 'seoaudp_hide_admin_page'); -
seo-ai-audit-tool/trunk/assets/css/seoaudp-style.css
r3359560 r3364780 803 803 } 804 804 805 .seoaudp-grid-two-columns { 806 display: grid; 807 grid-template-columns: 1fr 1fr; 808 gap: 20px; 809 } 810 811 .seoaudp-grid-two-columns .seoaudp-column { display: contents; } 812 813 .seoaudp-gsc-sub-columns { display: contents; } 814 815 .seoaudp-ahrefs-sub-columns { display: contents; } 816 817 /* Responsive design for smaller screens */ 818 @media (max-width: 768px) { 819 .seoaudp-grid-two-columns { 820 grid-template-columns: 1fr; 821 } 822 } 823 805 824 .seoaudp-form-group { 806 825 margin-bottom: 20px; … … 1402 1421 color: rgba(26, 41, 72, 1); 1403 1422 font-weight: 500; 1423 } 1424 .seoaudp-page-title-text{ 1425 display: inline-block; 1426 max-width: 220px; 1427 white-space: nowrap; 1428 overflow: hidden; 1429 text-overflow: ellipsis; 1430 vertical-align: bottom; 1404 1431 } 1405 1432 .seoaudp-page-title-container a small{ … … 1907 1934 1908 1935 .seoaudp-sticky-title { 1909 position: relative; 1936 position: relative; /* allow JS transform without native sticky gaps */ 1910 1937 transition: transform 0.02s ease-out; 1911 1938 background: white; … … 1923 1950 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); 1924 1951 } 1952 .seoaudp-is-scrolled .seoaudp-sticky-title .seoaudp-checkbox { display: none; } 1953 .seoaudp-is-scrolled .seoaudp-sticky-title br { display: none; } 1954 /* Show only the slug on sticky */ 1955 .seoaudp-is-scrolled .seoaudp-sticky-title .seoaudp-page-title-text { display: none; } 1956 .seoaudp-is-scrolled .seoaudp-sticky-title a small { display: inline-block; font-size: 13px; } 1925 1957 .seoaudp-page-name-column { 1926 1958 position: relative; … … 1957 1989 /* Responsive adjustments */ 1958 1990 @media screen and (max-width: 782px) { 1959 .seoaudp-sticky-header { 1960 top: 46px; /* Adjusted for mobile WP admin bar */ 1961 } 1962 1963 .seoaudp-sticky-title { 1964 padding: 10px; 1965 } 1991 .seoaudp-sticky-title { padding: 10px; } 1966 1992 } 1967 1993 … … 3613 3639 } 3614 3640 } 3641 3642 /* Banner Styles */ 3643 .seoaudp-banner { 3644 margin: 20px 0; 3645 padding: 15px 20px; 3646 border-radius: 6px; 3647 box-shadow: 0 2px 4px rgba(0,0,0,0.1); 3648 position: relative; 3649 display: flex; 3650 align-items: flex-start; 3651 gap: 12px; 3652 } 3653 3654 .seoaudp-banner--warning { 3655 border: 2px solid #ffb900; 3656 background: #fff8e5; 3657 color: #8a6914; 3658 } 3659 3660 .seoaudp-banner--info { 3661 border: 2px solid #0073aa; 3662 background: #e7f3ff; 3663 color: #0073aa; 3664 } 3665 3666 .seoaudp-banner--success { 3667 border: 2px solid #00a32a; 3668 background: #e7f7e7; 3669 color: #00a32a; 3670 } 3671 3672 .seoaudp-banner--error { 3673 border: 2px solid #d63638; 3674 background: #fce8e8; 3675 color: #d63638; 3676 } 3677 3678 .seoaudp-banner__icon { 3679 flex-shrink: 0; 3680 margin-top: 2px; 3681 font-size: 20px; 3682 } 3683 3684 .seoaudp-banner__content { 3685 flex: 1; 3686 padding-right: 25px; 3687 } 3688 3689 .seoaudp-banner__title { 3690 margin: 0 0 8px 0; 3691 font-weight: 600; 3692 font-size: 16px; 3693 } 3694 3695 .seoaudp-banner__message { 3696 margin: 0 0 12px 0; 3697 font-size: 14px; 3698 line-height: 1.5; 3699 } 3700 3701 .seoaudp-banner__actions { 3702 margin: 0; 3703 } 3704 3705 .seoaudp-banner__close { 3706 position: absolute; 3707 top: 8px; 3708 right: 8px; 3709 background: none; 3710 border: none; 3711 font-size: 18px; 3712 cursor: pointer; 3713 padding: 4px; 3714 line-height: 1; 3715 opacity: 0.7; 3716 transition: opacity 0.2s; 3717 } 3718 3719 .seoaudp-banner__close:hover { 3720 opacity: 1; 3721 } -
seo-ai-audit-tool/trunk/assets/js/seoaudp-script.js
r3359560 r3364780 849 849 }); 850 850 851 // Handler to open the Searchers Intention modal 852 jQuery( '.seoaudp-open-intention-modal').on('click', function(e) {851 // Handler to open the Searchers Intention modal (custom modal) 852 jQuery(document).on('click', '.seoaudp-open-intention-modal', function(e) { 853 853 e.preventDefault(); 854 854 var pageId = jQuery(this).data('page-id'); 855 855 var keywords = window.seoaudp_keywordIntentions[pageId] || []; 856 856 857 // Generate the modal content 858 var modalContent = jQuery('<div id="seoaudp-search-intention-modal" class="seoaudp-modal"></div>'); 859 var table = jQuery('<table class="wp-list-table widefat fixed striped seoaudp-intention-table">').appendTo(modalContent); 860 861 // Add table header 862 var thead = $('<thead>').appendTo(table); 863 var headerRow = $('<tr>').appendTo(thead); 864 [ 865 'Keyword', 866 'Search Volume', 867 'Search Intent', 868 'SERP Features' 869 ].forEach(function(headerText) { 870 $('<th>').text(headerText).appendTo(headerRow); 871 }); 872 873 // Add table body 874 var tbody = $('<tbody>').appendTo(table); 875 857 var $modal = jQuery('#intention-modal-' + pageId); 858 if (!$modal.length) { 859 return; 860 } 861 862 var $contentContainer = $modal.find('.intention-details-content'); 863 $contentContainer.empty(); 864 865 var $table = jQuery('<table class="wp-list-table widefat fixed striped seoaudp-intention-table">'); 866 var $thead = jQuery('<thead>').appendTo($table); 867 var $headerRow = jQuery('<tr>').appendTo($thead); 868 ['Keyword', 'Search Volume', 'Search Intent', 'SERP Features'].forEach(function(headerText) { 869 jQuery('<th>').text(headerText).appendTo($headerRow); 870 }); 871 872 var $tbody = jQuery('<tbody>').appendTo($table); 876 873 if (keywords.length > 0) { 877 874 keywords.forEach(function(item) { 878 var row = $('<tr>').appendTo(tbody); 879 880 // Keyword column 881 $('<td>').text(item.keyword || 'N/A').appendTo(row); 882 883 // Search Volume column 884 $('<td>').text(item.volume ? Number(item.volume).toLocaleString() : 'N/A').appendTo(row); 885 886 // Search Intent column (from database flags) 875 var $row = jQuery('<tr>').appendTo($tbody); 876 jQuery('<td>').text(item.keyword || 'N/A').appendTo($row); 877 jQuery('<td>').text(item.volume ? Number(item.volume).toLocaleString() : 'N/A').appendTo($row); 878 887 879 var intentV2 = []; 888 880 if (item.intent_flags) { … … 894 886 if (item.intent_flags.transactional) intentV2.push('Transactional'); 895 887 } 896 $('<td>').html(intentV2.length ? intentV2.join('<br>') : 'N/A').appendTo(row); 897 898 // SERP Features column 899 $('<td>').text(item.serp_features || 'N/A').appendTo(row); 888 jQuery('<td>').html(intentV2.length ? intentV2.join('<br>') : 'N/A').appendTo($row); 889 jQuery('<td>').text(item.serp_features || 'N/A').appendTo($row); 900 890 }); 901 891 } else { 902 $('<tr>').append( 903 $('<td colspan="4">').text('No keywords available.') 904 ).appendTo(tbody); 905 } 906 907 // Append modal to body and initialize 908 $('body').append(modalContent); 909 $('#seoaudp-search-intention-modal').dialog({ 910 modal: true, 911 width: 'auto', 912 maxWidth: '90%', 913 minWidth: 800, 914 height: 'auto', 915 maxHeight: $(window).height() * 0.9, 916 title: 'Searchers Intention Details', 917 classes: { 918 "ui-dialog": "seoaudp-modal-dialog" 919 }, 920 close: function() { 921 $(this).dialog('destroy').remove(); 922 } 923 }); 892 jQuery('<tr>').append( 893 jQuery('<td colspan="4">').text('No keywords available.') 894 ).appendTo($tbody); 895 } 896 897 $contentContainer.append($table); 898 if (typeof window.seoaudp_openModal === 'function') { 899 window.seoaudp_openModal('intention-modal-' + pageId); 900 } else { 901 $modal.show(); 902 } 903 }); 904 905 // Close handlers for the custom modal 906 jQuery(document).on('click', '.seoaudp-modal .seoaudp-close', function() { 907 jQuery(this).closest('.seoaudp-modal').hide(); 908 }); 909 jQuery(document).on('click', '.seoaudp-modal', function(e) { 910 if (jQuery(e.target).is('.seoaudp-modal')) { 911 jQuery(this).hide(); 912 } 924 913 }); 925 914 … … 1011 1000 function seoaudp_handleScroll() { 1012 1001 const stickyHeaders = document.querySelectorAll('.seoaudp-sticky-header'); 1013 const stickyTitles = document.querySelectorAll('.seoaudp-sticky-title'); 1014 const headerOffset = 32; // WP admin bar height 1002 const headerOffset = 0; // No extra gap above sticky slug 1015 1003 stickyHeaders.forEach(header => { 1016 const headerRect = header.getBoundingClientRect();1017 1004 const parentTable = header.closest('table'); 1018 1005 const tableRect = parentTable.getBoundingClientRect(); 1019 1006 const stickyTitles = header.querySelectorAll('.seoaudp-sticky-title'); 1020 1007 if (tableRect.top < headerOffset) { 1021 1008 header.classList.add('seoaudp-is-scrolled'); 1009 const translateY = Math.abs(tableRect.top - headerOffset); 1022 1010 stickyTitles.forEach(title => { 1023 title.style.transform = `translateY(${ Math.abs(tableRect.top - headerOffset)}px)`;1011 title.style.transform = `translateY(${translateY}px)`; 1024 1012 }); 1025 1013 } else { -
seo-ai-audit-tool/trunk/changelog.txt
r3359560 r3364780 1 = 1.1.6 2025-09-19 2 3 Improved: Check message consistency on Page auditor 4 Added: Import GSC & Ahrefs checklist 5 Improved: GUI improvements 6 Fixed: Broken pop-up modal when clicking 'Searchers Intention' 7 Added: GPT-5 settings support for respective pro Addon version 8 Added: Outdated Pro Addon message when manual update is necessary 9 1 10 = 1.1.5 2025-09-11 2 11 -
seo-ai-audit-tool/trunk/includes/class-seo-audit-data-import.php
r3359560 r3364780 82 82 <div class="wrap seoaudp-container"> 83 83 <h1 class="seoaudp-title">Import SEO Metrics</h1> 84 <div class="seoaudp-grid"> 85 <div class="seoaudp-card"> 86 <div class="seoaudp-card-header"> 87 <h2>Google Search Console</h2> 84 <div class="seoaudp-grid-two-columns"> 85 <div class="seoaudp-column"> 86 <div class="seoaudp-gsc-sub-columns"> 87 <div class="seoaudp-card"> 88 <div class="seoaudp-card-header"> 89 <h2>Google Search Console (Pages)</h2> 88 90 <?php if ($has_gsc_data): ?> 89 91 <span class="seoaudp-checkmark" title="Data exists in database">✓ <span class="seoaudp-checkmark-tooltip">Data exists in database</span></span> … … 145 147 </div> 146 148 147 <div class="seoaudp-card">148 <div class="seoaudp-card-header">149 <h2>Google Search Console</h2>150 <?php if ($has_gsc_queries_data): ?>151 <span class="seoaudp-checkmark" title="Data exists in database">✓ <span class="seoaudp-checkmark-tooltip">Data exists in database</span></span>152 <?php endif; ?>153 </div>154 <h4>Queries.csv Import</h4>149 <div class="seoaudp-card"> 150 <div class="seoaudp-card-header"> 151 <h2>Google Search Console (Queries)</h2> 152 <?php if ($has_gsc_queries_data): ?> 153 <span class="seoaudp-checkmark" title="Data exists in database">✓ <span class="seoaudp-checkmark-tooltip">Data exists in database</span></span> 154 <?php endif; ?> 155 </div> 156 <h4>Queries.csv Import</h4> 155 157 <div class="seoaudp-steps"> 156 158 <h3>How to export GSC Queries data:</h3> … … 205 207 </form> 206 208 </div> 209 </div> 210 </div> 207 211 </div> 208 212 209 <div class="seoaudp-card"> 210 <div class="seoaudp-card-header"> 211 <h2>Ahrefs Keywords</h2> 213 <div class="seoaudp-column"> 214 <div class="seoaudp-ahrefs-sub-columns"> 215 <div class="seoaudp-card"> 216 <div class="seoaudp-card-header"> 217 <h2>Ahrefs Keywords</h2> 212 218 <?php if ($has_ahrefs_data): ?> 213 219 <span class="seoaudp-checkmark" title="Data exists in database">✓ <span class="seoaudp-checkmark-tooltip">Data exists in database</span></span> … … 221 227 <li>Enter your domain</li> 222 228 <li>Click <b>Organic Keywords</b> (set the target country)</li> 229 <li>Click <b>By location</b> under Organic Keywords so the Keyword Intent columns are included</li> 223 230 <li>Click Export</li> 224 231 <li>Select all, select CSV <span style="color: red;">(UTF-8)</span></li> … … 269 276 </div> 270 277 271 <div class="seoaudp-card">272 <div class="seoaudp-card-header">273 <h2>Ahrefs Backlinks</h2>278 <div class="seoaudp-card"> 279 <div class="seoaudp-card-header"> 280 <h2>Ahrefs Backlinks</h2> 274 281 <?php if ($has_ahrefs_backlinks_data): ?> 275 282 <span class="seoaudp-checkmark" title="Data exists in database">✓ <span class="seoaudp-checkmark-tooltip">Data exists in database</span></span> … … 327 334 </form> 328 335 </div> 336 </div> 337 </div> 329 338 </div> 330 339 </div> … … 377 386 <p>You are about to import <?php echo intval($page_count); ?> pages of GSC data.</p> 378 387 <p><strong>Warning:</strong> This will overwrite any existing GSC data in the database.</p> 388 <?php 389 // Build a lightweight checklist summary based on columns present in first row 390 $first = $processed_data[0]; 391 $has_page = isset($first['page']); 392 $has_clicks = isset($first['clicks']); 393 $has_impr = isset($first['impressions']); 394 $has_ctr = isset($first['ctr']); 395 $has_pos = isset($first['position']); 396 ?> 397 <div style="margin:12px 0; padding:12px; border:1px solid #ddd; background:#fff;"> 398 <h2 style="margin:0 0 8px;">Import Checklist</h2> 399 <ul style="margin:0 0 8px 18px; list-style:disc;"> 400 <li style="color:<?php echo $has_page ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_page ? '✓' : '✗'; ?> Page URL</li> 401 <li style="color:<?php echo $has_clicks ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_clicks ? '✓' : '✗'; ?> Clicks</li> 402 <li style="color:<?php echo $has_impr ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_impr ? '✓' : '✗'; ?> Impressions</li> 403 <li style="color:<?php echo $has_ctr ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_ctr ? '✓' : '✗'; ?> CTR</li> 404 <li style="color:<?php echo $has_pos ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_pos ? '✓' : '✗'; ?> Position</li> 405 </ul> 406 <div style="font-size:12px;color:#555;">Rows detected: <?php echo intval($page_count); ?></div> 407 </div> 379 408 380 409 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"> … … 389 418 390 419 private function seoaudp_render_ahrefs_confirmation_page() { 391 $processed_data = get_transient('seoaudp_ahrefs_processed_data'); 420 $import_key = isset($_GET['import_key']) ? sanitize_text_field($_GET['import_key']) : get_transient('seoaudp_ahrefs_import_key'); 421 $processed_data = $import_key ? get_transient('seoaudp_ahrefs_processed_' . $import_key) : get_transient('seoaudp_ahrefs_processed_data'); 392 422 if (!$processed_data) { 393 423 wp_die('No data to import. Please upload a CSV file first.'); … … 400 430 <p><strong>Warning:</strong> This will overwrite any existing Ahrefs data in the database.</p> 401 431 432 <?php 433 $import_key = isset($_GET['import_key']) ? sanitize_text_field($_GET['import_key']) : get_transient('seoaudp_ahrefs_import_key'); 434 $meta = $import_key ? get_transient('seoaudp_ahrefs_import_meta_' . $import_key) : get_transient('seoaudp_ahrefs_import_meta'); 435 $validation_errors = $import_key ? get_transient('seoaudp_ahrefs_validation_errors_' . $import_key) : get_transient('seoaudp_ahrefs_validation_errors'); 436 if (is_array($meta)) { 437 $missing = array_map('esc_html', $meta['missing_optional'] ?? array()); 438 $present_required = array_map('esc_html', $meta['present_required'] ?? array()); 439 $intent_stats = $meta['intent_stats'] ?? array(); 440 ?> 441 <div style="margin:12px 0; padding:12px; border:1px solid #ddd; background:#fff;"> 442 <h2 style="margin:0 0 8px;">Import Checklist</h2> 443 <ul style="margin:0 0 8px 18px; list-style:disc;"> 444 <?php foreach ($present_required as $req): ?> 445 <li style="color:#2e7d32;">✓ Required present: <?php echo $req; ?></li> 446 <?php endforeach; ?> 447 <?php if (!empty($missing)): ?> 448 <?php foreach ($missing as $miss): ?> 449 <li style="color:#b71c1c;">✗ Optional missing: <?php echo $miss; ?></li> 450 <?php endforeach; ?> 451 <?php else: ?> 452 <li style="color:#2e7d32;">✓ All optional columns detected</li> 453 <?php endif; ?> 454 </ul> 455 456 <?php if (!empty($intent_stats)): ?> 457 <div style="margin:8px 0; padding:8px; background:#f8f9fa; border-left:3px solid #007cba;"> 458 <strong>Keyword Intent Data Summary:</strong><br> 459 <small> 460 • Rows with intent data: <?php echo intval($intent_stats['rows_with_intent_data'] ?? 0); ?><br> 461 • Rows missing intent data: <?php echo intval($intent_stats['rows_missing_intent_data'] ?? 0); ?><br> 462 <?php if (($intent_stats['invalid_intent_values'] ?? 0) > 0): ?> 463 • <span style="color:#d63384;">Invalid intent values found: <?php echo intval($intent_stats['invalid_intent_values']); ?></span><br> 464 <?php endif; ?> 465 </small> 466 </div> 467 <?php endif; ?> 468 469 <?php if (!empty($validation_errors) && is_array($validation_errors)): ?> 470 <div style="margin:8px 0; padding:8px; background:#fff3cd; border-left:3px solid #ffc107; max-height:150px; overflow-y:auto;"> 471 <strong style="color:#856404;">⚠️ Intent Data Validation Warnings:</strong><br> 472 <small style="color:#856404;"> 473 <?php foreach (array_slice($validation_errors, 0, 10) as $error): ?> 474 • <?php echo esc_html($error); ?><br> 475 <?php endforeach; ?> 476 <?php if (count($validation_errors) > 10): ?> 477 <em>... and <?php echo count($validation_errors) - 10; ?> more warnings.</em><br> 478 <?php endif; ?> 479 <strong>These rows will use default intent values (FALSE).</strong> 480 </small> 481 </div> 482 <?php endif; ?> 483 484 <div style="font-size:12px;color:#555;">Valid rows: <?php echo intval($meta['valid_rows'] ?? 0); ?> / Total rows: <?php echo intval($meta['total_rows'] ?? 0); ?></div> 485 </div> 486 <?php } ?> 487 402 488 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"> 403 489 <input type="hidden" name="action" value="seoaudp_confirm_ahrefs_import"> 490 <input type="hidden" name="import_key" value="<?php echo isset($import_key) ? esc_attr($import_key) : ''; ?>"> 404 491 <?php wp_nonce_field('seoaudp_confirm_ahrefs_import', 'seoaudp_ahrefs_confirm_nonce'); ?> 405 492 <?php submit_button('Confirm Import'); ?> … … 421 508 <p>You are about to import <?php echo intval($query_count); ?> queries of GSC data.</p> 422 509 <p><strong>Warning:</strong> This will overwrite any existing GSC queries data in the database.</p> 510 <?php 511 $first = $processed_data[0]; 512 $has_query = isset($first['query']); 513 $has_clicks = isset($first['clicks']); 514 $has_impr = isset($first['impressions']); 515 $has_ctr = isset($first['ctr']); 516 $has_pos = isset($first['position']); 517 ?> 518 <div style="margin:12px 0; padding:12px; border:1px solid #ddd; background:#fff;"> 519 <h2 style="margin:0 0 8px;">Import Checklist</h2> 520 <ul style="margin:0 0 8px 18px; list-style:disc;"> 521 <li style="color:<?php echo $has_query ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_query ? '✓' : '✗'; ?> Query</li> 522 <li style="color:<?php echo $has_clicks ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_clicks ? '✓' : '✗'; ?> Clicks</li> 523 <li style="color:<?php echo $has_impr ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_impr ? '✓' : '✗'; ?> Impressions</li> 524 <li style="color:<?php echo $has_ctr ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_ctr ? '✓' : '✗'; ?> CTR</li> 525 <li style="color:<?php echo $has_pos ? '#2e7d32' : '#b71c1c'; ?>;"><?php echo $has_pos ? '✓' : '✗'; ?> Position</li> 526 </ul> 527 <div style="font-size:12px;color:#555;">Rows detected: <?php echo intval($query_count); ?></div> 528 </div> 423 529 424 530 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"> … … 651 757 // Remove header row if it exists 652 758 $header = array_shift($csv_data); 653 654 // Check if the header contains expected columns 655 $required_columns = ['Keyword', 'Volume', 'KD', 'CPC', 'Current position', 'Current URL', 'SERP features']; 656 $traffic_column = in_array('Organic traffic', $header) ? 'Organic traffic' : 'Current organic traffic'; 657 $required_columns[] = $traffic_column; 658 659 $missing_columns = array_diff($required_columns, $header); 660 661 if (!empty($missing_columns)) { 662 wp_die('The CSV file is missing the following required columns: ' . implode(', ', array_map('esc_html', $missing_columns))); 663 } 664 665 // Get column indexes 759 if (!$header || !is_array($header)) { 760 wp_die('Invalid CSV: missing header row.'); 761 } 762 $header = array_map('trim', $header); 666 763 $column_indexes = array_flip($header); 764 765 // Required columns (super mandatory) 766 $required_columns = ['Keyword', 'Current URL']; 767 $missing_required = array_values(array_filter($required_columns, function($c) use ($column_indexes){ return !isset($column_indexes[$c]); })); 768 769 // Optional columns (nice-to-haves) 770 $optional_columns = ['Volume','KD','CPC','Current position','SERP features','Organic traffic','Current organic traffic','Branded','Local','Navigational','Informational','Commercial','Transactional']; 771 $missing_optional = array_values(array_filter($optional_columns, function($c) use ($column_indexes){ return !isset($column_indexes[$c]); })); 772 773 if (!empty($missing_required)) { 774 wp_die('The CSV file is missing required columns: ' . implode(', ', array_map('esc_html', $missing_required))); 775 } 776 777 // Traffic column can be either of these 778 $traffic_column = isset($column_indexes['Organic traffic']) ? 'Organic traffic' : (isset($column_indexes['Current organic traffic']) ? 'Current organic traffic' : null); 667 779 668 780 // Define the boolean columns we expect … … 675 787 'Transactional' 676 788 ]; 677 foreach ($csv_data as $row) { 678 // Check if the row has the expected number of columns 679 if (count($row) !== count($header)) { 680 continue; 681 } 789 790 $validation_errors = array(); 791 $intent_validation_stats = array( 792 'rows_with_intent_data' => 0, 793 'rows_missing_intent_data' => 0, 794 'invalid_intent_values' => 0 795 ); 796 797 foreach ($csv_data as $row_index => $row) { 798 if (empty($row)) { continue; } 799 // Skip if required fields are empty 800 if (!isset($row[$column_indexes['Keyword']]) || !isset($row[$column_indexes['Current URL']])) { continue; } 682 801 683 802 $url = trim($row[$column_indexes['Current URL']]); 684 if (!empty($url)) { 685 $data = array( 686 'keyword' => trim($row[$column_indexes['Keyword']]), 687 'volume' => intval($row[$column_indexes['Volume']]), 688 'kd' => intval($row[$column_indexes['KD']]), 689 'cpc' => floatval($row[$column_indexes['CPC']]), 690 'traffic' => intval($row[$column_indexes[$traffic_column]]), 691 'position' => intval($row[$column_indexes['Current position']]), 692 'page_url' => $url, 693 'serp_features' => trim($row[$column_indexes['SERP features']]), 694 ); 695 696 // Process boolean columns 697 foreach ($boolean_columns as $column) { 698 if (isset($column_indexes[$column])) { 699 $value = trim($row[$column_indexes[$column]]); 700 // Convert various TRUE values to 1, everything else to 0 701 $data[strtolower($column)] = in_array(strtoupper($value), ['TRUE', '1', 'YES', 'Y'], true) ? 1 : 0; 803 if ($url === '') { continue; } 804 805 $data = array( 806 'keyword' => trim($row[$column_indexes['Keyword']]), 807 'volume' => isset($column_indexes['Volume']) ? intval($row[$column_indexes['Volume']] ?? 0) : 0, 808 'kd' => isset($column_indexes['KD']) ? intval($row[$column_indexes['KD']] ?? 0) : 0, 809 'cpc' => isset($column_indexes['CPC']) ? floatval($row[$column_indexes['CPC']] ?? 0) : 0.0, 810 'traffic' => $traffic_column ? intval($row[$column_indexes[$traffic_column]] ?? 0) : 0, 811 'position' => isset($column_indexes['Current position']) ? intval($row[$column_indexes['Current position']] ?? 0) : 0, 812 'page_url' => $url, 813 'serp_features' => isset($column_indexes['SERP features']) ? trim($row[$column_indexes['SERP features']] ?? '') : '', 814 ); 815 816 // Validate and process intent boolean columns 817 $has_intent_data = false; 818 $intent_validation_errors = array(); 819 820 foreach ($boolean_columns as $column) { 821 $key = strtolower($column); 822 if (isset($column_indexes[$column])) { 823 $value = trim($row[$column_indexes[$column]] ?? ''); 824 825 // Check if this row has any intent data 826 if ($value !== '' && $value !== '0' && strtoupper($value) !== 'FALSE') { 827 $has_intent_data = true; 702 828 } 829 830 // Validate intent column values 831 $valid_values = ['TRUE', 'FALSE', '1', '0', 'YES', 'NO', 'Y', 'N', '']; 832 if ($value !== '' && !in_array(strtoupper($value), $valid_values, true)) { 833 $intent_validation_errors[] = "Row " . ($row_index + 2) . ": Invalid value '{$value}' in column '{$column}'. Expected TRUE/FALSE, 1/0, YES/NO, or empty."; 834 $intent_validation_stats['invalid_intent_values']++; 835 } 836 837 // Set boolean value 838 $data[$key] = in_array(strtoupper($value), ['TRUE','1','YES','Y'], true) ? 1 : 0; 839 } else { 840 $data[$key] = 0; 703 841 } 704 705 $processed_data[] = $data; 706 } 842 } 843 844 // Track intent data statistics 845 if ($has_intent_data) { 846 $intent_validation_stats['rows_with_intent_data']++; 847 } else { 848 $intent_validation_stats['rows_missing_intent_data']++; 849 } 850 851 // Add any validation errors for this row 852 if (!empty($intent_validation_errors)) { 853 $validation_errors = array_merge($validation_errors, $intent_validation_errors); 854 } 855 856 $processed_data[] = $data; 707 857 } 708 858 … … 711 861 } 712 862 863 // Handle validation errors - show warnings but don't stop import 864 if (!empty($validation_errors)) { 865 $error_message = '<strong>Intent Data Validation Warnings:</strong><br>' . implode('<br>', array_slice($validation_errors, 0, 10)); 866 if (count($validation_errors) > 10) { 867 $error_message .= '<br><em>... and ' . (count($validation_errors) - 10) . ' more warnings.</em>'; 868 } 869 $error_message .= '<br><br><strong>These rows will be imported with default intent values (FALSE). Continue?</strong>'; 870 871 // Store validation errors in transient for confirmation page 872 set_transient('seoaudp_ahrefs_validation_errors', $validation_errors, HOUR_IN_SECONDS); 873 } 874 875 // Save import meta for checklist UI on confirmation 876 $import_meta = array( 877 'total_rows' => count($csv_data), 878 'valid_rows' => count($processed_data), 879 'missing_optional' => $missing_optional, 880 'present_required' => $required_columns, 881 'intent_stats' => $intent_validation_stats, 882 'has_validation_errors' => !empty($validation_errors), 883 'validation_error_count' => count($validation_errors), 884 ); 885 set_transient('seoaudp_ahrefs_import_meta', $import_meta, HOUR_IN_SECONDS); 886 887 // Generate a unique import key so multiple imports won't collide and to reduce cache misses 888 $import_key = wp_generate_password(12, false); 889 set_transient('seoaudp_ahrefs_import_key', $import_key, HOUR_IN_SECONDS); 890 set_transient('seoaudp_ahrefs_import_meta_' . $import_key, $import_meta, HOUR_IN_SECONDS); 891 // Also store validation errors with the keyed name if present 892 $existing_validation_errors = get_transient('seoaudp_ahrefs_validation_errors'); 893 if (!empty($existing_validation_errors)) { 894 set_transient('seoaudp_ahrefs_validation_errors_' . $import_key, $existing_validation_errors, HOUR_IN_SECONDS); 895 } 896 713 897 // Check if existing data is present 714 898 $existing_data = $this->db->get_ahrefs_data_count(); … … 716 900 if ($existing_data && $existing_data > 0) { 717 901 // Store the processed data in a transient for later use 718 set_transient('seoaudp_ahrefs_processed_data', $processed_data, 60 * 60); // 1 hour expiration 902 set_transient('seoaudp_ahrefs_processed_data', $processed_data, 60 * 60); // legacy key for backward compatibility 903 set_transient('seoaudp_ahrefs_processed_' . $import_key, $processed_data, 60 * 60); 719 904 720 905 // Redirect to confirmation page 721 wp_redirect(add_query_arg(['page' => 'seo-audit-gsc', 'step' => 'confirm_ahrefs' ], admin_url('admin.php')));906 wp_redirect(add_query_arg(['page' => 'seo-audit-gsc', 'step' => 'confirm_ahrefs', 'import_key' => $import_key], admin_url('admin.php'))); 722 907 exit; 723 908 } else { … … 743 928 check_admin_referer('seoaudp_confirm_ahrefs_import', 'seoaudp_ahrefs_confirm_nonce'); 744 929 745 $processed_data = get_transient('seoaudp_ahrefs_processed_data'); 930 $import_key = isset($_POST['import_key']) ? sanitize_text_field($_POST['import_key']) : get_transient('seoaudp_ahrefs_import_key'); 931 $processed_data = $import_key ? get_transient('seoaudp_ahrefs_processed_' . $import_key) : get_transient('seoaudp_ahrefs_processed_data'); 746 932 747 933 if (!$processed_data) { … … 756 942 757 943 delete_transient('seoaudp_ahrefs_processed_data'); 944 if ($import_key) { 945 delete_transient('seoaudp_ahrefs_processed_' . $import_key); 946 delete_transient('seoaudp_ahrefs_import_meta_' . $import_key); 947 delete_transient('seoaudp_ahrefs_validation_errors_' . $import_key); 948 } 949 delete_transient('seoaudp_ahrefs_import_meta'); 950 delete_transient('seoaudp_ahrefs_validation_errors'); 758 951 if ($result) { 759 952 update_option('seoaudp_ahrefs_last_import', current_time('mysql')); … … 868 1061 // Import new data 869 1062 $result = $this->db->seoaudp_store_gsc_queries_data($processed_data); 870 1063 871 1064 delete_transient('seoaudp_gsc_queries_processed_data'); 872 1065 -
seo-ai-audit-tool/trunk/includes/class-seo-audit-db.php
r3356502 r3364780 738 738 global $wpdb; 739 739 $this->ensure_table_exists($this->ahrefs_keywords_table); 740 // Safety guard: ensure page_id is nullable to allow external URLs 741 $col_info = $wpdb->get_row("SHOW COLUMNS FROM {$this->ahrefs_keywords_table} LIKE 'page_id'"); 742 if ($col_info && strtoupper($col_info->Null) === 'NO') { 743 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared 744 $wpdb->query("ALTER TABLE {$this->ahrefs_keywords_table} MODIFY page_id bigint(20) DEFAULT NULL"); 745 } 740 746 741 747 $success = true; … … 909 915 $sql = "CREATE TABLE IF NOT EXISTS $table_name ( 910 916 id mediumint(9) NOT NULL AUTO_INCREMENT, 911 page_id bigint(20) NOT NULL,917 page_id bigint(20) DEFAULT NULL, 912 918 keyword varchar(255) NOT NULL, 913 919 volume int(11) NOT NULL, … … 932 938 dbDelta($sql); 933 939 940 // Ensure page_id allows NULL in case older installs have NOT NULL 941 $col_info = $wpdb->get_row("SHOW COLUMNS FROM $table_name LIKE 'page_id'"); 942 if ($col_info && strtoupper($col_info->Null) === 'NO') { 943 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared 944 $wpdb->query("ALTER TABLE $table_name MODIFY page_id bigint(20) DEFAULT NULL"); 945 } 946 934 947 // Add new columns if they don't exist 935 948 $new_columns = array( … … 1187 1200 } 1188 1201 } else { 1189 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Base query is escaped 1190 $query = $wpdb->prepare($base_query, []); 1202 // No dynamic placeholders; use the base query directly 1203 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Base query is built safely above 1204 $query = $base_query; 1191 1205 } 1192 1206 -
seo-ai-audit-tool/trunk/includes/class-seo-audit-settings-page.php
r3359560 r3364780 31 31 add_action('wp_ajax_seoaudp_clear_ai_logs', array($this, 'seoaudp_clear_ai_logs')); 32 32 $this->db = new \SEO_Audit_Tool\SEO_Audit_DB(); 33 } 34 35 /** 36 * Check if GPT-5 support is enabled (pro version 1.1.4 or greater) 37 */ 38 private function is_gpt_5_support_enabled() { 39 if (!seoaudp_pro_active()) { 40 return false; 41 } 42 43 if (defined('SEO_AUDIT_PRO_ADDON_VERSION')) { 44 $pro_version = SEO_AUDIT_PRO_ADDON_VERSION; 45 return version_compare($pro_version, '1.1.4', '>='); 46 } 47 48 return false; 33 49 } 34 50 … … 182 198 title="Select the AI model that best fits your needs - newer models generally provide better results but may cost more" 183 199 <?php echo seoaudp_pro_active() ? '' : 'disabled="disabled"'; ?>> 184 <!-- <option value="gpt-5" <!?php selected(get_option('seoaudp_openai_model'), 'gpt-5'); ?>>GPT-5 (latest)</option> 185 <option value="gpt-5-mini" <!?php selected(get_option('seoaudp_openai_model'), 'gpt-5-mini'); ?>>GPT-5 Mini (recommended)</option> --> 186 <option value="gpt-4.1-mini" <?php selected(get_option('seoaudp_openai_model'), 'gpt-4.1-mini'); ?>>GPT-4.1 Mini (recommended)</option> 187 <option value="gpt-4.1" <?php selected(get_option('seoaudp_openai_model'), 'gpt-4.1'); ?>>GPT-4.1</option> 188 <option value="gpt-4.1-nano" <?php selected(get_option('seoaudp_openai_model'), 'gpt-4.1-nano'); ?>>GPT-4.1 Nano</option> 189 <option value="o4-mini" <?php selected(get_option('seoaudp_openai_model'), 'o4-mini'); ?>>O4 Mini</option> 190 <option value="gpt-4o" <?php selected(get_option('seoaudp_openai_model'), 'gpt-4o'); ?>>GPT-4o</option> 200 <?php 201 $is_gpt_5_support_enabled = $this->is_gpt_5_support_enabled(); 202 203 if ($is_gpt_5_support_enabled) { 204 // Show only GPT-5 models for version 1.1.4 or greater 205 ?> 206 <option value="gpt-5-mini" <?php selected(get_option('seoaudp_openai_model'), 'gpt-5-mini'); ?>>GPT-5 Mini (recommended)</option> 207 <option value="gpt-5" <?php selected(get_option('seoaudp_openai_model'), 'gpt-5'); ?>>GPT-5 (latest)</option> 208 <?php 209 } else { 210 // Show all models except GPT-5 for older versions 211 ?> 212 <option value="gpt-4.1-mini" <?php selected(get_option('seoaudp_openai_model'), 'gpt-4.1-mini'); ?>>GPT-4.1 Mini (recommended)</option> 213 <option value="gpt-4.1" <?php selected(get_option('seoaudp_openai_model'), 'gpt-4.1'); ?>>GPT-4.1</option> 214 <option value="gpt-4.1-nano" <?php selected(get_option('seoaudp_openai_model'), 'gpt-4.1-nano'); ?>>GPT-4.1 Nano</option> 215 <option value="o4-mini" <?php selected(get_option('seoaudp_openai_model'), 'o4-mini'); ?>>O4 Mini</option> 216 <option value="gpt-4o" <?php selected(get_option('seoaudp_openai_model'), 'gpt-4o'); ?>>GPT-4o</option> 217 <?php 218 } 219 ?> 191 220 </select> 192 221 </div> -
seo-ai-audit-tool/trunk/includes/models/class-seo-audit-model.php
r3352841 r3364780 275 275 return array( 276 276 'status' => $status, 277 'message' => implode(". ", $message),277 'message' => $status === 'pass' ? "Pass" : "Fail", 278 278 'details' => "<strong>Title:</strong> {$title}<br>" . 279 279 "<strong>Keyword:</strong> {$keyword}<br>" . … … 367 367 return array( 368 368 'status' => $descLength > 50 && $descLength <= 160 && $hasKeyword ? 'pass' : 'fail', 369 'message' => sprintf( 370 "Length: %d chars. Keyword match: %.1f%%", 371 $descLength, 372 $matchPercentage 373 ), 369 'message' => $descLength > 50 && $descLength <= 160 && $hasKeyword ? "Pass" : "Fail", 374 370 'details' => "<strong>Description:</strong> " . $highlightedDesc . "<br>" . 375 371 "<strong>Source:</strong> " . esc_html($description_source) . "<br>" . … … 457 453 return array( 458 454 'status' => !empty($h1_content) && $hasKeyword ? 'pass' : 'fail', 459 'message' => sprintf( 460 "H1 %s. Keyword match: %.1f%%", 461 empty($h1_content) ? "not found" : "found", 462 $matchPercentage 463 ), 455 'message' => !empty($h1_content) && $hasKeyword ? "Pass" : "Fail", 464 456 'details' => sprintf( 465 457 "<strong>H1 Tag (%s):</strong> %s<br><strong>Keyword:</strong> %s", … … 521 513 return array( 522 514 'status' => $hasKeyword ? 'pass' : 'fail', 523 'message' => sprintf( 524 "Keyword in first H2: %.1f%%", 525 $matchPercentage 526 ), 515 'message' => $hasKeyword ? "Pass" : "Fail", 527 516 'details' => "<strong>First H2:</strong> {$highlightedH2}<br><strong>Keyword:</strong> " . esc_html($keyword) 528 517 ); … … 573 562 return array( 574 563 'status' => $hasKeyword ? 'pass' : 'fail', 575 'message' => sprintf( 576 "Keyword in first 100 words: %.1f%%", 577 $matchPercentage 578 ), 564 'message' => $hasKeyword ? "Pass" : "Fail", 579 565 'details' => "<strong>First 100 words:</strong><br>" . $highlightedContent . "<br><br><strong>Keyword:</strong> " . esc_html($keyword) 580 566 ); … … 586 572 return array( 587 573 'status' => $wordCount >= 300 ? 'pass' : 'fail', 588 'message' => "Word count: {$wordCount}",574 'message' => $wordCount >= 300 ? "Pass" : "Fail", 589 575 'details' => "<strong>Word count:</strong> {$wordCount}<br><strong>Recommended minimum:</strong> 300 words" 590 576 ); … … 599 585 return array( 600 586 'status' => $days <= 90 ? 'pass' : 'fail', 601 'message' => "Last modified: {$lastModified} ({$days} days ago)",587 'message' => $days <= 90 ? "Pass" : "Fail", 602 588 'details' => "<strong>Last modified:</strong> {$lastModified}<br><strong>Days since last update:</strong> {$days}<br><strong>Recommendation:</strong> " . ($days <= 90 ? "Content is up to date" : "Content may need updating") 603 589 ); … … 633 619 return array( 634 620 'status' => $bodyLinks > 0 ? 'pass' : 'fail', 635 'message' => $bodyLinks > 0 ? "Found {$bodyLinks} internal link(s)" : "No internal links found - Add internal links to improve SEO and user navigation",621 'message' => $bodyLinks > 0 ? $bodyLinks : "", 636 622 'details' => $details 637 623 ); … … 667 653 return array( 668 654 'status' => $uniqueExternalLinks > 0 ? 'pass' : 'fail', 669 'message' => $uniqueExternalLinks > 0 ? "Found {$uniqueExternalLinks} external link(s)" : "No external links found - Consider adding links to authoritative sources",655 'message' => $uniqueExternalLinks > 0 ? $uniqueExternalLinks : "", 670 656 'details' => $details 671 657 ); … … 743 729 744 730 $status = ($imagesWithAlt === $totalImages && $imagesWithKeyword > 0) ? 'pass' : 'fail'; 745 $message = "Images with alt text: {$imagesWithAlt}/{$totalImages}";731 $message = $status === 'pass' ? "Pass" : "Fail"; 746 732 $details = "<strong>Total images:</strong> {$totalImages}<br><strong>With alt:</strong> {$imagesWithAlt}<br><strong>Without alt:</strong> {$imagesWithoutAlt}<br>" 747 733 . "<strong>With keyword:</strong> {$imagesWithKeyword}<br><strong>WebP format:</strong> {$webpImages}<br><br>" … … 786 772 return array( 787 773 'status' => count($keywordRichImages) > 0 ? 'pass' : 'fail', 788 'message' => "Images with keyword in filename: " . count($keywordRichImages) . "/" . count($images),774 'message' => count($keywordRichImages) > 0 ? "Pass" : "Fail", 789 775 'details' => $details 790 776 ); … … 825 811 826 812 $status = $allWebP ? 'pass' : 'fail'; 827 $message = $ allWebP ? "All images are in WebP format" : "Not all images are in WebP format";813 $message = $status === 'pass' ? "Pass" : "Fail"; 828 814 829 815 $details = "<strong>WebP images:</strong> {$webpCount}<br>" … … 925 911 return array( 926 912 'status' => $hasKeyword ? 'pass' : 'fail', 927 'message' => sprintf( 928 "%s (%.1f%% match)", 929 $hasKeyword ? "Keyword found in URL" : "Keyword not sufficiently represented in URL", 930 $matchPercentage 931 ), 913 'message' => $hasKeyword ? "Pass" : "Fail", 932 914 'details' => "<strong>URL:</strong> {$highlightedUrl}<br>" . 933 915 "<strong>Keyword:</strong> {$keyword}<br>" . -
seo-ai-audit-tool/trunk/includes/views/class-seo-audit-view.php
r3242953 r3364780 110 110 <div class="wrap seoaudp-wrapper"> 111 111 <?php include(plugin_dir_path(__FILE__) . 'partials/header-partial.php'); ?> 112 113 <?php if (seoaudp_is_pro_version_outdated()): ?> 114 <div class="seoaudp-banner seoaudp-banner--warning"> 115 <div class="seoaudp-banner__icon"> 116 <span class="dashicons dashicons-warning"></span> 117 </div> 118 <div class="seoaudp-banner__content"> 119 <h4 class="seoaudp-banner__title"> 120 SEO AI Audit Tool Pro Addon Update Required 121 </h4> 122 <p class="seoaudp-banner__message"> 123 Your SEO AI Audit Tool <strong>Pro Addon</strong> is outdated. If you're having trouble updating from the plugins page, 124 download the new version from your <a href="https://seo-ai-audit-tool.designful.ca/my-account/" target="_blank"><strong>Members portal</strong></a>. 125 </p> 126 <p class="seoaudp-banner__actions"> 127 <a href="https://seo-ai-audit-tool.designful.ca/my-account/" 128 target="_blank" 129 class="button button-primary"> 130 <span class="dashicons dashicons-external" style="margin-right: 5px; font-size: 14px; vertical-align: middle;"></span> 131 Go to Members Portal 132 </a> 133 </p> 134 </div> 135 </div> 136 <?php endif; ?> 137 112 138 <div class="seoaudp-wrapper-body"> 113 139 <input type="hidden" id="seoaudp-page-filter" value="<?php echo esc_attr(sanitize_text_field($_GET['post_type'] ?? 'all')); ?>"> -
seo-ai-audit-tool/trunk/includes/views/partials/default-category-rows-partial.php
r3216569 r3364780 29 29 <?php 30 30 if (in_array($item, ['Internal Link Count', 'External Link Count'])) { 31 echo esc_html(ucfirst($result['status']) . ' (' . $result['message'] . ')'); 31 if (!empty($result['message'])) { 32 echo esc_html(ucfirst($result['status']) . ' (' . $result['message'] . ')'); 33 } else { 34 echo esc_html(ucfirst($result['status'])); 35 } 32 36 } else { 33 37 echo esc_html(ucfirst($result['status'])); -
seo-ai-audit-tool/trunk/includes/views/partials/table-header-partial.php
r3216569 r3364780 18 18 </label> 19 19 <?php } ?> 20 <a href="<?php echo esc_url(get_permalink($page->ID)); ?>" target="_blank" >21 < ?php echo esc_html($page->post_title); ?><br>20 <a href="<?php echo esc_url(get_permalink($page->ID)); ?>" target="_blank" title="<?php echo esc_attr($page->post_title); ?>"> 21 <span class="seoaudp-page-title-text" title="<?php echo esc_attr($page->post_title); ?>"><?php echo esc_html($page->post_title); ?></span><br> 22 22 <small><?php echo esc_html($page->post_name); ?> <span class="dashicons dashicons-admin-links"></span></small> 23 23 </a> -
seo-ai-audit-tool/trunk/readme.md
r3359560 r3364780 2 2 Contributors: Designful 3 3 Plugin URL: https://seo-ai-audit-tool.designful.ca/ 4 Version: 1.1. 54 Version: 1.1.6 5 5 Tags: seo audit, ai seo, conversion optimization, content analysis, search intent 6 6 Requires at least: 4.0 7 7 Tested up to: 6.8 8 Stable tag: 1.1. 58 Stable tag: 1.1.6 9 9 Requires PHP: 7.4 10 10 License: GPLv2 or later -
seo-ai-audit-tool/trunk/seo-ai-audit-tool.code-workspace
r3333926 r3364780 8 8 } 9 9 ], 10 "settings": {} 10 "settings": { 11 "workbench.colorCustomizations": { 12 "titleBar.activeBackground": "#8f6cff", 13 "titleBar.activeForeground": "#ffffff", 14 "titleBar.inactiveBackground": "#8f6cff", 15 "titleBar.inactiveForeground": "#ffffff", 16 "statusBar.background": "#5a1d5a", 17 "statusBar.foreground": "#e7e7e7" 18 } 19 } 11 20 } -
seo-ai-audit-tool/trunk/seo-ai-audit-tool.php
r3359560 r3364780 4 4 * Plugin URI: https://designful.ca/apps/seo-ai-audit-tool/ 5 5 * Description: A WordPress plugin to audit SEO elements of pages and perform AI-powered content analysis 6 * Version: 1.1. 56 * Version: 1.1.6 7 7 * Author: Designful 8 8 * License: GPL2 … … 20 20 define('SEOAUDP_PLUGIN_DIR', plugin_dir_path(__FILE__)); 21 21 define('SEOAUDP_PLUGIN_URL', plugin_dir_url(__FILE__)); 22 define('SEOAUDP_PLUGIN_VERSION', '1.1. 5');22 define('SEOAUDP_PLUGIN_VERSION', '1.1.6'); 23 23 define('SEOAUDP_PLUGIN_BETA_VERSION', false); 24 define('SEOAUDP_DB_VERSION', '1. 8');24 define('SEOAUDP_DB_VERSION', '1.9'); 25 25 define('SEOAUDP_EDD_ITEM_ID', 307); 26 26 define('SEOAUDP_STORE_URL', 'https://seo-ai-audit-tool.designful.ca'); … … 390 390 } 391 391 392 function seoaudp_is_pro_version_outdated() { 393 if (!seoaudp_pro_active()) { 394 return false; 395 } 396 397 if (defined('SEO_AUDIT_PRO_ADDON_VERSION')) { 398 $pro_version = SEO_AUDIT_PRO_ADDON_VERSION; 399 return version_compare($pro_version, '1.1.2', '<'); 400 } 401 402 return false; 403 } 404 392 405 function seoaudp_init_hooks() { 393 406 add_action('admin_print_scripts', 'seoaudp_hide_admin_page');
Note: See TracChangeset
for help on using the changeset viewer.