Plugin Directory

Changeset 3491287


Ignore:
Timestamp:
03/25/2026 10:11:16 PM (10 days ago)
Author:
ugoltsev
Message:

release 1.2.0

Location:
ask-my-content
Files:
2 added
6 edited
23 copied

Legend:

Unmodified
Added
Removed
  • ask-my-content/tags/1.2.0/ask-my-content.php

    r3486628 r3491287  
    44 * Plugin Name:       Ask My Content - AI Q&A Chatbot
    55 * Description:       AI-powered Q&A chatbot, allowing users to ask questions and receive answers sourced from the site’s own posts and pages.
    6  * Version:           1.1.0
     6 * Version:           1.2.0
    77 * Requires at least: 5.8
    88 * Requires PHP:      7.4
     
    1919}
    2020require_once plugin_dir_path(__FILE__) . 'includes/utils.php';
     21require_once plugin_dir_path(__FILE__) . 'includes/content.php';
    2122require_once plugin_dir_path(__FILE__) . 'includes/activate.php';
    2223require_once plugin_dir_path(__FILE__) . 'includes/api.php';
     
    9293    $settings_path = $base_dir . 'assets/js/askmyco-settings.js';
    9394
    94     $core_ver = file_exists($core_path) ? filemtime($core_path) : '1.1.0';
    95     $frontend_ver = file_exists($frontend_path) ? filemtime($frontend_path) : '1.1.0';
    96     $admin_init_ver = file_exists($admin_init_path) ? filemtime($admin_init_path) : '1.1.0';
    97     $style_ver = file_exists($style_path) ? filemtime($style_path) : '1.1.0';
    98     $settings_ver = file_exists($settings_path) ? filemtime($settings_path) : '1.1.0';
     95    $core_ver = file_exists($core_path) ? filemtime($core_path) : '1.2.0';
     96    $frontend_ver = file_exists($frontend_path) ? filemtime($frontend_path) : '1.2.0';
     97    $admin_init_ver = file_exists($admin_init_path) ? filemtime($admin_init_path) : '1.2.0';
     98    $style_ver = file_exists($style_path) ? filemtime($style_path) : '1.2.0';
     99    $settings_ver = file_exists($settings_path) ? filemtime($settings_path) : '1.2.0';
    99100
    100101    $payments_path = $base_dir . 'assets/js/amc-payments.js';
    101     $payments_ver = file_exists($payments_path) ? filemtime($payments_path) : '1.1.0';
     102    $payments_ver = file_exists($payments_path) ? filemtime($payments_path) : '1.2.0';
    102103
    103104    if (! wp_script_is($core_handle, 'registered')) {
     
    261262
    262263    $floating_path = plugin_dir_path(__FILE__) . 'assets/js/amc-floating.js';
    263     $floating_ver = file_exists($floating_path) ? filemtime($floating_path) : '1.1.0';
     264    $floating_ver = file_exists($floating_path) ? filemtime($floating_path) : '1.2.0';
    264265
    265266    wp_enqueue_script(
  • ask-my-content/tags/1.2.0/build/ask-my-content/block.json

    r3486628 r3491287  
    33  "apiVersion": 3,
    44  "name": "amc/ask-my-content",
    5   "version": "1.1.0",
     5  "version": "1.2.0",
    66  "title": "Ask My Content",
    77  "category": "widgets",
  • ask-my-content/tags/1.2.0/build/blocks-manifest.php

    r3486628 r3491287  
    66        'apiVersion' => 3,
    77        'name' => 'amc/ask-my-content',
    8         'version' => '1.1.0',
     8        'version' => '1.2.0',
    99        'title' => 'Ask My Content',
    1010        'category' => 'widgets',
  • ask-my-content/tags/1.2.0/includes/utils.php

    r3487385 r3491287  
    269269add_action('init', 'askmyco_bootstrap_session_cookies');
    270270
    271 function askmyco_get_product_content_parts(WP_Post $post): array
    272 {
    273     if ($post->post_type !== 'product' || ! function_exists('wc_get_product')) {
    274         return [];
    275     }
    276 
    277     $product = wc_get_product($post->ID);
    278     if (! $product) {
    279         return [];
    280     }
    281 
    282     $product_parts = [];
    283 
    284     $sku = trim((string) $product->get_sku());
    285     if ($sku !== '') {
    286         $product_parts[] = 'SKU: ' . $sku;
    287     }
    288 
    289     $price = trim((string) $product->get_price());
    290     if ($price !== '') {
    291         $product_parts[] = 'Price: ' . $price;
    292     }
    293 
    294     $weight = trim((string) $product->get_weight());
    295     if ($weight !== '') {
    296         $product_parts[] = 'Weight: ' . $weight;
    297     }
    298 
    299     $dimensions = array_filter([
    300         'Length: ' . trim((string) $product->get_length()),
    301         'Width: ' . trim((string) $product->get_width()),
    302         'Height: ' . trim((string) $product->get_height()),
    303     ], function ($value) {
    304         return substr($value, -2) !== ': ';
    305     });
    306 
    307     if ($dimensions !== []) {
    308         $product_parts[] = implode("\n", $dimensions);
    309     }
    310 
    311     $attributes = [];
    312     foreach ($product->get_attributes() as $attribute) {
    313         if (! is_object($attribute) || ! method_exists($attribute, 'get_name') || ! method_exists($attribute, 'get_options')) {
    314             continue;
    315         }
    316 
    317         $attribute_name = (string) $attribute->get_name();
    318         if ($attribute_name === '') {
    319             continue;
    320         }
    321 
    322         $label = function_exists('wc_attribute_label') ? wc_attribute_label($attribute_name) : $attribute_name;
    323         $values = [];
    324 
    325         if (method_exists($attribute, 'is_taxonomy') && $attribute->is_taxonomy() && function_exists('wc_get_product_terms')) {
    326             $values = wc_get_product_terms($post->ID, $attribute_name, ['fields' => 'names']);
    327             if (is_wp_error($values)) {
    328                 $values = [];
    329             }
    330         } else {
    331             $values = $attribute->get_options();
    332         }
    333 
    334         $values = array_values(array_filter(array_map('strval', (array) $values), function ($value) {
    335             return trim($value) !== '';
    336         }));
    337 
    338         if ($values === []) {
    339             continue;
    340         }
    341 
    342         $attributes[] = $label . ': ' . implode(', ', $values);
    343     }
    344 
    345     if ($attributes !== []) {
    346         $product_parts[] = "Attributes:\n" . implode("\n", $attributes);
    347     }
    348 
    349     return $product_parts;
    350 }
    351 
    352 function askmyco_convert_post_to_rest_like_array(WP_Post $post): array
    353 {
    354     $content_parts = [];
    355 
    356     $content = askmyco_get_plain_text_from_html($post->post_content);
    357     if ($content !== '') {
    358         $content_parts[] = $content;
    359     }
    360 
    361     $excerpt = askmyco_get_plain_text_from_html($post->post_excerpt);
    362     if ($excerpt !== '') {
    363         $content_parts[] = 'Excerpt: ' . $excerpt;
    364     }
    365 
    366     $product_parts = askmyco_get_product_content_parts($post);
    367     if ($product_parts !== []) {
    368         $content_parts[] = implode("\n", $product_parts);
    369     }
    370 
    371     return [
    372         'id' => $post->ID,
    373         'date' => get_post_time('c', false, $post), // local time in ISO 8601
    374         'date_gmt' => get_post_time('c', true, $post),
    375         'guid' => [
    376             'rendered' => $post->guid,
    377         ],
    378         'modified' => get_post_modified_time('c', false, $post),
    379         'modified_gmt' => get_post_modified_time('c', true, $post),
    380         'slug' => $post->post_name,
    381         'status' => $post->post_status,
    382         'type' => $post->post_type,
    383         'link' => askmyco_get_canonical_permalink($post),
    384         'title' => [
    385             'rendered' => get_the_title($post),
    386         ],
    387         'content' => [
    388             'rendered' => implode("\n\n", array_filter($content_parts, function ($value) {
    389                 return $value !== '';
    390             })),
    391         ],
    392     ];
    393 }
    394 
    395 /**
    396  * Build a permalink using the path from get_permalink($post) but forcing
    397  * scheme/host/port to match home_url(). This avoids cases where the host
    398  * resolves to an IP due to server headers or proxy configuration.
    399  */
    400 function askmyco_get_canonical_permalink($post): string
    401 {
    402     $permalink = get_permalink($post);
    403     $home      = home_url('/');
    404 
    405     $p = wp_parse_url($permalink);
    406     $h = wp_parse_url($home);
    407 
    408     if ($p === false || $h === false || empty($h['host'])) {
    409         return $permalink;
    410     }
    411     $scheme = $h['scheme'] ?? 'https';
    412     $host   = $h['host'];
    413     $port   = isset($h['port']) ? ':' . $h['port'] : '';
    414     $path   = $p['path'] ?? '/';
    415     $query  = isset($p['query']) ? '?' . $p['query'] : '';
    416     $frag   = isset($p['fragment']) ? '#' . $p['fragment'] : '';
    417 
    418     return $scheme . '://' . $host . $port . $path . $query . $frag;
    419 }
    420 
    421 /**
    422  * Build a canonical URL using home_url() for scheme/host/port and a provided
    423  * path/query/fragment. Useful for virtual resources like header/footer.
    424  */
    425 function askmyco_build_canonical_url(string $path = '/', array $query = [], ?string $fragment = null): string
    426 {
    427     $home = home_url('/');
    428     $h = wp_parse_url($home);
    429     if ($h === false || empty($h['host'])) {
    430         return $home;
    431     }
    432     $scheme = $h['scheme'] ?? 'https';
    433     $host   = $h['host'];
    434     $port   = isset($h['port']) ? ':' . $h['port'] : '';
    435     // Ensure path starts with '/'
    436     $path   = '/' . ltrim($path, '/');
    437     $query_str = '';
    438     if (!empty($query)) {
    439         $query_str = '?' . http_build_query($query, '', '&');
    440     }
    441     $frag_str = $fragment !== null && $fragment !== '' ? '#' . ltrim($fragment, '#') : '';
    442     return $scheme . '://' . $host . $port . $path . $query_str . $frag_str;
    443 }
    444 
    445271/**
    446272 * Return the last indexed timestamp (UTC) for a given kind ('pages' or 'posts').
     
    505331}
    506332
    507 // $html - The raw HTML‑containing block comment markup and paragraph tags
    508 function askmyco_get_plain_text_from_html($html): string
    509 {
    510     // 1. Remove any shortcode-derived tokens (just drops shortcodes)
    511     $no_shortcodes = strip_shortcodes($html);
    512 
    513     // 2. Strip ALL HTML, including scripts/styles, leaving visible text only
    514     $text = wp_strip_all_tags($no_shortcodes, true);
    515 
    516     // 3. Collapse whitespace (optional but helpful)
    517     $trimmed = preg_replace('/\s+/', ' ', $text);
    518 
    519     return trim($trimmed);
    520 }
    521 
    522 // $type: 'header' or 'footer'
    523 function askmyco_virtual_post_array_for_header_footer($type): ?array
    524 {
    525     if ($type !== 'header' && $type !== 'footer') {
    526         return null;
    527     }
    528 
    529     // Unique virtual post IDs
    530     $id = ($type === 'header') ? 1000001 : 1000002;
    531 
    532     // 1) Get rendered HTML for the header/footer
    533     $html = askmyco_render_header_footer_html($type);
    534 
    535     // 2) Extract visible text
    536     $text = askmyco_get_plain_text_from_html($html);
    537 
    538     // 3) Detect changes by hash so widget/customizer edits update modified date
    539     $hash_option = "askmyco_{$type}_content_hash";
    540     $old_hash    = get_option($hash_option);
    541     $new_hash    = md5($text);
    542 
    543     if ($new_hash !== $old_hash) {
    544         update_option($hash_option, $new_hash);
    545         $modified = current_time('c');
    546     } else {
    547         // Keep last known modified so you don’t thrash re-embeds
    548         $modified = get_option("askmyco_{$type}_modified") ?: current_time('c');
    549     }
    550     update_option("askmyco_{$type}_modified", $modified);
    551 
    552     // 4) Site info
    553     $site_uuid = get_option('askmyco_site_uuid');
    554     $url       = home_url('/');
    555     $title     = ucfirst($type);
    556 
    557     // 5) Return virtual post array
    558     return [
    559         'id'           => $id,
    560         'date'         => $modified,
    561         'date_gmt'     => get_gmt_from_date($modified, 'c'),
    562         'guid'         => ['rendered' => $url],
    563         'modified'     => $modified,
    564         'modified_gmt' => get_gmt_from_date($modified, 'c'),
    565         'slug'         => $type,
    566         'status'       => 'publish',
    567         'type'         => $type,
    568         'link'         => askmyco_build_canonical_url('/', [], $type),
    569         'title'        => ['rendered' => $title],
    570         'content'      => ['rendered' => $text],
    571         'siteId'       => $site_uuid,
    572     ];
    573 }
    574 
    575 function askmyco_render_header_footer_html($type)
    576 {
    577     if ($type !== 'header' && $type !== 'footer') return '';
    578 
    579     // Prefer child theme path first
    580     $classic = get_stylesheet_directory() . "/{$type}.php";
    581     if (file_exists($classic)) {
    582         ob_start();
    583         include $classic;
    584         return ob_get_clean();
    585     }
    586     // DB-stored template part (Site Editor edits)
    587     $theme = wp_get_theme()->get_stylesheet();
    588     if (function_exists('get_block_template')) {
    589         $template = get_block_template($theme . '//' . $type, 'wp_template_part');
    590         if ($template && !empty($template->content)) {
    591             $content = $template->content;
    592             return function_exists('do_blocks') ? do_blocks($content) : $content;
    593         }
    594     }
    595 
    596     $tpl = get_posts([
    597         'post_type'               => 'wp_template_part',
    598         'name'                    => $type,
    599         'posts_per_page'          => 3,
    600         'post_status'             => 'publish',
    601         'no_found_rows'           => true,
    602         'update_post_term_cache'  => true,
    603         'update_post_meta_cache'  => false,
    604     ]);
    605     foreach ($tpl as $candidate) {
    606         $terms = get_the_terms($candidate, 'wp_theme');
    607         if (is_wp_error($terms) || empty($terms)) {
    608             continue;
    609         }
    610         foreach ($terms as $term) {
    611             if ($term->slug === $theme) {
    612                 $content = $candidate->post_content;
    613                 return function_exists('do_blocks') ? do_blocks($content) : $content;
    614             }
    615         }
    616     }
    617     // Block template files fallback
    618     $theme_dir = get_stylesheet_directory();
    619     $cands = [
    620         "{$theme_dir}/parts/{$type}.html",
    621         "{$theme_dir}/templates/parts/{$type}.html",
    622         "{$theme_dir}/block-templates/parts/{$type}.html",
    623         "{$theme_dir}/block-templates/{$type}.html",
    624         "{$theme_dir}/templates/{$type}.html",
    625     ];
    626     foreach ($cands as $cand) {
    627         if (file_exists($cand)) {
    628             $raw = file_get_contents($cand);
    629             if (function_exists('do_blocks')) {
    630                 return do_blocks($raw);
    631             }
    632             return $raw;
    633         }
    634     }
    635     // Last resort
    636     ob_start();
    637     if ($type === 'header') {
    638         // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- core WP action
    639         do_action('wp_head');
    640     } else {
    641         // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- core WP action
    642         do_action('wp_footer');
    643     }
    644     return ob_get_clean();
    645 }
    646 
    647333/**
    648334 * Build full API endpoint from stored URL and optional port.
  • ask-my-content/tags/1.2.0/readme.txt

    r3486628 r3491287  
    55Requires PHP: 7.4
    66Tested up to: 6.9
    7 Stable tag: 1.1.0
     7Stable tag: 1.2.0
    88License: GPL-2.0-or-later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    1717Unlike standard content search plugins that rely on keyword matching, Ask My Content uses semantic embeddings and retrieval-augmented generation to return direct answers in full sentences.
    1818
    19 WordPress Block Editor is fully supported.
    20 However not all dynamic WordPress content creation themes and plugins are supported. See FAQ for details.
     19Supports Gutenberg, Elementor, Avada, and WooCommerce content.
     20Some highly customized dynamic content may still require review. See FAQ for details.
    2121
    2222**Key Features**
     
    5959
    6060= Why my indexed token count is low =
    61 Dynamic WordPress content creation themes and plugins (which may include Elementor Pro widgets) are currently not supported.
    62 You will see persistently low indexed token count, even after indexing is finished, and indexing token count stays low after clicking the Refresh button.
    63 In this case your chat questions will not be answered correctly ("No available content").
    64 For more info email to: info@pavelweb.com
     61Low indexed token count usually means the plugin did not find much readable page content to send for indexing.
     62
     63This can happen if:
     641. The page is mostly built from theme templates, banners, sliders, or other repeated global sections.
     652. The important content is loaded by a plugin or builder widget that does not expose much text to WordPress.
     663. Indexing finished before the latest content changes were re-run.
     67
     68Ask My Content now supports common WordPress content builders much better, including **Gutenberg**, **Elementor**, **Avada**, and **WooCommerce** product content.
     69
     70If your indexed token count still stays unexpectedly low after re-running indexing, see the next FAQ section: **What content is supported for indexing?**
     71
     72If the chat still answers with "No available content," the page may rely heavily on content generated outside the main WordPress content flow.
     73
     74= What content is supported for indexing? =
     75Ask My Content is designed to index the main readable text of your site content.
     76
     77Currently supported:
     78* Standard WordPress pages and posts
     79* Gutenberg block content
     80* Elementor content in many common setups
     81* Avada content in many common setups
     82* WooCommerce product content, including product descriptions, short descriptions, price, SKU, weight, dimensions, and attributes
     83* Other public post types, if enabled in plugin settings
     84
     85In general, the plugin reads the main post content the way WordPress normally renders it, using the standard content pipeline (`apply_filters('the_content', ...)`).
     86
     87That means it usually works well with content that lives inside the main editor content area, but it may still miss text that comes only from:
     88* headers or footers
     89* sliders, popups, and announcement bars
     90* heavily customized builder widgets
     91* content injected from external sources or loaded later with JavaScript
     92
     93If a page is important for search quality, open it in WordPress and make sure the main text is actually stored in the page, post, or product content itself before re-running indexing.
    6594
    6695= Does this plugin use my OpenAI API key? =
     
    167196== Changelog ==
    168197
     198= 1.2.0 =
     199Improved content indexing support for Gutenberg, Elementor, Avada, WooCommerce, and other post types.
     200
    169201= 1.1.0 =
    170202Added support for other content types (Products, Books, Events, etc.), including WooCommerce products.
     
    217249== Upgrade Notice ==
    218250
    219 = 1.1.0 =
    220 Added support for other content types (Products, Books, Events, etc.), including WooCommerce products.
    221 
    222 = 1.0.0 =
    223 Added `Payments` tab to `Ask My Content Settings`, where you can check your usage and pay the balance (currently with Stripe)
     251= 1.2.0 =
     252Improved content indexing support for Gutenberg, Elementor, Avada, WooCommerce, and other post types.
     253If your site was indexed with an older plugin version, email info@pavelweb.com for re-index instructions.
  • ask-my-content/tags/1.2.0/src/ask-my-content/block.json

    r3486628 r3491287  
    33    "apiVersion": 3,
    44    "name": "amc/ask-my-content",
    5     "version": "1.1.0",
     5    "version": "1.2.0",
    66    "title": "Ask My Content",
    77    "category": "widgets",
  • ask-my-content/trunk/ask-my-content.php

    r3486628 r3491287  
    44 * Plugin Name:       Ask My Content - AI Q&A Chatbot
    55 * Description:       AI-powered Q&A chatbot, allowing users to ask questions and receive answers sourced from the site’s own posts and pages.
    6  * Version:           1.1.0
     6 * Version:           1.2.0
    77 * Requires at least: 5.8
    88 * Requires PHP:      7.4
     
    1919}
    2020require_once plugin_dir_path(__FILE__) . 'includes/utils.php';
     21require_once plugin_dir_path(__FILE__) . 'includes/content.php';
    2122require_once plugin_dir_path(__FILE__) . 'includes/activate.php';
    2223require_once plugin_dir_path(__FILE__) . 'includes/api.php';
     
    9293    $settings_path = $base_dir . 'assets/js/askmyco-settings.js';
    9394
    94     $core_ver = file_exists($core_path) ? filemtime($core_path) : '1.1.0';
    95     $frontend_ver = file_exists($frontend_path) ? filemtime($frontend_path) : '1.1.0';
    96     $admin_init_ver = file_exists($admin_init_path) ? filemtime($admin_init_path) : '1.1.0';
    97     $style_ver = file_exists($style_path) ? filemtime($style_path) : '1.1.0';
    98     $settings_ver = file_exists($settings_path) ? filemtime($settings_path) : '1.1.0';
     95    $core_ver = file_exists($core_path) ? filemtime($core_path) : '1.2.0';
     96    $frontend_ver = file_exists($frontend_path) ? filemtime($frontend_path) : '1.2.0';
     97    $admin_init_ver = file_exists($admin_init_path) ? filemtime($admin_init_path) : '1.2.0';
     98    $style_ver = file_exists($style_path) ? filemtime($style_path) : '1.2.0';
     99    $settings_ver = file_exists($settings_path) ? filemtime($settings_path) : '1.2.0';
    99100
    100101    $payments_path = $base_dir . 'assets/js/amc-payments.js';
    101     $payments_ver = file_exists($payments_path) ? filemtime($payments_path) : '1.1.0';
     102    $payments_ver = file_exists($payments_path) ? filemtime($payments_path) : '1.2.0';
    102103
    103104    if (! wp_script_is($core_handle, 'registered')) {
     
    261262
    262263    $floating_path = plugin_dir_path(__FILE__) . 'assets/js/amc-floating.js';
    263     $floating_ver = file_exists($floating_path) ? filemtime($floating_path) : '1.1.0';
     264    $floating_ver = file_exists($floating_path) ? filemtime($floating_path) : '1.2.0';
    264265
    265266    wp_enqueue_script(
  • ask-my-content/trunk/build/ask-my-content/block.json

    r3486628 r3491287  
    33  "apiVersion": 3,
    44  "name": "amc/ask-my-content",
    5   "version": "1.1.0",
     5  "version": "1.2.0",
    66  "title": "Ask My Content",
    77  "category": "widgets",
  • ask-my-content/trunk/build/blocks-manifest.php

    r3486628 r3491287  
    66        'apiVersion' => 3,
    77        'name' => 'amc/ask-my-content',
    8         'version' => '1.1.0',
     8        'version' => '1.2.0',
    99        'title' => 'Ask My Content',
    1010        'category' => 'widgets',
  • ask-my-content/trunk/includes/utils.php

    r3487385 r3491287  
    269269add_action('init', 'askmyco_bootstrap_session_cookies');
    270270
    271 function askmyco_get_product_content_parts(WP_Post $post): array
    272 {
    273     if ($post->post_type !== 'product' || ! function_exists('wc_get_product')) {
    274         return [];
    275     }
    276 
    277     $product = wc_get_product($post->ID);
    278     if (! $product) {
    279         return [];
    280     }
    281 
    282     $product_parts = [];
    283 
    284     $sku = trim((string) $product->get_sku());
    285     if ($sku !== '') {
    286         $product_parts[] = 'SKU: ' . $sku;
    287     }
    288 
    289     $price = trim((string) $product->get_price());
    290     if ($price !== '') {
    291         $product_parts[] = 'Price: ' . $price;
    292     }
    293 
    294     $weight = trim((string) $product->get_weight());
    295     if ($weight !== '') {
    296         $product_parts[] = 'Weight: ' . $weight;
    297     }
    298 
    299     $dimensions = array_filter([
    300         'Length: ' . trim((string) $product->get_length()),
    301         'Width: ' . trim((string) $product->get_width()),
    302         'Height: ' . trim((string) $product->get_height()),
    303     ], function ($value) {
    304         return substr($value, -2) !== ': ';
    305     });
    306 
    307     if ($dimensions !== []) {
    308         $product_parts[] = implode("\n", $dimensions);
    309     }
    310 
    311     $attributes = [];
    312     foreach ($product->get_attributes() as $attribute) {
    313         if (! is_object($attribute) || ! method_exists($attribute, 'get_name') || ! method_exists($attribute, 'get_options')) {
    314             continue;
    315         }
    316 
    317         $attribute_name = (string) $attribute->get_name();
    318         if ($attribute_name === '') {
    319             continue;
    320         }
    321 
    322         $label = function_exists('wc_attribute_label') ? wc_attribute_label($attribute_name) : $attribute_name;
    323         $values = [];
    324 
    325         if (method_exists($attribute, 'is_taxonomy') && $attribute->is_taxonomy() && function_exists('wc_get_product_terms')) {
    326             $values = wc_get_product_terms($post->ID, $attribute_name, ['fields' => 'names']);
    327             if (is_wp_error($values)) {
    328                 $values = [];
    329             }
    330         } else {
    331             $values = $attribute->get_options();
    332         }
    333 
    334         $values = array_values(array_filter(array_map('strval', (array) $values), function ($value) {
    335             return trim($value) !== '';
    336         }));
    337 
    338         if ($values === []) {
    339             continue;
    340         }
    341 
    342         $attributes[] = $label . ': ' . implode(', ', $values);
    343     }
    344 
    345     if ($attributes !== []) {
    346         $product_parts[] = "Attributes:\n" . implode("\n", $attributes);
    347     }
    348 
    349     return $product_parts;
    350 }
    351 
    352 function askmyco_convert_post_to_rest_like_array(WP_Post $post): array
    353 {
    354     $content_parts = [];
    355 
    356     $content = askmyco_get_plain_text_from_html($post->post_content);
    357     if ($content !== '') {
    358         $content_parts[] = $content;
    359     }
    360 
    361     $excerpt = askmyco_get_plain_text_from_html($post->post_excerpt);
    362     if ($excerpt !== '') {
    363         $content_parts[] = 'Excerpt: ' . $excerpt;
    364     }
    365 
    366     $product_parts = askmyco_get_product_content_parts($post);
    367     if ($product_parts !== []) {
    368         $content_parts[] = implode("\n", $product_parts);
    369     }
    370 
    371     return [
    372         'id' => $post->ID,
    373         'date' => get_post_time('c', false, $post), // local time in ISO 8601
    374         'date_gmt' => get_post_time('c', true, $post),
    375         'guid' => [
    376             'rendered' => $post->guid,
    377         ],
    378         'modified' => get_post_modified_time('c', false, $post),
    379         'modified_gmt' => get_post_modified_time('c', true, $post),
    380         'slug' => $post->post_name,
    381         'status' => $post->post_status,
    382         'type' => $post->post_type,
    383         'link' => askmyco_get_canonical_permalink($post),
    384         'title' => [
    385             'rendered' => get_the_title($post),
    386         ],
    387         'content' => [
    388             'rendered' => implode("\n\n", array_filter($content_parts, function ($value) {
    389                 return $value !== '';
    390             })),
    391         ],
    392     ];
    393 }
    394 
    395 /**
    396  * Build a permalink using the path from get_permalink($post) but forcing
    397  * scheme/host/port to match home_url(). This avoids cases where the host
    398  * resolves to an IP due to server headers or proxy configuration.
    399  */
    400 function askmyco_get_canonical_permalink($post): string
    401 {
    402     $permalink = get_permalink($post);
    403     $home      = home_url('/');
    404 
    405     $p = wp_parse_url($permalink);
    406     $h = wp_parse_url($home);
    407 
    408     if ($p === false || $h === false || empty($h['host'])) {
    409         return $permalink;
    410     }
    411     $scheme = $h['scheme'] ?? 'https';
    412     $host   = $h['host'];
    413     $port   = isset($h['port']) ? ':' . $h['port'] : '';
    414     $path   = $p['path'] ?? '/';
    415     $query  = isset($p['query']) ? '?' . $p['query'] : '';
    416     $frag   = isset($p['fragment']) ? '#' . $p['fragment'] : '';
    417 
    418     return $scheme . '://' . $host . $port . $path . $query . $frag;
    419 }
    420 
    421 /**
    422  * Build a canonical URL using home_url() for scheme/host/port and a provided
    423  * path/query/fragment. Useful for virtual resources like header/footer.
    424  */
    425 function askmyco_build_canonical_url(string $path = '/', array $query = [], ?string $fragment = null): string
    426 {
    427     $home = home_url('/');
    428     $h = wp_parse_url($home);
    429     if ($h === false || empty($h['host'])) {
    430         return $home;
    431     }
    432     $scheme = $h['scheme'] ?? 'https';
    433     $host   = $h['host'];
    434     $port   = isset($h['port']) ? ':' . $h['port'] : '';
    435     // Ensure path starts with '/'
    436     $path   = '/' . ltrim($path, '/');
    437     $query_str = '';
    438     if (!empty($query)) {
    439         $query_str = '?' . http_build_query($query, '', '&');
    440     }
    441     $frag_str = $fragment !== null && $fragment !== '' ? '#' . ltrim($fragment, '#') : '';
    442     return $scheme . '://' . $host . $port . $path . $query_str . $frag_str;
    443 }
    444 
    445271/**
    446272 * Return the last indexed timestamp (UTC) for a given kind ('pages' or 'posts').
     
    505331}
    506332
    507 // $html - The raw HTML‑containing block comment markup and paragraph tags
    508 function askmyco_get_plain_text_from_html($html): string
    509 {
    510     // 1. Remove any shortcode-derived tokens (just drops shortcodes)
    511     $no_shortcodes = strip_shortcodes($html);
    512 
    513     // 2. Strip ALL HTML, including scripts/styles, leaving visible text only
    514     $text = wp_strip_all_tags($no_shortcodes, true);
    515 
    516     // 3. Collapse whitespace (optional but helpful)
    517     $trimmed = preg_replace('/\s+/', ' ', $text);
    518 
    519     return trim($trimmed);
    520 }
    521 
    522 // $type: 'header' or 'footer'
    523 function askmyco_virtual_post_array_for_header_footer($type): ?array
    524 {
    525     if ($type !== 'header' && $type !== 'footer') {
    526         return null;
    527     }
    528 
    529     // Unique virtual post IDs
    530     $id = ($type === 'header') ? 1000001 : 1000002;
    531 
    532     // 1) Get rendered HTML for the header/footer
    533     $html = askmyco_render_header_footer_html($type);
    534 
    535     // 2) Extract visible text
    536     $text = askmyco_get_plain_text_from_html($html);
    537 
    538     // 3) Detect changes by hash so widget/customizer edits update modified date
    539     $hash_option = "askmyco_{$type}_content_hash";
    540     $old_hash    = get_option($hash_option);
    541     $new_hash    = md5($text);
    542 
    543     if ($new_hash !== $old_hash) {
    544         update_option($hash_option, $new_hash);
    545         $modified = current_time('c');
    546     } else {
    547         // Keep last known modified so you don’t thrash re-embeds
    548         $modified = get_option("askmyco_{$type}_modified") ?: current_time('c');
    549     }
    550     update_option("askmyco_{$type}_modified", $modified);
    551 
    552     // 4) Site info
    553     $site_uuid = get_option('askmyco_site_uuid');
    554     $url       = home_url('/');
    555     $title     = ucfirst($type);
    556 
    557     // 5) Return virtual post array
    558     return [
    559         'id'           => $id,
    560         'date'         => $modified,
    561         'date_gmt'     => get_gmt_from_date($modified, 'c'),
    562         'guid'         => ['rendered' => $url],
    563         'modified'     => $modified,
    564         'modified_gmt' => get_gmt_from_date($modified, 'c'),
    565         'slug'         => $type,
    566         'status'       => 'publish',
    567         'type'         => $type,
    568         'link'         => askmyco_build_canonical_url('/', [], $type),
    569         'title'        => ['rendered' => $title],
    570         'content'      => ['rendered' => $text],
    571         'siteId'       => $site_uuid,
    572     ];
    573 }
    574 
    575 function askmyco_render_header_footer_html($type)
    576 {
    577     if ($type !== 'header' && $type !== 'footer') return '';
    578 
    579     // Prefer child theme path first
    580     $classic = get_stylesheet_directory() . "/{$type}.php";
    581     if (file_exists($classic)) {
    582         ob_start();
    583         include $classic;
    584         return ob_get_clean();
    585     }
    586     // DB-stored template part (Site Editor edits)
    587     $theme = wp_get_theme()->get_stylesheet();
    588     if (function_exists('get_block_template')) {
    589         $template = get_block_template($theme . '//' . $type, 'wp_template_part');
    590         if ($template && !empty($template->content)) {
    591             $content = $template->content;
    592             return function_exists('do_blocks') ? do_blocks($content) : $content;
    593         }
    594     }
    595 
    596     $tpl = get_posts([
    597         'post_type'               => 'wp_template_part',
    598         'name'                    => $type,
    599         'posts_per_page'          => 3,
    600         'post_status'             => 'publish',
    601         'no_found_rows'           => true,
    602         'update_post_term_cache'  => true,
    603         'update_post_meta_cache'  => false,
    604     ]);
    605     foreach ($tpl as $candidate) {
    606         $terms = get_the_terms($candidate, 'wp_theme');
    607         if (is_wp_error($terms) || empty($terms)) {
    608             continue;
    609         }
    610         foreach ($terms as $term) {
    611             if ($term->slug === $theme) {
    612                 $content = $candidate->post_content;
    613                 return function_exists('do_blocks') ? do_blocks($content) : $content;
    614             }
    615         }
    616     }
    617     // Block template files fallback
    618     $theme_dir = get_stylesheet_directory();
    619     $cands = [
    620         "{$theme_dir}/parts/{$type}.html",
    621         "{$theme_dir}/templates/parts/{$type}.html",
    622         "{$theme_dir}/block-templates/parts/{$type}.html",
    623         "{$theme_dir}/block-templates/{$type}.html",
    624         "{$theme_dir}/templates/{$type}.html",
    625     ];
    626     foreach ($cands as $cand) {
    627         if (file_exists($cand)) {
    628             $raw = file_get_contents($cand);
    629             if (function_exists('do_blocks')) {
    630                 return do_blocks($raw);
    631             }
    632             return $raw;
    633         }
    634     }
    635     // Last resort
    636     ob_start();
    637     if ($type === 'header') {
    638         // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- core WP action
    639         do_action('wp_head');
    640     } else {
    641         // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- core WP action
    642         do_action('wp_footer');
    643     }
    644     return ob_get_clean();
    645 }
    646 
    647333/**
    648334 * Build full API endpoint from stored URL and optional port.
  • ask-my-content/trunk/readme.txt

    r3486628 r3491287  
    55Requires PHP: 7.4
    66Tested up to: 6.9
    7 Stable tag: 1.1.0
     7Stable tag: 1.2.0
    88License: GPL-2.0-or-later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    1717Unlike standard content search plugins that rely on keyword matching, Ask My Content uses semantic embeddings and retrieval-augmented generation to return direct answers in full sentences.
    1818
    19 WordPress Block Editor is fully supported.
    20 However not all dynamic WordPress content creation themes and plugins are supported. See FAQ for details.
     19Supports Gutenberg, Elementor, Avada, and WooCommerce content.
     20Some highly customized dynamic content may still require review. See FAQ for details.
    2121
    2222**Key Features**
     
    5959
    6060= Why my indexed token count is low =
    61 Dynamic WordPress content creation themes and plugins (which may include Elementor Pro widgets) are currently not supported.
    62 You will see persistently low indexed token count, even after indexing is finished, and indexing token count stays low after clicking the Refresh button.
    63 In this case your chat questions will not be answered correctly ("No available content").
    64 For more info email to: info@pavelweb.com
     61Low indexed token count usually means the plugin did not find much readable page content to send for indexing.
     62
     63This can happen if:
     641. The page is mostly built from theme templates, banners, sliders, or other repeated global sections.
     652. The important content is loaded by a plugin or builder widget that does not expose much text to WordPress.
     663. Indexing finished before the latest content changes were re-run.
     67
     68Ask My Content now supports common WordPress content builders much better, including **Gutenberg**, **Elementor**, **Avada**, and **WooCommerce** product content.
     69
     70If your indexed token count still stays unexpectedly low after re-running indexing, see the next FAQ section: **What content is supported for indexing?**
     71
     72If the chat still answers with "No available content," the page may rely heavily on content generated outside the main WordPress content flow.
     73
     74= What content is supported for indexing? =
     75Ask My Content is designed to index the main readable text of your site content.
     76
     77Currently supported:
     78* Standard WordPress pages and posts
     79* Gutenberg block content
     80* Elementor content in many common setups
     81* Avada content in many common setups
     82* WooCommerce product content, including product descriptions, short descriptions, price, SKU, weight, dimensions, and attributes
     83* Other public post types, if enabled in plugin settings
     84
     85In general, the plugin reads the main post content the way WordPress normally renders it, using the standard content pipeline (`apply_filters('the_content', ...)`).
     86
     87That means it usually works well with content that lives inside the main editor content area, but it may still miss text that comes only from:
     88* headers or footers
     89* sliders, popups, and announcement bars
     90* heavily customized builder widgets
     91* content injected from external sources or loaded later with JavaScript
     92
     93If a page is important for search quality, open it in WordPress and make sure the main text is actually stored in the page, post, or product content itself before re-running indexing.
    6594
    6695= Does this plugin use my OpenAI API key? =
     
    167196== Changelog ==
    168197
     198= 1.2.0 =
     199Improved content indexing support for Gutenberg, Elementor, Avada, WooCommerce, and other post types.
     200
    169201= 1.1.0 =
    170202Added support for other content types (Products, Books, Events, etc.), including WooCommerce products.
     
    217249== Upgrade Notice ==
    218250
    219 = 1.1.0 =
    220 Added support for other content types (Products, Books, Events, etc.), including WooCommerce products.
    221 
    222 = 1.0.0 =
    223 Added `Payments` tab to `Ask My Content Settings`, where you can check your usage and pay the balance (currently with Stripe)
     251= 1.2.0 =
     252Improved content indexing support for Gutenberg, Elementor, Avada, WooCommerce, and other post types.
     253If your site was indexed with an older plugin version, email info@pavelweb.com for re-index instructions.
  • ask-my-content/trunk/src/ask-my-content/block.json

    r3486628 r3491287  
    33    "apiVersion": 3,
    44    "name": "amc/ask-my-content",
    5     "version": "1.1.0",
     5    "version": "1.2.0",
    66    "title": "Ask My Content",
    77    "category": "widgets",
Note: See TracChangeset for help on using the changeset viewer.