Changeset 3658740
- Timestamp:
- 08/21/2026 09:59:19 AM (2 months ago)
- Location:
- seo-auditor-tools
- Files:
-
- 52 added
- 15 edited
-
tags/2.0.3 (added)
-
tags/2.0.3/assets (added)
-
tags/2.0.3/assets/admin.css (added)
-
tags/2.0.3/assets/admin.js (added)
-
tags/2.0.3/assets/admin.min.css (added)
-
tags/2.0.3/assets/admin.min.js (added)
-
tags/2.0.3/assets/chart.min.js (added)
-
tags/2.0.3/assets/chatbot.css (added)
-
tags/2.0.3/assets/chatbot.js (added)
-
tags/2.0.3/assets/chatbot.min.css (added)
-
tags/2.0.3/assets/chatbot.min.js (added)
-
tags/2.0.3/assets/editor.js (added)
-
tags/2.0.3/assets/editor.min.js (added)
-
tags/2.0.3/assets/frontend.css (added)
-
tags/2.0.3/assets/frontend.js (added)
-
tags/2.0.3/assets/frontend.min.css (added)
-
tags/2.0.3/assets/frontend.min.js (added)
-
tags/2.0.3/assets/media-image-edit.js (added)
-
tags/2.0.3/assets/media-image-edit.min.js (added)
-
tags/2.0.3/assets/media-refresh.js (added)
-
tags/2.0.3/assets/media-refresh.min.js (added)
-
tags/2.0.3/assets/meta-importer.js (added)
-
tags/2.0.3/includes (added)
-
tags/2.0.3/includes/class-seoautopro-admin.php (added)
-
tags/2.0.3/includes/class-seoautopro-ai-helper.php (added)
-
tags/2.0.3/includes/class-seoautopro-ai-providers.php (added)
-
tags/2.0.3/includes/class-seoautopro-analyzer.php (added)
-
tags/2.0.3/includes/class-seoautopro-cache.php (added)
-
tags/2.0.3/includes/class-seoautopro-chatbot.php (added)
-
tags/2.0.3/includes/class-seoautopro-cookie-banner.php (added)
-
tags/2.0.3/includes/class-seoautopro-crawler.php (added)
-
tags/2.0.3/includes/class-seoautopro-exporter.php (added)
-
tags/2.0.3/includes/class-seoautopro-htaccess.php (added)
-
tags/2.0.3/includes/class-seoautopro-image-tools.php (added)
-
tags/2.0.3/includes/class-seoautopro-inventory.php (added)
-
tags/2.0.3/includes/class-seoautopro-llms.php (added)
-
tags/2.0.3/includes/class-seoautopro-meta-importer.php (added)
-
tags/2.0.3/includes/class-seoautopro-minify-tools.php (added)
-
tags/2.0.3/includes/class-seoautopro-opengraph.php (added)
-
tags/2.0.3/includes/class-seoautopro-pagespeed.php (added)
-
tags/2.0.3/includes/class-seoautopro-qr-code.php (added)
-
tags/2.0.3/includes/class-seoautopro-redirection.php (added)
-
tags/2.0.3/includes/class-seoautopro-robots.php (added)
-
tags/2.0.3/includes/class-seoautopro-rules.php (added)
-
tags/2.0.3/includes/class-seoautopro-security.php (added)
-
tags/2.0.3/includes/class-seoautopro-settings-export.php (added)
-
tags/2.0.3/includes/class-seoautopro-sitemap.php (added)
-
tags/2.0.3/includes/class-seoautopro-tracking.php (added)
-
tags/2.0.3/readme.txt (added)
-
tags/2.0.3/seo-auditor-tools.php (added)
-
trunk/assets/admin.js (modified) (2 diffs)
-
trunk/assets/admin.min.js (modified) (2 diffs)
-
trunk/assets/chatbot.css (modified) (2 diffs)
-
trunk/assets/chatbot.js (modified) (9 diffs)
-
trunk/assets/chatbot.min.css (modified) (1 diff)
-
trunk/assets/chatbot.min.js (modified) (9 diffs)
-
trunk/assets/editor.js (modified) (4 diffs)
-
trunk/assets/editor.min.js (modified) (4 diffs)
-
trunk/includes/class-seoautopro-admin.php (modified) (8 diffs)
-
trunk/includes/class-seoautopro-ai-helper.php (added)
-
trunk/includes/class-seoautopro-ai-providers.php (added)
-
trunk/includes/class-seoautopro-analyzer.php (modified) (7 diffs)
-
trunk/includes/class-seoautopro-chatbot.php (modified) (28 diffs)
-
trunk/includes/class-seoautopro-rules.php (modified) (2 diffs)
-
trunk/includes/class-seoautopro-settings-export.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (9 diffs)
-
trunk/seo-auditor-tools.php (modified) (10 diffs)
Legend:
- Unmodified
- Added
- Removed
-
seo-auditor-tools/trunk/assets/admin.js
r3561078 r3658740 46 46 .toggleClass('dashicons-visibility', !isHidden) 47 47 .toggleClass('dashicons-hidden', isHidden); 48 }); 49 50 // Read-only-on-load prevents password managers from treating API fields 51 // as login-password fields. A deliberate user interaction unlocks them. 52 $('.seoautopro-wrap').on('pointerdown keydown focus', '.seoautopro-sensitive-input[readonly]', function() { 53 $(this).removeAttr('readonly'); 48 54 }); 49 55 … … 686 692 } 687 693 694 // AI Helper provider, model, endpoint, and language controls. 695 var aiProviderDataNode = document.getElementById('seoautopro-ai-provider-data'); 696 if (aiProviderDataNode) { 697 var aiData = {}; 698 try { 699 aiData = JSON.parse(aiProviderDataNode.textContent || '{}'); 700 } catch (error) { 701 aiData = {}; 702 } 703 704 var providers = aiData.providers || {}; 705 var savedModels = aiData.models || {}; 706 var savedEndpoints = aiData.endpoints || {}; 707 var configuredKeys = aiData.configuredKeys || {}; 708 var $tier = $('#seoautopro-ai-tier'); 709 var $provider = $('#seoautopro-ai-provider'); 710 var $model = $('#seoautopro-ai-model'); 711 712 function providerSupportsTier(definition, tier) { 713 return definition && Array.isArray(definition.tiers) && definition.tiers.indexOf(tier) !== -1; 714 } 715 716 function refreshProviders(keepCurrent) { 717 var tier = $tier.val(); 718 var current = keepCurrent ? $provider.val() : ''; 719 $provider.empty(); 720 $.each(providers, function(id, definition) { 721 if (providerSupportsTier(definition, tier)) { 722 $('<option>').val(id).text(definition.label || id).appendTo($provider); 723 } 724 }); 725 if (current && $provider.find('option[value="' + current + '"]').length) { 726 $provider.val(current); 727 } 728 refreshProviderFields(true); 729 } 730 731 function refreshProviderFields(keepCurrent) { 732 var id = $provider.val(); 733 var tier = $tier.val(); 734 var definition = providers[id] || {}; 735 var options = (definition.models && definition.models[tier]) || []; 736 var current = keepCurrent ? (savedModels[id] || '') : ''; 737 var isListed = current && options.indexOf(current) !== -1; 738 $model.empty(); 739 $.each(options, function(index, model) { 740 $('<option>').val(model).text(model === '__custom__' ? 'Custom model…' : model).appendTo($model); 741 }); 742 if (current && isListed) { 743 $model.val(current); 744 } else if (current && options.indexOf('__custom__') !== -1) { 745 $model.val('__custom__'); 746 $('#seoautopro-ai-custom-model').val(current); 747 } 748 749 var endpoint = savedEndpoints[id] || definition.endpoint || ''; 750 $('#seoautopro-ai-endpoint').val(endpoint); 751 $('#seoautopro-ai-endpoint-row').toggle(Boolean(definition.endpoint_editable)); 752 $('#seoautopro-ai-api-key-row').toggle(Boolean(definition.api_key_required) || id === 'custom_openai'); 753 $('#seoautopro-ai-api-key').val('').attr('placeholder', configuredKeys[id] ? 'Key configured — leave blank to keep it' : 'Enter provider API key'); 754 refreshCustomModel(); 755 } 756 757 function refreshCustomModel() { 758 $('#seoautopro-ai-custom-model-row').toggle($model.val() === '__custom__'); 759 } 760 761 $tier.on('change', function() { refreshProviders(false); }); 762 $provider.on('change', function() { refreshProviderFields(true); }); 763 $model.on('change', refreshCustomModel); 764 $('#seoautopro-ai-language').on('change', function() { 765 $('#seoautopro-ai-custom-language-row').toggle($(this).val() === '__custom__'); 766 }); 767 } 768 688 769 function escapeHtml(text) { 689 770 if (!text) return ''; -
seo-auditor-tools/trunk/assets/admin.min.js
r3561078 r3658740 46 46 .toggleClass('dashicons-visibility', !isHidden) 47 47 .toggleClass('dashicons-hidden', isHidden); 48 }); 49 50 // Read-only-on-load prevents password managers from treating API fields 51 // as login-password fields. A deliberate user interaction unlocks them. 52 $('.seoautopro-wrap').on('pointerdown keydown focus', '.seoautopro-sensitive-input[readonly]', function() { 53 $(this).removeAttr('readonly'); 48 54 }); 49 55 … … 686 692 } 687 693 694 // AI Helper provider, model, endpoint, and language controls. 695 var aiProviderDataNode = document.getElementById('seoautopro-ai-provider-data'); 696 if (aiProviderDataNode) { 697 var aiData = {}; 698 try { 699 aiData = JSON.parse(aiProviderDataNode.textContent || '{}'); 700 } catch (error) { 701 aiData = {}; 702 } 703 704 var providers = aiData.providers || {}; 705 var savedModels = aiData.models || {}; 706 var savedEndpoints = aiData.endpoints || {}; 707 var configuredKeys = aiData.configuredKeys || {}; 708 var $tier = $('#seoautopro-ai-tier'); 709 var $provider = $('#seoautopro-ai-provider'); 710 var $model = $('#seoautopro-ai-model'); 711 712 function providerSupportsTier(definition, tier) { 713 return definition && Array.isArray(definition.tiers) && definition.tiers.indexOf(tier) !== -1; 714 } 715 716 function refreshProviders(keepCurrent) { 717 var tier = $tier.val(); 718 var current = keepCurrent ? $provider.val() : ''; 719 $provider.empty(); 720 $.each(providers, function(id, definition) { 721 if (providerSupportsTier(definition, tier)) { 722 $('<option>').val(id).text(definition.label || id).appendTo($provider); 723 } 724 }); 725 if (current && $provider.find('option[value="' + current + '"]').length) { 726 $provider.val(current); 727 } 728 refreshProviderFields(true); 729 } 730 731 function refreshProviderFields(keepCurrent) { 732 var id = $provider.val(); 733 var tier = $tier.val(); 734 var definition = providers[id] || {}; 735 var options = (definition.models && definition.models[tier]) || []; 736 var current = keepCurrent ? (savedModels[id] || '') : ''; 737 var isListed = current && options.indexOf(current) !== -1; 738 $model.empty(); 739 $.each(options, function(index, model) { 740 $('<option>').val(model).text(model === '__custom__' ? 'Custom model…' : model).appendTo($model); 741 }); 742 if (current && isListed) { 743 $model.val(current); 744 } else if (current && options.indexOf('__custom__') !== -1) { 745 $model.val('__custom__'); 746 $('#seoautopro-ai-custom-model').val(current); 747 } 748 749 var endpoint = savedEndpoints[id] || definition.endpoint || ''; 750 $('#seoautopro-ai-endpoint').val(endpoint); 751 $('#seoautopro-ai-endpoint-row').toggle(Boolean(definition.endpoint_editable)); 752 $('#seoautopro-ai-api-key-row').toggle(Boolean(definition.api_key_required) || id === 'custom_openai'); 753 $('#seoautopro-ai-api-key').val('').attr('placeholder', configuredKeys[id] ? 'Key configured — leave blank to keep it' : 'Enter provider API key'); 754 refreshCustomModel(); 755 } 756 757 function refreshCustomModel() { 758 $('#seoautopro-ai-custom-model-row').toggle($model.val() === '__custom__'); 759 } 760 761 $tier.on('change', function() { refreshProviders(false); }); 762 $provider.on('change', function() { refreshProviderFields(true); }); 763 $model.on('change', refreshCustomModel); 764 $('#seoautopro-ai-language').on('change', function() { 765 $('#seoautopro-ai-custom-language-row').toggle($(this).val() === '__custom__'); 766 }); 767 } 768 688 769 function escapeHtml(text) { 689 770 if (!text) return ''; -
seo-auditor-tools/trunk/assets/chatbot.css
r3592712 r3658740 78 78 padding: 12px 16px; 79 79 font-weight: 600; 80 display: flex; 81 align-items: center; 82 justify-content: space-between; 83 gap: 12px; 84 } 85 86 .seoautopro-chatbot-refresh { 87 appearance: none; 88 width: 30px; 89 height: 30px; 90 background: transparent; 91 color: inherit; 92 font: inherit; 93 font-size: 20px; 94 line-height: 1; 95 padding: 0; 96 cursor: pointer; 97 opacity: 0.9; 98 } 99 100 .seoautopro-chatbot-refresh:hover, 101 .seoautopro-chatbot-refresh:focus-visible { 102 opacity: 1; 103 outline-offset: 2px; 80 104 } 81 105 … … 197 221 } 198 222 223 .seoautopro-chatbot-contact-actions { 224 display: flex; 225 gap: 8px; 226 } 227 228 .seoautopro-chatbot-contact-actions button { 229 flex: 1 1 0; 230 } 231 232 .seoautopro-chatbot-contact .seoautopro-chatbot-continue { 233 background: transparent; 234 color: var(--seoautopro-chat-primary); 235 border: 1px solid var(--seoautopro-chat-primary); 236 } 237 199 238 .seoautopro-chatbot-input button:disabled, 200 239 .seoautopro-chatbot-contact button:disabled { -
seo-auditor-tools/trunk/assets/chatbot.js
r3592712 r3658740 16 16 var $input = $inputForm.find('input'); 17 17 var $contactForm = $chat.find('.seoautopro-chatbot-contact'); 18 var $refresh = $chat.find('.seoautopro-chatbot-refresh'); 19 var $continue = $chat.find('.seoautopro-chatbot-continue'); 20 var conversationId = 0; 18 21 19 22 function scrollToBottom() { … … 89 92 } 90 93 94 function startNewConversation() { 95 conversationId += 1; 96 $messages.empty(); 97 $input.val(''); 98 $contactForm.trigger('reset'); 99 $inputForm.find('button').prop('disabled', false); 100 $contactForm.find('button').prop('disabled', false); 101 hideContactForm(); 102 if (SEOAUTOPRO_CHATBOT.greeting) { 103 appendMessage(SEOAUTOPRO_CHATBOT.greeting, 'bot'); 104 } 105 $input.trigger('focus'); 106 } 107 91 108 setOpen(false); 92 109 … … 97 114 $toggle.on('click', function() { 98 115 setOpen(!$window.hasClass('is-open')); 116 }); 117 118 $refresh.on('click', function() { 119 startNewConversation(); 120 }); 121 122 $continue.on('click', function() { 123 hideContactForm(); 124 $input.trigger('focus'); 99 125 }); 100 126 … … 107 133 108 134 var $btn = $inputForm.find('button'); 135 var requestConversation = conversationId; 109 136 $btn.prop('disabled', true); 110 137 appendMessage(question, 'user'); … … 117 144 question: question 118 145 }).done(function(response) { 146 if (requestConversation !== conversationId) { 147 return; 148 } 119 149 if (!response || !response.success || !response.data) { 120 appendMessage( 'Sorry, something went wrong.', 'bot');150 appendMessage(SEOAUTOPRO_CHATBOT.error_message || 'Sorry, something went wrong.', 'bot'); 121 151 showContactForm(); 122 152 return; … … 126 156 appendSources(response.data.sources || []); 127 157 if (response.data.no_answer) { 128 appendMessage( 'Would you like to send a message to our support team for further assistance?', 'bot');158 appendMessage(SEOAUTOPRO_CHATBOT.support_prompt || 'Would you like to send a message to our support team for further assistance?', 'bot'); 129 159 showContactForm(); 130 160 } else { 131 161 hideContactForm(); 132 162 } 133 }).fail(function() { 134 appendMessage('Sorry, something went wrong.', 'bot'); 163 }).fail(function(xhr) { 164 if (requestConversation !== conversationId) { 165 return; 166 } 167 var message = xhr && xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message 168 ? xhr.responseJSON.data.message 169 : (SEOAUTOPRO_CHATBOT.error_message || 'Sorry, something went wrong.'); 170 appendMessage(message, 'bot'); 135 171 showContactForm(); 136 172 }).always(function() { 137 $btn.prop('disabled', false); 173 if (requestConversation === conversationId) { 174 $btn.prop('disabled', false); 175 } 138 176 }); 139 177 }); … … 142 180 event.preventDefault(); 143 181 144 var $btn = $contactForm.find('button'); 182 var $btn = $contactForm.find('button[type="submit"]'); 183 var requestConversation = conversationId; 145 184 $btn.prop('disabled', true); 146 185 … … 156 195 157 196 $.post(SEOAUTOPRO_CHATBOT.ajax_url, payload).done(function(response) { 197 if (requestConversation !== conversationId) { 198 return; 199 } 158 200 if (!response || !response.success || !response.data) { 159 201 appendMessage('Unable to send your message. Please try again.', 'bot'); … … 165 207 $contactForm.trigger('reset'); 166 208 hideContactForm(); 167 }).fail(function() { 168 appendMessage('Unable to send your message. Please try again.', 'bot'); 209 }).fail(function(xhr) { 210 if (requestConversation !== conversationId) { 211 return; 212 } 213 var message = xhr && xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message 214 ? xhr.responseJSON.data.message 215 : 'Unable to send your message. Please try again.'; 216 appendMessage(message, 'bot'); 169 217 }).always(function() { 170 $btn.prop('disabled', false); 218 if (requestConversation === conversationId) { 219 $btn.prop('disabled', false); 220 } 171 221 }); 172 222 }); -
seo-auditor-tools/trunk/assets/chatbot.min.css
r3592712 r3658740 1 .seoautopro-chatbot { 2 position: fixed; 3 right: 24px; 4 bottom: 24px; 5 z-index: 9999; 6 font-family: "Segoe UI", Arial, sans-serif; 7 --seoautopro-chat-primary: #0b6b8f; 8 --seoautopro-chat-bg: #ffffff; 9 --seoautopro-chat-text: #1b2a36; 10 --seoautopro-chat-bot: #f2f5f8; 11 --seoautopro-chat-user: #0b6b8f; 12 --seoautopro-chat-user-text: #ffffff; 13 } 14 15 .seoautopro-chatbot *, 16 .seoautopro-chatbot *::before, 17 .seoautopro-chatbot *::after { 18 box-sizing: border-box; 19 } 20 21 .seoautopro-chatbot-toggle { 22 background: var(--seoautopro-chat-primary); 23 color: var(--seoautopro-chat-user-text); 24 border: none; 25 border-radius: 999px; 26 padding: 10px 18px; 27 font-weight: 600; 28 cursor: pointer; 29 box-shadow: 0 10px 24px rgba(0, 0, 0, 0.2); 30 display: inline-flex; 31 align-items: center; 32 gap: 8px; 33 } 34 35 .seoautopro-chatbot-toggle-icon { 36 width: 20px; 37 height: 20px; 38 object-fit: contain; 39 border-radius: 50%; 40 } 41 42 .seoautopro-chatbot-toggle--icon-only { 43 width: 56px; 44 height: 56px; 45 padding: 0; 46 border-radius: 50%; 47 justify-content: center; 48 overflow: hidden; 49 } 50 51 .seoautopro-chatbot-toggle--icon-only .seoautopro-chatbot-toggle-icon { 52 width: 100%; 53 height: 100%; 54 object-fit: cover; 55 } 56 57 .seoautopro-chatbot-window { 58 width: 320px; 59 max-height: 70vh; 60 min-height: 20vh; 61 background: var(--seoautopro-chat-bg); 62 color: var(--seoautopro-chat-text); 63 border-radius: 16px; 64 box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2); 65 margin-top: 12px; 66 display: none; 67 overflow-y: auto; 68 flex-direction: column; 69 } 70 71 .seoautopro-chatbot-window.is-open { 72 display: flex; 73 } 74 75 .seoautopro-chatbot-header { 76 background: var(--seoautopro-chat-primary); 77 color: var(--seoautopro-chat-user-text); 78 padding: 12px 16px; 79 font-weight: 600; 80 } 81 82 .seoautopro-chatbot-messages { 83 padding: 16px; 84 overflow-y: auto; 85 display: flex; 86 flex-direction: column; 87 gap: 10px; 88 flex: 1 1 auto; 89 } 90 91 .seoautopro-chatbot-message { 92 font-size: 13px; 93 line-height: 1.5; 94 padding: 10px 12px; 95 border-radius: 12px; 96 max-width: 85%; 97 word-wrap: break-word; 98 overflow-wrap: break-word; 99 } 100 101 .seoautopro-chatbot-message--bot { 102 background: var(--seoautopro-chat-bot); 103 color: var(--seoautopro-chat-text); 104 align-self: flex-start; 105 } 106 107 .seoautopro-chatbot-message--user { 108 background: var(--seoautopro-chat-user); 109 color: var(--seoautopro-chat-user-text); 110 align-self: flex-end; 111 } 112 113 .seoautopro-chatbot-results { 114 display: flex; 115 flex-direction: column; 116 gap: 8px; 117 width: 100%; 118 } 119 120 .seoautopro-chatbot-result-card { 121 background: var(--seoautopro-chat-bot); 122 border-radius: 10px; 123 padding: 10px 12px; 124 display: flex; 125 flex-direction: column; 126 gap: 4px; 127 } 128 129 .seoautopro-chatbot-result-title { 130 font-size: 13px; 131 font-weight: 600; 132 color: var(--seoautopro-chat-primary); 133 text-decoration: none; 134 line-height: 1.3; 135 } 136 137 a.seoautopro-chatbot-result-title:hover { 138 text-decoration: underline; 139 } 140 141 .seoautopro-chatbot-result-excerpt { 142 font-size: 12px; 143 line-height: 1.45; 144 color: var(--seoautopro-chat-text); 145 margin: 2px 0 0; 146 opacity: 0.85; 147 } 148 149 .seoautopro-chatbot-sources { 150 margin: 0; 151 padding-left: 18px; 152 font-size: 12px; 153 color: #1b2a36; 154 } 155 156 .seoautopro-chatbot-input, 157 .seoautopro-chatbot-contact { 158 padding: 12px 16px 16px; 159 border-top: 1px solid #e6edf2; 160 } 161 162 .seoautopro-chatbot-input { 163 display: flex; 164 gap: 8px; 165 } 166 167 .seoautopro-chatbot-input input { 168 flex: 1; 169 min-width: 0; 170 padding: 8px 10px; 171 border-radius: 8px; 172 border: 1px solid #d2dbe3; 173 font-size: 13px; 174 } 175 176 .seoautopro-chatbot-input button { 177 background: var(--seoautopro-chat-primary); 178 color: var(--seoautopro-chat-user-text); 179 border: none; 180 border-radius: 8px; 181 padding: 8px 14px; 182 font-weight: 600; 183 cursor: pointer; 184 white-space: nowrap; 185 flex-shrink: 0; 186 } 187 188 .seoautopro-chatbot-contact button { 189 background: var(--seoautopro-chat-primary); 190 color: var(--seoautopro-chat-user-text); 191 border: none; 192 border-radius: 8px; 193 padding: 10px 12px; 194 font-weight: 600; 195 cursor: pointer; 196 width: 100%; 197 } 198 199 .seoautopro-chatbot-input button:disabled, 200 .seoautopro-chatbot-contact button:disabled { 201 opacity: 0.6; 202 cursor: not-allowed; 203 } 204 205 .seoautopro-chatbot-contact { 206 display: none; 207 flex-direction: column; 208 gap: 8px; 209 } 210 211 .seoautopro-chatbot-contact input, 212 .seoautopro-chatbot-contact textarea { 213 width: 100%; 214 padding: 8px 10px; 215 border-radius: 8px; 216 border: 1px solid #d2dbe3; 217 font-size: 13px; 218 font-family: inherit; 219 } 220 221 .seoautopro-chatbot-contact textarea { 222 resize: vertical; 223 } 224 225 .seoautopro-chatbot-contact p { 226 margin: 0 0 6px; 227 font-size: 12px; 228 line-height: 1.4; 229 color: var(--seoautopro-chat-text); 230 } 231 232 @media (max-width: 480px) { 233 .seoautopro-chatbot { 234 right: 16px; 235 bottom: 16px; 236 } 237 238 .seoautopro-chatbot-window { 239 width: calc(100vw - 32px); 240 max-height: 70vh; 241 } 242 } 1 .seoautopro-chatbot{position:fixed;right:24px;bottom:24px;z-index:9999;font-family:"Segoe UI",Arial,sans-serif;--seoautopro-chat-primary:#0b6b8f;--seoautopro-chat-bg:#ffffff;--seoautopro-chat-text:#1b2a36;--seoautopro-chat-bot:#f2f5f8;--seoautopro-chat-user:#0b6b8f;--seoautopro-chat-user-text:#ffffff}.seoautopro-chatbot *,.seoautopro-chatbot ::after,.seoautopro-chatbot ::before{box-sizing:border-box}.seoautopro-chatbot-toggle{background:var(--seoautopro-chat-primary);color:var(--seoautopro-chat-user-text);border:none;border-radius:999px;padding:10px 18px;font-weight:600;cursor:pointer;box-shadow:0 10px 24px rgba(0,0,0,.2);display:inline-flex;align-items:center;gap:8px}.seoautopro-chatbot-toggle-icon{width:20px;height:20px;object-fit:contain;border-radius:50%}.seoautopro-chatbot-toggle--icon-only{width:56px;height:56px;padding:0;border-radius:50%;justify-content:center;overflow:hidden}.seoautopro-chatbot-toggle--icon-only .seoautopro-chatbot-toggle-icon{width:100%;height:100%;object-fit:cover}.seoautopro-chatbot-window{width:320px;max-height:70vh;min-height:20vh;background:var(--seoautopro-chat-bg);color:var(--seoautopro-chat-text);border-radius:16px;box-shadow:0 16px 40px rgba(0,0,0,.2);margin-top:12px;display:none;overflow-y:auto;flex-direction:column}.seoautopro-chatbot-window.is-open{display:flex}.seoautopro-chatbot-header{background:var(--seoautopro-chat-primary);color:var(--seoautopro-chat-user-text);padding:12px 16px;font-weight:600;display:flex;align-items:center;justify-content:space-between;gap:12px}.seoautopro-chatbot-refresh{appearance:none;width:30px;height:30px;background:0 0;color:inherit;font:inherit;font-size:20px;line-height:1;padding:0;cursor:pointer;opacity:.9}.seoautopro-chatbot-refresh:focus-visible,.seoautopro-chatbot-refresh:hover{opacity:1;outline-offset:2px}.seoautopro-chatbot-messages{padding:16px;overflow-y:auto;display:flex;flex-direction:column;gap:10px;flex:1 1 auto}.seoautopro-chatbot-message{font-size:13px;line-height:1.5;padding:10px 12px;border-radius:12px;max-width:85%;word-wrap:break-word;overflow-wrap:break-word}.seoautopro-chatbot-message--bot{background:var(--seoautopro-chat-bot);color:var(--seoautopro-chat-text);align-self:flex-start}.seoautopro-chatbot-message--user{background:var(--seoautopro-chat-user);color:var(--seoautopro-chat-user-text);align-self:flex-end}.seoautopro-chatbot-results{display:flex;flex-direction:column;gap:8px;width:100%}.seoautopro-chatbot-result-card{background:var(--seoautopro-chat-bot);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:4px}.seoautopro-chatbot-result-title{font-size:13px;font-weight:600;color:var(--seoautopro-chat-primary);text-decoration:none;line-height:1.3}a.seoautopro-chatbot-result-title:hover{text-decoration:underline}.seoautopro-chatbot-result-excerpt{font-size:12px;line-height:1.45;color:var(--seoautopro-chat-text);margin:2px 0 0;opacity:.85}.seoautopro-chatbot-sources{margin:0;padding-left:18px;font-size:12px;color:#1b2a36}.seoautopro-chatbot-contact,.seoautopro-chatbot-input{padding:12px 16px 16px;border-top:1px solid #e6edf2}.seoautopro-chatbot-input{display:flex;gap:8px}.seoautopro-chatbot-input input{flex:1;min-width:0;padding:8px 10px;border-radius:8px;border:1px solid #d2dbe3;font-size:13px}.seoautopro-chatbot-input button{background:var(--seoautopro-chat-primary);color:var(--seoautopro-chat-user-text);border:none;border-radius:8px;padding:8px 14px;font-weight:600;cursor:pointer;white-space:nowrap;flex-shrink:0}.seoautopro-chatbot-contact button{background:var(--seoautopro-chat-primary);color:var(--seoautopro-chat-user-text);border:none;border-radius:8px;padding:10px 12px;font-weight:600;cursor:pointer;width:100%}.seoautopro-chatbot-contact-actions{display:flex;gap:8px}.seoautopro-chatbot-contact-actions button{flex:1 1 0}.seoautopro-chatbot-contact .seoautopro-chatbot-continue{background:0 0;color:var(--seoautopro-chat-primary);border:1px solid var(--seoautopro-chat-primary)}.seoautopro-chatbot-contact button:disabled,.seoautopro-chatbot-input button:disabled{opacity:.6;cursor:not-allowed}.seoautopro-chatbot-contact{display:none;flex-direction:column;gap:8px}.seoautopro-chatbot-contact input,.seoautopro-chatbot-contact textarea{width:100%;padding:8px 10px;border-radius:8px;border:1px solid #d2dbe3;font-size:13px;font-family:inherit}.seoautopro-chatbot-contact textarea{resize:vertical}.seoautopro-chatbot-contact p{margin:0 0 6px;font-size:12px;line-height:1.4;color:var(--seoautopro-chat-text)}@media (max-width:480px){.seoautopro-chatbot{right:16px;bottom:16px}.seoautopro-chatbot-window{width:calc(100vw - 32px);max-height:70vh}} -
seo-auditor-tools/trunk/assets/chatbot.min.js
r3592712 r3658740 16 16 var $input = $inputForm.find('input'); 17 17 var $contactForm = $chat.find('.seoautopro-chatbot-contact'); 18 var $refresh = $chat.find('.seoautopro-chatbot-refresh'); 19 var $continue = $chat.find('.seoautopro-chatbot-continue'); 20 var conversationId = 0; 18 21 19 22 function scrollToBottom() { … … 89 92 } 90 93 94 function startNewConversation() { 95 conversationId += 1; 96 $messages.empty(); 97 $input.val(''); 98 $contactForm.trigger('reset'); 99 $inputForm.find('button').prop('disabled', false); 100 $contactForm.find('button').prop('disabled', false); 101 hideContactForm(); 102 if (SEOAUTOPRO_CHATBOT.greeting) { 103 appendMessage(SEOAUTOPRO_CHATBOT.greeting, 'bot'); 104 } 105 $input.trigger('focus'); 106 } 107 91 108 setOpen(false); 92 109 … … 97 114 $toggle.on('click', function() { 98 115 setOpen(!$window.hasClass('is-open')); 116 }); 117 118 $refresh.on('click', function() { 119 startNewConversation(); 120 }); 121 122 $continue.on('click', function() { 123 hideContactForm(); 124 $input.trigger('focus'); 99 125 }); 100 126 … … 107 133 108 134 var $btn = $inputForm.find('button'); 135 var requestConversation = conversationId; 109 136 $btn.prop('disabled', true); 110 137 appendMessage(question, 'user'); … … 117 144 question: question 118 145 }).done(function(response) { 146 if (requestConversation !== conversationId) { 147 return; 148 } 119 149 if (!response || !response.success || !response.data) { 120 appendMessage( 'Sorry, something went wrong.', 'bot');150 appendMessage(SEOAUTOPRO_CHATBOT.error_message || 'Sorry, something went wrong.', 'bot'); 121 151 showContactForm(); 122 152 return; … … 126 156 appendSources(response.data.sources || []); 127 157 if (response.data.no_answer) { 128 appendMessage( 'Would you like to send a message to our support team for further assistance?', 'bot');158 appendMessage(SEOAUTOPRO_CHATBOT.support_prompt || 'Would you like to send a message to our support team for further assistance?', 'bot'); 129 159 showContactForm(); 130 160 } else { 131 161 hideContactForm(); 132 162 } 133 }).fail(function() { 134 appendMessage('Sorry, something went wrong.', 'bot'); 163 }).fail(function(xhr) { 164 if (requestConversation !== conversationId) { 165 return; 166 } 167 var message = xhr && xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message 168 ? xhr.responseJSON.data.message 169 : (SEOAUTOPRO_CHATBOT.error_message || 'Sorry, something went wrong.'); 170 appendMessage(message, 'bot'); 135 171 showContactForm(); 136 172 }).always(function() { 137 $btn.prop('disabled', false); 173 if (requestConversation === conversationId) { 174 $btn.prop('disabled', false); 175 } 138 176 }); 139 177 }); … … 142 180 event.preventDefault(); 143 181 144 var $btn = $contactForm.find('button'); 182 var $btn = $contactForm.find('button[type="submit"]'); 183 var requestConversation = conversationId; 145 184 $btn.prop('disabled', true); 146 185 … … 156 195 157 196 $.post(SEOAUTOPRO_CHATBOT.ajax_url, payload).done(function(response) { 197 if (requestConversation !== conversationId) { 198 return; 199 } 158 200 if (!response || !response.success || !response.data) { 159 201 appendMessage('Unable to send your message. Please try again.', 'bot'); … … 165 207 $contactForm.trigger('reset'); 166 208 hideContactForm(); 167 }).fail(function() { 168 appendMessage('Unable to send your message. Please try again.', 'bot'); 209 }).fail(function(xhr) { 210 if (requestConversation !== conversationId) { 211 return; 212 } 213 var message = xhr && xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message 214 ? xhr.responseJSON.data.message 215 : 'Unable to send your message. Please try again.'; 216 appendMessage(message, 'bot'); 169 217 }).always(function() { 170 $btn.prop('disabled', false); 218 if (requestConversation === conversationId) { 219 $btn.prop('disabled', false); 220 } 171 221 }); 172 222 }); -
seo-auditor-tools/trunk/assets/editor.js
r3645873 r3658740 13 13 var TextControl = wp.components.TextControl; 14 14 var SelectControl = wp.components.SelectControl; 15 var Button = wp.components.Button; 16 var Notice = wp.components.Notice; 15 17 var useSelect = wp.data.useSelect; 16 18 var useDispatch = wp.data.useDispatch; 19 var useState = wp.element.useState; 17 20 var el = wp.element.createElement; 18 21 19 22 var MetaPanel = function() { 20 var meta = useSelect(function(select){ 21 return select('core/editor').getEditedPostAttribute('meta') || {}; 23 var editorState = useSelect(function(select){ 24 var editor = select('core/editor'); 25 return { 26 meta: editor.getEditedPostAttribute('meta') || {}, 27 title: editor.getEditedPostAttribute('title') || '', 28 content: editor.getEditedPostAttribute('content') || '', 29 excerpt: editor.getEditedPostAttribute('excerpt') || '' 30 }; 22 31 }); 23 32 var editPost = useDispatch('core/editor').editPost; 33 var generatingState = useState(false); 34 var keywordGeneratingState = useState(false); 35 var noticeState = useState(null); 36 var isGenerating = generatingState[0]; 37 var setIsGenerating = generatingState[1]; 38 var isGeneratingKeywords = keywordGeneratingState[0]; 39 var setIsGeneratingKeywords = keywordGeneratingState[1]; 40 var notice = noticeState[0]; 41 var setNotice = noticeState[1]; 42 var meta = editorState.meta; 24 43 25 44 var description = meta['_seoautopro_meta_description'] || ''; … … 28 47 var robotsFollow = meta['_seoautopro_robots_follow'] || ''; 29 48 var isLite = !!SEOAUTOPRO_PAGE_META.lite; 49 50 var generateDescription = function() { 51 if (isGenerating || !SEOAUTOPRO_PAGE_META.post_id) { 52 return; 53 } 54 55 setIsGenerating(true); 56 setNotice(null); 57 58 var body = new window.FormData(); 59 body.append('action', 'seoautopro_ai_helper_editor_generate'); 60 body.append('nonce', SEOAUTOPRO_PAGE_META.nonce); 61 body.append('post_id', String(SEOAUTOPRO_PAGE_META.post_id)); 62 body.append('title', editorState.title); 63 body.append('content', editorState.content); 64 body.append('excerpt', editorState.excerpt); 65 66 window.fetch(SEOAUTOPRO_PAGE_META.ajax_url, { 67 method: 'POST', 68 credentials: 'same-origin', 69 body: body 70 }).then(function(response) { 71 return response.json(); 72 }).then(function(response) { 73 if (!response || !response.success || !response.data || !response.data.suggested_value) { 74 var message = response && response.data && response.data.message ? response.data.message : SEOAUTOPRO_PAGE_META.strings.error; 75 throw new Error(message); 76 } 77 78 editPost({ meta: Object.assign({}, meta, { _seoautopro_meta_description: response.data.suggested_value }) }); 79 setNotice({ status: 'success', message: SEOAUTOPRO_PAGE_META.strings.generated }); 80 }).catch(function(error) { 81 setNotice({ status: 'error', message: error && error.message ? error.message : SEOAUTOPRO_PAGE_META.strings.error }); 82 }).then(function() { 83 setIsGenerating(false); 84 }); 85 }; 86 87 var generateKeywords = function() { 88 if (isGeneratingKeywords || !SEOAUTOPRO_PAGE_META.post_id) { 89 return; 90 } 91 92 setIsGeneratingKeywords(true); 93 setNotice(null); 94 95 var body = new window.FormData(); 96 body.append('action', 'seoautopro_ai_helper_editor_generate'); 97 body.append('nonce', SEOAUTOPRO_PAGE_META.nonce); 98 body.append('task', 'keywords'); 99 body.append('post_id', String(SEOAUTOPRO_PAGE_META.post_id)); 100 body.append('title', editorState.title); 101 body.append('content', editorState.content); 102 body.append('excerpt', editorState.excerpt); 103 104 window.fetch(SEOAUTOPRO_PAGE_META.ajax_url, { 105 method: 'POST', 106 credentials: 'same-origin', 107 body: body 108 }).then(function(response) { 109 return response.json(); 110 }).then(function(response) { 111 if (!response || !response.success || !response.data || !response.data.suggested_value) { 112 var message = response && response.data && response.data.message ? response.data.message : SEOAUTOPRO_PAGE_META.strings.error; 113 throw new Error(message); 114 } 115 116 editPost({ meta: Object.assign({}, meta, { _seoautopro_meta_keywords: response.data.suggested_value }) }); 117 setNotice({ status: 'success', message: SEOAUTOPRO_PAGE_META.strings.keywords_generated }); 118 }).catch(function(error) { 119 setNotice({ status: 'error', message: error && error.message ? error.message : SEOAUTOPRO_PAGE_META.strings.error }); 120 }).then(function() { 121 setIsGeneratingKeywords(false); 122 }); 123 }; 30 124 31 125 var children = [ … … 40 134 ]; 41 135 136 children.push( 137 el('div', { className: 'seoautopro-editor-ai-actions' }, 138 el(Button, { 139 isSecondary: true, 140 isBusy: isGenerating, 141 disabled: isGenerating || !SEOAUTOPRO_PAGE_META.post_id, 142 onClick: generateDescription 143 }, isGenerating ? SEOAUTOPRO_PAGE_META.strings.generating : SEOAUTOPRO_PAGE_META.strings.generate), 144 el('a', { 145 href: SEOAUTOPRO_PAGE_META.ai_url, 146 target: '_blank', 147 rel: 'noopener noreferrer', 148 style: { marginLeft: '10px' } 149 }, SEOAUTOPRO_PAGE_META.strings.settings_label) 150 ) 151 ); 152 153 if (!SEOAUTOPRO_PAGE_META.post_id) { 154 children.push(el('p', { className: 'components-base-control__help' }, SEOAUTOPRO_PAGE_META.strings.save_first)); 155 } 156 157 if (notice) { 158 children.push(el(Notice, { 159 status: notice.status, 160 isDismissible: true, 161 onRemove: function(){ setNotice(null); } 162 }, notice.message)); 163 } 164 42 165 if (!isLite) { 43 166 children.push( 44 167 el(TextControl, { 45 168 label: 'Meta keywords', 46 help: 'Comma-separated keywords. Overrides global keywords for this page.',169 help: SEOAUTOPRO_PAGE_META.strings.keywords_help, 47 170 value: keywords, 48 171 onChange: function(value){ … … 50 173 } 51 174 }), 175 el('div', { className: 'seoautopro-editor-ai-actions', style: { marginBottom: '16px' } }, 176 el(Button, { 177 isSecondary: true, 178 isBusy: isGeneratingKeywords, 179 disabled: isGeneratingKeywords || !SEOAUTOPRO_PAGE_META.post_id, 180 onClick: generateKeywords 181 }, isGeneratingKeywords ? SEOAUTOPRO_PAGE_META.strings.generating : SEOAUTOPRO_PAGE_META.strings.generate_keywords) 182 ), 52 183 el(SelectControl, { 53 184 label: 'Indexing', -
seo-auditor-tools/trunk/assets/editor.min.js
r3645873 r3658740 13 13 var TextControl = wp.components.TextControl; 14 14 var SelectControl = wp.components.SelectControl; 15 var Button = wp.components.Button; 16 var Notice = wp.components.Notice; 15 17 var useSelect = wp.data.useSelect; 16 18 var useDispatch = wp.data.useDispatch; 19 var useState = wp.element.useState; 17 20 var el = wp.element.createElement; 18 21 19 22 var MetaPanel = function() { 20 var meta = useSelect(function(select){ 21 return select('core/editor').getEditedPostAttribute('meta') || {}; 23 var editorState = useSelect(function(select){ 24 var editor = select('core/editor'); 25 return { 26 meta: editor.getEditedPostAttribute('meta') || {}, 27 title: editor.getEditedPostAttribute('title') || '', 28 content: editor.getEditedPostAttribute('content') || '', 29 excerpt: editor.getEditedPostAttribute('excerpt') || '' 30 }; 22 31 }); 23 32 var editPost = useDispatch('core/editor').editPost; 33 var generatingState = useState(false); 34 var keywordGeneratingState = useState(false); 35 var noticeState = useState(null); 36 var isGenerating = generatingState[0]; 37 var setIsGenerating = generatingState[1]; 38 var isGeneratingKeywords = keywordGeneratingState[0]; 39 var setIsGeneratingKeywords = keywordGeneratingState[1]; 40 var notice = noticeState[0]; 41 var setNotice = noticeState[1]; 42 var meta = editorState.meta; 24 43 25 44 var description = meta['_seoautopro_meta_description'] || ''; … … 28 47 var robotsFollow = meta['_seoautopro_robots_follow'] || ''; 29 48 var isLite = !!SEOAUTOPRO_PAGE_META.lite; 49 50 var generateDescription = function() { 51 if (isGenerating || !SEOAUTOPRO_PAGE_META.post_id) { 52 return; 53 } 54 55 setIsGenerating(true); 56 setNotice(null); 57 58 var body = new window.FormData(); 59 body.append('action', 'seoautopro_ai_helper_editor_generate'); 60 body.append('nonce', SEOAUTOPRO_PAGE_META.nonce); 61 body.append('post_id', String(SEOAUTOPRO_PAGE_META.post_id)); 62 body.append('title', editorState.title); 63 body.append('content', editorState.content); 64 body.append('excerpt', editorState.excerpt); 65 66 window.fetch(SEOAUTOPRO_PAGE_META.ajax_url, { 67 method: 'POST', 68 credentials: 'same-origin', 69 body: body 70 }).then(function(response) { 71 return response.json(); 72 }).then(function(response) { 73 if (!response || !response.success || !response.data || !response.data.suggested_value) { 74 var message = response && response.data && response.data.message ? response.data.message : SEOAUTOPRO_PAGE_META.strings.error; 75 throw new Error(message); 76 } 77 78 editPost({ meta: Object.assign({}, meta, { _seoautopro_meta_description: response.data.suggested_value }) }); 79 setNotice({ status: 'success', message: SEOAUTOPRO_PAGE_META.strings.generated }); 80 }).catch(function(error) { 81 setNotice({ status: 'error', message: error && error.message ? error.message : SEOAUTOPRO_PAGE_META.strings.error }); 82 }).then(function() { 83 setIsGenerating(false); 84 }); 85 }; 86 87 var generateKeywords = function() { 88 if (isGeneratingKeywords || !SEOAUTOPRO_PAGE_META.post_id) { 89 return; 90 } 91 92 setIsGeneratingKeywords(true); 93 setNotice(null); 94 95 var body = new window.FormData(); 96 body.append('action', 'seoautopro_ai_helper_editor_generate'); 97 body.append('nonce', SEOAUTOPRO_PAGE_META.nonce); 98 body.append('task', 'keywords'); 99 body.append('post_id', String(SEOAUTOPRO_PAGE_META.post_id)); 100 body.append('title', editorState.title); 101 body.append('content', editorState.content); 102 body.append('excerpt', editorState.excerpt); 103 104 window.fetch(SEOAUTOPRO_PAGE_META.ajax_url, { 105 method: 'POST', 106 credentials: 'same-origin', 107 body: body 108 }).then(function(response) { 109 return response.json(); 110 }).then(function(response) { 111 if (!response || !response.success || !response.data || !response.data.suggested_value) { 112 var message = response && response.data && response.data.message ? response.data.message : SEOAUTOPRO_PAGE_META.strings.error; 113 throw new Error(message); 114 } 115 116 editPost({ meta: Object.assign({}, meta, { _seoautopro_meta_keywords: response.data.suggested_value }) }); 117 setNotice({ status: 'success', message: SEOAUTOPRO_PAGE_META.strings.keywords_generated }); 118 }).catch(function(error) { 119 setNotice({ status: 'error', message: error && error.message ? error.message : SEOAUTOPRO_PAGE_META.strings.error }); 120 }).then(function() { 121 setIsGeneratingKeywords(false); 122 }); 123 }; 30 124 31 125 var children = [ … … 40 134 ]; 41 135 136 children.push( 137 el('div', { className: 'seoautopro-editor-ai-actions' }, 138 el(Button, { 139 isSecondary: true, 140 isBusy: isGenerating, 141 disabled: isGenerating || !SEOAUTOPRO_PAGE_META.post_id, 142 onClick: generateDescription 143 }, isGenerating ? SEOAUTOPRO_PAGE_META.strings.generating : SEOAUTOPRO_PAGE_META.strings.generate), 144 el('a', { 145 href: SEOAUTOPRO_PAGE_META.ai_url, 146 target: '_blank', 147 rel: 'noopener noreferrer', 148 style: { marginLeft: '10px' } 149 }, SEOAUTOPRO_PAGE_META.strings.settings_label) 150 ) 151 ); 152 153 if (!SEOAUTOPRO_PAGE_META.post_id) { 154 children.push(el('p', { className: 'components-base-control__help' }, SEOAUTOPRO_PAGE_META.strings.save_first)); 155 } 156 157 if (notice) { 158 children.push(el(Notice, { 159 status: notice.status, 160 isDismissible: true, 161 onRemove: function(){ setNotice(null); } 162 }, notice.message)); 163 } 164 42 165 if (!isLite) { 43 166 children.push( 44 167 el(TextControl, { 45 168 label: 'Meta keywords', 46 help: 'Comma-separated keywords. Overrides global keywords for this page.',169 help: SEOAUTOPRO_PAGE_META.strings.keywords_help, 47 170 value: keywords, 48 171 onChange: function(value){ … … 50 173 } 51 174 }), 175 el('div', { className: 'seoautopro-editor-ai-actions', style: { marginBottom: '16px' } }, 176 el(Button, { 177 isSecondary: true, 178 isBusy: isGeneratingKeywords, 179 disabled: isGeneratingKeywords || !SEOAUTOPRO_PAGE_META.post_id, 180 onClick: generateKeywords 181 }, isGeneratingKeywords ? SEOAUTOPRO_PAGE_META.strings.generating : SEOAUTOPRO_PAGE_META.strings.generate_keywords) 182 ), 52 183 el(SelectControl, { 53 184 label: 'Indexing', -
seo-auditor-tools/trunk/includes/class-seoautopro-admin.php
r3645873 r3658740 606 606 'key' => 'crawl_limit', 607 607 'placeholder' => '50', 608 'type' => 'number', 609 'min' => 1, 610 'max' => 200, 608 611 ) 609 612 ); … … 619 622 'key' => 'crawl_depth', 620 623 'placeholder' => '2', 624 'type' => 'number', 625 'min' => 0, 626 'max' => 10, 621 627 ) 622 628 ); … … 632 638 'key' => 'crawl_timeout', 633 639 'placeholder' => '8', 640 'type' => 'number', 641 'min' => 1, 642 'max' => 60, 634 643 ) 635 644 ); … … 775 784 $key = $args['key']; 776 785 $placeholder = isset( $args['placeholder'] ) ? $args['placeholder'] : ''; 786 $type = isset( $args['type'] ) && in_array( $args['type'], array( 'text', 'number', 'url', 'email' ), true ) ? $args['type'] : 'text'; 787 $min = isset( $args['min'] ) ? $args['min'] : null; 788 $max = isset( $args['max'] ) ? $args['max'] : null; 777 789 778 790 $opts = get_option( $option_name, array() ); … … 787 799 } 788 800 ?> 789 <input type="text" class="regular-text" name="<?php echo esc_attr( $option_name . '[' . $key . ']' ); ?>" value="<?php echo esc_attr( $value ); ?>" placeholder="<?php echo esc_attr( $placeholder ); ?>" /> 801 <input 802 type="<?php echo esc_attr( $type ); ?>" 803 class="regular-text" 804 name="<?php echo esc_attr( $option_name . '[' . $key . ']' ); ?>" 805 value="<?php echo esc_attr( $value ); ?>" 806 placeholder="<?php echo esc_attr( $placeholder ); ?>" 807 autocomplete="off" 808 data-form-type="other" 809 data-lpignore="true" 810 data-1p-ignore="true" 811 <?php if ( null !== $min ) : ?>min="<?php echo esc_attr( $min ); ?>"<?php endif; ?> 812 <?php if ( null !== $max ) : ?>max="<?php echo esc_attr( $max ); ?>"<?php endif; ?> 813 /> 790 814 <?php 791 815 } … … 802 826 value="<?php echo esc_attr( $value ); ?>" 803 827 placeholder="<?php echo esc_attr( $placeholder ); ?>" 804 autocomplete="off" 828 autocomplete="new-password" 829 data-form-type="other" 830 data-lpignore="true" 831 data-1p-ignore="true" 832 data-bwignore="true" 833 autocapitalize="none" 834 autocorrect="off" 805 835 spellcheck="false" 836 readonly="readonly" 806 837 /> 807 838 <button type="button" class="button seoautopro-sensitive-toggle" aria-label="<?php esc_attr_e( 'Show value', 'seo-auditor-tools' ); ?>" data-show-label="<?php esc_attr_e( 'Show value', 'seo-auditor-tools' ); ?>" data-hide-label="<?php esc_attr_e( 'Hide value', 'seo-auditor-tools' ); ?>"> … … 1079 1110 1080 1111 <h2><?php esc_html_e( 'Audit settings', 'seo-auditor-tools' ); ?></h2> 1081 <form method="post" action="options.php" class="seoautopro-card seoautopro-form-card" >1112 <form method="post" action="options.php" class="seoautopro-card seoautopro-form-card" autocomplete="off" data-form-type="other"> 1082 1113 <?php 1083 1114 settings_fields( 'seoautopro_audit' ); … … 1334 1365 <p class="seoautopro-issue-detail"><?php echo esc_html__( 'Verify:', 'seo-auditor-tools' ); ?> <?php echo esc_html( $issue['verify'] ); ?></p> 1335 1366 <?php endif; ?> 1367 <?php if ( ! empty( $issue['id'] ) && class_exists( 'SEOAUTOPRO_AI_Helper' ) && SEOAUTOPRO_AI_Helper::supports_issue( $issue['id'] ) ) : ?> 1368 <p class="seoautopro-issue-detail"> 1369 <a class="button button-primary" href="<?php echo esc_url( SEOAUTOPRO_AI_Helper::get_issue_url( $issue['id'] ) ); ?>"><?php esc_html_e( 'Open AI Helper', 'seo-auditor-tools' ); ?></a> 1370 </p> 1371 <?php endif; ?> 1336 1372 <?php if ( ! empty( $resources ) ) : ?> 1337 1373 <p class="seoautopro-issue-detail"><?php esc_html_e( 'Resources:', 'seo-auditor-tools' ); ?></p> -
seo-auditor-tools/trunk/includes/class-seoautopro-analyzer.php
r3638852 r3658740 106 106 'accessibility_flags' => 0, 107 107 'seo_flags' => 0, 108 'missing_meta_descriptions'=> 0, 108 109 'missing_h1_urls' => array(), 109 110 'multiple_h1_urls' => array(), … … 112 113 'missing_size_urls' => array(), 113 114 'empty_link_urls' => array(), 115 'missing_meta_description_urls' => array(), 114 116 ); 115 117 … … 126 128 127 129 foreach ( $posts as $post_id ) { 130 if ( trim( (string) get_post_meta( $post_id, '_seoautopro_meta_description', true ) ) === '' ) { 131 $stats['missing_meta_descriptions']++; 132 $stats['seo_flags']++; 133 self::add_impacted_url( $stats['missing_meta_description_urls'], $post_id, $impact_limit ); 134 } 135 128 136 $content = get_post_field( 'post_content', $post_id ); 129 137 $content = apply_filters( 'the_content', $content ); … … 159 167 $has_missing_alt = false; 160 168 foreach ( $imgs[0] as $img_tag ) { 161 if ( ! preg_match( '/alt=["\'][^"\']*["\']/', $img_tag ) ) { 169 $has_alt = preg_match( '/\balt\s*=\s*(["\'])(.*?)\1/is', $img_tag, $alt_match ) && '' !== trim( html_entity_decode( $alt_match[2], ENT_QUOTES, 'UTF-8' ) ); 170 $decorative = preg_match( '/\brole\s*=\s*(["\'])(?:presentation|none)\1/i', $img_tag ) || preg_match( '/\baria-hidden\s*=\s*(["\'])true\1/i', $img_tag ); 171 if ( ! $has_alt && ! $decorative ) { 162 172 $stats['images_missing_alt']++; 163 173 $has_missing_alt = true; … … 229 239 $stats['images_total']++; 230 240 $alt = $img->getAttribute( 'alt' ); 231 if ( $alt === '' ) { 241 $role = strtolower( $img->getAttribute( 'role' ) ); 242 $decorative = in_array( $role, array( 'presentation', 'none' ), true ) || 'true' === strtolower( $img->getAttribute( 'aria-hidden' ) ); 243 if ( $alt === '' && ! $decorative ) { 232 244 $stats['images_missing_alt']++; 233 245 $has_missing_alt = true; … … 472 484 473 485 if ( $post_stats['total_posts'] > 0 ) { 474 $seo_penalty = ( $post_stats['no_h1'] * 3 ) + ( $post_stats['multiple_h1'] * 3 ) ;486 $seo_penalty = ( $post_stats['no_h1'] * 3 ) + ( $post_stats['multiple_h1'] * 3 ) + min( 20, intval( $post_stats['missing_meta_descriptions'] ) ); 475 487 $seo_penalty = min( 80, $seo_penalty ); 476 488 $seo_score = max( 10, 100 - $seo_penalty ); … … 526 538 'sequence_issues' => $post_stats['heading_sequence_issues'], 527 539 ), 540 'missing_meta_descriptions' => isset( $post_stats['missing_meta_descriptions'] ) ? intval( $post_stats['missing_meta_descriptions'] ) : 0, 528 541 'images' => array( 529 542 'total' => $post_stats['images_total'], -
seo-auditor-tools/trunk/includes/class-seoautopro-chatbot.php
r3645873 r3658740 10 10 11 11 public static function register_settings() { 12 self::migrate_legacy_documents(); 13 12 14 register_setting( 13 15 'seoautopro_chatbot', … … 32 34 33 35 add_settings_field( 36 'seoautopro_chatbot_ai_enabled', 37 __( 'Grounded AI answers', 'seo-auditor-tools' ), 38 array( __CLASS__, 'field_ai_enabled' ), 39 'seoautopro-chatbot', 40 'seoautopro_chatbot_main' 41 ); 42 43 add_settings_field( 44 'seoautopro_chatbot_hourly_ip_limit', 45 __( 'Questions per visitor/hour', 'seo-auditor-tools' ), 46 array( __CLASS__, 'field_hourly_ip_limit' ), 47 'seoautopro-chatbot', 48 'seoautopro_chatbot_main' 49 ); 50 51 add_settings_field( 52 'seoautopro_chatbot_daily_limit', 53 __( 'Total questions/day', 'seo-auditor-tools' ), 54 array( __CLASS__, 'field_daily_limit' ), 55 'seoautopro-chatbot', 56 'seoautopro_chatbot_main' 57 ); 58 59 add_settings_field( 34 60 'seoautopro_chatbot_email', 35 61 __( 'Support email', 'seo-auditor-tools' ), … … 138 164 $clean = is_array( $input ) ? $input : array(); 139 165 $clean['enabled'] = ! empty( $clean['enabled'] ) ? 1 : 0; 166 $clean['ai_enabled'] = ! empty( $clean['ai_enabled'] ) ? 1 : 0; 167 $clean['hourly_ip_limit'] = isset( $clean['hourly_ip_limit'] ) ? min( 100, max( 1, absint( $clean['hourly_ip_limit'] ) ) ) : 20; 168 $clean['daily_limit'] = isset( $clean['daily_limit'] ) ? min( 10000, max( 1, absint( $clean['daily_limit'] ) ) ) : 500; 140 169 $clean['support_email'] = isset( $clean['support_email'] ) ? sanitize_email( $clean['support_email'] ) : ''; 141 170 $clean['greeting'] = isset( $clean['greeting'] ) ? sanitize_textarea_field( $clean['greeting'] ) : ''; … … 162 191 $defaults = array( 163 192 'enabled' => 0, 193 'ai_enabled' => 0, 194 'hourly_ip_limit' => 20, 195 'daily_limit' => 500, 164 196 'support_email' => get_option( 'admin_email' ), 165 197 'greeting' => __( 'Hello. How may I assist you today?', 'seo-auditor-tools' ), … … 189 221 } 190 222 223 public static function field_ai_enabled() { 224 $settings = self::get_settings(); 225 ?> 226 <label> 227 <input type="checkbox" name="<?php echo esc_attr( self::SETTINGS_OPTION ); ?>[ai_enabled]" value="1" <?php checked( ! empty( $settings['ai_enabled'] ) ); ?> /> 228 <?php esc_html_e( 'Use the provider configured in AI Helper to compose answers from approved website sources.', 'seo-auditor-tools' ); ?> 229 </label> 230 <p class="description"><?php esc_html_e( 'The model receives only relevant public excerpts and approved document text. It cannot browse the web, and unsupported questions use the contact fallback.', 'seo-auditor-tools' ); ?></p> 231 <?php 232 } 233 234 public static function field_hourly_ip_limit() { 235 $settings = self::get_settings(); 236 ?> 237 <input type="number" class="small-text" min="1" max="100" name="<?php echo esc_attr( self::SETTINGS_OPTION ); ?>[hourly_ip_limit]" value="<?php echo esc_attr( $settings['hourly_ip_limit'] ); ?>" /> 238 <p class="description"><?php esc_html_e( 'Limits expensive or abusive requests. Visitor addresses are anonymised and hashed before the temporary counter is stored.', 'seo-auditor-tools' ); ?></p> 239 <?php 240 } 241 242 public static function field_daily_limit() { 243 $settings = self::get_settings(); 244 ?> 245 <input type="number" class="small-text" min="1" max="10000" name="<?php echo esc_attr( self::SETTINGS_OPTION ); ?>[daily_limit]" value="<?php echo esc_attr( $settings['daily_limit'] ); ?>" /> 246 <p class="description"><?php esc_html_e( 'Maximum chatbot questions across the website each UTC day.', 'seo-auditor-tools' ); ?></p> 247 <?php 248 } 249 191 250 public static function field_support_email() { 192 251 $settings = self::get_settings(); … … 300 359 <div class="wrap seoautopro-wrap"> 301 360 <h1><?php esc_html_e( 'Chatbot', 'seo-auditor-tools' ); ?></h1> 302 <p><?php esc_html_e( 'Configure the chatbot and upload documents to improve responses.', 'seo-auditor-tools' ); ?></p>361 <p><?php esc_html_e( 'Configure a website-only assistant. It can use public published posts and pages plus documents you explicitly approve.', 'seo-auditor-tools' ); ?></p> 303 362 304 363 <?php if ( $status === 'uploaded' ) : ?> … … 306 365 <?php elseif ( $status === 'deleted' ) : ?> 307 366 <div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Document deleted.', 'seo-auditor-tools' ); ?></p></div> 367 <?php elseif ( $status === 'updated' ) : ?> 368 <div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Document approval updated.', 'seo-auditor-tools' ); ?></p></div> 308 369 <?php elseif ( $status === 'error' ) : ?> 309 370 <div class="notice notice-error is-dismissible"><p><?php esc_html_e( 'Document upload failed.', 'seo-auditor-tools' ); ?></p></div> … … 320 381 <div class="seoautopro-card seoautopro-form-card"> 321 382 <h2><?php esc_html_e( 'Upload documents', 'seo-auditor-tools' ); ?></h2> 322 <p><?php esc_html_e( 'Upload PDF or DOC/DOCX files to add reference material for the chatbot.', 'seo-auditor-tools' ); ?></p>383 <p><?php esc_html_e( 'Upload PDF or DOC/DOCX reference material up to 5 MB. The plugin extracts limited plain text and does not retain a publicly downloadable copy of the original file.', 'seo-auditor-tools' ); ?></p> 323 384 <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data"> 324 385 <?php wp_nonce_field( 'seoautopro_chatbot_upload' ); ?> 325 386 <input type="hidden" name="action" value="seoautopro_chatbot_upload_doc" /> 326 387 <input type="file" name="seoautopro_chatbot_doc" accept=".pdf,.doc,.docx" required /> 388 <label style="margin-left:12px;"> 389 <input type="checkbox" name="approved" value="1" /> 390 <?php esc_html_e( 'Approve extracted text for public chatbot answers', 'seo-auditor-tools' ); ?> 391 </label> 327 392 <?php submit_button( __( 'Upload document', 'seo-auditor-tools' ), 'secondary', 'submit', false ); ?> 328 393 </form> … … 338 403 <tr> 339 404 <th><?php esc_html_e( 'File', 'seo-auditor-tools' ); ?></th> 405 <th><?php esc_html_e( 'Chatbot access', 'seo-auditor-tools' ); ?></th> 340 406 <th><?php esc_html_e( 'Uploaded', 'seo-auditor-tools' ); ?></th> 341 407 <th><?php esc_html_e( 'Actions', 'seo-auditor-tools' ); ?></th> … … 346 412 <tr> 347 413 <td> 348 <a href="<?php echo esc_url( $doc['url'] ); ?>" target="_blank" rel="noopener noreferrer"> 349 <?php echo esc_html( $doc['name'] ); ?> 350 </a> 414 <?php echo esc_html( isset( $doc['name'] ) ? $doc['name'] : __( 'Document', 'seo-auditor-tools' ) ); ?> 351 415 </td> 416 <td><?php echo ! empty( $doc['approved'] ) ? esc_html__( 'Approved', 'seo-auditor-tools' ) : esc_html__( 'Not approved', 'seo-auditor-tools' ); ?></td> 352 417 <td><?php echo esc_html( date_i18n( get_option( 'date_format' ), intval( $doc['uploaded'] ) ) ); ?></td> 353 418 <td> 354 <a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=seoautopro_chatbot_delete_doc&doc_id=' . intval( $doc['id'] ) ), 'seoautopro_chatbot_delete_doc' ) ); ?>"><?php esc_html_e( 'Delete', 'seo-auditor-tools' ); ?></a> 419 <?php 420 $doc_id = isset( $doc['id'] ) ? sanitize_text_field( (string) $doc['id'] ) : ''; 421 $toggle_url = add_query_arg( 422 array( 423 'action' => 'seoautopro_chatbot_toggle_doc', 424 'doc_id' => $doc_id, 425 ), 426 admin_url( 'admin-post.php' ) 427 ); 428 $delete_url = add_query_arg( 429 array( 430 'action' => 'seoautopro_chatbot_delete_doc', 431 'doc_id' => $doc_id, 432 ), 433 admin_url( 'admin-post.php' ) 434 ); 435 ?> 436 <a href="<?php echo esc_url( wp_nonce_url( $toggle_url, 'seoautopro_chatbot_toggle_doc' ) ); ?>"><?php echo ! empty( $doc['approved'] ) ? esc_html__( 'Remove approval', 'seo-auditor-tools' ) : esc_html__( 'Approve', 'seo-auditor-tools' ); ?></a> 437 | <a href="<?php echo esc_url( wp_nonce_url( $delete_url, 'seoautopro_chatbot_delete_doc' ) ); ?>"><?php esc_html_e( 'Delete', 'seo-auditor-tools' ); ?></a> 355 438 </td> 356 439 </tr> … … 397 480 'greeting' => $settings['greeting'], 398 481 'thank_you' => $settings['thank_you'], 482 'error_message' => __( 'Sorry, something went wrong.', 'seo-auditor-tools' ), 483 'support_prompt' => __( 'Would you like to send a message to our support team for further assistance?', 'seo-auditor-tools' ), 399 484 ) 400 485 ); … … 434 519 </button> 435 520 <div id="seoautopro-chatbot-window" class="seoautopro-chatbot-window" aria-hidden="true"> 436 <div class="seoautopro-chatbot-header"><?php echo esc_html( $settings['name'] ); ?></div> 521 <div class="seoautopro-chatbot-header"> 522 <span><?php echo esc_html( $settings['name'] ); ?></span> 523 <button type="button" class="seoautopro-chatbot-refresh" title="<?php esc_attr_e( 'Start a new conversation', 'seo-auditor-tools' ); ?>" aria-label="<?php esc_attr_e( 'Start a new conversation', 'seo-auditor-tools' ); ?>">↻</button> 524 </div> 437 525 <div class="seoautopro-chatbot-messages"></div> 438 526 <form class="seoautopro-chatbot-input"> 439 <input type="text" placeholder="<?php esc_attr_e( 'Type your question...', 'seo-auditor-tools' ); ?>" required />527 <input type="text" maxlength="300" autocomplete="off" placeholder="<?php esc_attr_e( 'Type your question...', 'seo-auditor-tools' ); ?>" required /> 440 528 <button type="submit"><?php esc_html_e( 'Send', 'seo-auditor-tools' ); ?></button> 441 529 </form> 442 530 <form class="seoautopro-chatbot-contact" style="display:none;"> 443 <p><?php esc_html_e( 'We could not find an answer. Please leave your details and we will reply by email.', 'seo-auditor-tools' ); ?></p>531 <p><?php esc_html_e( 'We could not find an answer. You can leave your details for an email reply or continue chatting.', 'seo-auditor-tools' ); ?></p> 444 532 <input type="text" name="first_name" placeholder="<?php esc_attr_e( 'Name', 'seo-auditor-tools' ); ?>" required /> 445 533 <input type="text" name="last_name" placeholder="<?php esc_attr_e( 'Surname', 'seo-auditor-tools' ); ?>" required /> … … 447 535 <input type="text" name="subject" placeholder="<?php esc_attr_e( 'Subject', 'seo-auditor-tools' ); ?>" required /> 448 536 <textarea name="message" rows="3" placeholder="<?php esc_attr_e( 'Message', 'seo-auditor-tools' ); ?>" required></textarea> 449 <button type="submit"><?php esc_html_e( 'Send message', 'seo-auditor-tools' ); ?></button> 537 <div class="seoautopro-chatbot-contact-actions"> 538 <button type="button" class="seoautopro-chatbot-continue"><?php esc_html_e( 'Continue chat', 'seo-auditor-tools' ); ?></button> 539 <button type="submit"><?php esc_html_e( 'Send message', 'seo-auditor-tools' ); ?></button> 540 </div> 450 541 </form> 451 542 </div> … … 466 557 467 558 $file = $_FILES['seoautopro_chatbot_doc']; 468 if ( ! empty( $file['error'] ) || empty( $file['tmp_name'] ) ) {559 if ( ! empty( $file['error'] ) || empty( $file['tmp_name'] ) || empty( $file['size'] ) || intval( $file['size'] ) > 5 * MB_IN_BYTES || ! is_uploaded_file( $file['tmp_name'] ) ) { 469 560 self::redirect_with_status( 'error' ); 470 561 } … … 475 566 } 476 567 477 $uploads = wp_upload_dir(); 478 $dir = trailingslashit( $uploads['basedir'] ) . 'seoautopro-chatbot'; 479 $url_base = trailingslashit( $uploads['baseurl'] ) . 'seoautopro-chatbot'; 480 if ( ! wp_mkdir_p( $dir ) ) { 568 $text = self::extract_text( $file['tmp_name'], $ext ); 569 if ( $text === '' ) { 481 570 self::redirect_with_status( 'error' ); 482 571 } 483 572 484 $filename = wp_unique_filename( $dir, sanitize_file_name( $file['name'] ) );485 $dest = trailingslashit( $dir ) . $filename;486 if ( ! @move_uploaded_file( $file['tmp_name'], $dest ) ) {487 self::redirect_with_status( 'error' );488 }489 490 $text = self::extract_text( $dest, $ext );491 573 $docs = get_option( self::DOC_OPTION, array() ); 492 574 if ( ! is_array( $docs ) ) { … … 495 577 496 578 $docs[] = array( 497 'id' => time(), 498 'name' => $file['name'], 499 'path' => $dest, 500 'url' => $url_base . '/' . $filename, 579 'id' => wp_generate_uuid4(), 580 'name' => sanitize_file_name( wp_unslash( $file['name'] ) ), 501 581 'ext' => $ext, 502 582 'text' => $text, 503 583 'uploaded' => time(), 584 'approved' => ! empty( $_POST['approved'] ) ? 1 : 0, 504 585 ); 505 586 … … 515 596 check_admin_referer( 'seoautopro_chatbot_delete_doc' ); 516 597 517 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only param, sanitized inline.518 $doc_id = isset( $_GET['doc_id'] ) ? intval( wp_unslash( $_GET['doc_id'] ) ) : 0;598 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce checked above, sanitized inline. 599 $doc_id = isset( $_GET['doc_id'] ) ? sanitize_text_field( wp_unslash( $_GET['doc_id'] ) ) : ''; 519 600 $docs = get_option( self::DOC_OPTION, array() ); 520 601 if ( ! is_array( $docs ) ) { … … 524 605 $filtered = array(); 525 606 foreach ( $docs as $doc ) { 526 if ( intval( $doc['id'] ) === $doc_id ) { 527 if ( ! empty( $doc['path'] ) && file_exists( $doc['path'] ) ) { 528 @unlink( $doc['path'] ); 529 } 607 $stored_id = isset( $doc['id'] ) ? (string) $doc['id'] : ''; 608 if ( $stored_id !== '' && hash_equals( $stored_id, $doc_id ) ) { 609 self::delete_legacy_file( $doc ); 530 610 continue; 531 611 } … … 535 615 update_option( self::DOC_OPTION, $filtered, false ); 536 616 self::redirect_with_status( 'deleted' ); 617 } 618 619 public static function handle_toggle_doc() { 620 if ( ! current_user_can( 'manage_options' ) ) { 621 wp_die( esc_html__( 'Permission denied.', 'seo-auditor-tools' ) ); 622 } 623 624 check_admin_referer( 'seoautopro_chatbot_toggle_doc' ); 625 626 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce checked above, sanitized inline. 627 $doc_id = isset( $_GET['doc_id'] ) ? sanitize_text_field( wp_unslash( $_GET['doc_id'] ) ) : ''; 628 $docs = get_option( self::DOC_OPTION, array() ); 629 if ( ! is_array( $docs ) ) { 630 $docs = array(); 631 } 632 633 foreach ( $docs as &$doc ) { 634 $stored_id = isset( $doc['id'] ) ? (string) $doc['id'] : ''; 635 if ( $stored_id !== '' && hash_equals( $stored_id, $doc_id ) ) { 636 $doc['approved'] = empty( $doc['approved'] ) ? 1 : 0; 637 break; 638 } 639 } 640 unset( $doc ); 641 642 update_option( self::DOC_OPTION, $docs, false ); 643 self::redirect_with_status( 'updated' ); 644 } 645 646 public static function migrate_legacy_documents() { 647 $docs = get_option( self::DOC_OPTION, array() ); 648 if ( ! is_array( $docs ) || empty( $docs ) ) { 649 return; 650 } 651 652 $changed = false; 653 foreach ( $docs as &$doc ) { 654 if ( ! array_key_exists( 'approved', $doc ) ) { 655 $doc['approved'] = 0; 656 $changed = true; 657 } 658 if ( isset( $doc['path'] ) || isset( $doc['url'] ) ) { 659 self::delete_legacy_file( $doc ); 660 unset( $doc['path'], $doc['url'] ); 661 $changed = true; 662 } 663 } 664 unset( $doc ); 665 666 if ( $changed ) { 667 update_option( self::DOC_OPTION, $docs, false ); 668 } 669 } 670 671 private static function delete_legacy_file( $doc ) { 672 if ( empty( $doc['path'] ) ) { 673 return; 674 } 675 676 $uploads = wp_upload_dir(); 677 $allowed_dir = realpath( trailingslashit( $uploads['basedir'] ) . 'seoautopro-chatbot' ); 678 $file_path = realpath( $doc['path'] ); 679 if ( ! $allowed_dir || ! $file_path ) { 680 return; 681 } 682 683 $allowed_prefix = trailingslashit( wp_normalize_path( $allowed_dir ) ); 684 $normalized_file = wp_normalize_path( $file_path ); 685 if ( strpos( $normalized_file, $allowed_prefix ) === 0 && is_file( $file_path ) ) { 686 @unlink( $file_path ); 687 } 537 688 } 538 689 … … 552 703 check_ajax_referer( 'seoautopro-chatbot', 'nonce' ); 553 704 705 $settings = self::get_settings(); 706 if ( empty( $settings['enabled'] ) ) { 707 wp_send_json_error( array( 'message' => __( 'The chatbot is currently unavailable.', 'seo-auditor-tools' ) ), 403 ); 708 } 709 554 710 $question = isset( $_POST['question'] ) ? sanitize_text_field( wp_unslash( $_POST['question'] ) ) : ''; 555 711 if ( $question === '' ) { 556 712 wp_send_json_error( array( 'message' => __( 'Please enter a question.', 'seo-auditor-tools' ) ) ); 557 713 } 714 if ( self::text_length( $question ) > 300 ) { 715 wp_send_json_error( array( 'message' => __( 'Please keep questions under 300 characters.', 'seo-auditor-tools' ) ), 400 ); 716 } 717 718 $limit_error = self::check_rate_limit(); 719 if ( is_wp_error( $limit_error ) ) { 720 wp_send_json_error( array( 'message' => $limit_error->get_error_message() ), 429 ); 721 } 558 722 559 723 $result = self::find_relevant_answers( $question ); … … 563 727 public static function ajax_contact() { 564 728 check_ajax_referer( 'seoautopro-chatbot', 'nonce' ); 729 730 $settings = self::get_settings(); 731 if ( empty( $settings['enabled'] ) ) { 732 wp_send_json_error( array( 'message' => __( 'The chatbot is currently unavailable.', 'seo-auditor-tools' ) ), 403 ); 733 } 734 $contact_limit_error = self::check_rate_limit( 'contact', 5, 100 ); 735 if ( is_wp_error( $contact_limit_error ) ) { 736 wp_send_json_error( array( 'message' => $contact_limit_error->get_error_message() ), 429 ); 737 } 565 738 566 739 $first = isset( $_POST['first_name'] ) ? sanitize_text_field( wp_unslash( $_POST['first_name'] ) ) : ''; … … 578 751 } 579 752 580 $settings = self::get_settings();581 753 $to = ! empty( $settings['support_email'] ) ? $settings['support_email'] : get_option( 'admin_email' ); 582 754 … … 603 775 604 776 $items = array(); 605 $llms_text = self::get_llms_text();606 if ( $llms_text !== '' ) {607 $score = self::score_text( $llms_text, $terms );608 if ( $score > 0 ) {609 $items[] = array(610 'type' => 'llms',611 'title' => __( 'Site summary', 'seo-auditor-tools' ),612 'url' => '',613 'excerpt' => self::extract_snippet( $llms_text, $terms ),614 'score' => $score,615 );616 }617 }618 777 $posts = get_posts( array( 619 778 'post_type' => array( 'page', 'post' ), 620 779 'post_status' => 'publish', 621 780 'numberposts' => 200, 781 'has_password' => false, 622 782 ) ); 623 783 624 784 foreach ( $posts as $post ) { 785 if ( 'publish' !== get_post_status( $post ) || ! empty( $post->post_password ) ) { 786 continue; 787 } 788 if ( function_exists( 'is_post_publicly_viewable' ) && ! is_post_publicly_viewable( $post ) ) { 789 continue; 790 } 625 791 $text = self::clean_post_text( $post->post_title . ' ' . $post->post_content ); 626 792 if ( strlen( $text ) < 20 ) { … … 643 809 if ( is_array( $docs ) ) { 644 810 foreach ( $docs as $doc ) { 811 if ( empty( $doc['approved'] ) ) { 812 continue; 813 } 645 814 $text = isset( $doc['text'] ) ? $doc['text'] : ''; 646 815 if ( $text === '' ) { … … 671 840 } ); 672 841 842 $items = array_slice( $items, 0, 5 ); 843 foreach ( $items as $index => &$item ) { 844 $item['id'] = 'source-' . ( $index + 1 ); 845 } 846 unset( $item ); 847 848 $settings = self::get_settings(); 849 if ( ! empty( $settings['ai_enabled'] ) && class_exists( 'SEOAUTOPRO_AI_Helper' ) ) { 850 $ai_result = SEOAUTOPRO_AI_Helper::generate_grounded_chatbot_answer( $question, $items ); 851 if ( ! is_wp_error( $ai_result ) ) { 852 if ( empty( $ai_result['supported'] ) ) { 853 return array( 854 'reply' => __( 'I am sorry, I could not find enough approved website information to answer that reliably.', 'seo-auditor-tools' ), 855 'no_answer' => true, 856 ); 857 } 858 859 $source_ids = isset( $ai_result['source_ids'] ) && is_array( $ai_result['source_ids'] ) ? $ai_result['source_ids'] : array(); 860 $cited_items = array_values( array_filter( $items, function( $item ) use ( $source_ids ) { 861 return in_array( $item['id'], $source_ids, true ); 862 } ) ); 863 if ( ! empty( $cited_items ) ) { 864 return array( 865 'reply' => $ai_result['reply'], 866 'sources' => $cited_items, 867 'no_answer' => false, 868 'ai_generated' => true, 869 ); 870 } 871 } 872 } 873 673 874 $items = array_slice( $items, 0, 3 ); 674 875 return array( … … 677 878 'no_answer' => false, 678 879 ); 880 } 881 882 private static function check_rate_limit( $bucket = 'answer', $hourly_override = 0, $daily_override = 0 ) { 883 $settings = self::get_settings(); 884 $hourly_limit = $hourly_override ? absint( $hourly_override ) : min( 100, max( 1, absint( $settings['hourly_ip_limit'] ) ) ); 885 $daily_limit = $daily_override ? absint( $daily_override ) : min( 10000, max( 1, absint( $settings['daily_limit'] ) ) ); 886 $bucket = sanitize_key( $bucket ); 887 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown'; 888 if ( function_exists( 'wp_privacy_anonymize_ip' ) ) { 889 $ip = wp_privacy_anonymize_ip( $ip ); 890 } 891 $visitor_hash = hash_hmac( 'sha256', $ip, wp_salt( 'nonce' ) ); 892 $visitor_key = 'seoautopro_chat_' . $bucket . '_hour_' . substr( $visitor_hash, 0, 20 ); 893 $daily_key = 'seoautopro_chat_' . $bucket . '_day_' . gmdate( 'Ymd' ); 894 $visitor_count = absint( get_transient( $visitor_key ) ); 895 $daily_count = absint( get_transient( $daily_key ) ); 896 897 if ( $visitor_count >= $hourly_limit || $daily_count >= $daily_limit ) { 898 return new WP_Error( 'seoautopro_chatbot_rate_limit', __( 'The chatbot request limit has been reached. Please try again later.', 'seo-auditor-tools' ) ); 899 } 900 901 set_transient( $visitor_key, $visitor_count + 1, HOUR_IN_SECONDS ); 902 set_transient( $daily_key, $daily_count + 1, DAY_IN_SECONDS + HOUR_IN_SECONDS ); 903 return true; 904 } 905 906 private static function text_length( $text ) { 907 return function_exists( 'mb_strlen' ) ? mb_strlen( $text ) : strlen( $text ); 679 908 } 680 909 … … 723 952 } 724 953 725 return $url; 954 $result_host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) ); 955 $site_host = strtolower( (string) wp_parse_url( home_url( '/' ), PHP_URL_HOST ) ); 956 if ( '' === $result_host || '' === $site_host || $result_host !== $site_host ) { 957 return ''; 958 } 959 960 return esc_url_raw( $url ); 726 961 } 727 962 … … 791 1026 $zip = new ZipArchive(); 792 1027 if ( $zip->open( $path ) === true ) { 793 $data = $zip->getFromName( 'word/document.xml' ); 794 if ( $data ) { 795 $text = wp_strip_all_tags( $data ); 1028 $entry = $zip->statName( 'word/document.xml' ); 1029 if ( is_array( $entry ) && ! empty( $entry['size'] ) && intval( $entry['size'] ) <= 2 * MB_IN_BYTES ) { 1030 $data = $zip->getFromName( 'word/document.xml' ); 1031 if ( $data ) { 1032 $text = wp_strip_all_tags( $data ); 1033 } 796 1034 } 797 1035 $zip->close(); -
seo-auditor-tools/trunk/includes/class-seoautopro-rules.php
r3592695 r3658740 21 21 $missing_size_urls = isset( $post_stats['missing_size_urls'] ) ? $post_stats['missing_size_urls'] : array(); 22 22 $empty_link_urls = isset( $post_stats['empty_link_urls'] ) ? $post_stats['empty_link_urls'] : array(); 23 $missing_meta_description_urls = isset( $post_stats['missing_meta_description_urls'] ) ? $post_stats['missing_meta_description_urls'] : array(); 24 25 if ( ! empty( $post_stats['missing_meta_descriptions'] ) ) { 26 $issues[] = self::issue( array( 27 'id' => 'seo_missing_meta_description', 28 'title' => __( 'Pages missing a custom meta description', 'seo-auditor-tools' ), 29 'category' => 'seo', 30 'severity' => 'warning', 31 'impact' => 'SEO', 32 'effort' => 'easy', 33 'evidence' => sprintf( _n( '%d page relies on a generated fallback description.', '%d pages rely on generated fallback descriptions.', $post_stats['missing_meta_descriptions'], 'seo-auditor-tools' ), $post_stats['missing_meta_descriptions'] ), 34 'impacted' => $missing_meta_description_urls, 35 'fix' => __( 'Create a concise, page-specific meta description. AI Helper can prepare a proposal for your approval.', 'seo-auditor-tools' ), 36 'verify' => __( 'View the page source after applying the change and confirm the description meta tag contains the approved text.', 'seo-auditor-tools' ), 37 ) ); 38 } 23 39 24 40 if ( ! empty( $post_stats['no_h1'] ) ) { … … 65 81 'evidence' => sprintf( _n( '%d image is missing alt text.', '%d images are missing alt text.', $post_stats['images_missing_alt'], 'seo-auditor-tools' ), $post_stats['images_missing_alt'] ), 66 82 'impacted' => $missing_alt_urls, 67 'fix' => __( 'Add a short description for each image (alt text). You can do this in the Media Library or in the image block settings.', 'seo-auditor-tools' ),68 'verify' => __( ' Open one of the listed pages and confirm the images have descriptions filled in.', 'seo-auditor-tools' ),83 'fix' => __( 'Add concise, contextual alt text for informative images. AI Helper can prepare a proposal for images saved in page or post content; decorative images should keep an empty alt attribute.', 'seo-auditor-tools' ), 84 'verify' => __( 'Run the audit again and confirm informative images have useful alt text while decorative images retain an empty alt attribute.', 'seo-auditor-tools' ), 69 85 ) ); 70 86 } -
seo-auditor-tools/trunk/includes/class-seoautopro-settings-export.php
r3592695 r3658740 85 85 $data[ $key ] = get_option( $key, array() ); 86 86 } 87 if ( isset( $data['seoautopro_ai_helper_settings']['api_keys'] ) ) { 88 unset( $data['seoautopro_ai_helper_settings']['api_keys'] ); 89 } 87 90 $data['seoautopro_exported_at'] = current_time( 'mysql' ); 88 91 $data['seoautopro_version'] = defined( 'SEOAUTOPRO_VERSION' ) ? SEOAUTOPRO_VERSION : ''; … … 105 108 'seoautopro_llms_settings', 106 109 'seoautopro_chatbot_settings', 110 'seoautopro_ai_helper_settings', 107 111 ); 108 112 } -
seo-auditor-tools/trunk/readme.txt
r3645916 r3658740 6 6 Tested up to: 7.0 7 7 Requires PHP: 7.4 8 Stable tag: 2.0. 28 Stable tag: 2.0.3 9 9 License: GPLv2 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 16 16 == Description == 17 17 18 Scan your WordPress site for SEO issues and get actionable fixes. Includes sitemap, robots.txt, LLMS.txt, Open Graph, cookie consent, analytics, UTM builder, QR code generator, image tools, caching, minification, security monitor, redirection manager, .htaccess editor, and chatbot. All features free - no Pro version.18 Scan your WordPress site for SEO issues and get actionable fixes. Includes AI Helper, sitemap, robots.txt, LLMS.txt, Open Graph, cookie consent, analytics, UTM builder, QR code generator, image tools, caching, minification, security monitor, redirection manager, .htaccess editor, and chatbot. All features free - no Pro version. 19 19 20 20 **SEO Auditor Tools** is a complete, no-paywall SEO and site management toolkit for WordPress. Run an automated site audit that crawls your pages and returns a fully scored report across four categories - SEO, Accessibility, Performance, and Best Practices - with plain-language issue cards and actionable fixes for every finding. … … 59 59 * **QR Code Generator** - Create QR codes for any URL. Download as PNG or copy Base64 inline. 60 60 * **Settings Export** - Export all plugin settings as JSON or CSV to migrate to another site in seconds. 61 * **AI Helper** - Use a self-hosted Ollama model to create structured meta-description proposals with preview, validation, approval, fix history, and undo. 61 62 * **Security Monitor** - File integrity checks, login hardening, HTTP security headers, and brute-force protection. 62 63 * **Image Tools** - Bulk add missing width/height dimensions, WebP conversion, regenerate thumbnails, and non-destructive image editing. … … 65 66 * **.htaccess Editor** - Safe .htaccess editing with automatic backup and one-click restore. 66 67 * **Redirection Manager** - 301/302 redirect rules with import/export and first-login redirect support. 67 * **Chatbot** - On-site assistant widget for visitor support, powered by your own uploaded documents.68 * **Chatbot** - Grounded website assistant using only public published content and explicitly approved document text, with optional AI-generated answers and source citations. 68 69 69 70 == Installation == … … 99 100 Enter your free Google API key in **SEO Auditor Tools → Audit Settings**. The plugin will fetch Lighthouse scores when you run a scan and display them directly in the dashboard. 100 101 102 = Does AI Helper change content automatically? = 103 104 No. AI Helper asks the configured provider for a structured proposal, validates it, and shows it to an administrator for review and editing. Nothing is written until the administrator explicitly approves it. Applied changes are recorded and can be undone if the target has not subsequently changed. 105 106 = Where must Ollama run? = 107 108 The default endpoint points to the same server that hosts WordPress. Ollama running on your personal computer is not reachable from a remotely hosted WordPress site unless you provide a deliberately secured connection. Trusted remote Ollama hosts can be enabled by a developer with the `seoautopro_ai_helper_allowed_ollama_hosts` filter. 109 110 = Which AI providers can I use? = 111 112 The Free list includes Ollama, Google Gemini, GroqCloud, OpenRouter, and a custom OpenAI-compatible endpoint. The Paid list includes Google Gemini, GroqCloud, OpenRouter, OpenAI, Anthropic Claude, and a custom OpenAI-compatible endpoint. A provider's own free-tier limits, model availability, pricing, and account requirements can change. You can always enter a custom model ID when a newer supported model is not yet listed. 113 114 API keys can be saved through the AI Helper screen or kept out of the WordPress database by defining the matching `SEOAUTOPRO_AI_GEMINI_API_KEY`, `SEOAUTOPRO_AI_GROQ_API_KEY`, `SEOAUTOPRO_AI_OPENROUTER_API_KEY`, `SEOAUTOPRO_AI_OPENAI_API_KEY`, `SEOAUTOPRO_AI_ANTHROPIC_API_KEY`, or `SEOAUTOPRO_AI_CUSTOM_API_KEY` constant in wp-config.php. Saved keys are never displayed or included in Settings Export. 115 116 = Can AI Helper suggest keywords and image alt text? = 117 118 Yes. Keyword proposals use the selected page content and are saved separately from meta descriptions after administrator approval. The block editor also provides a Generate keywords with AI action. Google Search does not use the meta-keywords tag for indexing or ranking, so treat these phrases as editorial planning aids: https://developers.google.com/search/docs/crawling-indexing/special-tags 119 120 The audit flags informative images with missing or empty alt text. AI Helper can propose contextual alt text for images stored directly in page or post content, using the page context, source filename, nearby text, and available attachment title or caption. Decorative images marked with `role="presentation"`, `role="none"`, or `aria-hidden="true"` are excluded. Every proposal must be reviewed; AI Helper does not infer details that are absent from the available context. 121 122 = Can the chatbot reveal drafts, private pages, or password-protected content? = 123 124 No. Chatbot retrieval is restricted to published, publicly viewable, non-password-protected posts and pages. Drafts, pending content, private content, password-protected content, and other non-public post statuses are excluded both when content is queried and again before a result is used. 125 126 Uploaded document text is unavailable to visitors until an administrator explicitly approves it for chatbot answers. Original uploaded documents are not retained in a public downloads directory, document download links are never shown, and chatbot conversations are not stored by the plugin. 127 128 = What does grounded AI do in the chatbot? = 129 130 When enabled, the chatbot sends the visitor's question and a small set of relevant, approved website excerpts to the provider configured in AI Helper. The model is instructed to answer only from those excerpts, treat source content as data rather than instructions, and identify the excerpts supporting its answer. Unsupported questions use the contact fallback. If AI is disabled or temporarily unavailable, the chatbot can still show its most relevant approved source excerpts. 131 101 132 == External Services == 102 133 … … 132 163 133 164 Data sent: your site's homepage URL. No personal data is transmitted. 165 166 = Ollama (optional) = 167 168 AI Helper can connect to an Ollama endpoint configured by a site administrator. By default, only loopback hosts on the WordPress server are allowed. Remote hosts must be explicitly trusted with the `seoautopro_ai_helper_allowed_ollama_hosts` filter. 169 170 * Service URL: configured by the site administrator; the default is http://127.0.0.1:11434/api/generate 171 * Ollama documentation: https://docs.ollama.com/ 172 173 Data sent: the selected page title, URL, excerpt, a limited plain-text content sample, current metadata, requested output language, and the generation instructions. For alt-text proposals, the image source URL, filename, nearby page text, and available attachment title or caption are also sent. When grounded chatbot AI is enabled, the visitor's question and up to five relevant excerpts from public pages or administrator-approved document text are sent. Draft, private, password-protected, and unapproved content is never included. The image file and original chatbot document files are not uploaded by SEO Auditor Tools. AI Helper never applies model output automatically. 174 175 = Cloud AI providers (optional) = 176 177 When selected and configured by an administrator, AI Helper can send generation requests to Google Gemini, GroqCloud, OpenRouter, OpenAI, or Anthropic Claude. These services are disabled until the administrator selects one and supplies any required API key. Provider accounts, quotas, free tiers, and charges are managed by the provider. 178 179 * Google Gemini API: https://generativelanguage.googleapis.com/ — Terms: https://ai.google.dev/gemini-api/terms — Privacy: https://policies.google.com/privacy 180 * GroqCloud API: https://api.groq.com/ — Terms: https://groq.com/terms-of-use/ — Privacy: https://groq.com/privacy-policy/ 181 * OpenRouter API: https://openrouter.ai/api/ — Terms: https://openrouter.ai/terms — Privacy: https://openrouter.ai/privacy 182 * OpenAI API: https://api.openai.com/ — Terms: https://openai.com/policies/terms-of-use/ — Privacy: https://openai.com/policies/privacy-policy/ 183 * Anthropic API: https://api.anthropic.com/ — Terms: https://www.anthropic.com/legal/commercial-terms — Privacy: https://www.anthropic.com/legal/privacy 184 185 Data sent: the selected page title, URL, excerpt, a limited plain-text content sample, current metadata, requested output language, and the generation instructions. For alt-text proposals, the image source URL, filename, nearby page text, and available attachment title or caption are also sent. When grounded chatbot AI is enabled, the visitor's question and up to five relevant excerpts from public pages or administrator-approved document text are sent. Draft, private, password-protected, and unapproved content is never included. The image file and original chatbot document files are not uploaded by SEO Auditor Tools. The configured API key is sent only to authenticate with the selected provider. AI Helper never applies model output automatically. 186 187 = Custom OpenAI-compatible endpoint (optional) = 188 189 An administrator can configure a third-party or self-hosted endpoint that implements the OpenAI chat-completions request and response format. Remote endpoints must use HTTPS; loopback endpoints may use HTTP. The endpoint operator's own terms and privacy policy apply. 190 191 Data sent: the selected page title, URL, excerpt, a limited plain-text content sample, current metadata, requested output language, generation instructions, and an API key when configured. For alt-text proposals, the image source URL, filename, nearby page text, and available attachment title or caption are also sent. When grounded chatbot AI is enabled, the visitor's question and up to five relevant excerpts from public pages or administrator-approved document text are sent. Draft, private, password-protected, and unapproved content is never included. The image file and original chatbot document files are not uploaded by SEO Auditor Tools. AI Helper never applies model output automatically. 134 192 135 193 == Screenshots == … … 144 202 == Changelog == 145 203 204 = 2.0.3 = 205 * Adds optional grounded AI chatbot answers using the provider configured in AI Helper, constrained to retrieved website excerpts with validated source citations and a safe unsupported-answer fallback. 206 * Restricts chatbot sources to published, publicly viewable, non-password-protected posts and pages; drafts, private pages, pending content, and password-protected content are excluded. 207 * Requires explicit administrator approval before extracted document text can appear in chatbot answers, removes document download links, and stops retaining original chatbot documents in public uploads. 208 * Adds anonymised per-visitor and site-wide chatbot request limits, a 300-character question limit, and no conversation-history storage. 209 * Adds a chatbot restart control and a Continue chat action so the optional contact form never traps visitors in a dead end. 210 * Expands AI Helper with Free/Paid selection and adapters for Ollama, Google Gemini, GroqCloud, OpenRouter, OpenAI, Anthropic Claude, and custom OpenAI-compatible endpoints. 211 * Adds provider-specific model choices, custom model IDs, API-key handling, a saved-connection test, and output-language selection with en-GB as the default. 212 * Keeps AI API keys write-only in the interface, supports wp-config.php constants, and excludes stored keys from JSON and CSV settings exports. 213 * Adds Generate with AI directly to the block editor meta-description panel; proposals use the current unsaved title, excerpt, and page content and remain unsaved until the editor approves the post update. 214 * Adds separate AI keyword proposals in AI Helper and a Generate keywords with AI action in the block editor, with unique-phrase validation and independent approval. 215 * Connects the missing-image-alt audit finding to AI Helper, where administrators can preview, edit, approve, verify, and undo contextual alt-text changes to a selected image in page or post content. 216 * Excludes explicitly decorative images from missing-alt findings and prevents stale image proposals from overwriting subsequently edited post content. 217 * Adds an audit finding for pages that rely on generated fallback meta descriptions. 218 * Generates structured meta-description proposals using limited page context and validates all output before it can be applied. 219 * Requires an administrator to preview and approve every AI-generated change; the model never writes directly to WordPress. 220 * Adds stale-preview conflict protection, durable fix history, per-change undo, cache invalidation, targeted verification, and queued re-auditing. 221 * Restricts Ollama to server-local endpoints by default, with an explicit filter for trusted remote hosts. 222 * Prevents browser password managers from placing WordPress login credentials in audit timeout and PageSpeed API-key fields. 223 146 224 = 2.0.2 = 147 225 * Adds a preview-first bulk SEO metadata importer for Yoast SEO, Rank Math SEO, AIOSEO, and SEOPress. … … 150 228 * Requires supported source SEO plugins to be deactivated before import and defers SEO Auditor meta output while one remains active. 151 229 * Includes rollback for the last completed import and preserves fields edited after import. 152 153 = 2.0.3 =154 230 * Sitemap.xml, robots.txt, and llms.txt are now written directly to the WordPress installation root instead of the uploads directory. 155 231 * The Sitemap, Robots.txt, and LLMS.txt tool pages now show the exact file location, public URL, and generation status. 156 232 * Cache prewarming and chatbot content lookup now read sitemap.xml and llms.txt from their new root locations. 157 233 * Successfully generating a root file removes its obsolete legacy copy from uploads/seo-auditor-tools. 158 159 = 2.0.2 =160 234 * Automatically purges all generated SEO Auditor page-cache files whenever the installed plugin version changes. 161 235 * Adds activation and WordPress updater safeguards so stale HTML and asset URLs cannot survive plugin updates. … … 192 266 == Upgrade Notice == 193 267 268 = 2.0.3 = 269 Configure a free-tier, paid, or self-hosted provider under SEO Auditor Tools → AI Helper to generate controlled, reversible meta-description, keyword, and image alt-text proposals. 270 194 271 = 2.0.2 = 195 Use the new importer in Open Graph & Meta Description to preview and migrate metadata after deactivating the source SEO plugin. 196 197 = 2.0.3 = 198 Regenerate sitemap.xml, robots.txt, and llms.txt once after updating so the files are created in the WordPress root and obsolete uploads copies are removed. 199 200 = 2.0.2 = 201 SEO Auditor now clears its complete page cache automatically after plugin updates, preventing stale pages and asset URLs from carrying over. 272 Includes metadata migration, root sitemap/robots/LLMS generation, and automatic cache purging after updates. 202 273 203 274 = 2.0.1 = -
seo-auditor-tools/trunk/seo-auditor-tools.php
r3645916 r3658740 3 3 Plugin Name: SEO Auditor Tools 4 4 Plugin URI: https://wordpress.org/plugins/seo-auditor-tools 5 Description: All-in-one free SEO and site management toolkit for WordPress. Audit, Open Graph, sitemap, robots.txt, LLMS.txt, cookie consent, analytics, UTM builder, QR codes, image tools, cache, minify, security, redirections, .htaccess editor, and chatbot. All features free — no Pro version.6 Version: 2.0. 25 Description: All-in-one free SEO and site management toolkit for WordPress. Audit, AI Helper, metadata, sitemap, robots.txt, LLMS.txt, performance, security, redirections, and more. All features free — no Pro version. 6 Version: 2.0.3 7 7 Author: fernandofas2909 8 8 Author URI: https://seoauditor.one … … 18 18 class SEOAUTOPRO_Plugin { 19 19 20 const VERSION = '2.0. 2';20 const VERSION = '2.0.3'; 21 21 const VERSION_OPTION = 'seoautopro_version'; 22 22 private static $instance = null; … … 54 54 require_once SEOAUTOPRO_PLUGIN_DIR . 'includes/class-seoautopro-cookie-banner.php'; 55 55 require_once SEOAUTOPRO_PLUGIN_DIR . 'includes/class-seoautopro-meta-importer.php'; 56 require_once SEOAUTOPRO_PLUGIN_DIR . 'includes/class-seoautopro-ai-providers.php'; 57 require_once SEOAUTOPRO_PLUGIN_DIR . 'includes/class-seoautopro-ai-helper.php'; 56 58 require_once SEOAUTOPRO_PLUGIN_DIR . 'includes/class-seoautopro-opengraph.php'; 57 59 require_once SEOAUTOPRO_PLUGIN_DIR . 'includes/class-seoautopro-tracking.php'; … … 97 99 add_action( 'admin_init', array( 'SEOAUTOPRO_Chatbot', 'register_settings' ) ); 98 100 add_action( 'admin_init', array( 'SEOAUTOPRO_Redirection', 'maybe_handle_form_submission' ) ); 101 add_action( 'admin_init', array( 'SEOAUTOPRO_AI_Helper', 'register_settings' ) ); 99 102 add_action( 'admin_notices', array( 'SEOAUTOPRO_Image_Tools', 'render_media_bulk_button' ) ); 100 103 add_action( 'init', array( 'SEOAUTOPRO_OpenGraph', 'register_post_meta' ), 99 ); … … 128 131 add_action( 'admin_post_seoautopro_chatbot_upload_doc', array( 'SEOAUTOPRO_Chatbot', 'handle_upload_doc' ) ); 129 132 add_action( 'admin_post_seoautopro_chatbot_delete_doc', array( 'SEOAUTOPRO_Chatbot', 'handle_delete_doc' ) ); 133 add_action( 'admin_post_seoautopro_chatbot_toggle_doc', array( 'SEOAUTOPRO_Chatbot', 'handle_toggle_doc' ) ); 134 add_action( 'admin_post_seoautopro_ai_helper_generate', array( 'SEOAUTOPRO_AI_Helper', 'handle_generate' ) ); 135 add_action( 'admin_post_seoautopro_ai_helper_test', array( 'SEOAUTOPRO_AI_Helper', 'handle_test_connection' ) ); 136 add_action( 'admin_post_seoautopro_ai_helper_apply', array( 'SEOAUTOPRO_AI_Helper', 'handle_apply' ) ); 137 add_action( 'admin_post_seoautopro_ai_helper_discard', array( 'SEOAUTOPRO_AI_Helper', 'handle_discard' ) ); 138 add_action( 'admin_post_seoautopro_ai_helper_undo', array( 'SEOAUTOPRO_AI_Helper', 'handle_undo' ) ); 130 139 131 140 // Frontend output … … 195 204 add_action( 'wp_ajax_seoautopro_meta_import_rollback_start', array( 'SEOAUTOPRO_Meta_Importer', 'ajax_rollback_start' ) ); 196 205 add_action( 'wp_ajax_seoautopro_meta_import_rollback_batch', array( 'SEOAUTOPRO_Meta_Importer', 'ajax_rollback_batch' ) ); 206 add_action( 'wp_ajax_seoautopro_ai_helper_editor_generate', array( 'SEOAUTOPRO_AI_Helper', 'ajax_generate_for_editor' ) ); 197 207 add_action( 'wp_ajax_seoautopro_generate_qr', array( 'SEOAUTOPRO_QR_Code', 'ajax_generate_qr' ) ); 198 208 add_action( 'admin_post_seoautopro_download_qr', array( 'SEOAUTOPRO_QR_Code', 'handle_download_qr' ) ); … … 210 220 add_action( 'seoautopro_cache_gc', array( 'SEOAUTOPRO_Cache_Tools', 'handle_gc' ) ); 211 221 add_action( 'seoautopro_minify_regenerate', array( 'SEOAUTOPRO_Minify_Tools', 'handle_regenerate' ), 10, 1 ); 222 add_action( 'seoautopro_ai_helper_reaudit', array( 'SEOAUTOPRO_AI_Helper', 'handle_scheduled_reaudit' ) ); 212 223 213 224 // Security … … 516 527 'seoautopro-chatbot', 517 528 array( 'SEOAUTOPRO_Chatbot', 'render_page' ) 529 ); 530 531 add_submenu_page( 532 'seoautopro-dashboard', 533 __( 'AI Helper', 'seo-auditor-tools' ), 534 __( 'AI Helper', 'seo-auditor-tools' ), 535 $cap, 536 'seoautopro-ai-helper', 537 array( 'SEOAUTOPRO_AI_Helper', 'render_page' ) 518 538 ); 519 539 … … 736 756 737 757 if ( $is_editor_page ) { 758 global $post; 759 $editor_post_id = $post instanceof WP_Post ? absint( $post->ID ) : 0; 760 738 761 wp_enqueue_script( 739 762 'seoautopro-editor', … … 747 770 'seoautopro-editor', 748 771 'SEOAUTOPRO_PAGE_META', 749 array( 'enabled' => true ) 772 array( 773 'enabled' => true, 774 'post_id' => $editor_post_id, 775 'ajax_url' => admin_url( 'admin-ajax.php' ), 776 'nonce' => wp_create_nonce( 'seoautopro-ai-editor' ), 777 'ai_url' => admin_url( 'admin.php?page=seoautopro-ai-helper' ), 778 'strings' => array( 779 'generate' => __( 'Generate with AI', 'seo-auditor-tools' ), 780 'generating' => __( 'Generating proposal…', 'seo-auditor-tools' ), 781 'generated' => __( 'Proposal inserted. Review it, then save or update the page.', 'seo-auditor-tools' ), 782 'generate_keywords' => __( 'Generate keywords with AI', 'seo-auditor-tools' ), 783 'keywords_generated' => __( 'Keyword proposal inserted. Review it, then save or update the page.', 'seo-auditor-tools' ), 784 'keywords_help' => __( 'Comma-separated editorial keywords. Google Search does not use the meta-keywords tag for ranking.', 'seo-auditor-tools' ), 785 'error' => __( 'AI Helper could not generate a proposal.', 'seo-auditor-tools' ), 786 'save_first' => __( 'Save the draft before generating a proposal.', 'seo-auditor-tools' ), 787 'settings_label' => __( 'AI Helper settings', 'seo-auditor-tools' ), 788 ), 789 ) 750 790 ); 751 791 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)