\n"
9 | "MIME-Version: 1.0\n"
10 | "Content-Type: text/plain; charset=UTF-8\n"
11 | "Content-Transfer-Encoding: 8bit\n"
12 | "POT-Creation-Date: 2025-05-23T22:19:33+00:00\n"
13 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
14 | "X-Generator: WP-CLI 2.12.0\n"
15 | "X-Domain: wpnlweb\n"
16 |
17 | #. Plugin Name of the plugin
18 | #: wpnlweb.php
19 | #: admin/class-wpnlweb-admin.php:80
20 | msgid "WPNLWeb"
21 | msgstr ""
22 |
23 | #. Plugin URI of the plugin
24 | #: wpnlweb.php
25 | msgid "https://wpnlweb.com"
26 | msgstr ""
27 |
28 | #. Description of the plugin
29 | #: wpnlweb.php
30 | msgid "Turn your WordPress site into a natural language interface for users, and AI agents using Microsoft's NLWeb Protocol."
31 | msgstr ""
32 |
33 | #. Author of the plugin
34 | #: wpnlweb.php
35 | msgid "wpnlweb"
36 | msgstr ""
37 |
38 | #. Author URI of the plugin
39 | #: wpnlweb.php
40 | msgid "https://wpnlweb.com/"
41 | msgstr ""
42 |
43 | #: admin/class-wpnlweb-admin.php:79
44 | msgid "WPNLWeb Settings"
45 | msgstr ""
46 |
47 | #: admin/class-wpnlweb-admin.php:349
48 | msgid "Customize the appearance of your WPNLWeb search forms and interface."
49 | msgstr ""
50 |
51 | #: admin/class-wpnlweb-admin.php:357
52 | #: admin/class-wpnlweb-admin.php:1108
53 | msgid "Settings"
54 | msgstr ""
55 |
56 | #: admin/class-wpnlweb-admin.php:358
57 | msgid "Configure your preferences"
58 | msgstr ""
59 |
60 | #: admin/class-wpnlweb-admin.php:364
61 | msgid "Theme"
62 | msgstr ""
63 |
64 | #: admin/class-wpnlweb-admin.php:368
65 | #: admin/class-wpnlweb-admin.php:456
66 | #: admin/class-wpnlweb-admin.php:464
67 | msgid "Custom CSS"
68 | msgstr ""
69 |
70 | #: admin/class-wpnlweb-admin.php:372
71 | #: admin/class-wpnlweb-admin.php:533
72 | #: admin/class-wpnlweb-admin.php:556
73 | msgid "Live Preview"
74 | msgstr ""
75 |
76 | #: admin/class-wpnlweb-admin.php:387
77 | msgid "Theme Customization"
78 | msgstr ""
79 |
80 | #: admin/class-wpnlweb-admin.php:389
81 | msgid "Customize the appearance of your WPNLWeb search forms."
82 | msgstr ""
83 |
84 | #: admin/class-wpnlweb-admin.php:394
85 | msgid "Theme Mode"
86 | msgstr ""
87 |
88 | #: admin/class-wpnlweb-admin.php:397
89 | msgid "Choose the theme mode for the search interface."
90 | msgstr ""
91 |
92 | #: admin/class-wpnlweb-admin.php:401
93 | msgid "Auto (Follow System)"
94 | msgstr ""
95 |
96 | #: admin/class-wpnlweb-admin.php:404
97 | msgid "Light Mode"
98 | msgstr ""
99 |
100 | #: admin/class-wpnlweb-admin.php:407
101 | msgid "Dark Mode"
102 | msgstr ""
103 |
104 | #: admin/class-wpnlweb-admin.php:414
105 | msgid "Primary Color"
106 | msgstr ""
107 |
108 | #: admin/class-wpnlweb-admin.php:417
109 | msgid "Choose the primary color for buttons and focus states."
110 | msgstr ""
111 |
112 | #: admin/class-wpnlweb-admin.php:436
113 | msgid "Preset Colors"
114 | msgstr ""
115 |
116 | #: admin/class-wpnlweb-admin.php:458
117 | msgid "Add custom CSS to further customize the appearance. This CSS will be applied to all WPNLWeb shortcodes."
118 | msgstr ""
119 |
120 | #: admin/class-wpnlweb-admin.php:468
121 | msgid "Copy Example"
122 | msgstr ""
123 |
124 | #: admin/class-wpnlweb-admin.php:471
125 | msgid "Reset"
126 | msgstr ""
127 |
128 | #: admin/class-wpnlweb-admin.php:480
129 | msgid "Add your custom CSS here..."
130 | msgstr ""
131 |
132 | #: admin/class-wpnlweb-admin.php:483
133 | msgid "Add custom CSS to override default styles. Example:"
134 | msgstr ""
135 |
136 | #: admin/class-wpnlweb-admin.php:489
137 | msgid "CSS Custom Properties Reference"
138 | msgstr ""
139 |
140 | #: admin/class-wpnlweb-admin.php:490
141 | msgid "You can use these CSS custom properties in your custom CSS to maintain consistency:"
142 | msgstr ""
143 |
144 | #: admin/class-wpnlweb-admin.php:495
145 | msgid "Main brand color"
146 | msgstr ""
147 |
148 | #: admin/class-wpnlweb-admin.php:497
149 | msgid "Hover state color"
150 | msgstr ""
151 |
152 | #: admin/class-wpnlweb-admin.php:501
153 | msgid "Main background color"
154 | msgstr ""
155 |
156 | #: admin/class-wpnlweb-admin.php:503
157 | msgid "Secondary background"
158 | msgstr ""
159 |
160 | #: admin/class-wpnlweb-admin.php:507
161 | msgid "Main text color"
162 | msgstr ""
163 |
164 | #: admin/class-wpnlweb-admin.php:509
165 | msgid "Secondary text color"
166 | msgstr ""
167 |
168 | #: admin/class-wpnlweb-admin.php:513
169 | msgid "Border radius"
170 | msgstr ""
171 |
172 | #: admin/class-wpnlweb-admin.php:515
173 | msgid "Small spacing (12px)"
174 | msgstr ""
175 |
176 | #: admin/class-wpnlweb-admin.php:519
177 | msgid "Medium spacing (20px)"
178 | msgstr ""
179 |
180 | #: admin/class-wpnlweb-admin.php:521
181 | msgid "Large spacing (30px)"
182 | msgstr ""
183 |
184 | #: admin/class-wpnlweb-admin.php:535
185 | msgid "Test your WPNLWeb search interface with live functionality. This preview uses the actual API endpoint."
186 | msgstr ""
187 |
188 | #: admin/class-wpnlweb-admin.php:540
189 | msgid "Shortcode Usage"
190 | msgstr ""
191 |
192 | #: admin/class-wpnlweb-admin.php:544
193 | msgid "Shortcode:"
194 | msgstr ""
195 |
196 | #: admin/class-wpnlweb-admin.php:549
197 | msgid "Live Functional Preview"
198 | msgstr ""
199 |
200 | #: admin/class-wpnlweb-admin.php:551
201 | msgid "This is a fully functional preview that connects to your site's content via the NLWeb API endpoint. Try searching for content on your site!"
202 | msgstr ""
203 |
204 | #: admin/class-wpnlweb-admin.php:558
205 | msgid "Refresh Preview"
206 | msgstr ""
207 |
208 | #: admin/class-wpnlweb-admin.php:564
209 | msgid "Loading preview..."
210 | msgstr ""
211 |
212 | #: admin/class-wpnlweb-admin.php:570
213 | msgid "Preview Information"
214 | msgstr ""
215 |
216 | #: admin/class-wpnlweb-admin.php:572
217 | msgid "This preview uses your current theme and color settings"
218 | msgstr ""
219 |
220 | #: admin/class-wpnlweb-admin.php:573
221 | msgid "Search results come from your actual site content"
222 | msgstr ""
223 |
224 | #: admin/class-wpnlweb-admin.php:574
225 | msgid "Changes to settings above will be reflected when you refresh the preview"
226 | msgstr ""
227 |
228 | #: admin/class-wpnlweb-admin.php:584
229 | msgid "Save Settings"
230 | msgstr ""
231 |
232 | #: admin/class-wpnlweb-admin.php:604
233 | #: public/class-wpnlweb-public.php:167
234 | msgid "Security check failed"
235 | msgstr ""
236 |
237 | #: public/class-wpnlweb-public.php:96
238 | msgid "Ask a question about this site..."
239 | msgstr ""
240 |
241 | #: public/class-wpnlweb-public.php:97
242 | msgid "Search"
243 | msgstr ""
244 |
245 | #: public/class-wpnlweb-public.php:128
246 | msgid "Searching..."
247 | msgstr ""
248 |
249 | #: public/class-wpnlweb-public.php:135
250 | msgid "Search Results"
251 | msgstr ""
252 |
253 | #: public/class-wpnlweb-public.php:175
254 | msgid "Please enter a question"
255 | msgstr ""
256 |
257 | #: public/class-wpnlweb-public.php:220
258 | msgid "Search functionality not available"
259 | msgstr ""
260 |
261 | #. translators: %s is the search query entered by the user
262 | #: public/class-wpnlweb-public.php:238
263 | #, php-format
264 | msgid "No results found for \"%s\""
265 | msgstr ""
266 |
--------------------------------------------------------------------------------
/admin/class-wpnlweb-admin-settings.php:
--------------------------------------------------------------------------------
1 |
29 | */
30 | class Wpnlweb_Admin_Settings {
31 |
32 |
33 | /**
34 | * The ID of this plugin.
35 | *
36 | * @since 1.0.0
37 | * @access private
38 | * @var string $plugin_name The ID of this plugin.
39 | */
40 | private $plugin_name;
41 |
42 | /**
43 | * The version of this plugin.
44 | *
45 | * @since 1.0.0
46 | * @access private
47 | * @var string $version The current version of this plugin.
48 | */
49 | private $version;
50 |
51 | /**
52 | * Initialize the class and set its properties.
53 | *
54 | * @since 1.0.0
55 | * @param string $plugin_name The name of this plugin.
56 | * @param string $version The version of this plugin.
57 | */
58 | public function __construct( $plugin_name, $version ) {
59 | $this->plugin_name = $plugin_name;
60 | $this->version = $version;
61 | }
62 |
63 | /**
64 | * Initialize WordPress settings registration.
65 | *
66 | * @since 1.0.0
67 | */
68 | public function init_settings() {
69 | // Register settings with proper sanitization callbacks.
70 | register_setting(
71 | 'wpnlweb_settings',
72 | 'wpnlweb_custom_css',
73 | array(
74 | 'sanitize_callback' => array( $this, 'sanitize_custom_css' ),
75 | 'default' => '',
76 | )
77 | );
78 |
79 | register_setting(
80 | 'wpnlweb_settings',
81 | 'wpnlweb_theme_mode',
82 | array(
83 | 'sanitize_callback' => array( $this, 'sanitize_theme_mode' ),
84 | 'default' => 'auto',
85 | )
86 | );
87 |
88 | register_setting(
89 | 'wpnlweb_settings',
90 | 'wpnlweb_primary_color',
91 | array(
92 | 'sanitize_callback' => array( $this, 'sanitize_primary_color' ),
93 | 'default' => '#3b82f6',
94 | )
95 | );
96 |
97 | // Add hooks to clear caches when settings are saved.
98 | add_action( 'update_option_wpnlweb_theme_mode', array( $this, 'clear_style_caches' ) );
99 | add_action( 'update_option_wpnlweb_primary_color', array( $this, 'clear_style_caches' ) );
100 | add_action( 'update_option_wpnlweb_custom_css', array( $this, 'clear_style_caches' ) );
101 | }
102 |
103 | /**
104 | * Sanitize custom CSS input.
105 | *
106 | * @since 1.0.0
107 | * @param string $input Raw CSS input.
108 | * @return string Sanitized CSS.
109 | */
110 | public function sanitize_custom_css( $input ) {
111 | if ( empty( $input ) ) {
112 | return '';
113 | }
114 |
115 | // Strip dangerous content.
116 | $input = wp_strip_all_tags( $input );
117 |
118 | // Remove potentially dangerous CSS patterns.
119 | $dangerous_patterns = array(
120 | '/javascript\s*:/i',
121 | '/vbscript\s*:/i',
122 | '/expression\s*\(/i',
123 | '/behavior\s*:/i',
124 | '/binding\s*:/i',
125 | '/@import/i',
126 | '/url\s*\(\s*["\']?\s*javascript/i',
127 | '/url\s*\(\s*["\']?\s*data:/i',
128 | );
129 |
130 | $input = preg_replace( $dangerous_patterns, '', $input );
131 |
132 | // Ensure we return clean CSS.
133 | return sanitize_textarea_field( $input );
134 | }
135 |
136 | /**
137 | * Sanitize theme mode setting.
138 | *
139 | * @since 1.0.0
140 | * @param string $input Theme mode input.
141 | * @return string Sanitized theme mode.
142 | */
143 | public function sanitize_theme_mode( $input ) {
144 | $valid_modes = array( 'auto', 'light', 'dark' );
145 |
146 | if ( in_array( $input, $valid_modes, true ) ) {
147 | return $input;
148 | }
149 |
150 | // Return default if invalid.
151 | return 'auto';
152 | }
153 |
154 | /**
155 | * Sanitize primary color setting.
156 | *
157 | * @since 1.0.0
158 | * @param string $input Color input.
159 | * @return string Sanitized hex color.
160 | */
161 | public function sanitize_primary_color( $input ) {
162 | // Remove any whitespace.
163 | $input = trim( $input );
164 |
165 | // Validate hex color format.
166 | if ( preg_match( '/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/', $input ) ) {
167 | return strtolower( $input );
168 | }
169 |
170 | // Try to add # if missing.
171 | if ( preg_match( '/^([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/', $input ) ) {
172 | return '#' . strtolower( $input );
173 | }
174 |
175 | // Return default color if invalid.
176 | return '#3b82f6';
177 | }
178 |
179 | /**
180 | * Clear style caches when settings are updated.
181 | *
182 | * @since 1.0.0
183 | */
184 | public function clear_style_caches() {
185 | // Clear any WordPress object cache.
186 | if ( function_exists( 'wp_cache_flush' ) ) {
187 | wp_cache_flush();
188 | }
189 |
190 | // Clear any transients we might use for compiled styles in the future.
191 | delete_transient( 'wpnlweb_compiled_styles' );
192 | delete_transient( 'wpnlweb_theme_css_cache' );
193 |
194 | // Allow other plugins/themes to clear their caches.
195 | do_action( 'wpnlweb_clear_style_caches' );
196 | }
197 |
198 | /**
199 | * Get default settings values.
200 | *
201 | * @since 1.0.0
202 | * @return array Default settings values.
203 | */
204 | public function get_default_settings() {
205 | return array(
206 | 'wpnlweb_theme_mode' => 'auto',
207 | 'wpnlweb_primary_color' => '#3b82f6',
208 | 'wpnlweb_custom_css' => '',
209 | );
210 | }
211 |
212 | /**
213 | * Get current settings values with defaults.
214 | *
215 | * @since 1.0.0
216 | * @return array Current settings values.
217 | */
218 | public function get_current_settings() {
219 | $defaults = $this->get_default_settings();
220 |
221 | return array(
222 | 'wpnlweb_theme_mode' => get_option( 'wpnlweb_theme_mode', $defaults['wpnlweb_theme_mode'] ),
223 | 'wpnlweb_primary_color' => get_option( 'wpnlweb_primary_color', $defaults['wpnlweb_primary_color'] ),
224 | 'wpnlweb_custom_css' => get_option( 'wpnlweb_custom_css', $defaults['wpnlweb_custom_css'] ),
225 | );
226 | }
227 |
228 | /**
229 | * Validate settings input array.
230 | *
231 | * @since 1.0.0
232 | * @param array $input Settings input array.
233 | * @return array Validated settings array.
234 | */
235 | public function validate_settings( $input ) {
236 | $validated = array();
237 |
238 | if ( isset( $input['wpnlweb_theme_mode'] ) ) {
239 | $validated['wpnlweb_theme_mode'] = $this->sanitize_theme_mode( $input['wpnlweb_theme_mode'] );
240 | }
241 |
242 | if ( isset( $input['wpnlweb_primary_color'] ) ) {
243 | $validated['wpnlweb_primary_color'] = $this->sanitize_primary_color( $input['wpnlweb_primary_color'] );
244 | }
245 |
246 | if ( isset( $input['wpnlweb_custom_css'] ) ) {
247 | $validated['wpnlweb_custom_css'] = $this->sanitize_custom_css( $input['wpnlweb_custom_css'] );
248 | }
249 |
250 | return $validated;
251 | }
252 |
253 | /**
254 | * Get preset color options.
255 | *
256 | * @since 1.0.0
257 | * @return array Preset colors array.
258 | */
259 | public function get_preset_colors() {
260 | return array(
261 | '#3b82f6', // Blue (default).
262 | '#ef4444', // Red.
263 | '#10b981', // Green.
264 | '#f59e0b', // Orange.
265 | '#8b5cf6', // Purple.
266 | '#06b6d4', // Teal.
267 | '#f97316', // Orange variant.
268 | '#84cc16', // Lime.
269 | );
270 | }
271 | }
272 |
--------------------------------------------------------------------------------
/public/js/wpnlweb-shortcode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WPNLWeb Shortcode JavaScript
3 | *
4 | * Handles AJAX search functionality for the [wpnlweb] shortcode
5 | *
6 | * @package Wpnlweb
7 | * @subpackage Wpnlweb/public/js
8 | * @since 1.0.0
9 | */
10 |
11 | (function ($) {
12 | "use strict";
13 |
14 | /**
15 | * Initialize shortcode functionality when document is ready
16 | */
17 | $(document).ready(function () {
18 | initializeWPNLWebShortcodes();
19 | });
20 |
21 | /**
22 | * Initialize all WPNLWeb shortcode forms on the page
23 | */
24 | function initializeWPNLWebShortcodes() {
25 | // Check if we have any shortcode data
26 | if (typeof window.wpnlweb_data === "undefined") {
27 | return;
28 | }
29 |
30 | // Initialize each shortcode form
31 | for (let formId in window.wpnlweb_data) {
32 | if (window.wpnlweb_data.hasOwnProperty(formId)) {
33 | initializeShortcodeForm(formId, window.wpnlweb_data[formId]);
34 | }
35 | }
36 | }
37 |
38 | /**
39 | * Initialize individual shortcode form
40 | */
41 | function initializeShortcodeForm(formId, config) {
42 | const form = $("#" + formId);
43 | const resultsContainer = $("#" + config.results_id);
44 | const loadingIndicator = form.find(".wpnlweb-loading");
45 | const searchButton = form.find(".wpnlweb-search-button");
46 | const searchInput = form.find(".wpnlweb-search-input");
47 |
48 | if (form.length === 0) {
49 | console.warn("WPNLWeb: Form not found for ID:", formId);
50 | return;
51 | }
52 |
53 | // Handle form submission
54 | form.on("submit", function (e) {
55 | e.preventDefault();
56 |
57 | const question = searchInput.val().trim();
58 |
59 | if (question === "") {
60 | displayError(resultsContainer, "Please enter a question");
61 | return;
62 | }
63 |
64 | performSearch(
65 | question,
66 | config,
67 | form,
68 | resultsContainer,
69 | loadingIndicator,
70 | searchButton
71 | );
72 | });
73 |
74 | // Handle Enter key in search input
75 | searchInput.on("keypress", function (e) {
76 | if (e.which === 13) {
77 | form.submit();
78 | }
79 | });
80 |
81 | // Auto-focus on search input when shortcode is in view
82 | if (isElementInViewport(form[0])) {
83 | searchInput.focus();
84 | }
85 | }
86 |
87 | /**
88 | * Perform AJAX search request
89 | */
90 | function performSearch(
91 | question,
92 | config,
93 | form,
94 | resultsContainer,
95 | loadingIndicator,
96 | searchButton
97 | ) {
98 | // Show loading state
99 | showLoadingState(loadingIndicator, searchButton, true);
100 |
101 | // Hide previous results
102 | if (config.show_results) {
103 | resultsContainer.hide();
104 | }
105 |
106 | // Prepare AJAX data
107 | const ajaxData = {
108 | action: "wpnlweb_search",
109 | question: question,
110 | max_results: config.max_results,
111 | wpnlweb_nonce: config.nonce,
112 | };
113 |
114 | // Perform AJAX request
115 | $.ajax({
116 | url: config.ajax_url,
117 | type: "POST",
118 | data: ajaxData,
119 | timeout: 30000, // 30 second timeout
120 | success: function (response) {
121 | handleSearchSuccess(response, config, resultsContainer);
122 | },
123 | error: function (xhr, status, error) {
124 | handleSearchError(xhr, status, error, resultsContainer);
125 | },
126 | complete: function () {
127 | showLoadingState(loadingIndicator, searchButton, false);
128 | },
129 | });
130 | }
131 |
132 | /**
133 | * Handle successful search response
134 | */
135 | function handleSearchSuccess(response, config, resultsContainer) {
136 | if (!config.show_results) {
137 | return;
138 | }
139 |
140 | if (response.success && response.data) {
141 | const resultsContent = resultsContainer.find(".wpnlweb-results-content");
142 | resultsContent.html(response.data.html);
143 |
144 | // Update results title with count
145 | const resultsTitle = resultsContainer.find(".wpnlweb-results-title");
146 | const count = response.data.count || 0;
147 | resultsTitle.text("Search Results (" + count + " found)");
148 |
149 | // Show results container
150 | resultsContainer.slideDown();
151 |
152 | // Scroll to results if needed
153 | scrollToResults(resultsContainer);
154 | } else {
155 | displayError(resultsContainer, response.data?.message || "Search failed");
156 | }
157 | }
158 |
159 | /**
160 | * Handle search error response
161 | */
162 | function handleSearchError(xhr, status, error, resultsContainer) {
163 | let errorMessage = "Search request failed";
164 |
165 | if (status === "timeout") {
166 | errorMessage = "Search request timed out. Please try again.";
167 | } else if (
168 | xhr.responseJSON &&
169 | xhr.responseJSON.data &&
170 | xhr.responseJSON.data.message
171 | ) {
172 | errorMessage = xhr.responseJSON.data.message;
173 | } else if (status === "error" && error) {
174 | errorMessage = "Network error: " + error;
175 | }
176 |
177 | displayError(resultsContainer, errorMessage);
178 | console.error("WPNLWeb search error:", { xhr, status, error });
179 | }
180 |
181 | /**
182 | * Display error message
183 | */
184 | function displayError(resultsContainer, message) {
185 | if (resultsContainer.length === 0) {
186 | alert("Error: " + message);
187 | return;
188 | }
189 |
190 | const errorHtml =
191 | '' + escapeHtml(message) + "
";
192 | const resultsContent = resultsContainer.find(".wpnlweb-results-content");
193 |
194 | resultsContent.html(errorHtml);
195 | resultsContainer.slideDown();
196 |
197 | scrollToResults(resultsContainer);
198 | }
199 |
200 | /**
201 | * Show/hide loading state
202 | */
203 | function showLoadingState(loadingIndicator, searchButton, isLoading) {
204 | if (isLoading) {
205 | loadingIndicator.show();
206 | searchButton.prop("disabled", true).text("Searching...");
207 | } else {
208 | loadingIndicator.hide();
209 | searchButton
210 | .prop("disabled", false)
211 | .text(searchButton.data("original-text") || "Search");
212 | }
213 | }
214 |
215 | /**
216 | * Scroll to results if they're not in viewport
217 | */
218 | function scrollToResults(resultsContainer) {
219 | if (!isElementInViewport(resultsContainer[0])) {
220 | $("html, body").animate(
221 | {
222 | scrollTop: resultsContainer.offset().top - 20,
223 | },
224 | 500
225 | );
226 | }
227 | }
228 |
229 | /**
230 | * Check if element is in viewport
231 | */
232 | function isElementInViewport(element) {
233 | if (!element) return false;
234 |
235 | const rect = element.getBoundingClientRect();
236 | return (
237 | rect.top >= 0 &&
238 | rect.left >= 0 &&
239 | rect.bottom <=
240 | (window.innerHeight || document.documentElement.clientHeight) &&
241 | rect.right <= (window.innerWidth || document.documentElement.clientWidth)
242 | );
243 | }
244 |
245 | /**
246 | * Escape HTML to prevent XSS
247 | */
248 | function escapeHtml(text) {
249 | const map = {
250 | "&": "&",
251 | "<": "<",
252 | ">": ">",
253 | '"': """,
254 | "'": "'",
255 | };
256 | return text.replace(/[&<>"']/g, function (m) {
257 | return map[m];
258 | });
259 | }
260 |
261 | /**
262 | * Store original button text for loading state
263 | */
264 | $(document).on("DOMContentLoaded", function () {
265 | $(".wpnlweb-search-button").each(function () {
266 | $(this).data("original-text", $(this).text());
267 | });
268 | });
269 |
270 | /**
271 | * Handle dynamic shortcode loading (for AJAX-loaded content)
272 | */
273 | window.wpnlweb_init_shortcode = function (containerId) {
274 | const container = $("#" + containerId);
275 | if (container.length > 0) {
276 | initializeWPNLWebShortcodes();
277 | }
278 | };
279 | })(jQuery);
280 |
--------------------------------------------------------------------------------
/TESTING_GUIDE.md:
--------------------------------------------------------------------------------
1 | # 🧪 **WPNLWeb Phase 2: Comprehensive Testing Guide**
2 |
3 | ## 📋 **Testing Overview**
4 |
5 | This guide provides step-by-step instructions for thoroughly testing WPNLWeb compatibility with WordPress core, themes, plugins, and browsers to ensure WordPress.org submission readiness.
6 |
7 | ## 🎯 **Testing Goals**
8 |
9 | - ✅ **WordPress 6.6 Compatibility**: Verify full functionality with latest WordPress
10 | - ✅ **Theme Compatibility**: Ensure shortcode works across popular themes
11 | - ✅ **Plugin Compatibility**: Test with most popular WordPress plugins
12 | - ✅ **Browser Compatibility**: Cross-browser functionality validation
13 | - ✅ **Performance Validation**: Meet WordPress.org performance standards
14 | - ✅ **Security Compliance**: No vulnerabilities or PHP errors
15 |
16 | ---
17 |
18 | ## 🚀 **Phase 2A: Core Functionality Testing**
19 |
20 | ### **Test Environment Setup**
21 |
22 | ```bash
23 | # Test with WordPress 6.6 "Dorsey"
24 | # PHP 7.4+
25 | # MySQL 5.6+
26 | # Apache/Nginx with mod_rewrite
27 | ```
28 |
29 | ### **1. REST API Endpoint Testing**
30 |
31 | ```bash
32 | # Test the main endpoint
33 | curl -X POST https://your-site.com/wp-json/nlweb/v1/ask \
34 | -H "Content-Type: application/json" \
35 | -d '{"question": "What is this website about?"}'
36 |
37 | # Expected Response:
38 | # {
39 | # "@context": "https://schema.org",
40 | # "@type": "SearchResultsPage",
41 | # "query": "What is this website about?",
42 | # "totalResults": 3,
43 | # "items": [...]
44 | # }
45 | ```
46 |
47 | ### **2. Admin AJAX Testing**
48 |
49 | - Navigate to Settings > WPNLWeb
50 | - Click "Test Interface" tab
51 | - Enter test query: "student debt"
52 | - Verify JSON response appears
53 | - Check browser console for JavaScript errors
54 |
55 | ### **3. Frontend Shortcode Testing**
56 |
57 | ```php
58 | // Add to test page content:
59 | [wpnlweb]
60 |
61 | // With custom attributes:
62 | [wpnlweb placeholder="Search our knowledge..." button_text="Find Answers" max_results="5"]
63 | ```
64 |
65 | ### **4. Schema.org Validation**
66 |
67 | - Use https://validator.schema.org/
68 | - Paste API response JSON
69 | - Verify SearchResultsPage structure validates
70 |
71 | ---
72 |
73 | ## 🎨 **Phase 2B: Theme Compatibility Testing**
74 |
75 | ### **Twenty Twenty-Four (WordPress 6.6 Default)**
76 |
77 | 1. Activate Twenty Twenty-Four theme
78 | 2. Create new page with `[wpnlweb]` shortcode
79 | 3. Test search functionality
80 | 4. Verify styling integrates properly
81 | 5. Check mobile responsiveness
82 |
83 | ### **Twenty Twenty-Three (Previous Default)**
84 |
85 | 1. Activate Twenty Twenty-Three
86 | 2. Add shortcode to various page templates
87 | 3. Test search results display
88 | 4. Verify CSS doesn't conflict
89 |
90 | ### **Astra (Popular Multipurpose)**
91 |
92 | 1. Install Astra theme
93 | 2. Test with various Astra starter templates
94 | 3. Verify shortcode works in sidebars/widgets
95 | 4. Check customizer color compatibility
96 |
97 | ### **GeneratePress (Lightweight Performance)**
98 |
99 | 1. Install GeneratePress
100 | 2. Test page loading speeds with shortcode
101 | 3. Verify minimal CSS footprint
102 | 4. Check Elements/Hooks compatibility
103 |
104 | ### **Storefront (WooCommerce)**
105 |
106 | 1. Install Storefront + WooCommerce
107 | 2. Add shortcode to shop pages
108 | 3. Test product search functionality
109 | 4. Verify WooCommerce integration
110 |
111 | ---
112 |
113 | ## 🔌 **Phase 2C: Plugin Compatibility Testing**
114 |
115 | ### **WooCommerce (5M+ Active)**
116 |
117 | ```php
118 | // Test Context:
119 | // - Product search via natural language
120 | // - Integration with product data
121 | // - No conflicts with shop functionality
122 | ```
123 |
124 | ### **Contact Form 7 (5M+ Active)**
125 |
126 | ```php
127 | // Test Context:
128 | // - Shortcode alongside CF7 forms
129 | // - No JavaScript conflicts
130 | // - Form submission still works
131 | ```
132 |
133 | ### **Yoast SEO (5M+ Active)**
134 |
135 | ```php
136 | // Test Context:
137 | // - SEO analysis includes shortcode content
138 | // - No meta description conflicts
139 | // - Schema.org markup doesn't interfere
140 | ```
141 |
142 | ### **Wordfence Security (4M+ Active)**
143 |
144 | ```php
145 | // Test Context:
146 | // - Security scans don't flag plugin
147 | // - Firewall doesn't block API endpoint
148 | // - Login protection doesn't interfere
149 | ```
150 |
151 | ### **Jetpack (5M+ Active)**
152 |
153 | ```php
154 | // Test Context:
155 | // - Site acceleration doesn't break AJAX
156 | // - CDN serves CSS/JS properly
157 | // - Search features don't conflict
158 | ```
159 |
160 | ---
161 |
162 | ## 🌐 **Phase 2D: Browser Compatibility Testing**
163 |
164 | ### **Desktop Browsers**
165 |
166 | | Browser | Version | Shortcode | AJAX | API | Notes |
167 | | ------- | ------- | --------- | ---- | --- | ----- |
168 | | Chrome | Latest | [ ] | [ ] | [ ] | |
169 | | Firefox | Latest | [ ] | [ ] | [ ] | |
170 | | Safari | Latest | [ ] | [ ] | [ ] | |
171 | | Edge | Latest | [ ] | [ ] | [ ] | |
172 |
173 | ### **Mobile Testing**
174 |
175 | | Device | Browser | Shortcode | Touch | Responsive | Notes |
176 | | ------- | ------- | --------- | ----- | ---------- | ----- |
177 | | iPhone | Safari | [ ] | [ ] | [ ] | |
178 | | Android | Chrome | [ ] | [ ] | [ ] | |
179 |
180 | ---
181 |
182 | ## ⚡ **Phase 2E: Performance Validation**
183 |
184 | ### **Response Time Testing**
185 |
186 | ```bash
187 | # API Endpoint Performance
188 | time curl -X POST https://your-site.com/wp-json/nlweb/v1/ask \
189 | -H "Content-Type: application/json" \
190 | -d '{"question": "test"}'
191 |
192 | # Target: <500ms response time
193 | ```
194 |
195 | ### **Memory Usage Profiling**
196 |
197 | ```php
198 | // Add to wp-config.php for testing:
199 | define('WP_DEBUG', true);
200 | define('WP_DEBUG_LOG', true);
201 | define('WP_MEMORY_LIMIT', '256M');
202 |
203 | // Monitor memory usage in debug.log
204 | // Target: <64MB additional overhead
205 | ```
206 |
207 | ### **Caching Plugin Testing**
208 |
209 | - **WP Rocket**: Verify AJAX calls bypass cache
210 | - **W3 Total Cache**: Test with minification enabled
211 | - **LiteSpeed Cache**: Check ESI compatibility
212 | - **Cloudflare**: Verify API endpoint caching rules
213 |
214 | ---
215 |
216 | ## 🔒 **Phase 2F: Security Validation**
217 |
218 | ### **WordPress Debug Mode**
219 |
220 | ```php
221 | // wp-config.php testing configuration:
222 | define('WP_DEBUG', true);
223 | define('WP_DEBUG_LOG', true);
224 | define('WP_DEBUG_DISPLAY', false);
225 | define('SCRIPT_DEBUG', true);
226 |
227 | // Check debug.log for any errors/warnings
228 | ```
229 |
230 | ### **Security Scanner Testing**
231 |
232 | - **Wordfence Scan**: Full malware/vulnerability scan
233 | - **Sucuri SiteCheck**: External security validation
234 | - **Plugin Security Checker**: WordPress.org automated scans
235 |
236 | ### **Input Sanitization Testing**
237 |
238 | ```php
239 | // Test malicious inputs:
240 | $test_inputs = [
241 | '',
242 | 'SELECT * FROM wp_posts',
243 | '../../../etc/passwd',
244 | ''
245 | ];
246 |
247 | // Verify all inputs are properly sanitized
248 | ```
249 |
250 | ---
251 |
252 | ## 📊 **Phase 2G: Testing Results Documentation**
253 |
254 | ### **Test Results Template**
255 |
256 | ```markdown
257 | ## Test: [Theme/Plugin/Browser Name]
258 |
259 | - **Date**: [YYYY-MM-DD]
260 | - **Environment**: WordPress 6.6, PHP 8.1
261 | - **Status**: ✅ PASS / ❌ FAIL / ⚠️ ISSUE
262 | - **Shortcode Display**: [Pass/Fail]
263 | - **AJAX Functionality**: [Pass/Fail]
264 | - **API Response**: [Pass/Fail]
265 | - **Performance**: [Response time in ms]
266 | - **Issues Found**: [None/List issues]
267 | - **Notes**: [Additional observations]
268 | ```
269 |
270 | ---
271 |
272 | ## 🎯 **Success Criteria**
273 |
274 | ### **WordPress.org Submission Ready When:**
275 |
276 | - ✅ All tests pass on WordPress 6.6
277 | - ✅ Compatible with top 5 themes tested
278 | - ✅ No conflicts with top 5 plugins tested
279 | - ✅ Cross-browser compatibility confirmed
280 | - ✅ Response times <500ms consistently
281 | - ✅ No PHP errors/warnings in debug mode
282 | - ✅ Security scans pass with no issues
283 | - ✅ Memory usage <64MB overhead
284 | - ✅ All user-facing strings internationalized
285 |
286 | ---
287 |
288 | ## 🚨 **Issue Resolution Workflow**
289 |
290 | ### **When Issues Are Found:**
291 |
292 | 1. **Document**: Record exact steps to reproduce
293 | 2. **Categorize**: Critical/High/Medium/Low priority
294 | 3. **Investigate**: Identify root cause
295 | 4. **Fix**: Implement solution
296 | 5. **Retest**: Verify fix works
297 | 6. **Regression Test**: Ensure no new issues
298 |
299 | ### **Critical Issues (Block Release):**
300 |
301 | - Plugin crashes/fatal errors
302 | - Security vulnerabilities
303 | - Data loss/corruption
304 | - Complete functionality failure
305 |
306 | ### **High Priority Issues (Should Fix):**
307 |
308 | - Performance degradation >1000ms
309 | - JavaScript errors in console
310 | - Styling conflicts with popular themes
311 | - API endpoint returning errors
312 |
313 | ---
314 |
315 | ## 📋 **Phase 2 Completion Checklist**
316 |
317 | - [ ] Core functionality tests completed
318 | - [ ] Theme compatibility validated
319 | - [ ] Plugin compatibility confirmed
320 | - [ ] Browser testing finished
321 | - [ ] Performance benchmarks met
322 | - [ ] Security validation passed
323 | - [ ] All issues documented and resolved
324 | - [ ] Test results compiled
325 | - [ ] WordPress.org submission criteria met
326 |
327 | **Phase 2 Complete Date**: \***\*\_\_\_\*\***
328 | **Signed Off By**: \***\*\_\_\_\*\***
329 |
--------------------------------------------------------------------------------
/includes/class-wpnlweb-server.php:
--------------------------------------------------------------------------------
1 |
27 | */
28 | class Wpnlweb_Server {
29 |
30 |
31 | /**
32 | * The ID of this plugin.
33 | *
34 | * @since 1.0.0
35 | * @access private
36 | * @var string $plugin_name The ID of this plugin.
37 | */
38 | private $plugin_name;
39 |
40 | /**
41 | * The version of this plugin.
42 | *
43 | * @since 1.0.0
44 | * @access private
45 | * @var string $version The current version of this plugin.
46 | */
47 | private $version;
48 |
49 | /**
50 | * Initialize the class and set its properties.
51 | *
52 | * @since 1.0.0
53 | * @param string $plugin_name The name of this plugin.
54 | * @param string $version The version of this plugin.
55 | */
56 | public function __construct( $plugin_name, $version ) {
57 | $this->plugin_name = $plugin_name;
58 | $this->version = $version;
59 |
60 | // Register REST API endpoints.
61 | add_action( 'rest_api_init', array( $this, 'register_routes' ) );
62 | add_action( 'rest_api_init', array( $this, 'add_cors_support' ) );
63 |
64 | // Register MCP AJAX handlers.
65 | add_action( 'wp_ajax_nopriv_mcp_ask', array( $this, 'handle_mcp_ask' ) );
66 | add_action( 'wp_ajax_mcp_ask', array( $this, 'handle_mcp_ask' ) );
67 | }
68 |
69 | /**
70 | * Register the /ask endpoint
71 | *
72 | * @since 1.0.0
73 | */
74 | public function register_routes() {
75 | register_rest_route(
76 | 'nlweb/v1',
77 | '/ask',
78 | array(
79 | 'methods' => 'POST',
80 | 'callback' => array( $this, 'handle_ask' ),
81 | 'permission_callback' => '__return_true', // Adjust as needed.
82 | )
83 | );
84 | }
85 |
86 | /**
87 | * Add CORS support for AI agents
88 | *
89 | * @since 1.0.0
90 | */
91 | public function add_cors_support() {
92 | remove_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );
93 | add_filter(
94 | 'rest_pre_serve_request',
95 | function ( $value ) {
96 | // More comprehensive CORS headers for AI agent compatibility.
97 | header( 'Access-Control-Allow-Origin: *' );
98 | header( 'Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE' );
99 | header( 'Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, Accept, Origin' );
100 | header( 'Access-Control-Allow-Credentials: false' );
101 | header( 'Access-Control-Max-Age: 86400' ); // 24 hours.
102 | header( 'Vary: Origin' );
103 |
104 | // Handle OPTIONS preflight request.
105 | if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'OPTIONS' === $_SERVER['REQUEST_METHOD'] ) {
106 | http_response_code( 200 );
107 | exit();
108 | }
109 |
110 | return $value;
111 | }
112 | );
113 |
114 | // Also add CORS headers specifically to our endpoint.
115 | add_action(
116 | 'rest_api_init',
117 | function () {
118 | add_filter(
119 | 'rest_post_dispatch',
120 | function ( $result, $server, $request ) {
121 | // Check if this is our endpoint.
122 | if ( strpos( $request->get_route(), '/nlweb/v1/ask' ) !== false ) {
123 | header( 'Access-Control-Allow-Origin: *' );
124 | header( 'Access-Control-Allow-Methods: POST, GET, OPTIONS' );
125 | header( 'Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With' );
126 | }
127 | return $result;
128 | },
129 | 10,
130 | 3
131 | );
132 | }
133 | );
134 | }
135 |
136 | /**
137 | * Main NLWeb /ask endpoint handler
138 | *
139 | * @since 1.0.0
140 | * @param WP_REST_Request $request WordPress REST request object.
141 | * @return array|WP_Error Schema.org formatted response or error
142 | */
143 | public function handle_ask( $request ) {
144 | // Add CORS headers for AI agents/browsers.
145 | header( 'Access-Control-Allow-Origin: *' );
146 | header( 'Access-Control-Allow-Methods: POST, GET, OPTIONS' );
147 | header( 'Access-Control-Allow-Headers: Content-Type, Authorization' );
148 |
149 | // Handle preflight OPTIONS request.
150 | if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'OPTIONS' === $_SERVER['REQUEST_METHOD'] ) {
151 | exit( 0 );
152 | }
153 |
154 | $question = $request->get_param( 'question' );
155 | $context = $request->get_param( 'context' ) ?? array();
156 |
157 | if ( empty( $question ) ) {
158 | return new WP_Error( 'missing_question', 'Question parameter required', array( 'status' => 400 ) );
159 | }
160 |
161 | // Process the natural language query.
162 | $results = $this->process_query( $question, $context );
163 |
164 | // Format response according to NLWeb spec.
165 | return $this->format_nlweb_response( $results, $question );
166 | }
167 |
168 | /**
169 | * Process natural language query into WordPress results
170 | *
171 | * @since 1.0.0
172 | * @param string $question Natural language question.
173 | * @param array $context Additional query context.
174 | * @return array Array of matching post objects
175 | */
176 | public function process_query( $question, $context = array() ) {
177 | // Simple keyword extraction (you'd enhance this with LLM/vector search).
178 | $keywords = $this->extract_keywords( $question );
179 |
180 | // Build WordPress query.
181 | $query_args = array(
182 | 'post_status' => 'publish',
183 | 'posts_per_page' => isset( $context['limit'] ) ? intval( $context['limit'] ) : 10,
184 | 's' => implode( ' ', $keywords ),
185 | );
186 |
187 | // Enhance with context if provided.
188 | if ( ! empty( $context['post_type'] ) ) {
189 | $query_args['post_type'] = sanitize_text_field( $context['post_type'] );
190 | } else {
191 | // Search all public post types by default.
192 | $query_args['post_type'] = array( 'post', 'page' );
193 | }
194 |
195 | if ( ! empty( $context['category'] ) ) {
196 | $query_args['category_name'] = sanitize_text_field( $context['category'] );
197 | }
198 |
199 | // First attempt: Full keyword search.
200 | $query = new WP_Query( $query_args );
201 | $posts = $query->posts;
202 |
203 | // If no results found, try fallback searches.
204 | if ( empty( $posts ) && ! empty( $keywords ) ) {
205 | // Fallback 1: Try with individual keywords.
206 | foreach ( $keywords as $keyword ) {
207 | $fallback_args = $query_args;
208 | $fallback_args['s'] = $keyword;
209 | $fallback_query = new WP_Query( $fallback_args );
210 | if ( $fallback_query->have_posts() ) {
211 | $posts = $fallback_query->posts;
212 | break;
213 | }
214 | }
215 | }
216 |
217 | // If still no results, try a more general search.
218 | if ( empty( $posts ) ) {
219 | // Fallback 2: Get latest posts if no search matches.
220 | $latest_args = array(
221 | 'post_status' => 'publish',
222 | 'posts_per_page' => min( 5, isset( $context['limit'] ) ? intval( $context['limit'] ) : 5 ),
223 | 'post_type' => array( 'post', 'page' ),
224 | 'orderby' => 'date',
225 | 'order' => 'DESC',
226 | );
227 | $latest_query = new WP_Query( $latest_args );
228 | $posts = $latest_query->posts;
229 | }
230 |
231 | return $posts;
232 | }
233 |
234 | /**
235 | * Format response according to NLWeb/Schema.org standards
236 | *
237 | * @since 1.0.0
238 | * @param array $posts Array of post objects.
239 | * @param string $question Original question.
240 | * @return array Schema.org formatted response
241 | */
242 | private function format_nlweb_response( $posts, $question ) {
243 | $items = array();
244 |
245 | foreach ( $posts as $post ) {
246 | $items[] = array(
247 | '@type' => 'Article', // Could be dynamic based on post type.
248 | '@id' => get_permalink( $post->ID ),
249 | 'name' => $post->post_title,
250 | 'description' => wp_trim_words( $post->post_content, 30 ),
251 | 'url' => get_permalink( $post->ID ),
252 | 'datePublished' => $post->post_date,
253 | 'author' => array(
254 | '@type' => 'Person',
255 | 'name' => get_the_author_meta( 'display_name', $post->post_author ),
256 | ),
257 | );
258 | }
259 |
260 | return array(
261 | '@context' => 'https://schema.org',
262 | '@type' => 'SearchResultsPage',
263 | 'query' => $question,
264 | 'totalResults' => count( $items ),
265 | 'items' => $items,
266 | );
267 | }
268 |
269 | /**
270 | * Simple keyword extraction (enhance with NLP)
271 | *
272 | * @since 1.0.0
273 | * @param string $question Natural language question.
274 | * @return array Array of extracted keywords
275 | */
276 | private function extract_keywords( $question ) {
277 | // Remove common words, extract meaningful terms.
278 | $stop_words = array( 'what', 'where', 'when', 'how', 'is', 'are', 'the', 'a', 'an', 'and', 'or', 'but' );
279 | $words = explode( ' ', strtolower( sanitize_text_field( $question ) ) );
280 |
281 | return array_filter(
282 | $words,
283 | function ( $word ) use ( $stop_words ) {
284 | return ! in_array( $word, $stop_words, true ) && strlen( $word ) > 2;
285 | }
286 | );
287 | }
288 |
289 | /**
290 | * Handle MCP (Model Context Protocol) AJAX requests
291 | *
292 | * @since 1.0.0
293 | */
294 | public function handle_mcp_ask() {
295 | $input = json_decode( file_get_contents( 'php://input' ), true );
296 |
297 | if ( isset( $input['method'] ) && 'ask' === $input['method'] ) {
298 | $question = sanitize_text_field( $input['params']['question'] );
299 |
300 | // Create a proper WP_REST_Request object.
301 | $request = new WP_REST_Request( 'POST' );
302 | $request->set_param( 'question', $question );
303 |
304 | $response = $this->handle_ask( $request );
305 |
306 | wp_send_json(
307 | array(
308 | 'jsonrpc' => '2.0',
309 | 'id' => $input['id'],
310 | 'result' => $response,
311 | )
312 | );
313 | }
314 |
315 | wp_die();
316 | }
317 | }
318 |
--------------------------------------------------------------------------------
/README.txt:
--------------------------------------------------------------------------------
1 | === WPNLWeb ===
2 | Contributors: wpnlweb
3 | Donate link: https://wpnlweb.com/donate
4 | Tags: ai, nlweb, artificial intelligence, natural language, nlp
5 | Requires at least: 5.0
6 | Tested up to: 6.8
7 | Requires PHP: 7.4
8 | Stable tag: 1.0.2
9 | License: GPLv2 or later
10 | License URI: http://www.gnu.org/licenses/gpl-2.0.html
11 |
12 | AI-power your WordPress site. Natural language search meets the agentic web.
13 |
14 | == Description ==
15 |
16 | **WPNLWeb** turns your WordPress website into a conversational interface for both users and AI agents. It implements Microsoft's NLWeb protocol, making your site's content accessible via natural language queries through both REST API endpoints and an easy-to-use frontend shortcode.
17 |
18 | = Key Features =
19 |
20 | * **NLWeb Protocol Implementation** - Standards-compliant REST API endpoint `/wp-json/nlweb/v1/ask`
21 | * **Frontend Search Shortcode** - Add `[wpnlweb]` to any page for visitor search functionality
22 | * **Schema.org Compliant Responses** - Structured data that AI agents understand
23 | * **MCP Server Compatibility** - Works with Model Context Protocol systems
24 | * **WordPress Integration** - Native support for all post types, taxonomies, and custom fields
25 | * **AI Agent Ready** - CORS headers and proper formatting for ChatGPT, Claude, and other AI systems
26 | * **Admin Dashboard** - Settings, analytics, and test interface
27 | * **Performance Optimized** - <500ms response times with caching support
28 |
29 | = How It Works =
30 |
31 | 1. **For AI Agents**: Your site becomes queryable via natural language through the REST API endpoint
32 | 2. **For Website Visitors**: Add the `[wpnlweb]` shortcode to any page for an interactive search experience
33 | 3. **For Developers**: Extend and customize the search functionality with WordPress hooks and filters
34 |
35 | = Use Cases =
36 |
37 | * **Customer Support**: Let visitors ask questions and get instant answers from your knowledge base
38 | * **Content Discovery**: Help users find relevant content using natural language
39 | * **AI Agent Integration**: Make your site accessible to ChatGPT, Claude, and other AI systems
40 | * **Documentation Sites**: Enable natural language search through technical documentation
41 | * **E-commerce**: Help customers find products by describing what they need
42 |
43 | == Installation ==
44 |
45 | = Automatic Installation =
46 |
47 | 1. Log in to your WordPress admin panel
48 | 2. Go to Plugins > Add New
49 | 3. Search for "WPNLWeb"
50 | 4. Click "Install Now" and then "Activate"
51 |
52 | = Manual Installation =
53 |
54 | 1. Download the plugin ZIP file
55 | 2. Upload the `wpnlweb` folder to `/wp-content/plugins/`
56 | 3. Activate the plugin through the 'Plugins' menu in WordPress
57 |
58 | = Setup =
59 |
60 | 1. Go to Settings > WPNLWeb in your admin panel
61 | 2. Configure your settings (rate limits, API keys, etc.)
62 | 3. Test the endpoint using the built-in test interface
63 | 4. Add the `[wpnlweb]` shortcode to any page where you want search functionality
64 |
65 | == Usage ==
66 |
67 | = Shortcode Usage =
68 |
69 | Add natural language search to any page or post:
70 |
71 | **Basic Usage:**
72 | `[wpnlweb]`
73 |
74 | **Customized:**
75 | `[wpnlweb placeholder="Search our knowledge base..." button_text="Find Answers" max_results="5"]`
76 |
77 | **With Custom Styling:**
78 | `[wpnlweb class="my-custom-search" show_results="true"]`
79 |
80 | = Shortcode Attributes =
81 |
82 | * `placeholder` - Custom placeholder text for the search input (default: "Ask a question about this site...")
83 | * `button_text` - Custom text for the search button (default: "Search")
84 | * `max_results` - Maximum number of results to display, 1-50 (default: 10)
85 | * `show_results` - Whether to show results on the same page, true/false (default: true)
86 | * `class` - Additional CSS class for custom styling (default: "wpnlweb-search-form")
87 |
88 | = Theme Customization =
89 |
90 | **Modern Light Theme:** WPNLWeb now features a beautiful, modern light theme by default with:
91 |
92 | * Clean white backgrounds with subtle shadows
93 | * Professional blue color scheme
94 | * Smooth animations and hover effects
95 | * Responsive design for all devices
96 | * Automatic dark mode support based on user preferences
97 |
98 | **Admin Settings:** Go to Settings > WPNLWeb to customize:
99 |
100 | * Theme mode (Auto, Light, Dark)
101 | * Primary color picker
102 | * Custom CSS editor with syntax reference
103 |
104 | **CSS Custom Properties:** Easily customize colors using CSS variables:
105 |
106 | ```css
107 | :root {
108 | --wpnlweb-primary-color: #3b82f6; /* Main brand color */
109 | --wpnlweb-primary-hover: #2563eb; /* Hover state */
110 | --wpnlweb-bg-primary: #ffffff; /* Background */
111 | --wpnlweb-text-primary: #1f2937; /* Text color */
112 | --wpnlweb-border-radius: 8px; /* Rounded corners */
113 | }
114 | ```
115 |
116 | **Developer Hooks:** Use filters to customize programmatically:
117 |
118 | ```php
119 | // Change primary color
120 | add_filter('wpnlweb_primary_color', function() {
121 | return '#ff6b6b'; // Custom red
122 | });
123 |
124 | // Add custom CSS
125 | add_filter('wpnlweb_custom_css', function($css) {
126 | return $css . '.wpnlweb-search-container { max-width: 800px; }';
127 | });
128 | ```
129 |
130 | = API Usage =
131 |
132 | **Endpoint:** `https://yoursite.com/wp-json/nlweb/v1/ask`
133 |
134 | **Method:** POST
135 |
136 | **Request Body:**
137 | ```json
138 | {
139 | "question": "What is this website about?",
140 | "context": {
141 | "post_type": "post",
142 | "category": "tutorials",
143 | "limit": 10
144 | }
145 | }
146 | ```
147 |
148 | **Response:**
149 | ```json
150 | {
151 | "@context": "https://schema.org",
152 | "@type": "SearchResultsPage",
153 | "query": "What is this website about?",
154 | "totalResults": 3,
155 | "items": [
156 | {
157 | "@type": "Article",
158 | "@id": "https://yoursite.com/about/",
159 | "name": "About Us",
160 | "description": "Learn about our company mission...",
161 | "url": "https://yoursite.com/about/",
162 | "datePublished": "2024-01-15",
163 | "author": {
164 | "@type": "Person",
165 | "name": "John Doe"
166 | }
167 | }
168 | ]
169 | }
170 | ```
171 |
172 | = Integration with AI Agents =
173 |
174 | To connect your site with ChatGPT or other AI agents:
175 |
176 | 1. Share your endpoint URL: `https://yoursite.com/wp-json/nlweb/v1/ask`
177 | 2. Instruct the AI to send POST requests with natural language questions
178 | 3. The AI will receive structured, searchable responses about your content
179 |
180 | == Frequently Asked Questions ==
181 |
182 | = How do I add search to my website? =
183 |
184 | Simply add the `[wpnlweb]` shortcode to any page, post, or widget area where you want the search functionality to appear.
185 |
186 | = Is this compatible with my theme? =
187 |
188 | Yes! The shortcode is designed to work with any WordPress theme. The search form uses responsive CSS that adapts to your theme's styling.
189 |
190 | = How do AI agents like ChatGPT use this? =
191 |
192 | AI agents can send natural language questions to your `/wp-json/nlweb/v1/ask` endpoint and receive structured responses. This makes your website's content accessible to AI systems.
193 |
194 | = Can I customize the search results? =
195 |
196 | Yes! You can customize the shortcode appearance using the available attributes, add custom CSS classes, and use WordPress hooks to modify the search behavior.
197 |
198 | = Does this work with custom post types? =
199 |
200 | Absolutely! WPNLWeb works with all WordPress post types, including custom post types, pages, and any content created by other plugins.
201 |
202 | = Is this secure? =
203 |
204 | Yes! The plugin includes input sanitization, XSS protection, rate limiting, and optional API key authentication. All WordPress security best practices are followed.
205 |
206 | = Will this slow down my website? =
207 |
208 | No! The plugin is performance-optimized with response times under 500ms. Assets are only loaded when the shortcode is used, and the API endpoint is cached.
209 |
210 | = Can I track usage? =
211 |
212 | Yes! The admin dashboard includes usage statistics showing total queries, response times, and success rates.
213 |
214 | == Screenshots ==
215 |
216 | 1. Admin dashboard showing plugin status, endpoint URL, and usage statistics
217 | 2. Settings page with configuration options and test interface
218 | 3. Frontend search shortcode in action on a website
219 | 4. Search results displayed in a clean, responsive format
220 | 5. Test interface showing JSON API response for developers
221 |
222 | == Changelog ==
223 |
224 | = 1.0.2 =
225 | * WordPress.org submission preparation
226 | * Code standards compliance improvements
227 | * Enhanced documentation and comments
228 | * Version number synchronization across all files
229 | * Bug fixes and stability improvements
230 |
231 | = 1.0.0 =
232 | * Initial release
233 | * NLWeb protocol implementation with REST API endpoint
234 | * Frontend search shortcode with configurable attributes
235 | * Schema.org compliant JSON responses
236 | * MCP server compatibility
237 | * Admin dashboard with settings and analytics
238 | * Performance optimization and caching
239 | * Security features including rate limiting
240 | * WordPress.org compliance and standards
241 |
242 | == Upgrade Notice ==
243 |
244 | = 1.0.2 =
245 | Pre-release refinements: Enhanced code standards compliance, improved documentation, and bug fixes in preparation for WordPress.org plugin directory submission.
246 |
247 | = 1.0.0 =
248 | Major Release: Transform your WordPress site into an AI-accessible knowledge base! Features NLWeb protocol implementation, modern search interface, performance optimization, and enterprise security. Connect ChatGPT, Claude, and other AI systems instantly.
249 |
250 | == Developer Information ==
251 |
252 | = Hooks and Filters =
253 |
254 | The plugin provides several hooks for customization:
255 |
256 | * `wpnlweb_search_results` - Filter search results before display
257 | * `wpnlweb_query_args` - Modify WP_Query arguments
258 | * `wpnlweb_response_format` - Customize API response format
259 | * `wpnlweb_shortcode_attributes` - Filter shortcode attributes
260 |
261 | = Technical Specifications =
262 |
263 | * **Response Time:** <500ms average
264 | * **Concurrent Requests:** Supports 100+ simultaneous requests
265 | * **Memory Usage:** <64MB additional overhead
266 | * **Database Queries:** <5 per request
267 | * **Caching:** WordPress object cache integration
268 | * **Security:** Input sanitization, rate limiting, optional API authentication
269 |
270 | = System Requirements =
271 |
272 | * WordPress 5.0 or higher
273 | * PHP 7.4 or higher
274 | * MySQL 5.6 or higher
275 | * mod_rewrite enabled (for pretty permalinks)
276 |
277 | For technical support and documentation, visit [wpnlweb.com](https://wpnlweb.com)
--------------------------------------------------------------------------------
/includes/licensing/class-wpnlweb-license-manager.php:
--------------------------------------------------------------------------------
1 | load_dependencies();
80 | $this->init_components();
81 | $this->setup_hooks();
82 | }
83 |
84 | /**
85 | * Load required dependencies.
86 | *
87 | * @since 1.1.0
88 | * @access private
89 | */
90 | private function load_dependencies() {
91 | require_once plugin_dir_path( __FILE__ ) . 'class-wpnlweb-license-validator.php';
92 | require_once plugin_dir_path( __FILE__ ) . 'class-wpnlweb-license-cache.php';
93 | require_once plugin_dir_path( __FILE__ ) . 'class-wpnlweb-license-tiers.php';
94 | require_once plugin_dir_path( __FILE__ ) . 'class-wpnlweb-edd-integration.php';
95 | }
96 |
97 | /**
98 | * Initialize component instances.
99 | *
100 | * @since 1.1.0
101 | * @access private
102 | */
103 | private function init_components() {
104 | $this->cache = new Wpnlweb_License_Cache();
105 | $this->edd = new Wpnlweb_Edd_Integration();
106 | $this->validator = new Wpnlweb_License_Validator( $this->cache, $this->edd );
107 | $this->tiers = new Wpnlweb_License_Tiers();
108 | }
109 |
110 | /**
111 | * Setup WordPress hooks.
112 | *
113 | * @since 1.1.0
114 | * @access private
115 | */
116 | private function setup_hooks() {
117 | // Background sync hooks.
118 | add_action( 'wpnlweb_license_sync', array( $this, 'background_sync' ) );
119 | add_action( 'init', array( $this, 'schedule_background_sync' ) );
120 |
121 | // License management hooks.
122 | add_action( 'wpnlweb_license_activated', array( $this, 'on_license_activated' ) );
123 | add_action( 'wpnlweb_license_deactivated', array( $this, 'on_license_deactivated' ) );
124 |
125 | // Multi-site hooks.
126 | if ( is_multisite() ) {
127 | add_action( 'wp_initialize_site', array( $this, 'on_site_created' ) );
128 | add_action( 'wp_delete_site', array( $this, 'on_site_deleted' ) );
129 | }
130 | }
131 |
132 | /**
133 | * Get current license information.
134 | *
135 | * @since 1.1.0
136 | * @return array License data including tier, status, and expiration.
137 | */
138 | public function get_license() {
139 | if ( null === $this->current_license ) {
140 | $this->current_license = $this->load_license();
141 | }
142 |
143 | return $this->current_license;
144 | }
145 |
146 | /**
147 | * Validate license for specific feature access.
148 | *
149 | * @since 1.1.0
150 | * @param string $feature Feature identifier to check access for.
151 | * @return bool True if license allows feature access.
152 | */
153 | public function validate_feature_access( $feature ) {
154 | $license = $this->get_license();
155 |
156 | if ( empty( $license ) || 'active' !== $license['status'] ) {
157 | return $this->tiers->is_free_feature( $feature );
158 | }
159 |
160 | return $this->tiers->has_feature_access( $license['tier'], $feature );
161 | }
162 |
163 | /**
164 | * Get license tier information.
165 | *
166 | * @since 1.1.0
167 | * @return string Current license tier (free, pro, enterprise, agency).
168 | */
169 | public function get_tier() {
170 | $license = $this->get_license();
171 | return isset( $license['tier'] ) ? $license['tier'] : 'free';
172 | }
173 |
174 | /**
175 | * Check if license is valid and active.
176 | *
177 | * @since 1.1.0
178 | * @return bool True if license is valid and active.
179 | */
180 | public function is_valid() {
181 | $license = $this->get_license();
182 | return ! empty( $license ) && 'active' === $license['status'];
183 | }
184 |
185 | /**
186 | * Activate license with provided license key.
187 | *
188 | * @since 1.1.0
189 | * @param string $license_key License key to activate.
190 | * @return array Activation result with success status and message.
191 | */
192 | public function activate_license( $license_key ) {
193 | $result = $this->validator->activate( $license_key );
194 |
195 | if ( $result['success'] ) {
196 | $this->cache->invalidate_license();
197 | $this->current_license = null;
198 |
199 | /**
200 | * Fires when license is successfully activated.
201 | *
202 | * @since 1.1.0
203 | * @param array $result License activation result.
204 | */
205 | do_action( 'wpnlweb_license_activated', $result );
206 | }
207 |
208 | return $result;
209 | }
210 |
211 | /**
212 | * Deactivate current license.
213 | *
214 | * @since 1.1.0
215 | * @return array Deactivation result with success status and message.
216 | */
217 | public function deactivate_license() {
218 | $result = $this->validator->deactivate();
219 |
220 | if ( $result['success'] ) {
221 | $this->cache->invalidate_license();
222 | $this->current_license = null;
223 |
224 | /**
225 | * Fires when license is successfully deactivated.
226 | *
227 | * @since 1.1.0
228 | * @param array $result License deactivation result.
229 | */
230 | do_action( 'wpnlweb_license_deactivated', $result );
231 | }
232 |
233 | return $result;
234 | }
235 |
236 | /**
237 | * Load license data using hybrid validation approach.
238 | *
239 | * @since 1.1.0
240 | * @access private
241 | * @return array License data or empty array if no license.
242 | */
243 | private function load_license() {
244 | // Try cache first (5-minute cache).
245 | $license = $this->cache->get_license();
246 |
247 | if ( false !== $license ) {
248 | return $license;
249 | }
250 |
251 | // Cache miss - validate with EDD and cache result.
252 | $license = $this->validator->validate();
253 |
254 | if ( ! empty( $license ) ) {
255 | $this->cache->set_license( $license );
256 | }
257 |
258 | return $license;
259 | }
260 |
261 | /**
262 | * Schedule background sync for license status.
263 | *
264 | * @since 1.1.0
265 | * @access public
266 | */
267 | public function schedule_background_sync() {
268 | if ( ! wp_next_scheduled( 'wpnlweb_license_sync' ) ) {
269 | wp_schedule_event( time(), 'hourly', 'wpnlweb_license_sync' );
270 | }
271 | }
272 |
273 | /**
274 | * Perform background license sync.
275 | *
276 | * @since 1.1.0
277 | * @access public
278 | */
279 | public function background_sync() {
280 | $current_license = $this->cache->get_license();
281 |
282 | if ( false === $current_license ) {
283 | return; // No license to sync.
284 | }
285 |
286 | // Validate current license status with EDD.
287 | $updated_license = $this->validator->validate( true ); // Force remote check.
288 |
289 | if ( ! empty( $updated_license ) && $updated_license !== $current_license ) {
290 | $this->cache->set_license( $updated_license );
291 |
292 | /**
293 | * Fires when license is updated via background sync.
294 | *
295 | * @since 1.1.0
296 | * @param array $updated_license Updated license data.
297 | * @param array $current_license Previous license data.
298 | */
299 | do_action( 'wpnlweb_license_updated', $updated_license, $current_license );
300 | }
301 | }
302 |
303 | /**
304 | * Handle license activation event.
305 | *
306 | * @since 1.1.0
307 | * @param array $result License activation result.
308 | */
309 | public function on_license_activated( $result ) {
310 | // Log license activation.
311 | error_log( sprintf(
312 | 'WPNLWeb: License activated for site %s - Tier: %s',
313 | get_site_url(),
314 | isset( $result['tier'] ) ? $result['tier'] : 'unknown'
315 | ) );
316 | }
317 |
318 | /**
319 | * Handle license deactivation event.
320 | *
321 | * @since 1.1.0
322 | * @param array $result License deactivation result.
323 | */
324 | public function on_license_deactivated( $result ) {
325 | // Log license deactivation.
326 | error_log( sprintf(
327 | 'WPNLWeb: License deactivated for site %s',
328 | get_site_url()
329 | ) );
330 | }
331 |
332 | /**
333 | * Handle new site creation in multisite.
334 | *
335 | * @since 1.1.0
336 | * @param WP_Site $new_site New site object.
337 | */
338 | public function on_site_created( $new_site ) {
339 | // Inherit network license if available.
340 | if ( is_network_admin() && $this->is_valid() ) {
341 | switch_to_blog( $new_site->blog_id );
342 | $this->cache->invalidate_license();
343 | restore_current_blog();
344 | }
345 | }
346 |
347 | /**
348 | * Handle site deletion in multisite.
349 | *
350 | * @since 1.1.0
351 | * @param WP_Site $old_site Deleted site object.
352 | */
353 | public function on_site_deleted( $old_site ) {
354 | // Clean up license cache for deleted site.
355 | switch_to_blog( $old_site->blog_id );
356 | $this->cache->invalidate_license();
357 | restore_current_blog();
358 | }
359 |
360 | /**
361 | * Get license statistics for admin dashboard.
362 | *
363 | * @since 1.1.0
364 | * @return array License statistics and usage information.
365 | */
366 | public function get_license_stats() {
367 | $license = $this->get_license();
368 | $stats = array(
369 | 'tier' => $this->get_tier(),
370 | 'status' => isset( $license['status'] ) ? $license['status'] : 'inactive',
371 | 'expires_at' => isset( $license['expires_at'] ) ? $license['expires_at'] : null,
372 | 'sites_used' => 1,
373 | 'sites_limit' => $this->tiers->get_sites_limit( $this->get_tier() ),
374 | 'features' => $this->tiers->get_tier_features( $this->get_tier() ),
375 | );
376 |
377 | if ( is_multisite() && is_network_admin() ) {
378 | $stats['sites_used'] = get_blog_count();
379 | }
380 |
381 | return $stats;
382 | }
383 | }
--------------------------------------------------------------------------------
/includes/licensing/class-wpnlweb-license-tiers.php:
--------------------------------------------------------------------------------
1 | 'Free',
37 | 'pro' => 'Pro',
38 | 'enterprise' => 'Enterprise',
39 | 'agency' => 'Agency',
40 | );
41 |
42 | /**
43 | * Tier feature matrix.
44 | *
45 | * @since 1.1.0
46 | * @access private
47 | * @var array
48 | */
49 | private $features;
50 |
51 | /**
52 | * Tier limitations matrix.
53 | *
54 | * @since 1.1.0
55 | * @access private
56 | * @var array
57 | */
58 | private $limitations;
59 |
60 | /**
61 | * Initialize the License Tiers.
62 | *
63 | * @since 1.1.0
64 | */
65 | public function __construct() {
66 | $this->init_features();
67 | $this->init_limitations();
68 | }
69 |
70 | /**
71 | * Initialize feature access matrix.
72 | *
73 | * @since 1.1.0
74 | * @access private
75 | */
76 | private function init_features() {
77 | $this->features = array(
78 | 'free' => array(
79 | 'api_endpoint',
80 | 'search_shortcode',
81 | 'admin_interface',
82 | 'schema_org_responses',
83 | 'query_enhancement',
84 | 'basic_caching',
85 | 'security_features',
86 | 'mobile_responsive',
87 | ),
88 | 'pro' => array(
89 | 'vector_embeddings',
90 | 'analytics_dashboard',
91 | 'advanced_filtering',
92 | 'custom_templates',
93 | 'priority_support',
94 | ),
95 | 'enterprise' => array(
96 | 'realtime_suggestions',
97 | 'advanced_analytics',
98 | 'multisite_licenses',
99 | 'custom_integrations',
100 | 'white_label',
101 | ),
102 | 'agency' => array(
103 | 'automation_agents',
104 | 'reseller_management',
105 | 'client_dashboard',
106 | 'bulk_operations',
107 | 'custom_development',
108 | ),
109 | );
110 | }
111 |
112 | /**
113 | * Initialize tier limitations matrix.
114 | *
115 | * @since 1.1.0
116 | * @access private
117 | */
118 | private function init_limitations() {
119 | $this->limitations = array(
120 | 'free' => array(
121 | 'sites_limit' => 1,
122 | 'api_calls_month' => 1000,
123 | 'storage_mb' => 10,
124 | 'support_level' => 'community',
125 | ),
126 | 'pro' => array(
127 | 'sites_limit' => 1,
128 | 'api_calls_month' => 10000,
129 | 'storage_mb' => 100,
130 | 'support_level' => 'priority',
131 | ),
132 | 'enterprise' => array(
133 | 'sites_limit' => 100,
134 | 'api_calls_month' => 100000,
135 | 'storage_mb' => 1000,
136 | 'support_level' => 'dedicated',
137 | ),
138 | 'agency' => array(
139 | 'sites_limit' => 1000,
140 | 'api_calls_month' => 1000000,
141 | 'storage_mb' => 10000,
142 | 'support_level' => 'white_glove',
143 | ),
144 | );
145 | }
146 |
147 | /**
148 | * Get all available tiers.
149 | *
150 | * @since 1.1.0
151 | * @return array Available license tiers.
152 | */
153 | public function get_tiers() {
154 | return $this->tiers;
155 | }
156 |
157 | /**
158 | * Get features for specific tier.
159 | *
160 | * @since 1.1.0
161 | * @param string $tier License tier to get features for.
162 | * @return array Features available for the tier.
163 | */
164 | public function get_tier_features( $tier ) {
165 | if ( ! isset( $this->features[ $tier ] ) ) {
166 | return $this->features['free'];
167 | }
168 |
169 | // Include all features from lower tiers.
170 | $all_features = array();
171 | foreach ( $this->tiers as $tier_key => $tier_name ) {
172 | if ( isset( $this->features[ $tier_key ] ) ) {
173 | $all_features = array_merge( $all_features, $this->features[ $tier_key ] );
174 | }
175 | if ( $tier_key === $tier ) {
176 | break;
177 | }
178 | }
179 |
180 | return array_unique( $all_features );
181 | }
182 |
183 | /**
184 | * Check if tier has access to specific feature.
185 | *
186 | * @since 1.1.0
187 | * @param string $tier License tier to check.
188 | * @param string $feature Feature to check access for.
189 | * @return bool True if tier has access to feature.
190 | */
191 | public function has_feature_access( $tier, $feature ) {
192 | $tier_features = $this->get_tier_features( $tier );
193 | return in_array( $feature, $tier_features, true );
194 | }
195 |
196 | /**
197 | * Check if feature is available in free tier.
198 | *
199 | * @since 1.1.0
200 | * @param string $feature Feature to check.
201 | * @return bool True if feature is available in free tier.
202 | */
203 | public function is_free_feature( $feature ) {
204 | return $this->has_feature_access( 'free', $feature );
205 | }
206 |
207 | /**
208 | * Get tier limitations.
209 | *
210 | * @since 1.1.0
211 | * @param string $tier License tier to get limitations for.
212 | * @return array Limitations for the tier.
213 | */
214 | public function get_tier_limitations( $tier ) {
215 | return isset( $this->limitations[ $tier ] )
216 | ? $this->limitations[ $tier ]
217 | : $this->limitations['free'];
218 | }
219 |
220 | /**
221 | * Get sites limit for tier.
222 | *
223 | * @since 1.1.0
224 | * @param string $tier License tier.
225 | * @return int Maximum sites allowed for tier.
226 | */
227 | public function get_sites_limit( $tier ) {
228 | $limitations = $this->get_tier_limitations( $tier );
229 | return isset( $limitations['sites_limit'] ) ? $limitations['sites_limit'] : 1;
230 | }
231 |
232 | /**
233 | * Get API calls limit for tier.
234 | *
235 | * @since 1.1.0
236 | * @param string $tier License tier.
237 | * @return int Monthly API calls limit for tier.
238 | */
239 | public function get_api_calls_limit( $tier ) {
240 | $limitations = $this->get_tier_limitations( $tier );
241 | return isset( $limitations['api_calls_month'] ) ? $limitations['api_calls_month'] : 1000;
242 | }
243 |
244 | /**
245 | * Get storage limit for tier.
246 | *
247 | * @since 1.1.0
248 | * @param string $tier License tier.
249 | * @return int Storage limit in MB for tier.
250 | */
251 | public function get_storage_limit( $tier ) {
252 | $limitations = $this->get_tier_limitations( $tier );
253 | return isset( $limitations['storage_mb'] ) ? $limitations['storage_mb'] : 10;
254 | }
255 |
256 | /**
257 | * Get support level for tier.
258 | *
259 | * @since 1.1.0
260 | * @param string $tier License tier.
261 | * @return string Support level for tier.
262 | */
263 | public function get_support_level( $tier ) {
264 | $limitations = $this->get_tier_limitations( $tier );
265 | return isset( $limitations['support_level'] ) ? $limitations['support_level'] : 'community';
266 | }
267 |
268 | /**
269 | * Get tier display information.
270 | *
271 | * @since 1.1.0
272 | * @param string $tier License tier.
273 | * @return array Tier display information.
274 | */
275 | public function get_tier_info( $tier ) {
276 | $features = $this->get_tier_features( $tier );
277 | $limitations = $this->get_tier_limitations( $tier );
278 |
279 | return array(
280 | 'name' => isset( $this->tiers[ $tier ] ) ? $this->tiers[ $tier ] : 'Unknown',
281 | 'features' => $features,
282 | 'limitations' => $limitations,
283 | 'pricing' => $this->get_tier_pricing( $tier ),
284 | );
285 | }
286 |
287 | /**
288 | * Get tier pricing information.
289 | *
290 | * @since 1.1.0
291 | * @param string $tier License tier.
292 | * @return array Pricing information for tier.
293 | */
294 | public function get_tier_pricing( $tier ) {
295 | $pricing = array(
296 | 'free' => array(
297 | 'price' => 0,
298 | 'currency' => 'USD',
299 | 'period' => 'lifetime',
300 | ),
301 | 'pro' => array(
302 | 'price' => 29,
303 | 'currency' => 'USD',
304 | 'period' => 'monthly',
305 | ),
306 | 'enterprise' => array(
307 | 'price' => 99,
308 | 'currency' => 'USD',
309 | 'period' => 'monthly',
310 | ),
311 | 'agency' => array(
312 | 'price' => 299,
313 | 'currency' => 'USD',
314 | 'period' => 'monthly',
315 | ),
316 | );
317 |
318 | return isset( $pricing[ $tier ] ) ? $pricing[ $tier ] : $pricing['free'];
319 | }
320 |
321 | /**
322 | * Get upgrade path for tier.
323 | *
324 | * @since 1.1.0
325 | * @param string $tier Current license tier.
326 | * @return string Next available tier for upgrade.
327 | */
328 | public function get_upgrade_tier( $tier ) {
329 | $tier_order = array_keys( $this->tiers );
330 | $current_index = array_search( $tier, $tier_order, true );
331 |
332 | if ( false === $current_index || $current_index >= count( $tier_order ) - 1 ) {
333 | return null; // Already at highest tier or invalid tier.
334 | }
335 |
336 | return $tier_order[ $current_index + 1 ];
337 | }
338 |
339 | /**
340 | * Get downgrade path for tier.
341 | *
342 | * @since 1.1.0
343 | * @param string $tier Current license tier.
344 | * @return string Previous tier for downgrade.
345 | */
346 | public function get_downgrade_tier( $tier ) {
347 | $tier_order = array_keys( $this->tiers );
348 | $current_index = array_search( $tier, $tier_order, true );
349 |
350 | if ( false === $current_index || $current_index <= 0 ) {
351 | return null; // Already at lowest tier or invalid tier.
352 | }
353 |
354 | return $tier_order[ $current_index - 1 ];
355 | }
356 |
357 | /**
358 | * Check if tier supports multisite.
359 | *
360 | * @since 1.1.0
361 | * @param string $tier License tier to check.
362 | * @return bool True if tier supports multisite.
363 | */
364 | public function supports_multisite( $tier ) {
365 | return $this->has_feature_access( $tier, 'multisite_licenses' );
366 | }
367 |
368 | /**
369 | * Get tier comparison matrix.
370 | *
371 | * @since 1.1.0
372 | * @return array Comparison matrix for all tiers.
373 | */
374 | public function get_tier_comparison() {
375 | $comparison = array();
376 |
377 | foreach ( $this->tiers as $tier_key => $tier_name ) {
378 | $comparison[ $tier_key ] = $this->get_tier_info( $tier_key );
379 | }
380 |
381 | return $comparison;
382 | }
383 |
384 | /**
385 | * Validate tier name.
386 | *
387 | * @since 1.1.0
388 | * @param string $tier Tier name to validate.
389 | * @return bool True if tier is valid.
390 | */
391 | public function is_valid_tier( $tier ) {
392 | return isset( $this->tiers[ $tier ] );
393 | }
394 |
395 | /**
396 | * Get feature requirements for upgrade.
397 | *
398 | * @since 1.1.0
399 | * @param string $feature Feature to check upgrade requirements for.
400 | * @return string Minimum tier required for feature.
401 | */
402 | public function get_feature_tier_requirement( $feature ) {
403 | foreach ( $this->tiers as $tier_key => $tier_name ) {
404 | if ( $this->has_feature_access( $tier_key, $feature ) ) {
405 | return $tier_key;
406 | }
407 | }
408 |
409 | return null; // Feature not found.
410 | }
411 | }
--------------------------------------------------------------------------------
/public/css/wpnlweb-shortcode.css:
--------------------------------------------------------------------------------
1 | /**
2 | * WPNLWeb Shortcode Styles - Modern Light Theme
3 | *
4 | * Clean, modern light theme with professional appearance
5 | * Fully responsive with dark mode support
6 | * Easily customizable via CSS custom properties
7 | *
8 | * @package Wpnlweb
9 | * @subpackage Wpnlweb/public/css
10 | * @since 1.0.0
11 | */
12 |
13 | /* ====================
14 | CSS CUSTOM PROPERTIES
15 | ==================== */
16 |
17 | :root {
18 | /* Primary colors - easily customizable */
19 | --wpnlweb-primary-color: #3b82f6;
20 | --wpnlweb-primary-hover: #2563eb;
21 | --wpnlweb-primary-active: #1d4ed8;
22 |
23 | /* Background colors */
24 | --wpnlweb-bg-primary: #ffffff;
25 | --wpnlweb-bg-secondary: #f9fafb;
26 | --wpnlweb-bg-results: #ffffff;
27 |
28 | /* Text colors */
29 | --wpnlweb-text-primary: #1f2937;
30 | --wpnlweb-text-secondary: #4b5563;
31 | --wpnlweb-text-muted: #6b7280;
32 | --wpnlweb-text-placeholder: #9ca3af;
33 |
34 | /* Border colors */
35 | --wpnlweb-border-primary: #e5e7eb;
36 | --wpnlweb-border-secondary: #f3f4f6;
37 | --wpnlweb-border-focus: var(--wpnlweb-primary-color);
38 |
39 | /* Shadows */
40 | --wpnlweb-shadow-sm: 0 2px 8px rgba(59, 130, 246, 0.25);
41 | --wpnlweb-shadow-md: 0 4px 12px rgba(59, 130, 246, 0.35);
42 | --wpnlweb-shadow-lg: 0 8px 25px rgba(0, 0, 0, 0.1);
43 | --wpnlweb-shadow-container: 0 4px 20px rgba(0, 0, 0, 0.08);
44 |
45 | /* Spacing */
46 | --wpnlweb-border-radius: 8px;
47 | --wpnlweb-border-radius-lg: 12px;
48 | --wpnlweb-spacing-sm: 12px;
49 | --wpnlweb-spacing-md: 20px;
50 | --wpnlweb-spacing-lg: 30px;
51 | }
52 |
53 | /* ====================
54 | DEFAULT LIGHT THEME
55 | ==================== */
56 |
57 | /* Search Container - Clean white with subtle shadow */
58 | .wpnlweb-search-container {
59 | max-width: 600px;
60 | margin: var(--wpnlweb-spacing-md) auto;
61 | padding: var(--wpnlweb-spacing-lg);
62 | background: var(--wpnlweb-bg-primary);
63 | border-radius: var(--wpnlweb-border-radius-lg);
64 | box-shadow: var(--wpnlweb-shadow-container);
65 | border: 1px solid var(--wpnlweb-border-primary);
66 | }
67 |
68 | /* Search Form */
69 | .wpnlweb-search-form {
70 | margin-bottom: 25px;
71 | }
72 |
73 | .wpnlweb-search-input-wrapper {
74 | display: flex;
75 | gap: var(--wpnlweb-spacing-sm);
76 | align-items: center;
77 | flex-wrap: wrap;
78 | }
79 |
80 | /* Input Field - Modern design with focus states */
81 | .wpnlweb-search-input {
82 | flex: 1;
83 | min-width: 250px;
84 | padding: 14px 18px;
85 | border: 2px solid var(--wpnlweb-border-primary);
86 | border-radius: var(--wpnlweb-border-radius);
87 | font-size: 16px;
88 | background: var(--wpnlweb-bg-primary);
89 | color: var(--wpnlweb-text-primary);
90 | transition: all 0.2s ease;
91 | font-family: inherit;
92 | }
93 |
94 | .wpnlweb-search-input:focus {
95 | outline: none;
96 | border-color: var(--wpnlweb-border-focus);
97 | box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
98 | background: var(--wpnlweb-bg-primary);
99 | }
100 |
101 | .wpnlweb-search-input::placeholder {
102 | color: var(--wpnlweb-text-placeholder);
103 | }
104 |
105 | /* Search Button - Modern blue with hover effects */
106 | .wpnlweb-search-button {
107 | padding: 14px 28px;
108 | background: linear-gradient(
109 | 135deg,
110 | var(--wpnlweb-primary-color),
111 | var(--wpnlweb-primary-hover)
112 | );
113 | color: #ffffff;
114 | border: none;
115 | border-radius: var(--wpnlweb-border-radius);
116 | font-size: 16px;
117 | font-weight: 600;
118 | cursor: pointer;
119 | transition: all 0.2s ease;
120 | white-space: nowrap;
121 | box-shadow: var(--wpnlweb-shadow-sm);
122 | }
123 |
124 | .wpnlweb-search-button:hover {
125 | background: linear-gradient(
126 | 135deg,
127 | var(--wpnlweb-primary-hover),
128 | var(--wpnlweb-primary-active)
129 | );
130 | transform: translateY(-1px);
131 | box-shadow: var(--wpnlweb-shadow-md);
132 | }
133 |
134 | .wpnlweb-search-button:active {
135 | transform: translateY(0);
136 | box-shadow: 0 2px 6px rgba(59, 130, 246, 0.3);
137 | }
138 |
139 | .wpnlweb-search-button:disabled {
140 | background: #d1d5db;
141 | color: #9ca3af;
142 | cursor: not-allowed;
143 | transform: none;
144 | box-shadow: none;
145 | }
146 |
147 | /* Loading State - Elegant spinner */
148 | .wpnlweb-loading {
149 | display: flex;
150 | align-items: center;
151 | gap: var(--wpnlweb-spacing-sm);
152 | margin-top: 18px;
153 | color: var(--wpnlweb-text-muted);
154 | font-style: italic;
155 | font-size: 15px;
156 | }
157 |
158 | .wpnlweb-spinner {
159 | width: 18px;
160 | height: 18px;
161 | border: 3px solid var(--wpnlweb-border-secondary);
162 | border-top: 3px solid var(--wpnlweb-primary-color);
163 | border-radius: 50%;
164 | animation: wpnlweb-spin 1s linear infinite;
165 | }
166 |
167 | @keyframes wpnlweb-spin {
168 | 0% {
169 | transform: rotate(0deg);
170 | }
171 | 100% {
172 | transform: rotate(360deg);
173 | }
174 | }
175 |
176 | /* Results Container - Clean separation */
177 | .wpnlweb-search-results {
178 | margin-top: var(--wpnlweb-spacing-lg);
179 | padding-top: 25px;
180 | border-top: 2px solid var(--wpnlweb-border-secondary);
181 | }
182 |
183 | .wpnlweb-results-title {
184 | margin: 0 0 var(--wpnlweb-spacing-md) 0;
185 | color: var(--wpnlweb-text-primary);
186 | font-size: 20px;
187 | font-weight: 700;
188 | letter-spacing: -0.025em;
189 | }
190 |
191 | .wpnlweb-results-content {
192 | /* Container for dynamic results */
193 | }
194 |
195 | /* No Results - Friendly empty state */
196 | .wpnlweb-no-results {
197 | padding: 24px;
198 | text-align: center;
199 | color: var(--wpnlweb-text-muted);
200 | font-style: italic;
201 | background: var(--wpnlweb-bg-secondary);
202 | border: 2px dashed var(--wpnlweb-border-primary);
203 | border-radius: var(--wpnlweb-border-radius);
204 | font-size: 15px;
205 | }
206 |
207 | /* Results List */
208 | .wpnlweb-results-list {
209 | display: flex;
210 | flex-direction: column;
211 | gap: var(--wpnlweb-spacing-md);
212 | }
213 |
214 | /* Result Items - Card-like design */
215 | .wpnlweb-result-item {
216 | padding: 24px;
217 | background: var(--wpnlweb-bg-results);
218 | border: 1px solid var(--wpnlweb-border-primary);
219 | border-radius: var(--wpnlweb-border-radius);
220 | transition: all 0.2s ease;
221 | position: relative;
222 | }
223 |
224 | .wpnlweb-result-item:hover {
225 | box-shadow: var(--wpnlweb-shadow-lg);
226 | border-color: #d1d5db;
227 | transform: translateY(-2px);
228 | }
229 |
230 | .wpnlweb-result-title {
231 | margin: 0 0 var(--wpnlweb-spacing-sm) 0;
232 | font-size: 18px;
233 | line-height: 1.4;
234 | font-weight: 600;
235 | }
236 |
237 | .wpnlweb-result-title a {
238 | color: var(--wpnlweb-text-primary);
239 | text-decoration: none;
240 | transition: color 0.2s ease;
241 | }
242 |
243 | .wpnlweb-result-title a:hover {
244 | color: var(--wpnlweb-primary-color);
245 | text-decoration: none;
246 | }
247 |
248 | .wpnlweb-result-excerpt {
249 | margin: var(--wpnlweb-spacing-sm) 0 16px 0;
250 | color: var(--wpnlweb-text-secondary);
251 | line-height: 1.6;
252 | font-size: 15px;
253 | }
254 |
255 | .wpnlweb-result-excerpt p {
256 | margin: 0;
257 | }
258 |
259 | .wpnlweb-result-meta {
260 | display: flex;
261 | gap: 16px;
262 | margin-top: 16px;
263 | font-size: 14px;
264 | color: var(--wpnlweb-text-placeholder);
265 | border-top: 1px solid var(--wpnlweb-border-secondary);
266 | padding-top: var(--wpnlweb-spacing-sm);
267 | }
268 |
269 | .wpnlweb-result-date,
270 | .wpnlweb-result-author {
271 | white-space: nowrap;
272 | }
273 |
274 | .wpnlweb-result-date::before {
275 | content: "📅 ";
276 | margin-right: 4px;
277 | }
278 |
279 | .wpnlweb-result-author::before {
280 | content: "👤 ";
281 | margin-right: 4px;
282 | }
283 |
284 | /* Error Messages - Clear error styling */
285 | .wpnlweb-error {
286 | padding: 16px var(--wpnlweb-spacing-md);
287 | background: #fef2f2;
288 | color: #dc2626;
289 | border: 1px solid #fecaca;
290 | border-radius: var(--wpnlweb-border-radius);
291 | margin-top: 15px;
292 | font-weight: 500;
293 | }
294 |
295 | .wpnlweb-error::before {
296 | content: "⚠️ ";
297 | margin-right: 8px;
298 | }
299 |
300 | /* ====================
301 | RESPONSIVE DESIGN
302 | ==================== */
303 |
304 | @media (max-width: 600px) {
305 | .wpnlweb-search-container {
306 | margin: 15px;
307 | padding: 20px;
308 | }
309 |
310 | .wpnlweb-search-input-wrapper {
311 | flex-direction: column;
312 | align-items: stretch;
313 | }
314 |
315 | .wpnlweb-search-input {
316 | min-width: unset;
317 | margin-bottom: 12px;
318 | }
319 |
320 | .wpnlweb-search-button {
321 | width: 100%;
322 | }
323 |
324 | .wpnlweb-result-meta {
325 | flex-direction: column;
326 | gap: 8px;
327 | }
328 | }
329 |
330 | @media (max-width: 480px) {
331 | .wpnlweb-search-container {
332 | margin: 10px;
333 | padding: 16px;
334 | }
335 |
336 | .wpnlweb-result-item {
337 | padding: 18px;
338 | }
339 |
340 | .wpnlweb-result-title {
341 | font-size: 17px;
342 | }
343 |
344 | .wpnlweb-results-title {
345 | font-size: 18px;
346 | }
347 | }
348 |
349 | /* ====================
350 | DARK MODE SUPPORT
351 | ==================== */
352 |
353 | @media (prefers-color-scheme: dark) {
354 | .wpnlweb-search-container {
355 | background: #1f2937;
356 | color: #f3f4f6;
357 | border-color: #374151;
358 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
359 | }
360 |
361 | .wpnlweb-search-input {
362 | background: #374151;
363 | border-color: #4b5563;
364 | color: #f3f4f6;
365 | }
366 |
367 | .wpnlweb-search-input:focus {
368 | border-color: #60a5fa;
369 | box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2);
370 | background: #374151;
371 | }
372 |
373 | .wpnlweb-search-input::placeholder {
374 | color: #9ca3af;
375 | }
376 |
377 | .wpnlweb-search-button {
378 | background: linear-gradient(135deg, #2563eb, #1d4ed8);
379 | box-shadow: 0 2px 8px rgba(37, 99, 235, 0.4);
380 | }
381 |
382 | .wpnlweb-search-button:hover {
383 | background: linear-gradient(135deg, #1d4ed8, #1e40af);
384 | box-shadow: 0 4px 12px rgba(37, 99, 235, 0.5);
385 | }
386 |
387 | .wpnlweb-loading {
388 | color: #d1d5db;
389 | }
390 |
391 | .wpnlweb-spinner {
392 | border-color: #4b5563;
393 | border-top-color: #60a5fa;
394 | }
395 |
396 | .wpnlweb-search-results {
397 | border-top-color: #374151;
398 | }
399 |
400 | .wpnlweb-results-title {
401 | color: #f3f4f6;
402 | }
403 |
404 | .wpnlweb-result-item {
405 | background: #374151;
406 | border-color: #4b5563;
407 | }
408 |
409 | .wpnlweb-result-item:hover {
410 | box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4);
411 | border-color: #6b7280;
412 | }
413 |
414 | .wpnlweb-result-title a {
415 | color: #f3f4f6;
416 | }
417 |
418 | .wpnlweb-result-title a:hover {
419 | color: #60a5fa;
420 | }
421 |
422 | .wpnlweb-result-excerpt {
423 | color: #d1d5db;
424 | }
425 |
426 | .wpnlweb-result-meta {
427 | color: #9ca3af;
428 | border-top-color: #4b5563;
429 | }
430 |
431 | .wpnlweb-no-results {
432 | background: #374151;
433 | color: #d1d5db;
434 | border-color: #4b5563;
435 | }
436 |
437 | .wpnlweb-error {
438 | background: #7f1d1d;
439 | color: #fca5a5;
440 | border-color: #991b1b;
441 | }
442 | }
443 |
444 | /* ====================
445 | ACCESSIBILITY
446 | ==================== */
447 |
448 | @media (prefers-reduced-motion: reduce) {
449 | .wpnlweb-search-input,
450 | .wpnlweb-search-button,
451 | .wpnlweb-result-item,
452 | .wpnlweb-result-title a {
453 | transition: none;
454 | }
455 |
456 | .wpnlweb-spinner {
457 | animation: none;
458 | }
459 |
460 | .wpnlweb-search-button:hover,
461 | .wpnlweb-result-item:hover {
462 | transform: none;
463 | }
464 | }
465 |
466 | /* High contrast mode support */
467 | @media (prefers-contrast: high) {
468 | .wpnlweb-search-input {
469 | border-width: 3px;
470 | }
471 |
472 | .wpnlweb-search-button {
473 | border: 2px solid currentColor;
474 | }
475 |
476 | .wpnlweb-result-item {
477 | border-width: 2px;
478 | }
479 | }
480 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🤖 WPNLWeb - WordPress Natural Language AI Plugin
2 |
3 |
4 |
5 | [](https://wordpress.org/plugins/wpnlweb/)
6 | [](https://php.net/)
7 | [](https://wordpress.org/)
8 | [](https://www.gnu.org/licenses/gpl-2.0.html)
9 |
10 | > Transform your WordPress site into a conversational interface for users and AI agents using Microsoft's NLWeb Protocol.
11 |
12 | ## 🚀 Features
13 |
14 | - **🔌 NLWeb Protocol Implementation** - Standards-compliant REST API endpoint
15 | - **🎯 Frontend Search Shortcode** - Natural language search for visitors
16 | - **🤖 AI Agent Ready** - Compatible with ChatGPT, Claude, and other AI systems
17 | - **📋 Schema.org Compliant** - Structured responses that AI agents understand
18 | - **⚡ High Performance** - <500ms response times with caching
19 | - **🎨 Modern UI** - Beautiful, responsive search interface
20 | - **🔧 Developer Friendly** - Extensive hooks, filters, and customization options
21 | - **🛡️ Security First** - Input sanitization, CORS headers, rate limiting
22 | - **📱 Mobile Optimized** - Works seamlessly on all devices
23 |
24 | ## 📸 Screenshots
25 |
26 | 
27 | _Settings and configuration panel_
28 |
29 | 
30 | _Natural language search interface_
31 |
32 | 
33 | _Schema.org compliant API responses_
34 |
35 | ## 🎯 Quick Start
36 |
37 | ### For End Users
38 |
39 | 1. **Install the Plugin**
40 |
41 | ```bash
42 | # From WordPress Admin (⚠️ PENDING PLUGIN REVIEW ⚠️)
43 | Plugins > Add New > Search "WPNLWeb" > Install > Activate
44 |
45 | # Or download from WordPress.org (⚠️ PENDING PLUGIN REVIEW ⚠️)
46 | wget https://downloads.wordpress.org/plugin/wpnlweb.zip
47 |
48 | # Download Plugin Zip File Until Approved by Wordpress
49 | https://github.com/gigabit-eth/wpnlweb/releases
50 | ```
51 |
52 | 2. **Add Search to Any Page**
53 |
54 | ```php
55 | [wpnlweb placeholder="Ask anything about our site..." max_results="5"]
56 | ```
57 |
58 | 3. **Configure Settings**
59 | - Go to `Settings > WPNLWeb` in your WordPress admin
60 | - Customize colors, themes, and behavior
61 | - Test the API using the built-in interface
62 |
63 | ### For AI Agents
64 |
65 | ```bash
66 | # Query your WordPress site via natural language
67 | curl -X POST https://yoursite.com/wp-json/nlweb/v1/ask \
68 | -H "Content-Type: application/json" \
69 | -d '{"question": "What products do you sell?"}'
70 | ```
71 |
72 | ## 🛠️ Development Setup
73 |
74 | ### Prerequisites
75 |
76 | - **PHP 7.4+**
77 | - **WordPress 5.0+**
78 | - **Composer** (for development)
79 | - **Node.js 18+** (for frontend development)
80 |
81 | ### Installation
82 |
83 | ```bash
84 | # Clone the repository
85 | git clone https://github.com/gigabit-eth/wpnlweb.git
86 | cd wpnlweb
87 |
88 | # Install PHP dependencies
89 | composer install
90 |
91 | # Set up development environment
92 | composer run dev-setup
93 |
94 | # Run code quality checks
95 | composer run lint
96 | ```
97 |
98 | ### Development Commands
99 |
100 | ```bash
101 | # PHP Code Standards
102 | composer run lint # Check code standards
103 | composer run lint-fix # Auto-fix code standards
104 | composer run check-syntax # Check PHP syntax
105 |
106 | # WordPress Development
107 | wp plugin activate wpnlweb # Activate plugin
108 | wp plugin deactivate wpnlweb # Deactivate plugin
109 | wp plugin uninstall wpnlweb # Uninstall plugin
110 | ```
111 |
112 | ## 📚 API Documentation
113 |
114 | ### REST Endpoint
115 |
116 | **Endpoint:** `POST /wp-json/nlweb/v1/ask`
117 |
118 | #### Request Format
119 |
120 | ```json
121 | {
122 | "question": "What is this website about?",
123 | "context": {
124 | "post_type": ["post", "page"],
125 | "category": "tutorials",
126 | "limit": 10,
127 | "meta_query": {
128 | "featured": "yes"
129 | }
130 | }
131 | }
132 | ```
133 |
134 | #### Response Format
135 |
136 | ```json
137 | {
138 | "@context": "https://schema.org",
139 | "@type": "SearchResultsPage",
140 | "query": "What is this website about?",
141 | "totalResults": 3,
142 | "processingTime": "0.245s",
143 | "items": [
144 | {
145 | "@type": "Article",
146 | "@id": "https://yoursite.com/about/",
147 | "name": "About Us",
148 | "description": "Learn about our company mission and values...",
149 | "url": "https://yoursite.com/about/",
150 | "datePublished": "2024-01-15T10:30:00Z",
151 | "dateModified": "2024-01-20T14:15:00Z",
152 | "author": {
153 | "@type": "Person",
154 | "name": "John Doe"
155 | },
156 | "keywords": ["about", "company", "mission"],
157 | "relevanceScore": 0.95
158 | }
159 | ]
160 | }
161 | ```
162 |
163 | ### Shortcode Options
164 |
165 | ```php
166 | [wpnlweb
167 | placeholder="Custom placeholder text..."
168 | button_text="Search Now"
169 | max_results="10"
170 | show_results="true"
171 | class="my-custom-class"
172 | theme="dark"
173 | show_metadata="true"
174 | ]
175 | ```
176 |
177 | ## 🎨 Customization
178 |
179 | ### CSS Variables
180 |
181 | ```css
182 | :root {
183 | --wpnlweb-primary-color: #3b82f6;
184 | --wpnlweb-primary-hover: #2563eb;
185 | --wpnlweb-bg-primary: #ffffff;
186 | --wpnlweb-text-primary: #1f2937;
187 | --wpnlweb-border-radius: 8px;
188 | --wpnlweb-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
189 | }
190 | ```
191 |
192 | ### WordPress Hooks
193 |
194 | ```php
195 | // Modify search results
196 | add_filter('wpnlweb_search_results', function($results, $query) {
197 | // Custom logic here
198 | return $results;
199 | }, 10, 2);
200 |
201 | // Customize API response
202 | add_filter('wpnlweb_api_response', function($response, $question) {
203 | $response['custom_field'] = 'custom_value';
204 | return $response;
205 | }, 10, 2);
206 |
207 | // Add custom post types to search
208 | add_filter('wpnlweb_searchable_post_types', function($post_types) {
209 | $post_types[] = 'product';
210 | $post_types[] = 'event';
211 | return $post_types;
212 | });
213 | ```
214 |
215 | ### Theme Integration
216 |
217 | ```php
218 | // In your theme's functions.php
219 | function custom_wpnlweb_styling() {
220 | wp_add_inline_style('wpnlweb-public', '
221 | .wpnlweb-search-container {
222 | max-width: 800px;
223 | margin: 2rem auto;
224 | }
225 | .wpnlweb-search-form {
226 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
227 | border-radius: 12px;
228 | padding: 2rem;
229 | }
230 | ');
231 | }
232 | add_action('wp_enqueue_scripts', 'custom_wpnlweb_styling');
233 | ```
234 |
235 | ## 🧪 Testing
236 |
237 | ### Running Tests
238 |
239 | ```bash
240 | # Run all tests
241 | composer run test
242 |
243 | # Run specific test suites
244 | composer run test:unit
245 | composer run test:integration
246 |
247 | # Test API endpoints
248 | php debug-api-test.php
249 | ```
250 |
251 | ### Test Coverage
252 |
253 | - ✅ **API Functionality**: 100% (26/26 tests)
254 | - ✅ **WordPress.org Compliance**: 96.8% (61/63 tests)
255 | - ✅ **Security**: 100% (All vulnerabilities resolved)
256 | - ✅ **Performance**: Optimized (<500ms response time)
257 |
258 | ## 🔒 Security
259 |
260 | ### Implemented Protections
261 |
262 | - **Input Sanitization**: All user inputs sanitized using WordPress functions
263 | - **Output Escaping**: All outputs properly escaped
264 | - **ABSPATH Protection**: Direct file access prevention
265 | - **Nonce Verification**: CSRF protection for admin forms
266 | - **Rate Limiting**: API endpoint protection
267 | - **CORS Headers**: Controlled cross-origin access
268 |
269 | ### Reporting Security Issues
270 |
271 | Please see [SECURITY.md](SECURITY.md) for our security policy and how to report vulnerabilities.
272 |
273 | ## 🌐 AI Agent Integration
274 |
275 | ### ChatGPT Integration
276 |
277 | ```javascript
278 | // Custom GPT Instructions
279 | You can query WordPress sites with WPNLWeb by sending POST requests to:
280 | https://SITE_URL/wp-json/nlweb/v1/ask
281 |
282 | Send questions in this format:
283 | {
284 | "question": "What are your latest blog posts about AI?",
285 | "context": {
286 | "post_type": "post",
287 | "limit": 5
288 | }
289 | }
290 | ```
291 |
292 | ### Claude/Anthropic Integration
293 |
294 | ```python
295 | import requests
296 |
297 | def query_wordpress_site(site_url, question):
298 | response = requests.post(
299 | f"{site_url}/wp-json/nlweb/v1/ask",
300 | json={"question": question},
301 | headers={"Content-Type": "application/json"}
302 | )
303 | return response.json()
304 |
305 | # Usage
306 | results = query_wordpress_site(
307 | "https://example.com",
308 | "What services do you offer?"
309 | )
310 | ```
311 |
312 | ## 🤝 Contributing
313 |
314 | We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details.
315 |
316 | ### Development Workflow
317 |
318 | 1. **Fork** the repository
319 | 2. **Clone** your fork locally
320 | 3. **Create** a feature branch: `git checkout -b feature/amazing-feature`
321 | 4. **Make** your changes and add tests
322 | 5. **Run** tests: `composer run test`
323 | 6. **Check** code standards: `composer run lint`
324 | 7. **Commit** your changes: `git commit -m 'Add amazing feature'`
325 | 8. **Push** to your fork: `git push origin feature/amazing-feature`
326 | 9. **Submit** a Pull Request
327 |
328 | ### Code Standards
329 |
330 | - Follow [WordPress PHP Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/)
331 | - Write comprehensive PHPDoc comments
332 | - Include unit tests for new functionality
333 | - Ensure all tests pass before submitting PR
334 |
335 | ## 📖 Documentation
336 |
337 | - **[Installation Guide](INSTALL.txt)** - Detailed installation instructions
338 | - **[Testing Guide](TESTING_GUIDE.md)** - Comprehensive testing procedures
339 | - **[API Reference](docs/api.md)** - Complete API documentation
340 | - **[Hooks Reference](docs/hooks.md)** - WordPress hooks and filters
341 | - **[Customization Guide](docs/customization.md)** - Theme and styling options
342 |
343 | ## 🗺️ Roadmap
344 |
345 | ### Version 1.1 (Planned)
346 |
347 | - [ ] Advanced search filters and faceting
348 | - [ ] Qdrant Vector Embeddings
349 | - [ ] Real-time search suggestions
350 | - [ ] Search analytics dashboard
351 | - [ ] Multi-language support improvements
352 |
353 | ### Version 1.2 (Planned)
354 |
355 | - [ ] Custom AI model integration
356 | - [ ] LanceDB, Milvus integration
357 | - [ ] Advanced caching mechanisms
358 | - [ ] Elasticsearch integration
359 | - [ ] GraphQL endpoint support
360 |
361 | ### Version 2.0 (Future)
362 |
363 | - [ ] Machine learning-powered relevance scoring
364 | - [ ] Voice search capabilities
365 | - [ ] Advanced AI agent tools
366 | - [ ] Enterprise features
367 |
368 | ## 📄 License
369 |
370 | This project is licensed under the GPL v2 or later - see the [LICENSE.txt](LICENSE.txt) file for details.
371 |
372 | ## 🙏 Acknowledgments
373 |
374 | - **Microsoft** for the NLWeb Protocol specification
375 | - **WordPress Community** for coding standards and best practices
376 | - **Schema.org** for structured data standards
377 | - **Contributors** who have helped improve this plugin
378 |
379 | ## 📞 Support
380 |
381 | - **Documentation**: [Official Docs](https://wpnlweb.com/docs)
382 | - **WordPress.org**: [Plugin Support Forum](https://wordpress.org/support/plugin/wpnlweb/)
383 | - **GitHub Issues**: [Report Bugs](https://github.com/wpnlweb/wpnlweb/issues)
384 | - **Email**: [hey@wpnlweb.com](mailto:hey@wpnlweb.com)
385 |
386 | ---
387 |
388 |
389 |
390 | **[Website](https://wpnlweb.com)** • **[Documentation](https://wpnlweb.com/docs)** • **[WordPress.org](https://wordpress.org/plugins/wpnlweb/)** • **[Support](https://wordpress.org/support/plugin/wpnlweb/)**
391 |
392 | Made with ❤️ by [wpnlweb](https://wpnlweb.com)
393 |
394 |
395 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # 📚 WPNLWeb API Reference
2 |
3 | Complete documentation for the WPNLWeb Plugin REST API and integration methods.
4 |
5 | ## 🚀 Quick Start
6 |
7 | The WPNLWeb plugin provides a RESTful API endpoint that allows AI agents and applications to query WordPress content using natural language.
8 |
9 | **Base URL:** `https://yoursite.com/wp-json/nlweb/v1/`
10 |
11 | ## 🔌 REST API Endpoints
12 |
13 | ### POST /ask
14 |
15 | Ask natural language questions about your WordPress content.
16 |
17 | **Endpoint:** `POST /wp-json/nlweb/v1/ask`
18 |
19 | #### Authentication
20 |
21 | - **Public Access:** No authentication required
22 | - **CORS Support:** Enabled for AI agents and browser applications
23 | - **Rate Limiting:** Not implemented (consider adding for production)
24 |
25 | #### Request Format
26 |
27 | ```json
28 | {
29 | "question": "string (required)",
30 | "context": {
31 | "post_type": "string|array (optional)",
32 | "category": "string (optional)",
33 | "limit": "integer (optional, default: 10)",
34 | "meta_query": "object (optional)"
35 | }
36 | }
37 | ```
38 |
39 | #### Request Parameters
40 |
41 | | Parameter | Type | Required | Description |
42 | | -------------------- | ------------ | -------- | -------------------------------------------------------------- |
43 | | `question` | string | Yes | Natural language question to ask |
44 | | `context` | object | No | Additional query context and filters |
45 | | `context.post_type` | string/array | No | WordPress post type(s) to search (default: `["post", "page"]`) |
46 | | `context.category` | string | No | Category slug to filter by |
47 | | `context.limit` | integer | No | Maximum number of results (default: 10) |
48 | | `context.meta_query` | object | No | WordPress meta query parameters |
49 |
50 | #### Response Format
51 |
52 | The API returns Schema.org compliant responses:
53 |
54 | ```json
55 | {
56 | "@context": "https://schema.org",
57 | "@type": "SearchResultsPage",
58 | "query": "What is this website about?",
59 | "totalResults": 3,
60 | "processingTime": "0.245s",
61 | "items": [
62 | {
63 | "@type": "Article",
64 | "@id": "https://yoursite.com/about/",
65 | "name": "About Us",
66 | "description": "Learn about our company mission and values...",
67 | "url": "https://yoursite.com/about/",
68 | "datePublished": "2024-01-15T10:30:00Z",
69 | "dateModified": "2024-01-20T14:15:00Z",
70 | "author": {
71 | "@type": "Person",
72 | "name": "John Doe"
73 | },
74 | "keywords": ["about", "company", "mission"],
75 | "relevanceScore": 0.95
76 | }
77 | ]
78 | }
79 | ```
80 |
81 | #### Response Fields
82 |
83 | | Field | Type | Description |
84 | | ------------------------ | ------- | ------------------------------------- |
85 | | `@context` | string | Schema.org context URL |
86 | | `@type` | string | Schema.org type (SearchResultsPage) |
87 | | `query` | string | Original question asked |
88 | | `totalResults` | integer | Number of results found |
89 | | `processingTime` | string | Time taken to process request |
90 | | `items` | array | Array of search results |
91 | | `items[].@type` | string | Schema.org type (Article, Page, etc.) |
92 | | `items[].@id` | string | Canonical URL of the content |
93 | | `items[].name` | string | Title of the content |
94 | | `items[].description` | string | Excerpt or summary |
95 | | `items[].url` | string | Public URL |
96 | | `items[].datePublished` | string | Publication date (ISO 8601) |
97 | | `items[].dateModified` | string | Last modified date (ISO 8601) |
98 | | `items[].author` | object | Author information |
99 | | `items[].keywords` | array | Extracted keywords |
100 | | `items[].relevanceScore` | float | Relevance score (0-1) |
101 |
102 | #### Error Responses
103 |
104 | ```json
105 | {
106 | "code": "missing_question",
107 | "message": "Question parameter required",
108 | "data": {
109 | "status": 400
110 | }
111 | }
112 | ```
113 |
114 | Common error codes:
115 |
116 | - `missing_question` - No question parameter provided
117 | - `invalid_context` - Invalid context parameters
118 | - `search_failed` - Internal search error
119 |
120 | ## 🤖 AI Agent Integration
121 |
122 | ### ChatGPT Integration
123 |
124 | ```javascript
125 | // Custom GPT Instructions
126 | const wpnlwebQuery = async (siteUrl, question, context = {}) => {
127 | const response = await fetch(`${siteUrl}/wp-json/nlweb/v1/ask`, {
128 | method: "POST",
129 | headers: {
130 | "Content-Type": "application/json",
131 | },
132 | body: JSON.stringify({
133 | question: question,
134 | context: context,
135 | }),
136 | });
137 |
138 | return response.json();
139 | };
140 |
141 | // Usage
142 | const results = await wpnlwebQuery(
143 | "https://example.com",
144 | "What are your latest blog posts?",
145 | { post_type: "post", limit: 5 }
146 | );
147 | ```
148 |
149 | ### Claude/Anthropic Integration
150 |
151 | ```python
152 | import requests
153 |
154 | class WPNLWebClient:
155 | def __init__(self, base_url):
156 | self.base_url = base_url.rstrip('/')
157 |
158 | def ask(self, question, context=None):
159 | """Query WordPress site via natural language"""
160 | endpoint = f"{self.base_url}/wp-json/nlweb/v1/ask"
161 |
162 | payload = {"question": question}
163 | if context:
164 | payload["context"] = context
165 |
166 | response = requests.post(
167 | endpoint,
168 | json=payload,
169 | headers={"Content-Type": "application/json"}
170 | )
171 |
172 | return response.json()
173 |
174 | # Usage
175 | client = WPNLWebClient("https://example.com")
176 | results = client.ask(
177 | "What services do you offer?",
178 | context={"post_type": "service", "limit": 10}
179 | )
180 | ```
181 |
182 | ### curl Examples
183 |
184 | ```bash
185 | # Basic question
186 | curl -X POST https://example.com/wp-json/nlweb/v1/ask \
187 | -H "Content-Type: application/json" \
188 | -d '{"question": "What is this website about?"}'
189 |
190 | # With context filters
191 | curl -X POST https://example.com/wp-json/nlweb/v1/ask \
192 | -H "Content-Type: application/json" \
193 | -d '{
194 | "question": "Show me recent tutorials",
195 | "context": {
196 | "post_type": "post",
197 | "category": "tutorials",
198 | "limit": 5
199 | }
200 | }'
201 |
202 | # Search specific post types
203 | curl -X POST https://example.com/wp-json/nlweb/v1/ask \
204 | -H "Content-Type: application/json" \
205 | -d '{
206 | "question": "What products do you sell?",
207 | "context": {
208 | "post_type": ["product", "service"],
209 | "limit": 10
210 | }
211 | }'
212 | ```
213 |
214 | ## 🔄 AJAX Endpoints
215 |
216 | ### WordPress AJAX Handler
217 |
218 | **Endpoint:** `admin-ajax.php?action=wpnlweb_search`
219 |
220 | Used internally by the frontend shortcode for same-origin requests.
221 |
222 | #### Parameters
223 |
224 | | Parameter | Type | Required | Description |
225 | | --------------- | ------- | -------- | ----------------------------- |
226 | | `action` | string | Yes | Must be `wpnlweb_search` |
227 | | `question` | string | Yes | The question to ask |
228 | | `max_results` | integer | No | Maximum results (default: 10) |
229 | | `wpnlweb_nonce` | string | Yes | WordPress nonce for security |
230 |
231 | #### Response
232 |
233 | Returns the same Schema.org format as the REST API but wrapped in a WordPress AJAX response.
234 |
235 | ## 🚦 Rate Limiting & Performance
236 |
237 | ### Current Implementation
238 |
239 | - **No Rate Limiting:** Currently not implemented
240 | - **Caching:** Not implemented (consider adding)
241 | - **Response Time:** Typically < 500ms
242 |
243 | ### Recommended Production Settings
244 |
245 | ```php
246 | // Add to your theme's functions.php or plugin
247 | add_filter('wpnlweb_rate_limit', function($limit) {
248 | return 60; // 60 requests per hour per IP
249 | });
250 |
251 | add_filter('wpnlweb_cache_timeout', function($timeout) {
252 | return 300; // Cache for 5 minutes
253 | });
254 | ```
255 |
256 | ## 🔒 Security Considerations
257 |
258 | ### CORS Headers
259 |
260 | The plugin automatically adds CORS headers for AI agent compatibility:
261 |
262 | ```
263 | Access-Control-Allow-Origin: *
264 | Access-Control-Allow-Methods: POST, GET, OPTIONS
265 | Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With
266 | ```
267 |
268 | ### Input Sanitization
269 |
270 | All inputs are sanitized using WordPress functions:
271 |
272 | - `sanitize_text_field()` for question parameter
273 | - `intval()` for numeric parameters
274 | - `array_map()` for array sanitization
275 |
276 | ### Recommended Security Enhancements
277 |
278 | ```php
279 | // Restrict access to specific domains
280 | add_filter('wpnlweb_allowed_origins', function($origins) {
281 | return ['https://youragent.com', 'https://trusted-ai.com'];
282 | });
283 |
284 | // Add API key authentication
285 | add_filter('wpnlweb_require_auth', '__return_true');
286 | ```
287 |
288 | ## 🧪 Testing & Debugging
289 |
290 | ### Test the API
291 |
292 | ```bash
293 | # Test connectivity
294 | curl -I https://yoursite.com/wp-json/nlweb/v1/ask
295 |
296 | # Test basic functionality
297 | curl -X POST https://yoursite.com/wp-json/nlweb/v1/ask \
298 | -H "Content-Type: application/json" \
299 | -d '{"question": "test"}' \
300 | | jq .
301 | ```
302 |
303 | ### Debug Mode
304 |
305 | Add to `wp-config.php`:
306 |
307 | ```php
308 | define('WPNLWEB_DEBUG', true);
309 | ```
310 |
311 | This enables additional logging and error reporting.
312 |
313 | ### WordPress Debug Info
314 |
315 | ```php
316 | // Check if plugin is active
317 | if (class_exists('Wpnlweb_Server')) {
318 | echo "WPNLWeb plugin is active";
319 | }
320 |
321 | // Test API endpoint programmatically
322 | $request = new WP_REST_Request('POST');
323 | $request->set_param('question', 'test question');
324 |
325 | $server = new Wpnlweb_Server('wpnlweb', WPNLWEB_VERSION);
326 | $response = $server->handle_ask($request);
327 | ```
328 |
329 | ## 📊 Response Examples
330 |
331 | ### Successful Search
332 |
333 | ```json
334 | {
335 | "@context": "https://schema.org",
336 | "@type": "SearchResultsPage",
337 | "query": "latest blog posts about WordPress",
338 | "totalResults": 3,
339 | "processingTime": "0.182s",
340 | "items": [
341 | {
342 | "@type": "Article",
343 | "@id": "https://example.com/wordpress-security-guide/",
344 | "name": "Complete WordPress Security Guide 2024",
345 | "description": "Learn the essential security practices to protect your WordPress site from threats and vulnerabilities...",
346 | "url": "https://example.com/wordpress-security-guide/",
347 | "datePublished": "2024-01-20T10:00:00Z",
348 | "dateModified": "2024-01-21T14:30:00Z",
349 | "author": {
350 | "@type": "Person",
351 | "name": "Jane Smith"
352 | }
353 | }
354 | ]
355 | }
356 | ```
357 |
358 | ### Empty Results
359 |
360 | ```json
361 | {
362 | "@context": "https://schema.org",
363 | "@type": "SearchResultsPage",
364 | "query": "nonexistent content",
365 | "totalResults": 0,
366 | "processingTime": "0.045s",
367 | "items": []
368 | }
369 | ```
370 |
371 | ### Error Response
372 |
373 | ```json
374 | {
375 | "code": "missing_question",
376 | "message": "Question parameter required",
377 | "data": {
378 | "status": 400
379 | }
380 | }
381 | ```
382 |
383 | ## 🔗 Related Documentation
384 |
385 | - [Hooks Reference](hooks.md) - WordPress filters and actions
386 | - [Customization Guide](customization.md) - Theming and styling
387 | - [WordPress REST API](https://developer.wordpress.org/rest-api/) - Official WordPress REST API docs
388 | - [Schema.org](https://schema.org/) - Structured data standards
389 |
--------------------------------------------------------------------------------
/admin/js/wpnlweb-admin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WPNLWeb Admin JavaScript
3 | *
4 | * Handles tabbed interface, color picker, live preview,
5 | * and all interactive functionality for the admin settings page
6 | *
7 | * @package Wpnlweb
8 | * @subpackage Wpnlweb/admin/js
9 | * @since 1.0.0
10 | */
11 |
12 | (function ($) {
13 | "use strict";
14 |
15 | /**
16 | * All of the code for your admin-facing JavaScript source
17 | * should reside in this file.
18 | *
19 | * Note: It has been assumed you will write jQuery code here, so the
20 | * $ function reference has been prepared for usage within the scope
21 | * of this function.
22 | *
23 | * This enables you to define handlers, for when the DOM is ready:
24 | *
25 | * $(function() {
26 | *
27 | * });
28 | *
29 | * When the window is loaded:
30 | *
31 | * $( window ).load(function() {
32 | *
33 | * });
34 | *
35 | * ...and/or other possibilities.
36 | *
37 | * Ideally, it is not considered best practise to attach more than a
38 | * single DOM-ready or window-load handler for a particular page.
39 | * Although scripts in the WordPress core, Plugins and Themes may be
40 | * practising this, we should strive to set a better example in our own work.
41 | */
42 |
43 | /**
44 | * Initialize admin functionality when DOM is ready
45 | */
46 | $(document).ready(function () {
47 | initTabNavigation();
48 | initColorPicker();
49 | initCSSEditor();
50 | initLivePreview();
51 | });
52 |
53 | /**
54 | * Initialize tab navigation
55 | */
56 | function initTabNavigation() {
57 | $(".wpnlweb-nav-item").on("click", function (e) {
58 | e.preventDefault();
59 |
60 | const tabId = $(this).data("tab");
61 |
62 | // Update navigation active state
63 | $(".wpnlweb-nav-item").removeClass("active");
64 | $(this).addClass("active");
65 |
66 | // Update tab content visibility
67 | $(".wpnlweb-tab-content").removeClass("active");
68 | $("#" + tabId + "-tab").addClass("active");
69 |
70 | // Update URL hash for bookmarking
71 | window.location.hash = tabId;
72 | });
73 |
74 | // Check for hash on page load
75 | const hash = window.location.hash.substring(1);
76 | if (hash && $('.wpnlweb-nav-item[data-tab="' + hash + '"]').length) {
77 | $('.wpnlweb-nav-item[data-tab="' + hash + '"]').click();
78 | }
79 | }
80 |
81 | /**
82 | * Initialize color picker functionality
83 | */
84 | function initColorPicker() {
85 | const $colorPicker = $("#wpnlweb_primary_color");
86 | const $colorText = $("#wpnlweb_primary_color_text");
87 | const $presetColors = $(".wpnlweb-preset-color");
88 |
89 | // Sync color picker with text input
90 | $colorPicker.on("change", function () {
91 | const color = $(this).val();
92 | $colorText.val(color);
93 | updatePresetSelection(color);
94 | updateLivePreviewColor(color);
95 | });
96 |
97 | // Sync text input with color picker
98 | $colorText.on("change keyup", function () {
99 | const color = $(this).val();
100 | if (isValidHexColor(color)) {
101 | $colorPicker.val(color);
102 | updatePresetSelection(color);
103 | updateLivePreviewColor(color);
104 | }
105 | });
106 |
107 | // Handle preset color clicks
108 | $presetColors.on("click", function (e) {
109 | e.preventDefault();
110 | const color = $(this).data("color");
111 | $colorPicker.val(color);
112 | $colorText.val(color);
113 | updatePresetSelection(color);
114 | updateLivePreviewColor(color);
115 | });
116 |
117 | /**
118 | * Update preset color selection
119 | */
120 | function updatePresetSelection(selectedColor) {
121 | $presetColors.removeClass("active");
122 | $presetColors
123 | .filter('[data-color="' + selectedColor + '"]')
124 | .addClass("active");
125 | }
126 |
127 | /**
128 | * Update live preview color
129 | */
130 | function updateLivePreviewColor(color) {
131 | $(".wpnlweb-preview-button").css("background-color", color);
132 |
133 | // Update CSS custom property for real-time preview
134 | $(":root").css("--wpnlweb-primary-color", color);
135 | }
136 |
137 | /**
138 | * Validate hex color format
139 | */
140 | function isValidHexColor(color) {
141 | return /^#[0-9A-F]{6}$/i.test(color);
142 | }
143 |
144 | // Initialize preset selection on load
145 | updatePresetSelection($colorPicker.val());
146 | }
147 |
148 | /**
149 | * Initialize CSS editor functionality
150 | */
151 | function initCSSEditor() {
152 | const $cssEditor = $("#wpnlweb_custom_css");
153 | const $copyButton = $("#wpnlweb-copy-example");
154 | const $resetButton = $("#wpnlweb-reset-css");
155 |
156 | // Copy example CSS
157 | $copyButton.on("click", function () {
158 | const exampleCSS = `.wpnlweb-search-container { border-radius: 20px; }
159 | .wpnlweb-search-button { background: var(--wpnlweb-primary-color); }
160 | .wpnlweb-search-input { border-color: var(--wpnlweb-primary-color); }`;
161 |
162 | $cssEditor.val(exampleCSS);
163 | $cssEditor.focus();
164 |
165 | // Show temporary success message
166 | showTemporaryMessage($copyButton, "✅ Copied!", 2000);
167 | });
168 |
169 | // Reset CSS
170 | $resetButton.on("click", function () {
171 | if (confirm("Are you sure you want to clear all custom CSS?")) {
172 | $cssEditor.val("");
173 | $cssEditor.focus();
174 | showTemporaryMessage($resetButton, "✅ Reset!", 2000);
175 | }
176 | });
177 |
178 | // Add syntax highlighting hints (basic)
179 | $cssEditor.on("input", function () {
180 | const content = $(this).val();
181 |
182 | // Simple validation
183 | if (content.includes("{") && !content.includes("}")) {
184 | $(this).css("border-color", "#f59e0b"); // Warning orange
185 | } else {
186 | $(this).css("border-color", ""); // Reset to default
187 | }
188 | });
189 |
190 | /**
191 | * Show temporary message on button
192 | */
193 | function showTemporaryMessage($button, message, duration) {
194 | const originalText = $button.html();
195 | $button.html(message);
196 | $button.prop("disabled", true);
197 |
198 | setTimeout(function () {
199 | $button.html(originalText);
200 | $button.prop("disabled", false);
201 | }, duration);
202 | }
203 | }
204 |
205 | /**
206 | * Initialize live preview functionality
207 | */
208 | function initLivePreview() {
209 | const $livePreview = $("#wpnlweb-live-preview");
210 | const $themeMode = $("#wpnlweb_theme_mode");
211 | const $refreshButton = $("#wpnlweb-refresh-preview");
212 | let previewLoaded = false;
213 |
214 | // Update preview when theme mode changes
215 | $themeMode.on("change", function () {
216 | if (previewLoaded) {
217 | updateLivePreview();
218 | }
219 | });
220 |
221 | // Handle refresh button click
222 | $refreshButton.on("click", function (e) {
223 | e.preventDefault();
224 | updateLivePreview();
225 | });
226 |
227 | /**
228 | * Update live preview via AJAX
229 | */
230 | function updateLivePreview() {
231 | const formData = {
232 | action: "wpnlweb_preview_shortcode",
233 | nonce: wpnlweb_admin.nonce,
234 | theme_mode: $("#wpnlweb_theme_mode").val(),
235 | primary_color: $("#wpnlweb_primary_color").val(),
236 | custom_css: $("#wpnlweb_custom_css").val(),
237 | };
238 |
239 | // Show loading state
240 | $livePreview.html(
241 | ' Loading live preview...
'
242 | );
243 |
244 | // Disable refresh button during load
245 | $refreshButton.prop("disabled", true);
246 |
247 | $.post(wpnlweb_admin.ajax_url, formData)
248 | .done(function (response) {
249 | if (response.success && response.data.html) {
250 | $livePreview.html(response.data.html);
251 | previewLoaded = true;
252 |
253 | // Show success message briefly
254 | showTemporaryMessage($refreshButton, "✅ Updated!", 2000);
255 | } else {
256 | $livePreview.html(
257 | '❌ Preview failed to load. Please check your settings and try again.
'
258 | );
259 | }
260 | })
261 | .fail(function (xhr, status, error) {
262 | console.error("Preview AJAX Error:", status, error);
263 | $livePreview.html(
264 | '❌ Connection error. Please check your network and try again.
'
265 | );
266 | })
267 | .always(function () {
268 | // Re-enable refresh button
269 | $refreshButton.prop("disabled", false);
270 | });
271 | }
272 |
273 | // Load preview when switching to live preview tab (first time only)
274 | $('.wpnlweb-nav-item[data-tab="live-preview"]').on("click", function () {
275 | if (!previewLoaded) {
276 | setTimeout(updateLivePreview, 100); // Small delay to ensure tab is visible
277 | }
278 | });
279 |
280 | // Auto-refresh when primary color changes (with debouncing)
281 | let colorChangeTimeout;
282 | $("#wpnlweb_primary_color, #wpnlweb_primary_color_text").on(
283 | "change",
284 | function () {
285 | if (previewLoaded) {
286 | clearTimeout(colorChangeTimeout);
287 | colorChangeTimeout = setTimeout(function () {
288 | updateLivePreview();
289 | }, 1000); // Wait 1 second after last change
290 | }
291 | }
292 | );
293 |
294 | /**
295 | * Show temporary message on button
296 | */
297 | function showTemporaryMessage($button, message, duration) {
298 | const originalText = $button.html();
299 | $button.html(message);
300 | $button.prop("disabled", true);
301 |
302 | setTimeout(function () {
303 | $button.html(originalText);
304 | $button.prop("disabled", false);
305 | }, duration);
306 | }
307 | }
308 |
309 | /**
310 | * Handle form submission with validation
311 | */
312 | $("#wpnlweb-settings-form").on("submit", function (e) {
313 | const $form = $(this);
314 | const $submitButton = $form.find(".wpnlweb-button-primary");
315 |
316 | // Validate color field
317 | const colorValue = $("#wpnlweb_primary_color_text").val();
318 | if (colorValue && !/^#[0-9A-F]{6}$/i.test(colorValue)) {
319 | e.preventDefault();
320 | alert("Please enter a valid hex color (e.g., #3b82f6)");
321 | $("#wpnlweb_primary_color_text").focus();
322 | return;
323 | }
324 |
325 | // Show saving state
326 | const originalText = $submitButton.html();
327 | $submitButton.html("💾 Saving...");
328 | $submitButton.prop("disabled", true);
329 |
330 | // Re-enable after form submission (in case of errors)
331 | setTimeout(function () {
332 | $submitButton.html(originalText);
333 | $submitButton.prop("disabled", false);
334 | }, 3000);
335 | });
336 |
337 | /**
338 | * Add keyboard shortcuts
339 | */
340 | $(document).on("keydown", function (e) {
341 | // Ctrl/Cmd + S to save
342 | if ((e.ctrlKey || e.metaKey) && e.which === 83) {
343 | e.preventDefault();
344 | $("#wpnlweb-settings-form").submit();
345 | }
346 |
347 | // Tab navigation with keyboard
348 | if (e.altKey) {
349 | switch (e.which) {
350 | case 49: // Alt + 1
351 | $('.wpnlweb-nav-item[data-tab="theme"]').click();
352 | break;
353 | case 50: // Alt + 2
354 | $('.wpnlweb-nav-item[data-tab="custom-css"]').click();
355 | break;
356 | case 51: // Alt + 3
357 | $('.wpnlweb-nav-item[data-tab="live-preview"]').click();
358 | break;
359 | }
360 | }
361 | });
362 |
363 | /**
364 | * Add smooth animations
365 | */
366 | function initAnimations() {
367 | // Fade in settings groups on tab switch
368 | $(".wpnlweb-nav-item").on("click", function () {
369 | const tabId = $(this).data("tab");
370 | const $tabContent = $("#" + tabId + "-tab");
371 |
372 | $tabContent.css("opacity", "0").animate({ opacity: 1 }, 300);
373 | });
374 |
375 | // Add hover effects to interactive elements
376 | $(".wpnlweb-preset-color").hover(
377 | function () {
378 | $(this).css("transform", "scale(1.1)");
379 | },
380 | function () {
381 | if (!$(this).hasClass("active")) {
382 | $(this).css("transform", "scale(1)");
383 | }
384 | }
385 | );
386 | }
387 |
388 | // Initialize animations
389 | initAnimations();
390 |
391 | /**
392 | * Auto-save functionality (optional)
393 | */
394 | function initAutoSave() {
395 | let autoSaveTimeout;
396 |
397 | $("input, textarea, select").on("change", function () {
398 | clearTimeout(autoSaveTimeout);
399 |
400 | autoSaveTimeout = setTimeout(function () {
401 | // Show auto-save indicator
402 | const $indicator = $(
403 | '💾 Auto-saved
'
404 | );
405 | $("body").append($indicator);
406 |
407 | setTimeout(function () {
408 | $indicator.fadeOut(function () {
409 | $(this).remove();
410 | });
411 | }, 2000);
412 | }, 5000); // Auto-save after 5 seconds of inactivity
413 | });
414 | }
415 |
416 | // Uncomment to enable auto-save
417 | // initAutoSave();
418 | })(jQuery);
419 |
420 | /**
421 | * Additional CSS for loading and error states
422 | */
423 | jQuery(document).ready(function ($) {
424 | // Add loading and error styles dynamically
425 | const styles = `
426 |
455 | `;
456 |
457 | $("head").append(styles);
458 | });
459 |
--------------------------------------------------------------------------------
/includes/features/class-wpnlweb-feature-gates.php:
--------------------------------------------------------------------------------
1 | license_manager = $license_manager;
64 | $this->registry = $registry;
65 | $this->setup_hooks();
66 | }
67 |
68 | /**
69 | * Setup WordPress hooks.
70 | *
71 | * @since 1.1.0
72 | * @access private
73 | */
74 | private function setup_hooks() {
75 | // Feature access hooks.
76 | add_filter( 'wpnlweb_can_access_feature', array( $this, 'check_feature_access' ), 10, 2 );
77 | add_action( 'wpnlweb_feature_access_denied', array( $this, 'handle_access_denied' ), 10, 3 );
78 |
79 | // Admin hooks for feature management.
80 | add_action( 'admin_init', array( $this, 'register_feature_capabilities' ) );
81 | add_filter( 'user_has_cap', array( $this, 'filter_user_capabilities' ), 10, 4 );
82 |
83 | // AJAX hooks for feature validation.
84 | add_action( 'wp_ajax_wpnlweb_validate_feature', array( $this, 'ajax_validate_feature' ) );
85 | add_action( 'wp_ajax_nopriv_wpnlweb_validate_feature', array( $this, 'ajax_validate_feature' ) );
86 | }
87 |
88 | /**
89 | * Check if current user can access specific feature.
90 | *
91 | * @since 1.1.0
92 | * @param string $feature Feature identifier to check.
93 | * @param int $user_id Optional. User ID to check. Defaults to current user.
94 | * @return bool True if user can access feature.
95 | */
96 | public function can_access_feature( $feature, $user_id = null ) {
97 | if ( null === $user_id ) {
98 | $user_id = get_current_user_id();
99 | }
100 |
101 | // Check cache first.
102 | $cache_key = $feature . '_' . $user_id;
103 | if ( isset( $this->denied_cache[ $cache_key ] ) ) {
104 | return ! $this->denied_cache[ $cache_key ];
105 | }
106 |
107 | // Check WordPress capability first.
108 | if ( ! $this->check_capability( $feature, $user_id ) ) {
109 | $this->denied_cache[ $cache_key ] = true;
110 | return false;
111 | }
112 |
113 | // Check license tier access.
114 | $has_access = $this->license_manager->validate_feature_access( $feature );
115 |
116 | if ( ! $has_access ) {
117 | $this->denied_cache[ $cache_key ] = true;
118 |
119 | /**
120 | * Fires when feature access is denied.
121 | *
122 | * @since 1.1.0
123 | * @param string $feature Feature that was denied.
124 | * @param int $user_id User ID that was denied.
125 | * @param string $reason Reason for denial.
126 | */
127 | do_action( 'wpnlweb_feature_access_denied', $feature, $user_id, 'license_tier' );
128 | }
129 |
130 | return $has_access;
131 | }
132 |
133 | /**
134 | * Require feature access or die with error.
135 | *
136 | * @since 1.1.0
137 | * @param string $feature Feature identifier to require.
138 | * @param string $message Optional. Custom error message.
139 | */
140 | public function require_feature_access( $feature, $message = '' ) {
141 | if ( ! $this->can_access_feature( $feature ) ) {
142 | if ( empty( $message ) ) {
143 | $feature_info = $this->registry->get_feature_info( $feature );
144 | $required_tier = isset( $feature_info['required_tier'] ) ? $feature_info['required_tier'] : 'pro';
145 |
146 | $message = sprintf(
147 | /* translators: %1$s: feature name, %2$s: required tier */
148 | __( 'This feature (%1$s) requires a %2$s license or higher.', 'wpnlweb' ),
149 | $feature_info['name'] ?? $feature,
150 | ucfirst( $required_tier )
151 | );
152 | }
153 |
154 | wp_die(
155 | esc_html( $message ),
156 | esc_html__( 'Feature Access Denied', 'wpnlweb' ),
157 | array( 'response' => 403 )
158 | );
159 | }
160 | }
161 |
162 | /**
163 | * Get upgrade prompt for denied feature.
164 | *
165 | * @since 1.1.0
166 | * @param string $feature Feature identifier.
167 | * @return array Upgrade prompt information.
168 | */
169 | public function get_upgrade_prompt( $feature ) {
170 | $feature_info = $this->registry->get_feature_info( $feature );
171 | $required_tier = isset( $feature_info['required_tier'] ) ? $feature_info['required_tier'] : 'pro';
172 | $current_tier = $this->license_manager->get_tier();
173 |
174 | return array(
175 | 'feature' => $feature,
176 | 'feature_name' => $feature_info['name'] ?? $feature,
177 | 'current_tier' => $current_tier,
178 | 'required_tier' => $required_tier,
179 | 'upgrade_url' => $this->get_upgrade_url( $required_tier ),
180 | 'message' => $this->get_upgrade_message( $feature, $required_tier ),
181 | );
182 | }
183 |
184 | /**
185 | * Display upgrade notice for feature.
186 | *
187 | * @since 1.1.0
188 | * @param string $feature Feature identifier.
189 | * @param array $args Optional. Display arguments.
190 | */
191 | public function display_upgrade_notice( $feature, $args = array() ) {
192 | $prompt = $this->get_upgrade_prompt( $feature );
193 |
194 | $defaults = array(
195 | 'type' => 'notice',
196 | 'dismissible' => true,
197 | 'class' => 'wpnlweb-upgrade-notice',
198 | );
199 |
200 | $args = wp_parse_args( $args, $defaults );
201 |
202 | $notice_class = sprintf(
203 | 'notice notice-%s %s %s',
204 | esc_attr( $args['type'] ),
205 | $args['dismissible'] ? 'is-dismissible' : '',
206 | esc_attr( $args['class'] )
207 | );
208 |
209 | ?>
210 |
224 | registry->is_registered_feature( $feature ) ) {
244 | $allcaps[ $cap ] = $this->license_manager->validate_feature_access( $feature );
245 | }
246 | }
247 | }
248 |
249 | return $allcaps;
250 | }
251 |
252 | /**
253 | * Register feature capabilities with WordPress.
254 | *
255 | * @since 1.1.0
256 | */
257 | public function register_feature_capabilities() {
258 | $features = $this->registry->get_all_features();
259 |
260 | foreach ( $features as $feature => $info ) {
261 | $capability = 'wpnlweb_' . $feature;
262 |
263 | // Add capability to administrator role if not exists.
264 | $admin_role = get_role( 'administrator' );
265 | if ( $admin_role && ! $admin_role->has_cap( $capability ) ) {
266 | $admin_role->add_cap( $capability );
267 | }
268 | }
269 | }
270 |
271 | /**
272 | * Handle AJAX feature validation request.
273 | *
274 | * @since 1.1.0
275 | */
276 | public function ajax_validate_feature() {
277 | // Verify nonce.
278 | if ( ! wp_verify_nonce( $_POST['nonce'] ?? '', 'wpnlweb_feature_validation' ) ) {
279 | wp_die( esc_html__( 'Security check failed.', 'wpnlweb' ), '', array( 'response' => 403 ) );
280 | }
281 |
282 | $feature = sanitize_text_field( $_POST['feature'] ?? '' );
283 |
284 | if ( empty( $feature ) ) {
285 | wp_send_json_error( array( 'message' => __( 'Feature not specified.', 'wpnlweb' ) ) );
286 | }
287 |
288 | $can_access = $this->can_access_feature( $feature );
289 |
290 | if ( $can_access ) {
291 | wp_send_json_success( array( 'access' => true ) );
292 | } else {
293 | $prompt = $this->get_upgrade_prompt( $feature );
294 | wp_send_json_error( array(
295 | 'access' => false,
296 | 'prompt' => $prompt,
297 | ) );
298 | }
299 | }
300 |
301 | /**
302 | * Check WordPress capability for feature.
303 | *
304 | * @since 1.1.0
305 | * @access private
306 | * @param string $feature Feature identifier.
307 | * @param int $user_id User ID to check.
308 | * @return bool True if user has capability.
309 | */
310 | private function check_capability( $feature, $user_id ) {
311 | $capability = 'wpnlweb_' . $feature;
312 | return user_can( $user_id, $capability );
313 | }
314 |
315 | /**
316 | * Get upgrade URL for tier.
317 | *
318 | * @since 1.1.0
319 | * @access private
320 | * @param string $tier Required tier.
321 | * @return string Upgrade URL.
322 | */
323 | private function get_upgrade_url( $tier ) {
324 | // TODO: Replace with actual upgrade URL once EDD store is setup.
325 | $base_url = 'https://wpnlweb.com/pricing/';
326 |
327 | return add_query_arg( array(
328 | 'tier' => $tier,
329 | 'utm_source' => 'plugin',
330 | 'utm_medium' => 'upgrade_prompt',
331 | 'utm_campaign' => 'feature_gate',
332 | ), $base_url );
333 | }
334 |
335 | /**
336 | * Get upgrade message for feature.
337 | *
338 | * @since 1.1.0
339 | * @access private
340 | * @param string $feature Feature identifier.
341 | * @param string $required_tier Required tier.
342 | * @return string Upgrade message.
343 | */
344 | private function get_upgrade_message( $feature, $required_tier ) {
345 | $feature_info = $this->registry->get_feature_info( $feature );
346 | $feature_name = $feature_info['name'] ?? $feature;
347 |
348 | return sprintf(
349 | /* translators: %1$s: feature name, %2$s: required tier */
350 | __( 'The %1$s feature requires a %2$s license or higher. Upgrade now to unlock this powerful functionality!', 'wpnlweb' ),
351 | $feature_name,
352 | ucfirst( $required_tier )
353 | );
354 | }
355 |
356 | /**
357 | * Handle feature access denied event.
358 | *
359 | * @since 1.1.0
360 | * @param string $feature Feature that was denied.
361 | * @param int $user_id User ID that was denied.
362 | * @param string $reason Reason for denial.
363 | */
364 | public function handle_access_denied( $feature, $user_id, $reason ) {
365 | // Log access denial for analytics.
366 | error_log( sprintf(
367 | 'WPNLWeb: Feature access denied - Feature: %s, User: %d, Reason: %s',
368 | $feature,
369 | $user_id,
370 | $reason
371 | ) );
372 |
373 | // Track for conversion analytics.
374 | $this->track_upgrade_opportunity( $feature, $user_id, $reason );
375 | }
376 |
377 | /**
378 | * Track upgrade opportunity for analytics.
379 | *
380 | * @since 1.1.0
381 | * @access private
382 | * @param string $feature Feature that was denied.
383 | * @param int $user_id User ID that was denied.
384 | * @param string $reason Reason for denial.
385 | */
386 | private function track_upgrade_opportunity( $feature, $user_id, $reason ) {
387 | // Store upgrade opportunity for analytics.
388 | $opportunities = get_option( 'wpnlweb_upgrade_opportunities', array() );
389 |
390 | $opportunity = array(
391 | 'feature' => $feature,
392 | 'user_id' => $user_id,
393 | 'reason' => $reason,
394 | 'timestamp' => time(),
395 | 'site_url' => get_site_url(),
396 | );
397 |
398 | $opportunities[] = $opportunity;
399 |
400 | // Keep only last 100 opportunities to avoid database bloat.
401 | if ( count( $opportunities ) > 100 ) {
402 | $opportunities = array_slice( $opportunities, -100 );
403 | }
404 |
405 | update_option( 'wpnlweb_upgrade_opportunities', $opportunities );
406 | }
407 |
408 | /**
409 | * Get feature access statistics.
410 | *
411 | * @since 1.1.0
412 | * @return array Feature access statistics.
413 | */
414 | public function get_access_stats() {
415 | $opportunities = get_option( 'wpnlweb_upgrade_opportunities', array() );
416 | $stats = array(
417 | 'total_denials' => count( $opportunities ),
418 | 'features_denied' => array(),
419 | 'recent_denials' => 0,
420 | );
421 |
422 | $week_ago = time() - WEEK_IN_SECONDS;
423 |
424 | foreach ( $opportunities as $opportunity ) {
425 | $feature = $opportunity['feature'];
426 |
427 | if ( ! isset( $stats['features_denied'][ $feature ] ) ) {
428 | $stats['features_denied'][ $feature ] = 0;
429 | }
430 |
431 | $stats['features_denied'][ $feature ]++;
432 |
433 | if ( $opportunity['timestamp'] > $week_ago ) {
434 | $stats['recent_denials']++;
435 | }
436 | }
437 |
438 | return $stats;
439 | }
440 |
441 | /**
442 | * Clear denied features cache.
443 | *
444 | * @since 1.1.0
445 | */
446 | public function clear_cache() {
447 | $this->denied_cache = array();
448 | }
449 |
450 | /**
451 | * Check filter for feature access.
452 | *
453 | * @since 1.1.0
454 | * @param bool $can_access Current access status.
455 | * @param string $feature Feature identifier.
456 | * @return bool Modified access status.
457 | */
458 | public function check_feature_access( $can_access, $feature ) {
459 | if ( $can_access ) {
460 | return $this->can_access_feature( $feature );
461 | }
462 |
463 | return $can_access;
464 | }
465 | }
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # 🤝 Contributing to WPNLWeb
2 |
3 | Thank you for your interest in contributing to WPNLWeb! This guide will help you get started with contributing to our WordPress Natural Language Web plugin.
4 |
5 | ## 📋 Table of Contents
6 |
7 | - [Code of Conduct](#code-of-conduct)
8 | - [Getting Started](#getting-started)
9 | - [Development Setup](#development-setup)
10 | - [Contributing Guidelines](#contributing-guidelines)
11 | - [Coding Standards](#coding-standards)
12 | - [Testing](#testing)
13 | - [Submitting Changes](#submitting-changes)
14 | - [Issue Reporting](#issue-reporting)
15 | - [Community](#community)
16 |
17 | ## 🤝 Code of Conduct
18 |
19 | This project and everyone participating in it is governed by our commitment to creating a welcoming, inclusive environment. By participating, you agree to:
20 |
21 | - **Be respectful** and considerate in all interactions
22 | - **Be collaborative** and help others learn and grow
23 | - **Be patient** with newcomers and different skill levels
24 | - **Focus on constructive feedback** and solutions
25 | - **Respect different viewpoints** and experiences
26 |
27 | Report any unacceptable behavior to [hey@wpnlweb.com](mailto:hey@wpnlweb.com).
28 |
29 | ## 🚀 Getting Started
30 |
31 | ### Prerequisites
32 |
33 | Before you begin, ensure you have:
34 |
35 | - **PHP 7.4 or higher**
36 | - **WordPress 5.0 or higher** (local development environment)
37 | - **Composer** for dependency management
38 | - **Git** for version control
39 | - **Code editor** with PHP support (VS Code, PhpStorm, etc.)
40 |
41 | ### Development Tools
42 |
43 | We recommend these tools for the best development experience:
44 |
45 | - **[Local by Flywheel](https://localwp.com/)** or **[XAMPP](https://www.apachefriends.org/)** for local WordPress
46 | - **[WP-CLI](https://wp-cli.org/)** for WordPress command-line operations
47 | - **[PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer)** for code standards
48 | - **[Composer](https://getcomposer.org/)** for PHP dependencies
49 |
50 | ## 🛠️ Development Setup
51 |
52 | ### 1. Fork and Clone
53 |
54 | ```bash
55 | # Fork the repository on GitHub, then clone your fork
56 | git clone https://github.com/YOUR_USERNAME/wpnlweb.git
57 | cd wpnlweb
58 |
59 | # Add the original repository as upstream
60 | git remote add upstream https://github.com/gigabit-eth/wpnlweb.git
61 | ```
62 |
63 | ### 2. Install Dependencies
64 |
65 | ```bash
66 | # Install PHP dependencies
67 | composer install
68 |
69 | # Set up development environment
70 | composer run dev-setup
71 | ```
72 |
73 | ### 3. Set Up WordPress Environment
74 |
75 | ```bash
76 | # If using WP-CLI (recommended)
77 | wp core download
78 | wp config create --dbname=wpnlweb_dev --dbuser=root --dbpass=password
79 | wp core install --url=wpnlweb.local --title="WPNLWeb Dev" --admin_user=admin --admin_password=password --admin_email=dev@example.com
80 |
81 | # Symlink the plugin to WordPress
82 | ln -s $(pwd) /path/to/wordpress/wp-content/plugins/wpnlweb
83 |
84 | # Activate the plugin
85 | wp plugin activate wpnlweb
86 | ```
87 |
88 | ### 4. Verify Setup
89 |
90 | ```bash
91 | # Check code standards
92 | composer run lint
93 |
94 | # Check PHP syntax
95 | composer run check-syntax
96 |
97 | # Test API endpoint (adjust URL as needed)
98 | curl -X POST http://wpnlweb.local/wp-json/nlweb/v1/ask \
99 | -H "Content-Type: application/json" \
100 | -d '{"question": "test"}'
101 | ```
102 |
103 | ## 📝 Contributing Guidelines
104 |
105 | ### Types of Contributions
106 |
107 | We welcome various types of contributions:
108 |
109 | - **🐛 Bug Reports** - Help us identify and fix issues
110 | - **✨ Feature Requests** - Suggest new functionality
111 | - **📚 Documentation** - Improve guides, comments, and examples
112 | - **🔧 Code Improvements** - Optimize performance, refactor code
113 | - **🧪 Tests** - Add or improve test coverage
114 | - **🌐 Translations** - Help make WPNLWeb available in more languages
115 |
116 | ### Before You Start
117 |
118 | 1. **Check existing issues** to avoid duplicating work
119 | 2. **Discuss major changes** in an issue before implementing
120 | 3. **Follow our coding standards** and best practices
121 | 4. **Write tests** for new functionality
122 | 5. **Update documentation** as needed
123 |
124 | ## 📏 Coding Standards
125 |
126 | ### WordPress Standards
127 |
128 | We follow the [WordPress PHP Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/):
129 |
130 | ```php
131 | property_name = sanitize_text_field( $param );
163 | }
164 |
165 | /**
166 | * Method with proper documentation
167 | *
168 | * @param array $args Method arguments.
169 | * @return array Modified arguments.
170 | */
171 | public function example_method( $args = array() ) {
172 | $defaults = array(
173 | 'option_one' => 'default_value',
174 | 'option_two' => 123,
175 | );
176 |
177 | $args = wp_parse_args( $args, $defaults );
178 |
179 | // Process and return
180 | return apply_filters( 'wpnlweb_example_method', $args );
181 | }
182 | }
183 | ```
184 |
185 | ### Code Quality Rules
186 |
187 | 1. **Security First**
188 |
189 | ```php
190 | // ✅ Good - Sanitize input
191 | $user_input = sanitize_text_field( $_POST['input'] );
192 |
193 | // ✅ Good - Escape output
194 | echo esc_html( $user_data );
195 |
196 | // ✅ Good - Use nonces for forms
197 | wp_nonce_field( 'wpnlweb_action', 'wpnlweb_nonce' );
198 | ```
199 |
200 | 2. **WordPress Integration**
201 |
202 | ```php
203 | // ✅ Good - Use WordPress functions
204 | $posts = get_posts( $args );
205 |
206 | // ✅ Good - Use hooks appropriately
207 | add_action( 'init', array( $this, 'initialize' ) );
208 | add_filter( 'wpnlweb_results', array( $this, 'modify_results' ) );
209 | ```
210 |
211 | 3. **Performance**
212 | ```php
213 | // ✅ Good - Cache expensive operations
214 | $cache_key = 'wpnlweb_results_' . md5( $query );
215 | $results = get_transient( $cache_key );
216 | if ( false === $results ) {
217 | $results = $this->expensive_operation( $query );
218 | set_transient( $cache_key, $results, HOUR_IN_SECONDS );
219 | }
220 | ```
221 |
222 | ### File Organization
223 |
224 | ```
225 | wpnlweb/
226 | ├── wpnlweb.php # Main plugin file
227 | ├── includes/ # Core functionality
228 | │ ├── class-wpnlweb.php # Main plugin class
229 | │ ├── class-wpnlweb-server.php
230 | │ └── ...
231 | ├── admin/ # Admin interface
232 | │ ├── class-wpnlweb-admin.php
233 | │ └── partials/
234 | ├── public/ # Frontend functionality
235 | │ ├── class-wpnlweb-public.php
236 | │ └── js/
237 | └── languages/ # Translation files
238 | ```
239 |
240 | ## 🧪 Testing
241 |
242 | ### Running Tests
243 |
244 | ```bash
245 | # Check all code standards
246 | composer run lint
247 |
248 | # Fix auto-fixable issues
249 | composer run lint-fix
250 |
251 | # Check only errors (ignore warnings)
252 | composer run lint-errors-only
253 |
254 | # Test syntax of all PHP files
255 | composer run check-syntax
256 |
257 | # Test API functionality
258 | php debug-api-test.php
259 | ```
260 |
261 | ### Writing Tests
262 |
263 | 1. **Test API Endpoints**
264 |
265 | ```php
266 | /**
267 | * Test the NLWeb API endpoint
268 | */
269 | public function test_nlweb_api_endpoint() {
270 | $request = new WP_REST_Request( 'POST', '/nlweb/v1/ask' );
271 | $request->set_body_params( array(
272 | 'question' => 'test question',
273 | ) );
274 |
275 | $response = rest_do_request( $request );
276 | $this->assertEquals( 200, $response->get_status() );
277 | }
278 | ```
279 |
280 | 2. **Test Shortcodes**
281 | ```php
282 | /**
283 | * Test shortcode output
284 | */
285 | public function test_wpnlweb_shortcode() {
286 | $output = do_shortcode( '[wpnlweb]' );
287 | $this->assertStringContains( 'wpnlweb-search-form', $output );
288 | }
289 | ```
290 |
291 | ### Test Coverage Requirements
292 |
293 | - **New features** must include tests
294 | - **Bug fixes** should include regression tests
295 | - **API changes** require updated endpoint tests
296 | - **Shortcode changes** need frontend tests
297 |
298 | ## 📤 Submitting Changes
299 |
300 | ### Pull Request Process
301 |
302 | 1. **Create a Feature Branch**
303 |
304 | ```bash
305 | git checkout -b feature/your-feature-name
306 | # or
307 | git checkout -b fix/issue-number-description
308 | ```
309 |
310 | 2. **Make Your Changes**
311 |
312 | - Follow coding standards
313 | - Add tests for new functionality
314 | - Update documentation as needed
315 | - Test thoroughly
316 |
317 | 3. **Commit Your Changes**
318 |
319 | ```bash
320 | # Stage your changes
321 | git add .
322 |
323 | # Commit with descriptive message
324 | git commit -m "Add: Natural language query caching
325 |
326 | - Implement Redis-based caching for API responses
327 | - Add cache invalidation on content updates
328 | - Include cache statistics in admin dashboard
329 | - Fixes #123"
330 | ```
331 |
332 | 4. **Update Your Branch**
333 |
334 | ```bash
335 | # Fetch latest changes from upstream
336 | git fetch upstream
337 | git rebase upstream/main
338 | ```
339 |
340 | 5. **Run Final Checks**
341 |
342 | ```bash
343 | # Ensure code meets standards
344 | composer run lint
345 |
346 | # Test functionality
347 | php debug-api-test.php
348 | ```
349 |
350 | 6. **Push and Create PR**
351 |
352 | ```bash
353 | git push origin feature/your-feature-name
354 | ```
355 |
356 | Then create a pull request on GitHub with:
357 |
358 | - Clear description of changes
359 | - Reference to related issues
360 | - Screenshots (if UI changes)
361 | - Test instructions
362 |
363 | ### Commit Message Format
364 |
365 | Use this format for commit messages:
366 |
367 | ```
368 | Type: Brief description
369 |
370 | - Detailed explanation of changes
371 | - Why the change was made
372 | - Any breaking changes
373 | - Related issue numbers
374 |
375 | Fixes #123
376 | ```
377 |
378 | **Types:**
379 |
380 | - `Add:` New features
381 | - `Fix:` Bug fixes
382 | - `Update:` Changes to existing features
383 | - `Remove:` Removed features
384 | - `Docs:` Documentation changes
385 | - `Test:` Test additions or changes
386 | - `Refactor:` Code restructuring
387 |
388 | ### Pull Request Checklist
389 |
390 | Before submitting, ensure:
391 |
392 | - [ ] Code follows WordPress PHP standards
393 | - [ ] All tests pass (`composer run lint`)
394 | - [ ] New features include tests
395 | - [ ] Documentation is updated
396 | - [ ] No debugging code left in
397 | - [ ] Backwards compatibility maintained
398 | - [ ] Security best practices followed
399 | - [ ] Performance impact considered
400 |
401 | ## 🐛 Issue Reporting
402 |
403 | ### Bug Reports
404 |
405 | When reporting bugs, include:
406 |
407 | 1. **WordPress version**
408 | 2. **PHP version**
409 | 3. **Plugin version**
410 | 4. **Steps to reproduce**
411 | 5. **Expected behavior**
412 | 6. **Actual behavior**
413 | 7. **Error messages** (if any)
414 | 8. **Browser/environment** details
415 |
416 | ### Bug Report Template
417 |
418 | ```markdown
419 | **WordPress Version:** 6.6
420 | **PHP Version:** 8.1
421 | **Plugin Version:** 1.0.0
422 |
423 | **Steps to Reproduce:**
424 |
425 | 1. Go to Settings > WPNLWeb
426 | 2. Click "Test API"
427 | 3. Error appears
428 |
429 | **Expected:** API test should return results
430 | **Actual:** 500 error returned
431 |
432 | **Error Message:**
433 | ```
434 |
435 | Fatal error: Call to undefined function...
436 |
437 | ```
438 |
439 | **Additional Context:**
440 | Using Local by Flywheel on macOS
441 | ```
442 |
443 | ### Feature Requests
444 |
445 | For feature requests, provide:
446 |
447 | 1. **Use case** - Why is this needed?
448 | 2. **Proposed solution** - How should it work?
449 | 3. **Alternatives** - What other options exist?
450 | 4. **Additional context** - Screenshots, examples, etc.
451 |
452 | ## 🌟 Recognition
453 |
454 | Contributors will be recognized in:
455 |
456 | - **Plugin credits** (wpnlweb.php header)
457 | - **CONTRIBUTORS.md** file
458 | - **Release notes** for significant contributions
459 | - **Plugin directory** acknowledgments
460 |
461 | ### Types of Recognition
462 |
463 | - **Code Contributors** - Direct code contributions
464 | - **Documentation Contributors** - Improve guides and docs
465 | - **Community Contributors** - Help with support and discussions
466 | - **Testing Contributors** - Bug reports and testing
467 | - **Translation Contributors** - Help with internationalization
468 |
469 | ## 🗣️ Community
470 |
471 | ### Communication Channels
472 |
473 | - **GitHub Issues** - Bug reports and feature requests
474 | - **GitHub Discussions** - General questions and ideas
475 | - **Email** - [hey@wpnlweb.com](mailto:hey@wpnlweb.com)
476 | - **WordPress.org Forum** - User support
477 |
478 | ### Getting Help
479 |
480 | - **WordPress Development** - [WordPress Developer Resources](https://developer.wordpress.org/)
481 | - **PHP Best Practices** - [PHP: The Right Way](https://phptherightway.com/)
482 | - **Git Workflow** - [Atlassian Git Tutorials](https://www.atlassian.com/git/tutorials)
483 |
484 | ### Contributing Levels
485 |
486 | **🥉 Bronze Contributors**
487 |
488 | - Fix typos and small bugs
489 | - Improve documentation
490 | - Report detailed bug reports
491 |
492 | **🥈 Silver Contributors**
493 |
494 | - Add new features
495 | - Improve test coverage
496 | - Help with code reviews
497 |
498 | **🥇 Gold Contributors**
499 |
500 | - Architectural improvements
501 | - Security enhancements
502 | - Mentoring other contributors
503 |
504 | ## ❓ Questions?
505 |
506 | Don't hesitate to ask! We're here to help:
507 |
508 | - **Technical Questions** - Create a GitHub Discussion
509 | - **Process Questions** - Email [hey@wpnlweb.com](mailto:hey@wpnlweb.com)
510 | - **Ideas and Feedback** - Open a GitHub Issue
511 |
512 | ---
513 |
514 | Thank you for contributing to WPNLWeb! Your efforts help make WordPress more accessible to AI agents and provide better search experiences for users worldwide. 🎉
515 |
--------------------------------------------------------------------------------
/docs/hooks.md:
--------------------------------------------------------------------------------
1 | # 🔗 WPNLWeb Hooks Reference
2 |
3 | Complete reference for all WordPress filters and actions provided by the WPNLWeb plugin.
4 |
5 | ## 🎯 Overview
6 |
7 | WPNLWeb provides numerous hooks to customize functionality, modify search results, customize styling, and extend the plugin's capabilities. All hooks follow WordPress coding standards and conventions.
8 |
9 | ## 🔧 Filters
10 |
11 | ### Search & Content Filters
12 |
13 | #### `wpnlweb_search_results`
14 |
15 | Modify search results before they are returned to the user.
16 |
17 | **Usage:**
18 |
19 | ```php
20 | add_filter('wpnlweb_search_results', 'customize_search_results', 10, 2);
21 |
22 | function customize_search_results($results, $query) {
23 | // Add custom logic to modify results
24 | foreach ($results as &$result) {
25 | $result['custom_field'] = 'custom_value';
26 | }
27 | return $results;
28 | }
29 | ```
30 |
31 | **Parameters:**
32 |
33 | - `$results` (array) - Array of search result posts
34 | - `$query` (string) - Original search query
35 |
36 | **Return:** Modified array of results
37 |
38 | ---
39 |
40 | #### `wpnlweb_api_response`
41 |
42 | Customize the final API response before it's sent to the client.
43 |
44 | **Usage:**
45 |
46 | ```php
47 | add_filter('wpnlweb_api_response', 'customize_api_response', 10, 2);
48 |
49 | function customize_api_response($response, $question) {
50 | // Add custom metadata
51 | $response['custom_metadata'] = array(
52 | 'site_name' => get_bloginfo('name'),
53 | 'timestamp' => current_time('c'),
54 | 'version' => WPNLWEB_VERSION
55 | );
56 | return $response;
57 | }
58 | ```
59 |
60 | **Parameters:**
61 |
62 | - `$response` (array) - Schema.org formatted response
63 | - `$question` (string) - Original question asked
64 |
65 | **Return:** Modified response array
66 |
67 | ---
68 |
69 | #### `wpnlweb_searchable_post_types`
70 |
71 | Add or remove post types from the search.
72 |
73 | **Usage:**
74 |
75 | ```php
76 | add_filter('wpnlweb_searchable_post_types', 'add_custom_post_types');
77 |
78 | function add_custom_post_types($post_types) {
79 | $post_types[] = 'product';
80 | $post_types[] = 'event';
81 | $post_types[] = 'testimonial';
82 | return $post_types;
83 | }
84 | ```
85 |
86 | **Parameters:**
87 |
88 | - `$post_types` (array) - Current searchable post types
89 |
90 | **Return:** Modified array of post types
91 |
92 | **Default Post Types:** `['post', 'page']`
93 |
94 | ---
95 |
96 | #### `wpnlweb_search_query_args`
97 |
98 | Modify WordPress query arguments before search execution.
99 |
100 | **Usage:**
101 |
102 | ```php
103 | add_filter('wpnlweb_search_query_args', 'customize_query_args', 10, 2);
104 |
105 | function customize_query_args($args, $question) {
106 | // Add meta query for featured content
107 | $args['meta_query'] = array(
108 | array(
109 | 'key' => 'featured',
110 | 'value' => 'yes',
111 | 'compare' => '='
112 | )
113 | );
114 |
115 | // Boost certain post types
116 | if (strpos($question, 'product') !== false) {
117 | $args['post_type'] = array('product');
118 | }
119 |
120 | return $args;
121 | }
122 | ```
123 |
124 | **Parameters:**
125 |
126 | - `$args` (array) - WP_Query arguments
127 | - `$question` (string) - Search question
128 |
129 | **Return:** Modified query arguments
130 |
131 | ---
132 |
133 | #### `wpnlweb_extract_keywords`
134 |
135 | Customize keyword extraction from natural language questions.
136 |
137 | **Usage:**
138 |
139 | ```php
140 | add_filter('wpnlweb_extract_keywords', 'custom_keyword_extraction', 10, 2);
141 |
142 | function custom_keyword_extraction($keywords, $question) {
143 | // Add domain-specific keyword processing
144 | $domain_keywords = array(
145 | 'ecommerce' => array('buy', 'purchase', 'order', 'cart'),
146 | 'blog' => array('article', 'post', 'read', 'latest'),
147 | 'service' => array('consultation', 'hire', 'contact')
148 | );
149 |
150 | foreach ($domain_keywords as $domain => $terms) {
151 | foreach ($terms as $term) {
152 | if (strpos(strtolower($question), $term) !== false) {
153 | $keywords[] = $domain;
154 | break;
155 | }
156 | }
157 | }
158 |
159 | return array_unique($keywords);
160 | }
161 | ```
162 |
163 | **Parameters:**
164 |
165 | - `$keywords` (array) - Extracted keywords
166 | - `$question` (string) - Original question
167 |
168 | **Return:** Modified keywords array
169 |
170 | ### Styling & UI Filters
171 |
172 | #### `wpnlweb_custom_css`
173 |
174 | Add custom CSS to the search interface.
175 |
176 | **Usage:**
177 |
178 | ```php
179 | add_filter('wpnlweb_custom_css', 'add_custom_search_styles');
180 |
181 | function add_custom_search_styles($css) {
182 | $custom_css = '
183 | .wpnlweb-search-container {
184 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
185 | border-radius: 16px;
186 | padding: 2rem;
187 | box-shadow: 0 20px 40px rgba(0,0,0,0.1);
188 | }
189 |
190 | .wpnlweb-search-input {
191 | background: rgba(255,255,255,0.9);
192 | backdrop-filter: blur(10px);
193 | }
194 | ';
195 |
196 | return $css . $custom_css;
197 | }
198 | ```
199 |
200 | **Parameters:**
201 |
202 | - `$css` (string) - Existing CSS
203 |
204 | **Return:** Modified CSS string
205 |
206 | ---
207 |
208 | #### `wpnlweb_primary_color`
209 |
210 | Customize the primary color used throughout the interface.
211 |
212 | **Usage:**
213 |
214 | ```php
215 | add_filter('wpnlweb_primary_color', function($color) {
216 | return '#e74c3c'; // Custom red color
217 | });
218 | ```
219 |
220 | **Parameters:**
221 |
222 | - `$color` (string) - Current primary color (hex)
223 |
224 | **Return:** New color (hex format)
225 |
226 | **Default:** `#3b82f6`
227 |
228 | ---
229 |
230 | #### `wpnlweb_secondary_color`
231 |
232 | Customize the secondary color.
233 |
234 | **Usage:**
235 |
236 | ```php
237 | add_filter('wpnlweb_secondary_color', function($color) {
238 | return '#2c3e50';
239 | });
240 | ```
241 |
242 | **Parameters:**
243 |
244 | - `$color` (string) - Current secondary color
245 |
246 | **Return:** New secondary color
247 |
248 | **Default:** `#1f2937`
249 |
250 | ---
251 |
252 | #### `wpnlweb_background_color`
253 |
254 | Customize the background color.
255 |
256 | **Usage:**
257 |
258 | ```php
259 | add_filter('wpnlweb_background_color', function($color) {
260 | return '#f8f9fa';
261 | });
262 | ```
263 |
264 | **Default:** `#ffffff`
265 |
266 | ---
267 |
268 | #### `wpnlweb_text_color`
269 |
270 | Customize the text color.
271 |
272 | **Usage:**
273 |
274 | ```php
275 | add_filter('wpnlweb_text_color', function($color) {
276 | return '#2c3e50';
277 | });
278 | ```
279 |
280 | **Default:** `#1f2937`
281 |
282 | ---
283 |
284 | #### `wpnlweb_border_radius`
285 |
286 | Customize border radius for UI elements.
287 |
288 | **Usage:**
289 |
290 | ```php
291 | add_filter('wpnlweb_border_radius', function($radius) {
292 | return '12px';
293 | });
294 | ```
295 |
296 | **Default:** `8px`
297 |
298 | ### Security & Performance Filters
299 |
300 | #### `wpnlweb_rate_limit`
301 |
302 | Set rate limiting for API requests.
303 |
304 | **Usage:**
305 |
306 | ```php
307 | add_filter('wpnlweb_rate_limit', function($limit) {
308 | return 30; // 30 requests per hour per IP
309 | });
310 | ```
311 |
312 | **Parameters:**
313 |
314 | - `$limit` (int) - Requests per hour
315 |
316 | **Return:** New rate limit
317 |
318 | **Default:** No rate limiting
319 |
320 | ---
321 |
322 | #### `wpnlweb_allowed_origins`
323 |
324 | Restrict CORS origins for enhanced security.
325 |
326 | **Usage:**
327 |
328 | ```php
329 | add_filter('wpnlweb_allowed_origins', function($origins) {
330 | return array(
331 | 'https://youragent.com',
332 | 'https://trusted-ai-service.com',
333 | 'https://yourapp.com'
334 | );
335 | });
336 | ```
337 |
338 | **Parameters:**
339 |
340 | - `$origins` (array) - Allowed origin URLs
341 |
342 | **Return:** Modified origins array
343 |
344 | **Default:** `['*']` (all origins allowed)
345 |
346 | ---
347 |
348 | #### `wpnlweb_cache_timeout`
349 |
350 | Set cache timeout for search results.
351 |
352 | **Usage:**
353 |
354 | ```php
355 | add_filter('wpnlweb_cache_timeout', function($timeout) {
356 | return 600; // 10 minutes
357 | });
358 | ```
359 |
360 | **Parameters:**
361 |
362 | - `$timeout` (int) - Cache timeout in seconds
363 |
364 | **Return:** New timeout value
365 |
366 | **Default:** No caching implemented
367 |
368 | ---
369 |
370 | #### `wpnlweb_require_auth`
371 |
372 | Enable authentication requirement for API access.
373 |
374 | **Usage:**
375 |
376 | ```php
377 | add_filter('wpnlweb_require_auth', '__return_true');
378 |
379 | // Then handle authentication
380 | add_filter('wpnlweb_authenticate_request', function($authenticated, $request) {
381 | $api_key = $request->get_header('X-API-Key');
382 | return $api_key === 'your-secret-key';
383 | }, 10, 2);
384 | ```
385 |
386 | **Default:** `false`
387 |
388 | ### Shortcode Filters
389 |
390 | #### `wpnlweb_shortcode_defaults`
391 |
392 | Customize default shortcode attributes.
393 |
394 | **Usage:**
395 |
396 | ```php
397 | add_filter('wpnlweb_shortcode_defaults', function($defaults) {
398 | $defaults['placeholder'] = 'Ask me anything about our services...';
399 | $defaults['button_text'] = 'Find Answer';
400 | $defaults['max_results'] = '8';
401 | return $defaults;
402 | });
403 | ```
404 |
405 | **Parameters:**
406 |
407 | - `$defaults` (array) - Default attribute values
408 |
409 | **Return:** Modified defaults
410 |
411 | ---
412 |
413 | #### `wpnlweb_result_template`
414 |
415 | Customize the HTML template for search results.
416 |
417 | **Usage:**
418 |
419 | ```php
420 | add_filter('wpnlweb_result_template', function($template, $result) {
421 | return '
422 |
423 |
424 |
' . esc_html($result['description']) . '
425 |
426 | ' . esc_html($result['datePublished']) . '
427 | by ' . esc_html($result['author']['name']) . '
428 |
429 |
430 | ';
431 | }, 10, 2);
432 | ```
433 |
434 | **Parameters:**
435 |
436 | - `$template` (string) - Current template HTML
437 | - `$result` (array) - Individual search result data
438 |
439 | **Return:** Modified template HTML
440 |
441 | ## 🎬 Actions
442 |
443 | ### `wpnlweb_settings_updated`
444 |
445 | Triggered when plugin settings are saved in the admin.
446 |
447 | **Usage:**
448 |
449 | ```php
450 | add_action('wpnlweb_settings_updated', 'handle_settings_update');
451 |
452 | function handle_settings_update() {
453 | // Clear caches
454 | wp_cache_flush();
455 |
456 | // Update external services
457 | update_search_index();
458 |
459 | // Log the update
460 | error_log('WPNLWeb settings updated at ' . current_time('mysql'));
461 | }
462 | ```
463 |
464 | ---
465 |
466 | ### `wpnlweb_search_performed`
467 |
468 | Triggered after each search is performed.
469 |
470 | **Usage:**
471 |
472 | ```php
473 | add_action('wpnlweb_search_performed', 'track_search_analytics', 10, 3);
474 |
475 | function track_search_analytics($question, $results_count, $processing_time) {
476 | // Track search analytics
477 | $analytics_data = array(
478 | 'question' => $question,
479 | 'results_count' => $results_count,
480 | 'processing_time' => $processing_time,
481 | 'timestamp' => current_time('mysql'),
482 | 'user_ip' => $_SERVER['REMOTE_ADDR']
483 | );
484 |
485 | // Send to analytics service or save to database
486 | save_search_analytics($analytics_data);
487 | }
488 | ```
489 |
490 | **Parameters:**
491 |
492 | - `$question` (string) - Search question
493 | - `$results_count` (int) - Number of results found
494 | - `$processing_time` (float) - Time taken to process (seconds)
495 |
496 | ---
497 |
498 | ### `wpnlweb_api_request_start`
499 |
500 | Triggered at the beginning of each API request.
501 |
502 | **Usage:**
503 |
504 | ```php
505 | add_action('wpnlweb_api_request_start', 'log_api_request');
506 |
507 | function log_api_request($request) {
508 | error_log(sprintf(
509 | 'WPNLWeb API request: %s from %s',
510 | $request->get_param('question'),
511 | $_SERVER['REMOTE_ADDR']
512 | ));
513 | }
514 | ```
515 |
516 | **Parameters:**
517 |
518 | - `$request` (WP_REST_Request) - WordPress REST request object
519 |
520 | ---
521 |
522 | ### `wpnlweb_api_request_end`
523 |
524 | Triggered at the end of each API request.
525 |
526 | **Usage:**
527 |
528 | ```php
529 | add_action('wpnlweb_api_request_end', 'log_api_response', 10, 2);
530 |
531 | function log_api_response($response, $request) {
532 | $processing_time = microtime(true) - $GLOBALS['wpnlweb_request_start'];
533 |
534 | error_log(sprintf(
535 | 'WPNLWeb API response: %d results in %f seconds',
536 | $response['totalResults'],
537 | $processing_time
538 | ));
539 | }
540 | ```
541 |
542 | **Parameters:**
543 |
544 | - `$response` (array) - API response data
545 | - `$request` (WP_REST_Request) - Original request
546 |
547 | ## 💡 Usage Examples
548 |
549 | ### Complete Customization Example
550 |
551 | ```php
552 | // Add to your theme's functions.php or custom plugin
553 |
554 | class WPNLWeb_Customizations {
555 |
556 | public function __construct() {
557 | // Search customization
558 | add_filter('wpnlweb_searchable_post_types', array($this, 'add_post_types'));
559 | add_filter('wpnlweb_search_results', array($this, 'enhance_results'), 10, 2);
560 |
561 | // Styling
562 | add_filter('wpnlweb_primary_color', array($this, 'brand_color'));
563 | add_filter('wpnlweb_custom_css', array($this, 'custom_styles'));
564 |
565 | // Analytics
566 | add_action('wpnlweb_search_performed', array($this, 'track_searches'), 10, 3);
567 |
568 | // Security
569 | add_filter('wpnlweb_allowed_origins', array($this, 'restrict_origins'));
570 | }
571 |
572 | public function add_post_types($post_types) {
573 | return array_merge($post_types, array('product', 'service', 'testimonial'));
574 | }
575 |
576 | public function enhance_results($results, $query) {
577 | foreach ($results as &$result) {
578 | // Add featured image
579 | if (has_post_thumbnail($result->ID)) {
580 | $result->featured_image = get_the_post_thumbnail_url($result->ID, 'medium');
581 | }
582 |
583 | // Add custom fields
584 | $result->custom_data = array(
585 | 'views' => get_post_meta($result->ID, 'views', true),
586 | 'rating' => get_post_meta($result->ID, 'rating', true),
587 | );
588 | }
589 | return $results;
590 | }
591 |
592 | public function brand_color($color) {
593 | return '#e74c3c'; // Your brand color
594 | }
595 |
596 | public function custom_styles($css) {
597 | return $css . '
598 | .wpnlweb-search-container {
599 | font-family: "Helvetica Neue", sans-serif;
600 | max-width: 600px;
601 | margin: 0 auto;
602 | }
603 | ';
604 | }
605 |
606 | public function track_searches($question, $count, $time) {
607 | // Send to your analytics platform
608 | wp_remote_post('https://analytics.yoursite.com/track', array(
609 | 'body' => json_encode(array(
610 | 'event' => 'wpnlweb_search',
611 | 'question' => $question,
612 | 'results' => $count,
613 | 'time' => $time
614 | ))
615 | ));
616 | }
617 |
618 | public function restrict_origins($origins) {
619 | return array(
620 | 'https://yoursite.com',
621 | 'https://youragent.com'
622 | );
623 | }
624 | }
625 |
626 | new WPNLWeb_Customizations();
627 | ```
628 |
629 | ### E-commerce Integration Example
630 |
631 | ```php
632 | // Enhance for WooCommerce integration
633 | add_filter('wpnlweb_searchable_post_types', function($post_types) {
634 | $post_types[] = 'product';
635 | return $post_types;
636 | });
637 |
638 | add_filter('wpnlweb_search_results', function($results, $query) {
639 | foreach ($results as &$result) {
640 | if ($result->post_type === 'product') {
641 | $product = wc_get_product($result->ID);
642 | if ($product) {
643 | $result->price = $product->get_price_html();
644 | $result->in_stock = $product->is_in_stock();
645 | $result->product_url = $product->get_permalink();
646 | }
647 | }
648 | }
649 | return $results;
650 | }, 10, 2);
651 | ```
652 |
653 | ## 🔗 Related Documentation
654 |
655 | - [API Reference](api.md) - Complete API documentation
656 | - [Customization Guide](customization.md) - Theming and styling
657 | - [WordPress Plugin API](https://developer.wordpress.org/plugins/hooks/) - Official WordPress hooks documentation
658 |
--------------------------------------------------------------------------------