Plugin Directory

Changeset 3364780


Ignore:
Timestamp:
09/19/2025 10:45:07 PM (5 months ago)
Author:
designful
Message:

Update to version 1.1.6 from GitHub

Location:
seo-ai-audit-tool
Files:
26 edited
1 copied

Legend:

Unmodified
Added
Removed
  • seo-ai-audit-tool/tags/1.1.6/assets/css/seoaudp-style.css

    r3359560 r3364780  
    803803}
    804804
     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
    805824.seoaudp-form-group {
    806825    margin-bottom: 20px;
     
    14021421    color: rgba(26, 41, 72, 1);
    14031422    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;
    14041431}
    14051432.seoaudp-page-title-container a small{
     
    19071934
    19081935.seoaudp-sticky-title {
    1909     position: relative;
     1936    position: relative; /* allow JS transform without native sticky gaps */
    19101937    transition: transform 0.02s ease-out;
    19111938    background: white;
     
    19231950    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
    19241951}
     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; }
    19251957.seoaudp-page-name-column {
    19261958    position: relative;
     
    19571989/* Responsive adjustments */
    19581990@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; }
    19661992}
    19671993
     
    36133639    }
    36143640}
     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  
    849849    });
    850850
    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) {
    853853        e.preventDefault();
    854854        var pageId = jQuery(this).data('page-id');
    855855        var keywords = window.seoaudp_keywordIntentions[pageId] || [];
    856856
    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);
    876873        if (keywords.length > 0) {
    877874            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
    887879                var intentV2 = [];
    888880                if (item.intent_flags) {
     
    894886                    if (item.intent_flags.transactional) intentV2.push('Transactional');
    895887                }
    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);
    900890            });
    901891        } 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        }
    924913    });
    925914
     
    10111000    function seoaudp_handleScroll() {
    10121001        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
    10151003        stickyHeaders.forEach(header => {
    1016             const headerRect = header.getBoundingClientRect();
    10171004            const parentTable = header.closest('table');
    10181005            const tableRect = parentTable.getBoundingClientRect();
    1019            
     1006            const stickyTitles = header.querySelectorAll('.seoaudp-sticky-title');
    10201007            if (tableRect.top < headerOffset) {
    10211008                header.classList.add('seoaudp-is-scrolled');
     1009                const translateY = Math.abs(tableRect.top - headerOffset);
    10221010                stickyTitles.forEach(title => {
    1023                     title.style.transform = `translateY(${Math.abs(tableRect.top - headerOffset)}px)`;
     1011                    title.style.transform = `translateY(${translateY}px)`;
    10241012                });
    10251013            } else {
  • seo-ai-audit-tool/tags/1.1.6/changelog.txt

    r3359560 r3364780  
     1= 1.1.6 2025-09-19
     2
     3Improved: Check message consistency on Page auditor
     4Added: Import GSC & Ahrefs checklist
     5Improved: GUI improvements
     6Fixed: Broken pop-up modal when clicking 'Searchers Intention'
     7Added: GPT-5 settings support for respective pro Addon version
     8Added: Outdated Pro Addon message when manual update is necessary
     9
    110= 1.1.5 2025-09-11
    211
  • seo-ai-audit-tool/tags/1.1.6/includes/class-seo-audit-data-import.php

    r3359560 r3364780  
    8282        <div class="wrap seoaudp-container">
    8383            <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>
    8890                        <?php if ($has_gsc_data): ?>
    8991                            <span class="seoaudp-checkmark" title="Data exists in database">✓ <span class="seoaudp-checkmark-tooltip">Data exists in database</span></span>
     
    145147                </div>
    146148
    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>
    155157                    <div class="seoaudp-steps">
    156158                        <h3>How to export GSC Queries data:</h3>
     
    205207                        </form>
    206208                    </div>
     209                        </div>
     210                    </div>
    207211                </div>
    208212
    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>
    212218                        <?php if ($has_ahrefs_data): ?>
    213219                            <span class="seoaudp-checkmark" title="Data exists in database">✓ <span class="seoaudp-checkmark-tooltip">Data exists in database</span></span>
     
    221227                            <li>Enter your domain</li>
    222228                            <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>
    223230                            <li>Click Export</li>
    224231                            <li>Select all, select CSV <span style="color: red;">(UTF-8)</span></li>
     
    269276                </div>
    270277
    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>
    274281                        <?php if ($has_ahrefs_backlinks_data): ?>
    275282                            <span class="seoaudp-checkmark" title="Data exists in database">✓ <span class="seoaudp-checkmark-tooltip">Data exists in database</span></span>
     
    327334                        </form>
    328335                    </div>
     336                        </div>
     337                    </div>
    329338                </div>
    330339            </div>
     
    377386            <p>You are about to import <?php echo intval($page_count); ?> pages of GSC data.</p>
    378387            <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>
    379408           
    380409            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
     
    389418
    390419    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');
    392422        if (!$processed_data) {
    393423            wp_die('No data to import. Please upload a CSV file first.');
     
    400430            <p><strong>Warning:</strong> This will overwrite any existing Ahrefs data in the database.</p>
    401431           
     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
    402488            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
    403489                <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) : ''; ?>">
    404491                <?php wp_nonce_field('seoaudp_confirm_ahrefs_import', 'seoaudp_ahrefs_confirm_nonce'); ?>
    405492                <?php submit_button('Confirm Import'); ?>
     
    421508            <p>You are about to import <?php echo intval($query_count); ?> queries of GSC data.</p>
    422509            <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>
    423529           
    424530            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
     
    651757        // Remove header row if it exists
    652758        $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);
    666763        $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);
    667779
    668780        // Define the boolean columns we expect
     
    675787            'Transactional'
    676788        ];
    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; }
    682801
    683802            $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;
    702828                    }
     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;
    703841                }
    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;
    707857        }
    708858
     
    711861        }
    712862
     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
    713897        // Check if existing data is present
    714898        $existing_data = $this->db->get_ahrefs_data_count();
     
    716900        if ($existing_data && $existing_data > 0) {
    717901            // 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);
    719904           
    720905            // 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')));
    722907            exit;
    723908        } else {
     
    743928        check_admin_referer('seoaudp_confirm_ahrefs_import', 'seoaudp_ahrefs_confirm_nonce');
    744929
    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');
    746932
    747933        if (!$processed_data) {
     
    756942
    757943        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');
    758951        if ($result) {
    759952            update_option('seoaudp_ahrefs_last_import', current_time('mysql'));
     
    8681061        // Import new data
    8691062        $result = $this->db->seoaudp_store_gsc_queries_data($processed_data);
    870 
     1063       
    8711064        delete_transient('seoaudp_gsc_queries_processed_data');
    8721065
  • seo-ai-audit-tool/tags/1.1.6/includes/class-seo-audit-db.php

    r3356502 r3364780  
    738738        global $wpdb;
    739739        $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        }
    740746       
    741747        $success = true;
     
    909915        $sql = "CREATE TABLE IF NOT EXISTS $table_name (
    910916            id mediumint(9) NOT NULL AUTO_INCREMENT,
    911             page_id bigint(20) NOT NULL,
     917            page_id bigint(20) DEFAULT NULL,
    912918            keyword varchar(255) NOT NULL,
    913919            volume int(11) NOT NULL,
     
    932938        dbDelta($sql);
    933939
     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
    934947        // Add new columns if they don't exist
    935948        $new_columns = array(
     
    11871200                }
    11881201            } 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;
    11911205            }
    11921206           
  • seo-ai-audit-tool/tags/1.1.6/includes/class-seo-audit-settings-page.php

    r3359560 r3364780  
    3131        add_action('wp_ajax_seoaudp_clear_ai_logs', array($this, 'seoaudp_clear_ai_logs'));
    3232        $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;
    3349    }
    3450
     
    182198                            title="Select the AI model that best fits your needs - newer models generally provide better results but may cost more"
    183199                            <?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                        ?>
    191220                    </select>
    192221                </div>
  • seo-ai-audit-tool/tags/1.1.6/includes/models/class-seo-audit-model.php

    r3352841 r3364780  
    275275        return array(
    276276            'status' => $status,
    277             'message' => implode(". ", $message),
     277            'message' => $status === 'pass' ? "Pass" : "Fail",
    278278            'details' => "<strong>Title:</strong> {$title}<br>" .
    279279                        "<strong>Keyword:</strong> {$keyword}<br>" .
     
    367367        return array(
    368368            '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",
    374370            'details' => "<strong>Description:</strong> " . $highlightedDesc . "<br>" .
    375371                        "<strong>Source:</strong> " . esc_html($description_source) . "<br>" .
     
    457453        return array(
    458454            '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",
    464456            'details' => sprintf(
    465457                "<strong>H1 Tag (%s):</strong> %s<br><strong>Keyword:</strong> %s",
     
    521513        return array(
    522514            'status' => $hasKeyword ? 'pass' : 'fail',
    523             'message' => sprintf(
    524                 "Keyword in first H2: %.1f%%",
    525                 $matchPercentage
    526             ),
     515            'message' => $hasKeyword ? "Pass" : "Fail",
    527516            'details' => "<strong>First H2:</strong> {$highlightedH2}<br><strong>Keyword:</strong> " . esc_html($keyword)
    528517        );
     
    573562        return array(
    574563            'status' => $hasKeyword ? 'pass' : 'fail',
    575             'message' => sprintf(
    576                 "Keyword in first 100 words: %.1f%%",
    577                 $matchPercentage
    578             ),
     564            'message' => $hasKeyword ? "Pass" : "Fail",
    579565            'details' => "<strong>First 100 words:</strong><br>" . $highlightedContent . "<br><br><strong>Keyword:</strong> " . esc_html($keyword)
    580566        );
     
    586572        return array(
    587573            'status' => $wordCount >= 300 ? 'pass' : 'fail',
    588             'message' => "Word count: {$wordCount}",
     574            'message' => $wordCount >= 300 ? "Pass" : "Fail",
    589575            'details' => "<strong>Word count:</strong> {$wordCount}<br><strong>Recommended minimum:</strong> 300 words"
    590576        );
     
    599585        return array(
    600586            'status' => $days <= 90 ? 'pass' : 'fail',
    601             'message' => "Last modified: {$lastModified} ({$days} days ago)",
     587            'message' => $days <= 90 ? "Pass" : "Fail",
    602588            '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")
    603589        );
     
    633619        return array(
    634620            '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 : "",
    636622            'details' => $details
    637623        );
     
    667653        return array(
    668654            '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 : "",
    670656            'details' => $details
    671657        );
     
    743729
    744730        $status = ($imagesWithAlt === $totalImages && $imagesWithKeyword > 0) ? 'pass' : 'fail';
    745         $message = "Images with alt text: {$imagesWithAlt}/{$totalImages}";
     731        $message = $status === 'pass' ? "Pass" : "Fail";
    746732        $details = "<strong>Total images:</strong> {$totalImages}<br><strong>With alt:</strong> {$imagesWithAlt}<br><strong>Without alt:</strong> {$imagesWithoutAlt}<br>"
    747733                 . "<strong>With keyword:</strong> {$imagesWithKeyword}<br><strong>WebP format:</strong> {$webpImages}<br><br>"
     
    786772        return array(
    787773            'status' => count($keywordRichImages) > 0 ? 'pass' : 'fail',
    788             'message' => "Images with keyword in filename: " . count($keywordRichImages) . "/" . count($images),
     774            'message' => count($keywordRichImages) > 0 ? "Pass" : "Fail",
    789775            'details' => $details
    790776        );
     
    825811       
    826812        $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";
    828814       
    829815        $details = "<strong>WebP images:</strong> {$webpCount}<br>"
     
    925911        return array(
    926912            '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",
    932914            'details' => "<strong>URL:</strong> {$highlightedUrl}<br>" .
    933915                        "<strong>Keyword:</strong> {$keyword}<br>" .
  • seo-ai-audit-tool/tags/1.1.6/includes/views/class-seo-audit-view.php

    r3242953 r3364780  
    110110        <div class="wrap seoaudp-wrapper">
    111111            <?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           
    112138            <div class="seoaudp-wrapper-body">
    113139            <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  
    2929                    <?php
    3030                    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                        }
    3236                    } else {
    3337                        echo esc_html(ucfirst($result['status']));
  • seo-ai-audit-tool/tags/1.1.6/includes/views/partials/table-header-partial.php

    r3216569 r3364780  
    1818                        </label>
    1919                        <?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>
    2222                            <small><?php echo esc_html($page->post_name); ?> <span class="dashicons dashicons-admin-links"></span></small>
    2323                        </a>
  • seo-ai-audit-tool/tags/1.1.6/readme.md

    r3359560 r3364780  
    22Contributors: Designful
    33Plugin URL: https://seo-ai-audit-tool.designful.ca/
    4 Version: 1.1.5
     4Version: 1.1.6
    55Tags: seo audit, ai seo, conversion optimization, content analysis, search intent
    66Requires at least: 4.0
    77Tested up to: 6.8
    8 Stable tag: 1.1.5
     8Stable tag: 1.1.6
    99Requires PHP: 7.4
    1010License: GPLv2 or later
  • seo-ai-audit-tool/tags/1.1.6/seo-ai-audit-tool.code-workspace

    r3333926 r3364780  
    88        }
    99    ],
    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    }
    1120}
  • seo-ai-audit-tool/tags/1.1.6/seo-ai-audit-tool.php

    r3359560 r3364780  
    44 * Plugin URI: https://designful.ca/apps/seo-ai-audit-tool/
    55 * Description: A WordPress plugin to audit SEO elements of pages and perform AI-powered content analysis
    6  * Version:     1.1.5
     6 * Version:     1.1.6
    77 * Author:      Designful
    88 * License:     GPL2
     
    2020define('SEOAUDP_PLUGIN_DIR', plugin_dir_path(__FILE__));
    2121define('SEOAUDP_PLUGIN_URL', plugin_dir_url(__FILE__));
    22 define('SEOAUDP_PLUGIN_VERSION', '1.1.5');
     22define('SEOAUDP_PLUGIN_VERSION', '1.1.6');
    2323define('SEOAUDP_PLUGIN_BETA_VERSION', false);
    24 define('SEOAUDP_DB_VERSION', '1.8');
     24define('SEOAUDP_DB_VERSION', '1.9');
    2525define('SEOAUDP_EDD_ITEM_ID', 307);
    2626define('SEOAUDP_STORE_URL', 'https://seo-ai-audit-tool.designful.ca');
     
    390390}
    391391
     392function 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
    392405function seoaudp_init_hooks() {
    393406    add_action('admin_print_scripts', 'seoaudp_hide_admin_page');
  • seo-ai-audit-tool/trunk/assets/css/seoaudp-style.css

    r3359560 r3364780  
    803803}
    804804
     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
    805824.seoaudp-form-group {
    806825    margin-bottom: 20px;
     
    14021421    color: rgba(26, 41, 72, 1);
    14031422    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;
    14041431}
    14051432.seoaudp-page-title-container a small{
     
    19071934
    19081935.seoaudp-sticky-title {
    1909     position: relative;
     1936    position: relative; /* allow JS transform without native sticky gaps */
    19101937    transition: transform 0.02s ease-out;
    19111938    background: white;
     
    19231950    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
    19241951}
     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; }
    19251957.seoaudp-page-name-column {
    19261958    position: relative;
     
    19571989/* Responsive adjustments */
    19581990@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; }
    19661992}
    19671993
     
    36133639    }
    36143640}
     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  
    849849    });
    850850
    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) {
    853853        e.preventDefault();
    854854        var pageId = jQuery(this).data('page-id');
    855855        var keywords = window.seoaudp_keywordIntentions[pageId] || [];
    856856
    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);
    876873        if (keywords.length > 0) {
    877874            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
    887879                var intentV2 = [];
    888880                if (item.intent_flags) {
     
    894886                    if (item.intent_flags.transactional) intentV2.push('Transactional');
    895887                }
    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);
    900890            });
    901891        } 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        }
    924913    });
    925914
     
    10111000    function seoaudp_handleScroll() {
    10121001        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
    10151003        stickyHeaders.forEach(header => {
    1016             const headerRect = header.getBoundingClientRect();
    10171004            const parentTable = header.closest('table');
    10181005            const tableRect = parentTable.getBoundingClientRect();
    1019            
     1006            const stickyTitles = header.querySelectorAll('.seoaudp-sticky-title');
    10201007            if (tableRect.top < headerOffset) {
    10211008                header.classList.add('seoaudp-is-scrolled');
     1009                const translateY = Math.abs(tableRect.top - headerOffset);
    10221010                stickyTitles.forEach(title => {
    1023                     title.style.transform = `translateY(${Math.abs(tableRect.top - headerOffset)}px)`;
     1011                    title.style.transform = `translateY(${translateY}px)`;
    10241012                });
    10251013            } else {
  • seo-ai-audit-tool/trunk/changelog.txt

    r3359560 r3364780  
     1= 1.1.6 2025-09-19
     2
     3Improved: Check message consistency on Page auditor
     4Added: Import GSC & Ahrefs checklist
     5Improved: GUI improvements
     6Fixed: Broken pop-up modal when clicking 'Searchers Intention'
     7Added: GPT-5 settings support for respective pro Addon version
     8Added: Outdated Pro Addon message when manual update is necessary
     9
    110= 1.1.5 2025-09-11
    211
  • seo-ai-audit-tool/trunk/includes/class-seo-audit-data-import.php

    r3359560 r3364780  
    8282        <div class="wrap seoaudp-container">
    8383            <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>
    8890                        <?php if ($has_gsc_data): ?>
    8991                            <span class="seoaudp-checkmark" title="Data exists in database">✓ <span class="seoaudp-checkmark-tooltip">Data exists in database</span></span>
     
    145147                </div>
    146148
    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>
    155157                    <div class="seoaudp-steps">
    156158                        <h3>How to export GSC Queries data:</h3>
     
    205207                        </form>
    206208                    </div>
     209                        </div>
     210                    </div>
    207211                </div>
    208212
    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>
    212218                        <?php if ($has_ahrefs_data): ?>
    213219                            <span class="seoaudp-checkmark" title="Data exists in database">✓ <span class="seoaudp-checkmark-tooltip">Data exists in database</span></span>
     
    221227                            <li>Enter your domain</li>
    222228                            <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>
    223230                            <li>Click Export</li>
    224231                            <li>Select all, select CSV <span style="color: red;">(UTF-8)</span></li>
     
    269276                </div>
    270277
    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>
    274281                        <?php if ($has_ahrefs_backlinks_data): ?>
    275282                            <span class="seoaudp-checkmark" title="Data exists in database">✓ <span class="seoaudp-checkmark-tooltip">Data exists in database</span></span>
     
    327334                        </form>
    328335                    </div>
     336                        </div>
     337                    </div>
    329338                </div>
    330339            </div>
     
    377386            <p>You are about to import <?php echo intval($page_count); ?> pages of GSC data.</p>
    378387            <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>
    379408           
    380409            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
     
    389418
    390419    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');
    392422        if (!$processed_data) {
    393423            wp_die('No data to import. Please upload a CSV file first.');
     
    400430            <p><strong>Warning:</strong> This will overwrite any existing Ahrefs data in the database.</p>
    401431           
     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
    402488            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
    403489                <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) : ''; ?>">
    404491                <?php wp_nonce_field('seoaudp_confirm_ahrefs_import', 'seoaudp_ahrefs_confirm_nonce'); ?>
    405492                <?php submit_button('Confirm Import'); ?>
     
    421508            <p>You are about to import <?php echo intval($query_count); ?> queries of GSC data.</p>
    422509            <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>
    423529           
    424530            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
     
    651757        // Remove header row if it exists
    652758        $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);
    666763        $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);
    667779
    668780        // Define the boolean columns we expect
     
    675787            'Transactional'
    676788        ];
    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; }
    682801
    683802            $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;
    702828                    }
     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;
    703841                }
    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;
    707857        }
    708858
     
    711861        }
    712862
     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
    713897        // Check if existing data is present
    714898        $existing_data = $this->db->get_ahrefs_data_count();
     
    716900        if ($existing_data && $existing_data > 0) {
    717901            // 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);
    719904           
    720905            // 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')));
    722907            exit;
    723908        } else {
     
    743928        check_admin_referer('seoaudp_confirm_ahrefs_import', 'seoaudp_ahrefs_confirm_nonce');
    744929
    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');
    746932
    747933        if (!$processed_data) {
     
    756942
    757943        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');
    758951        if ($result) {
    759952            update_option('seoaudp_ahrefs_last_import', current_time('mysql'));
     
    8681061        // Import new data
    8691062        $result = $this->db->seoaudp_store_gsc_queries_data($processed_data);
    870 
     1063       
    8711064        delete_transient('seoaudp_gsc_queries_processed_data');
    8721065
  • seo-ai-audit-tool/trunk/includes/class-seo-audit-db.php

    r3356502 r3364780  
    738738        global $wpdb;
    739739        $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        }
    740746       
    741747        $success = true;
     
    909915        $sql = "CREATE TABLE IF NOT EXISTS $table_name (
    910916            id mediumint(9) NOT NULL AUTO_INCREMENT,
    911             page_id bigint(20) NOT NULL,
     917            page_id bigint(20) DEFAULT NULL,
    912918            keyword varchar(255) NOT NULL,
    913919            volume int(11) NOT NULL,
     
    932938        dbDelta($sql);
    933939
     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
    934947        // Add new columns if they don't exist
    935948        $new_columns = array(
     
    11871200                }
    11881201            } 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;
    11911205            }
    11921206           
  • seo-ai-audit-tool/trunk/includes/class-seo-audit-settings-page.php

    r3359560 r3364780  
    3131        add_action('wp_ajax_seoaudp_clear_ai_logs', array($this, 'seoaudp_clear_ai_logs'));
    3232        $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;
    3349    }
    3450
     
    182198                            title="Select the AI model that best fits your needs - newer models generally provide better results but may cost more"
    183199                            <?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                        ?>
    191220                    </select>
    192221                </div>
  • seo-ai-audit-tool/trunk/includes/models/class-seo-audit-model.php

    r3352841 r3364780  
    275275        return array(
    276276            'status' => $status,
    277             'message' => implode(". ", $message),
     277            'message' => $status === 'pass' ? "Pass" : "Fail",
    278278            'details' => "<strong>Title:</strong> {$title}<br>" .
    279279                        "<strong>Keyword:</strong> {$keyword}<br>" .
     
    367367        return array(
    368368            '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",
    374370            'details' => "<strong>Description:</strong> " . $highlightedDesc . "<br>" .
    375371                        "<strong>Source:</strong> " . esc_html($description_source) . "<br>" .
     
    457453        return array(
    458454            '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",
    464456            'details' => sprintf(
    465457                "<strong>H1 Tag (%s):</strong> %s<br><strong>Keyword:</strong> %s",
     
    521513        return array(
    522514            'status' => $hasKeyword ? 'pass' : 'fail',
    523             'message' => sprintf(
    524                 "Keyword in first H2: %.1f%%",
    525                 $matchPercentage
    526             ),
     515            'message' => $hasKeyword ? "Pass" : "Fail",
    527516            'details' => "<strong>First H2:</strong> {$highlightedH2}<br><strong>Keyword:</strong> " . esc_html($keyword)
    528517        );
     
    573562        return array(
    574563            'status' => $hasKeyword ? 'pass' : 'fail',
    575             'message' => sprintf(
    576                 "Keyword in first 100 words: %.1f%%",
    577                 $matchPercentage
    578             ),
     564            'message' => $hasKeyword ? "Pass" : "Fail",
    579565            'details' => "<strong>First 100 words:</strong><br>" . $highlightedContent . "<br><br><strong>Keyword:</strong> " . esc_html($keyword)
    580566        );
     
    586572        return array(
    587573            'status' => $wordCount >= 300 ? 'pass' : 'fail',
    588             'message' => "Word count: {$wordCount}",
     574            'message' => $wordCount >= 300 ? "Pass" : "Fail",
    589575            'details' => "<strong>Word count:</strong> {$wordCount}<br><strong>Recommended minimum:</strong> 300 words"
    590576        );
     
    599585        return array(
    600586            'status' => $days <= 90 ? 'pass' : 'fail',
    601             'message' => "Last modified: {$lastModified} ({$days} days ago)",
     587            'message' => $days <= 90 ? "Pass" : "Fail",
    602588            '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")
    603589        );
     
    633619        return array(
    634620            '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 : "",
    636622            'details' => $details
    637623        );
     
    667653        return array(
    668654            '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 : "",
    670656            'details' => $details
    671657        );
     
    743729
    744730        $status = ($imagesWithAlt === $totalImages && $imagesWithKeyword > 0) ? 'pass' : 'fail';
    745         $message = "Images with alt text: {$imagesWithAlt}/{$totalImages}";
     731        $message = $status === 'pass' ? "Pass" : "Fail";
    746732        $details = "<strong>Total images:</strong> {$totalImages}<br><strong>With alt:</strong> {$imagesWithAlt}<br><strong>Without alt:</strong> {$imagesWithoutAlt}<br>"
    747733                 . "<strong>With keyword:</strong> {$imagesWithKeyword}<br><strong>WebP format:</strong> {$webpImages}<br><br>"
     
    786772        return array(
    787773            'status' => count($keywordRichImages) > 0 ? 'pass' : 'fail',
    788             'message' => "Images with keyword in filename: " . count($keywordRichImages) . "/" . count($images),
     774            'message' => count($keywordRichImages) > 0 ? "Pass" : "Fail",
    789775            'details' => $details
    790776        );
     
    825811       
    826812        $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";
    828814       
    829815        $details = "<strong>WebP images:</strong> {$webpCount}<br>"
     
    925911        return array(
    926912            '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",
    932914            'details' => "<strong>URL:</strong> {$highlightedUrl}<br>" .
    933915                        "<strong>Keyword:</strong> {$keyword}<br>" .
  • seo-ai-audit-tool/trunk/includes/views/class-seo-audit-view.php

    r3242953 r3364780  
    110110        <div class="wrap seoaudp-wrapper">
    111111            <?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           
    112138            <div class="seoaudp-wrapper-body">
    113139            <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  
    2929                    <?php
    3030                    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                        }
    3236                    } else {
    3337                        echo esc_html(ucfirst($result['status']));
  • seo-ai-audit-tool/trunk/includes/views/partials/table-header-partial.php

    r3216569 r3364780  
    1818                        </label>
    1919                        <?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>
    2222                            <small><?php echo esc_html($page->post_name); ?> <span class="dashicons dashicons-admin-links"></span></small>
    2323                        </a>
  • seo-ai-audit-tool/trunk/readme.md

    r3359560 r3364780  
    22Contributors: Designful
    33Plugin URL: https://seo-ai-audit-tool.designful.ca/
    4 Version: 1.1.5
     4Version: 1.1.6
    55Tags: seo audit, ai seo, conversion optimization, content analysis, search intent
    66Requires at least: 4.0
    77Tested up to: 6.8
    8 Stable tag: 1.1.5
     8Stable tag: 1.1.6
    99Requires PHP: 7.4
    1010License: GPLv2 or later
  • seo-ai-audit-tool/trunk/seo-ai-audit-tool.code-workspace

    r3333926 r3364780  
    88        }
    99    ],
    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    }
    1120}
  • seo-ai-audit-tool/trunk/seo-ai-audit-tool.php

    r3359560 r3364780  
    44 * Plugin URI: https://designful.ca/apps/seo-ai-audit-tool/
    55 * Description: A WordPress plugin to audit SEO elements of pages and perform AI-powered content analysis
    6  * Version:     1.1.5
     6 * Version:     1.1.6
    77 * Author:      Designful
    88 * License:     GPL2
     
    2020define('SEOAUDP_PLUGIN_DIR', plugin_dir_path(__FILE__));
    2121define('SEOAUDP_PLUGIN_URL', plugin_dir_url(__FILE__));
    22 define('SEOAUDP_PLUGIN_VERSION', '1.1.5');
     22define('SEOAUDP_PLUGIN_VERSION', '1.1.6');
    2323define('SEOAUDP_PLUGIN_BETA_VERSION', false);
    24 define('SEOAUDP_DB_VERSION', '1.8');
     24define('SEOAUDP_DB_VERSION', '1.9');
    2525define('SEOAUDP_EDD_ITEM_ID', 307);
    2626define('SEOAUDP_STORE_URL', 'https://seo-ai-audit-tool.designful.ca');
     
    390390}
    391391
     392function 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
    392405function seoaudp_init_hooks() {
    393406    add_action('admin_print_scripts', 'seoaudp_hide_admin_page');
Note: See TracChangeset for help on using the changeset viewer.