Changeset 3732072
- Timestamp:
- 10/07/2026 08:06:32 AM (2 weeks ago)
- Location:
- world-inspire-product-feed-for-google-shopping
- Files:
-
- 4 edited
- 1 copied
-
tags/1.1.0 (copied) (copied from world-inspire-product-feed-for-google-shopping/trunk )
-
tags/1.1.0/readme.txt (modified) (4 diffs)
-
tags/1.1.0/world-inspire-product-feed-for-google-shopping.php (modified) (31 diffs)
-
trunk/readme.txt (modified) (4 diffs)
-
trunk/world-inspire-product-feed-for-google-shopping.php (modified) (31 diffs)
Legend:
- Unmodified
- Added
- Removed
-
world-inspire-product-feed-for-google-shopping/tags/1.1.0/readme.txt
r3730435 r3732072 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1. 0.07 Stable tag: 1.1.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 21 21 * **Multi-Engine eCommerce Compatibility**: Native auto-detection and data extraction for both **SureCart** (`sc_product`) and **WooCommerce** (`product`). Zero fatal errors if either or neither is active. 22 22 * **Google Merchant Center Specification**: Built according to official Google RSS 2.0 standards with all essential namespace attributes: `<g:id>`, `<g:title>`, `<g:description>`, `<g:link>`, `<g:image_link>`, `<g:availability>`, `<g:price>`, `<g:brand>`, `<g:condition>`, `<g:identifier_exists>`, `<g:google_product_category>`, `<g:color>`, `<g:age_group>`, and `<g:gender>`. 23 * **Custom Labels 0–4 for Google Ads / PMax**: Full support for `<g:custom_label_0>` through `<g:custom_label_4>` with rules for price tiers (< $25, $25-$50, $50-$100, $100+), inventory status, category mapping, clearance/promotion tag keywords, and custom post meta fields. 24 * **Variant Grouping (`<g:item_group_id>`)**: Automatically expands WooCommerce variable products (`product_variation`) into individual variant items linked by parent `<g:item_group_id>`, enabling swatch selection in Google Shopping results. 25 * **Variant Attribute Optimization**: Smart extraction for `<g:size>`, `<g:material>` (Gold, Silver, Leather, Cotton, Gemstones, etc.), and `<g:pattern>` (Solid, Striped, Floral, Beaded) directly from attributes or intelligent NLP heuristics. 23 26 * **Multi-Country Shipping Generator**: Automatically embeds multi-destination `<g:shipping>` nodes (defaulting to 8 core global markets: US, CA, UK, JP, SG, HK, AE, MX with custom shipping service names and rates). 24 * ** Intelligent Color & Demographic Extraction**: Automatically maps jewelry, apparel, gemstone, and accessory titles/descriptions into recognized Google taxonomy colors (over 35 shades) and adult/unisex demographics.27 * **Live Diagnostics & Product Inspector Simulator**: Test and audit any product ID directly in WordPress Admin to verify extracted attributes, custom labels matching reasons, and preview raw `<item>` XML snippets. 25 28 * **Automatic Pet Category Mapping**: Heuristically detects pet collars, harnesses, and animal accessories and maps them to `Animals & Pet Supplies > Pet Supplies > Pet Collars & Harnesses`. 26 29 * **Trustmate Review Catalog Sync**: Dedicated `<products>` XML feed format ready for instant connection in your Trustmate merchant dashboard. … … 39 42 40 43 = Does this plugin work with SureCart? = 41 Yes! It has full native support for SureCart products, pricing, inventory statuses (`sc_inventory_status`), featured images, and product galleries.44 Yes! It has full native support for SureCart products, pricing, inventory statuses (`sc_inventory_status`), featured images, collections, tags, and product galleries. 42 45 43 46 = Does this plugin work with WooCommerce? = 44 Yes! It automatically detects WooCommerce installations and outputs compliant feeds from published WooCommerce products. 47 Yes! It automatically detects WooCommerce installations and outputs compliant feeds from published WooCommerce products and product variations with `<g:item_group_id>`. 48 49 = How do Custom Labels (0-4) work? = 50 Navigate to **Settings > Product Feeds > Google Shopping Rules & Custom Labels**. You can configure rules for each slot: Price Range Tiers, Inventory Status, Product Category, Tag/Keyword Matching (e.g. `clearance:Clearance, sale:On Sale, bestseller:Best Seller`), or Custom Post Meta fields. 45 51 46 52 = Why does this plugin use extension-free REST URLs? = … … 53 59 54 60 1. Feed URLs & Health Dashboard: Dynamic REST API endpoints and real-time catalog status. 55 2. Google Shopping Rules : Global fallback brand defaults, product taxonomies, and NLP color detection.61 2. Google Shopping Rules & Custom Labels: Global fallback brand defaults, product taxonomies, NLP color/size/material detection, and custom labels 0-4. 56 62 3. Multi-Country Shipping: International destination targeting and custom shipping rates. 57 4. Live Diagnostics : Catalog extraction verification and Google Merchant Center spec compliance audit.63 4. Live Diagnostics & Product Inspector: Catalog extraction audit, custom label rule matching traces, and raw XML simulator. 58 64 59 65 == Changelog == 66 67 = 1.1.0 = 68 * Added Google Shopping Custom Labels (custom_label_0 to custom_label_4) with configurable rules: Price Tiers, Inventory Status, Category Mapping, Promotional Tag Keywords, and Custom Post Meta. 69 * Added WooCommerce variable product expansion with `<g:item_group_id>` linking parent and variant products. 70 * Added smart attribute extraction for `<g:size>`, `<g:material>`, and `<g:pattern>` with jewelry/apparel heuristic engines. 71 * Added Live Product Inspector & XML Simulator to Diagnostics tab with rule matching explanation traces. 72 * Enforced Google Shopping 100-character maximum length limit and XML entity escaping on all custom labels. 73 * Added 87 zero-dependency unit and mock E2E integration tests passing 100% green. 60 74 61 75 = 1.0.0 = -
world-inspire-product-feed-for-google-shopping/tags/1.1.0/world-inspire-product-feed-for-google-shopping.php
r3730219 r3732072 3 3 * Plugin Name: World Inspire Product Feed for Google Shopping 4 4 * Plugin URI: https://wordpress.org/plugins/world-inspire-product-feed-for-google-shopping/ 5 * Description: High-performance real-time product feed generator for Google Shopping (Merchant Center) and Trustmate reviews sync from WooCommerce and SureCart. Features multi-country shipping, smart color and category detection, and anti-cache streaming architecture.6 * Version: 1. 0.05 * Description: High-performance real-time product feed generator for Google Shopping (Merchant Center) and Trustmate reviews sync from WooCommerce and SureCart. Features multi-country shipping, smart color/size/material detection, custom labels (0-4), variant item_group_id grouping, and anti-cache streaming architecture. 6 * Version: 1.1.0 7 7 * Author: World Inspire LLC, Vec 8 8 * Author URI: https://worldinspirelab.com … … 18 18 } 19 19 20 define( 'WIPF_VERSION', '1. 0.0' );20 define( 'WIPF_VERSION', '1.1.0' ); 21 21 define( 'WIPF_FILE', __FILE__ ); 22 22 define( 'WIPF_PATH', plugin_dir_path( __FILE__ ) ); … … 31 31 function wipf_get_default_settings() { 32 32 return array( 33 'brand_name' => get_bloginfo( 'name' ), 34 'default_category' => 'Apparel & Accessories > Jewelry', 35 'default_age_group' => 'adult', 36 'default_gender' => 'unisex', 37 'default_condition' => 'new', 38 'identifier_exists' => 'no', 39 'auto_detect_color' => 'yes', 40 'shipping_countries' => 'US,CA,GB,JP,SG,HK,AE,MX', 41 'shipping_service_name' => 'Standard Free Shipping', 42 'shipping_cost' => '0.00', 43 'shipping_currency' => 'USD', 44 'enable_google_feed' => 'yes', 45 'enable_trustmate_feed' => 'yes', 46 'items_per_feed' => 500, 33 'brand_name' => get_bloginfo( 'name' ), 34 'default_category' => 'Apparel & Accessories > Jewelry', 35 'default_age_group' => 'adult', 36 'default_gender' => 'unisex', 37 'default_condition' => 'new', 38 'identifier_exists' => 'no', 39 'auto_detect_color' => 'yes', 40 'auto_detect_material' => 'yes', 41 'auto_detect_size' => 'yes', 42 'auto_detect_pattern' => 'yes', 43 'shipping_countries' => 'US,CA,GB,JP,SG,HK,AE,MX', 44 'shipping_service_name' => 'Standard Free Shipping', 45 'shipping_cost' => '0.00', 46 'shipping_currency' => 'USD', 47 'enable_google_feed' => 'yes', 48 'enable_trustmate_feed' => 'yes', 49 'items_per_feed' => 500, 50 'enable_custom_labels' => 'yes', 51 'custom_label_0_type' => 'price_tier', 52 'custom_label_0_value' => '25,50,100', 53 'custom_label_0_fallback' => 'Standard', 54 'custom_label_1_type' => 'inventory', 55 'custom_label_1_value' => '', 56 'custom_label_1_fallback' => 'Out of Stock', 57 'custom_label_2_type' => 'category', 58 'custom_label_2_value' => '', 59 'custom_label_2_fallback' => 'Jewelry', 60 'custom_label_3_type' => 'tag_keywords', 61 'custom_label_3_value' => 'clearance:Clearance, sale:On Sale, bestseller:Best Seller, new:New Arrival', 62 'custom_label_3_fallback' => 'Regular', 63 'custom_label_4_type' => 'static', 64 'custom_label_4_value' => 'World Inspire Catalog', 65 'custom_label_4_fallback' => '', 47 66 ); 48 67 } … … 78 97 // Priority compound phrases checked first 79 98 $compounds = array( 80 'rose gold' => 'Rose Gold',81 'tiger eye' => 'Brown',99 'rose gold' => 'Rose Gold', 100 'tiger eye' => 'Brown', 82 101 'lapis lazuli' => 'Blue', 83 102 ); … … 154 173 155 174 /** 175 * Intelligent material extraction heuristic. 176 * 177 * @param string $title Product title. 178 * @param string $desc Product description. 179 * @return string Extracted material name or empty. 180 */ 181 function wipf_detect_material( $title, $desc = '' ) { 182 $text = strtolower( $title . ' ' . $desc ); 183 184 // Priority compound phrases checked first 185 $compounds = array( 186 'sterling silver' => 'Sterling Silver', 187 '925 silver' => 'Sterling Silver', 188 'rose gold' => 'Rose Gold', 189 'white gold' => 'White Gold', 190 'yellow gold' => 'Yellow Gold', 191 '14k gold' => '14K Gold', 192 '18k gold' => '18K Gold', 193 '24k gold' => '24K Gold', 194 'stainless steel' => 'Stainless Steel', 195 'genuine leather' => 'Leather', 196 'faux leather' => 'Faux Leather', 197 'vegan leather' => 'Faux Leather', 198 'rose quartz' => 'Rose Quartz', 199 'lapis lazuli' => 'Lapis Lazuli', 200 'cubic zirconia' => 'Cubic Zirconia', 201 ); 202 203 $detected = array(); 204 foreach ( $compounds as $phrase => $mat_name ) { 205 if ( false !== strpos( $text, $phrase ) ) { 206 if ( ! in_array( $mat_name, $detected, true ) ) { 207 $detected[] = $mat_name; 208 } 209 $text = str_replace( $phrase, '', $text ); 210 } 211 } 212 213 $single_map = array( 214 'gold' => 'Gold', 215 'silver' => 'Silver', 216 'platinum' => 'Platinum', 217 'titanium' => 'Titanium', 218 'brass' => 'Brass', 219 'copper' => 'Copper', 220 'bronze' => 'Bronze', 221 'steel' => 'Steel', 222 'diamond' => 'Diamond', 223 'pearl' => 'Pearl', 224 'amethyst' => 'Amethyst', 225 'quartz' => 'Quartz', 226 'emerald' => 'Emerald', 227 'ruby' => 'Ruby', 228 'sapphire' => 'Sapphire', 229 'jade' => 'Jade', 230 'turquoise' => 'Turquoise', 231 'obsidian' => 'Obsidian', 232 'onyx' => 'Onyx', 233 'opal' => 'Opal', 234 'moonstone' => 'Moonstone', 235 'topaz' => 'Topaz', 236 'garnet' => 'Garnet', 237 'tourmaline' => 'Tourmaline', 238 'amber' => 'Amber', 239 'carnelian' => 'Carnelian', 240 'citrine' => 'Citrine', 241 'crystal' => 'Crystal', 242 'leather' => 'Leather', 243 'silk' => 'Silk', 244 'velvet' => 'Velvet', 245 'cotton' => 'Cotton', 246 'linen' => 'Linen', 247 'wool' => 'Wool', 248 'cashmere' => 'Cashmere', 249 'satin' => 'Satin', 250 'polyester' => 'Polyester', 251 'canvas' => 'Canvas', 252 'denim' => 'Denim', 253 'ceramic' => 'Ceramic', 254 'glass' => 'Glass', 255 'wood' => 'Wood', 256 'silicone' => 'Silicone', 257 'resin' => 'Resin', 258 ); 259 260 foreach ( $single_map as $needle => $mat_name ) { 261 if ( preg_match( '/\b' . preg_quote( $needle, '/' ) . '\b/', $text ) ) { 262 if ( ! in_array( $mat_name, $detected, true ) ) { 263 $detected[] = $mat_name; 264 } 265 } 266 } 267 268 if ( count( $detected ) > 2 ) { 269 return 'Mixed'; 270 } elseif ( ! empty( $detected ) ) { 271 return implode( '/', $detected ); 272 } 273 274 return ''; 275 } 276 277 /** 278 * Intelligent pattern extraction heuristic. 279 * 280 * @param string $title Product title. 281 * @param string $desc Product description. 282 * @return string Extracted pattern or empty. 283 */ 284 function wipf_detect_pattern( $title, $desc = '' ) { 285 $text = strtolower( $title . ' ' . $desc ); 286 287 $patterns = array( 288 'polka dot' => 'Polka Dot', 289 'polka dots' => 'Polka Dot', 290 'animal print' => 'Animal Print', 291 'striped' => 'Striped', 292 'stripes' => 'Striped', 293 'stripe' => 'Striped', 294 'floral' => 'Floral', 295 'flower' => 'Floral', 296 'geometric' => 'Geometric', 297 'beaded' => 'Beaded', 298 'braided' => 'Braided', 299 'plaid' => 'Plaid', 300 'checkered' => 'Checkered', 301 'leopard' => 'Animal Print', 302 'cheetah' => 'Animal Print', 303 'zebra' => 'Animal Print', 304 'camouflage' => 'Camouflage', 305 'camo' => 'Camouflage', 306 'marble' => 'Marble', 307 'gradient' => 'Gradient', 308 'solid' => 'Solid', 309 ); 310 311 foreach ( $patterns as $needle => $pat_name ) { 312 if ( preg_match( '/\b' . preg_quote( $needle, '/' ) . '\b/', $text ) ) { 313 return $pat_name; 314 } 315 } 316 317 return ''; 318 } 319 320 /** 321 * Intelligent size extraction heuristic. 322 * 323 * @param string $title Product title. 324 * @param string $desc Product description. 325 * @return string Extracted size or empty. 326 */ 327 function wipf_detect_size( $title, $desc = '' ) { 328 $text = $title . ' ' . $desc; 329 330 // 1. One Size / OS 331 if ( preg_match( '/\b(one\s*size|osfm|osfa)\b/i', $text ) ) { 332 return 'One Size'; 333 } 334 335 // 2. Ring or shoe sizes: "Size 7", "Size 7.5", "US 8" 336 if ( preg_match( '/\b(?:size|us|eu)\s*([0-9]+(?:\.[0-9]+)?)\b/i', $text, $matches ) ) { 337 return $matches[1]; 338 } 339 340 // 3. Dimensional bead / diameter sizes: "8mm", "10mm", "12mm" 341 if ( preg_match( '/\b([0-9]+(?:\.[0-9]+)?\s*(?:mm|cm|in|inch|inches))\b/i', $text, $matches ) ) { 342 return trim( $matches[1] ); 343 } 344 345 // 4. Apparel sizes: XS, S, M, L, XL, XXL, 2XL, 3XL 346 if ( preg_match( '/\b(XS|S|M|L|XL|XXL|2XL|3XL)\b/', $text, $matches ) ) { 347 return $matches[1]; 348 } 349 350 return ''; 351 } 352 353 /** 354 * Evaluates a Google Shopping custom label (0-4) according to configured rules. 355 * 356 * @param int $slot Custom label index (0 to 4). 357 * @param array $product Normalized product data. 358 * @param array $config Plugin settings. 359 * @return array Array with keys 'value', 'reason', 'type'. 360 */ 361 function wipf_evaluate_custom_label( $slot, $product, $config ) { 362 $slot = absint( $slot ); 363 if ( $slot < 0 || $slot > 4 ) { 364 return array( 'value' => '', 'reason' => 'Invalid slot', 'type' => 'disabled' ); 365 } 366 367 $type = ! empty( $config["custom_label_{$slot}_type"] ) ? $config["custom_label_{$slot}_type"] : 'disabled'; 368 $rule_val = ! empty( $config["custom_label_{$slot}_value"] ) ? trim( $config["custom_label_{$slot}_value"] ) : ''; 369 $fallback = ! empty( $config["custom_label_{$slot}_fallback"] ) ? trim( $config["custom_label_{$slot}_fallback"] ) : ''; 370 371 $result = ''; 372 $reason = ''; 373 374 switch ( $type ) { 375 case 'disabled': 376 return array( 377 'value' => '', 378 'reason' => 'Slot disabled', 379 'type' => 'disabled', 380 ); 381 382 case 'static': 383 $result = $rule_val; 384 $reason = 'Static configured value: ' . $rule_val; 385 break; 386 387 case 'price_tier': 388 $price = isset( $product['price'] ) ? (float) $product['price'] : 0.0; 389 $curr = ! empty( $product['currency'] ) ? $product['currency'] : ( $config['shipping_currency'] ?? 'USD' ); 390 391 // Check custom thresholds e.g. "50" or "30,80" or "25,50,100" 392 $thresholds = array( 25.0, 50.0, 100.0 ); 393 if ( ! empty( $rule_val ) ) { 394 $parsed = array(); 395 $raw_parts = explode( ',', $rule_val ); 396 foreach ( $raw_parts as $rp ) { 397 $fv = (float) trim( $rp ); 398 if ( $fv > 0 ) { 399 $parsed[] = $fv; 400 } 401 } 402 if ( ! empty( $parsed ) ) { 403 sort( $parsed ); 404 $thresholds = array_values( array_unique( $parsed ) ); 405 } 406 } 407 408 $count = count( $thresholds ); 409 if ( $price < $thresholds[0] ) { 410 $result = 'Under ' . (int) $thresholds[0]; 411 $reason = sprintf( 'Price %0.2f %s < %0.2f', $price, $curr, $thresholds[0] ); 412 } elseif ( $price >= $thresholds[ $count - 1 ] ) { 413 $result = (int) $thresholds[ $count - 1 ] . ' and Above'; 414 $reason = sprintf( 'Price %0.2f %s >= %0.2f', $price, $curr, $thresholds[ $count - 1 ] ); 415 } else { 416 for ( $ti = 0; $ti < $count - 1; $ti++ ) { 417 if ( $price >= $thresholds[ $ti ] && $price < $thresholds[ $ti + 1 ] ) { 418 $result = (int) $thresholds[ $ti ] . ' to ' . (int) $thresholds[ $ti + 1 ]; 419 $reason = sprintf( 'Price %0.2f %s between %0.2f and %0.2f', $price, $curr, $thresholds[ $ti ], $thresholds[ $ti + 1 ] ); 420 break; 421 } 422 } 423 } 424 break; 425 426 case 'inventory': 427 $status = ! empty( $product['availability'] ) ? $product['availability'] : 'in_stock'; 428 $in_label = 'In Stock'; 429 $out_label = 'Out of Stock'; 430 if ( ! empty( $rule_val ) && false !== strpos( $rule_val, ':' ) ) { 431 $pairs = explode( ',', $rule_val ); 432 foreach ( $pairs as $pair ) { 433 $p = array_map( 'trim', explode( ':', $pair, 2 ) ); 434 if ( count( $p ) === 2 ) { 435 if ( 'in_stock' === $p[0] ) { 436 $in_label = $p[1]; 437 } elseif ( 'out_of_stock' === $p[0] ) { 438 $out_label = $p[1]; 439 } 440 } 441 } 442 } 443 444 if ( 'out_of_stock' === $status ) { 445 $result = $out_label; 446 $reason = 'Inventory status is out_of_stock'; 447 } else { 448 $result = $in_label; 449 $reason = 'Inventory status is in_stock'; 450 } 451 break; 452 453 case 'category': 454 if ( ! empty( $product['categories'] ) && is_array( $product['categories'] ) ) { 455 $result = reset( $product['categories'] ); 456 $reason = 'Derived from primary category: ' . $result; 457 } elseif ( ! empty( $product['category'] ) ) { 458 $result = $product['category']; 459 $reason = 'Derived from product category: ' . $result; 460 } else { 461 $result = ! empty( $config['default_category'] ) ? $config['default_category'] : ''; 462 $reason = 'Fallback to default category'; 463 } 464 break; 465 466 case 'tag_keywords': 467 $tags_list = ! empty( $product['tags'] ) && is_array( $product['tags'] ) ? $product['tags'] : array(); 468 $text_to_search = strtolower( ( $product['title'] ?? '' ) . ' ' . implode( ' ', $tags_list ) ); 469 470 if ( ! empty( $rule_val ) ) { 471 $pairs = explode( ',', $rule_val ); 472 foreach ( $pairs as $pair ) { 473 $parts = array_map( 'trim', explode( ':', $pair, 2 ) ); 474 $keyword = strtolower( $parts[0] ); 475 $label = isset( $parts[1] ) && '' !== $parts[1] ? $parts[1] : ucwords( $parts[0] ); 476 477 if ( '' !== $keyword && false !== strpos( $text_to_search, $keyword ) ) { 478 $result = $label; 479 $reason = "Matched keyword '{$keyword}' in title or tags"; 480 break; 481 } 482 } 483 } 484 485 if ( empty( $result ) ) { 486 $reason = 'No matching tag or keyword found'; 487 } 488 break; 489 490 case 'custom_meta': 491 $meta_key = $rule_val; 492 if ( ! empty( $meta_key ) && ! empty( $product['id'] ) && function_exists( 'get_post_meta' ) ) { 493 $meta_val = get_post_meta( $product['id'], $meta_key, true ); 494 if ( empty( $meta_val ) && ! empty( $product['item_group_id'] ) ) { 495 $meta_val = get_post_meta( $product['item_group_id'], $meta_key, true ); 496 } 497 if ( ! empty( $meta_val ) && is_scalar( $meta_val ) ) { 498 $result = (string) $meta_val; 499 $reason = "Retrieved from post meta '{$meta_key}'"; 500 } else { 501 $reason = "Post meta '{$meta_key}' is empty"; 502 } 503 } else { 504 $reason = 'Custom meta key not specified'; 505 } 506 break; 507 508 default: 509 $reason = 'Unknown rule type: ' . $type; 510 break; 511 } 512 513 // Apply fallback if result is empty 514 if ( '' === $result && '' !== $fallback ) { 515 $result = $fallback; 516 $reason .= ' (Fallback applied: ' . $fallback . ')'; 517 } 518 519 // Google Shopping max length for custom_label_0-4 is 100 characters 520 if ( '' !== $result ) { 521 $clean_result = sanitize_text_field( $result ); 522 if ( function_exists( 'mb_strlen' ) ) { 523 if ( mb_strlen( $clean_result, 'UTF-8' ) > 100 ) { 524 $clean_result = mb_substr( $clean_result, 0, 100, 'UTF-8' ); 525 } 526 } else { 527 $clean_result = substr( $clean_result, 0, 100 ); 528 } 529 $result = $clean_result; 530 } 531 532 return array( 533 'value' => $result, 534 'reason' => $reason, 535 'type' => $type, 536 ); 537 } 538 539 /** 156 540 * Safe XML entity escaper. 157 541 * … … 165 549 /** 166 550 * Fetches normalized product catalog from active e-commerce platforms. 167 * Supports SureCart and WooCommerce seamlessly .551 * Supports SureCart and WooCommerce seamlessly including product variations. 168 552 * 169 553 * @param int $limit Maximum number of products to return. … … 176 560 $limit = 500; 177 561 } 562 563 $settings = wipf_get_settings(); 178 564 179 565 // 1. Check SureCart products (sc_product) … … 239 625 } 240 626 627 // Categories & Tags 628 $categories = array(); 629 if ( function_exists( 'wp_get_post_terms' ) ) { 630 $sc_terms = wp_get_post_terms( $post_id, 'sc_collection', array( 'fields' => 'names' ) ); 631 if ( is_array( $sc_terms ) ) { 632 $categories = $sc_terms; 633 } 634 } 635 636 $tags = array(); 637 if ( function_exists( 'wp_get_post_terms' ) ) { 638 $sc_tag_terms = wp_get_post_terms( $post_id, 'sc_tag', array( 'fields' => 'names' ) ); 639 if ( is_array( $sc_tag_terms ) ) { 640 $tags = $sc_tag_terms; 641 } 642 } 643 644 // Variant attributes 645 $parent_id = get_post_meta( $post_id, 'sc_parent_id', true ); 646 647 $sc_color = get_post_meta( $post_id, 'sc_color', true ); 648 if ( empty( $sc_color ) && 'yes' === ( $settings['auto_detect_color'] ?? 'yes' ) ) { 649 $sc_color = wipf_detect_color( $title, $content ); 650 } 651 652 $sc_size = get_post_meta( $post_id, 'sc_size', true ); 653 if ( empty( $sc_size ) && 'yes' === ( $settings['auto_detect_size'] ?? 'yes' ) ) { 654 $sc_size = wipf_detect_size( $title, $content ); 655 } 656 657 $sc_mat = get_post_meta( $post_id, 'sc_material', true ); 658 if ( empty( $sc_mat ) && 'yes' === ( $settings['auto_detect_material'] ?? 'yes' ) ) { 659 $sc_mat = wipf_detect_material( $title, $content ); 660 } 661 662 $sc_pat = get_post_meta( $post_id, 'sc_pattern', true ); 663 if ( empty( $sc_pat ) && 'yes' === ( $settings['auto_detect_pattern'] ?? 'yes' ) ) { 664 $sc_pat = wipf_detect_pattern( $title, $content ); 665 } 666 241 667 $products[] = array( 242 668 'id' => $post_id, 669 'item_group_id' => ! empty( $parent_id ) ? (string) $parent_id : '', 243 670 'title' => $title, 244 671 'description' => $content, … … 250 677 'availability' => $inv_status, 251 678 'source' => 'surecart', 679 'color' => $sc_color, 680 'size' => $sc_size, 681 'material' => $sc_mat, 682 'pattern' => $sc_pat, 683 'categories' => $categories, 684 'tags' => $tags, 252 685 ); 686 687 if ( count( $products ) >= $limit ) { 688 break; 689 } 253 690 } 254 691 wp_reset_postdata(); … … 257 694 258 695 // 2. Check WooCommerce products (product) 259 if ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' )) {696 if ( ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) ) && count( $products ) < $limit ) { 260 697 $wc_args = array( 261 698 'post_type' => 'product', 262 699 'post_status' => 'publish', 263 'posts_per_page' => $limit ,700 'posts_per_page' => $limit - count( $products ), 264 701 'orderby' => 'ID', 265 702 'order' => 'DESC', … … 284 721 $inv_status = 'in_stock'; 285 722 286 if ( function_exists( 'wc_get_product' ) ) { 287 $wc_prod = wc_get_product( $post_id ); 288 if ( $wc_prod ) { 289 $price = $wc_prod->get_price(); 290 $inv_status = $wc_prod->is_in_stock() ? 'in_stock' : 'out_of_stock'; 723 // Categories & Tags 724 $categories = array(); 725 if ( function_exists( 'wp_get_post_terms' ) ) { 726 $wc_terms = wp_get_post_terms( $post_id, 'product_cat', array( 'fields' => 'names' ) ); 727 if ( is_array( $wc_terms ) ) { 728 $categories = $wc_terms; 291 729 } 292 } else { 293 $raw_price = get_post_meta( $post_id, '_price', true ); 294 if ( is_numeric( $raw_price ) ) { 295 $price = (float) $raw_price; 730 } 731 732 $tags = array(); 733 if ( function_exists( 'wp_get_post_terms' ) ) { 734 $wc_tag_terms = wp_get_post_terms( $post_id, 'product_tag', array( 'fields' => 'names' ) ); 735 if ( is_array( $wc_tag_terms ) ) { 736 $tags = $wc_tag_terms; 296 737 } 297 738 } … … 314 755 } 315 756 757 // Check WooCommerce Product object & Variable product handling 758 $wc_prod = function_exists( 'wc_get_product' ) ? wc_get_product( $post_id ) : null; 759 if ( $wc_prod ) { 760 // Variable products: expand into variations with item_group_id 761 if ( method_exists( $wc_prod, 'is_type' ) && $wc_prod->is_type( 'variable' ) ) { 762 $variation_ids = method_exists( $wc_prod, 'get_children' ) ? $wc_prod->get_children() : array(); 763 if ( ! empty( $variation_ids ) ) { 764 foreach ( $variation_ids as $var_id ) { 765 $var_obj = wc_get_product( $var_id ); 766 if ( ! $var_obj ) { 767 continue; 768 } 769 770 $var_price = $var_obj->get_price(); 771 $var_inv = $var_obj->is_in_stock() ? 'in_stock' : 'out_of_stock'; 772 $var_link = $var_obj->get_permalink() ?: $permalink; 773 774 $var_img_id = $var_obj->get_image_id(); 775 $var_image = $var_img_id ? wp_get_attachment_url( $var_img_id ) : $image_url; 776 777 // Variation attributes 778 $var_color = $var_obj->get_attribute( 'pa_color' ) ?: $var_obj->get_attribute( 'color' ); 779 if ( empty( $var_color ) && 'yes' === ( $settings['auto_detect_color'] ?? 'yes' ) ) { 780 $var_color = wipf_detect_color( $var_obj->get_name() . ' ' . $title, $content ); 781 } 782 783 $var_size = $var_obj->get_attribute( 'pa_size' ) ?: $var_obj->get_attribute( 'size' ); 784 if ( empty( $var_size ) && 'yes' === ( $settings['auto_detect_size'] ?? 'yes' ) ) { 785 $var_size = wipf_detect_size( $var_obj->get_name() . ' ' . $title, $content ); 786 } 787 788 $var_mat = $var_obj->get_attribute( 'pa_material' ) ?: $var_obj->get_attribute( 'material' ); 789 if ( empty( $var_mat ) && 'yes' === ( $settings['auto_detect_material'] ?? 'yes' ) ) { 790 $var_mat = wipf_detect_material( $var_obj->get_name() . ' ' . $title, $content ); 791 } 792 793 $var_pat = $var_obj->get_attribute( 'pa_pattern' ) ?: $var_obj->get_attribute( 'pattern' ); 794 if ( empty( $var_pat ) && 'yes' === ( $settings['auto_detect_pattern'] ?? 'yes' ) ) { 795 $var_pat = wipf_detect_pattern( $var_obj->get_name() . ' ' . $title, $content ); 796 } 797 798 $products[] = array( 799 'id' => $var_id, 800 'item_group_id' => (string) $post_id, 801 'title' => $var_obj->get_name() ?: $title, 802 'description' => $content, 803 'permalink' => $var_link, 804 'image_url' => $var_image, 805 'gallery_images' => $gallery_images, 806 'price' => number_format( (float) $var_price, 2, '.', '' ), 807 'currency' => $currency, 808 'availability' => $var_inv, 809 'source' => 'woocommerce', 810 'color' => $var_color, 811 'size' => $var_size, 812 'material' => $var_mat, 813 'pattern' => $var_pat, 814 'categories' => $categories, 815 'tags' => $tags, 816 ); 817 818 if ( count( $products ) >= $limit ) { 819 break 2; 820 } 821 } 822 continue; // Variations added, skip parent product 823 } 824 } 825 826 // Simple product 827 $price = $wc_prod->get_price(); 828 $inv_status = $wc_prod->is_in_stock() ? 'in_stock' : 'out_of_stock'; 829 } else { 830 $raw_price = get_post_meta( $post_id, '_price', true ); 831 if ( is_numeric( $raw_price ) ) { 832 $price = (float) $raw_price; 833 } 834 } 835 836 // Simple product attributes 837 $prod_color = ''; 838 $prod_size = ''; 839 $prod_mat = ''; 840 $prod_pat = ''; 841 842 if ( $wc_prod && method_exists( $wc_prod, 'get_attribute' ) ) { 843 $prod_color = $wc_prod->get_attribute( 'pa_color' ) ?: $wc_prod->get_attribute( 'color' ); 844 $prod_size = $wc_prod->get_attribute( 'pa_size' ) ?: $wc_prod->get_attribute( 'size' ); 845 $prod_mat = $wc_prod->get_attribute( 'pa_material' ) ?: $wc_prod->get_attribute( 'material' ); 846 $prod_pat = $wc_prod->get_attribute( 'pa_pattern' ) ?: $wc_prod->get_attribute( 'pattern' ); 847 } 848 849 if ( empty( $prod_color ) && 'yes' === ( $settings['auto_detect_color'] ?? 'yes' ) ) { 850 $prod_color = wipf_detect_color( $title, $content ); 851 } 852 if ( empty( $prod_size ) && 'yes' === ( $settings['auto_detect_size'] ?? 'yes' ) ) { 853 $prod_size = wipf_detect_size( $title, $content ); 854 } 855 if ( empty( $prod_mat ) && 'yes' === ( $settings['auto_detect_material'] ?? 'yes' ) ) { 856 $prod_mat = wipf_detect_material( $title, $content ); 857 } 858 if ( empty( $prod_pat ) && 'yes' === ( $settings['auto_detect_pattern'] ?? 'yes' ) ) { 859 $prod_pat = wipf_detect_pattern( $title, $content ); 860 } 861 316 862 $products[] = array( 317 863 'id' => $post_id, 864 'item_group_id' => '', 318 865 'title' => $title, 319 866 'description' => $content, … … 325 872 'availability' => $inv_status, 326 873 'source' => 'woocommerce', 874 'color' => $prod_color, 875 'size' => $prod_size, 876 'material' => $prod_mat, 877 'pattern' => $prod_pat, 878 'categories' => $categories, 879 'tags' => $tags, 327 880 ); 881 882 if ( count( $products ) >= $limit ) { 883 break; 884 } 328 885 } 329 886 wp_reset_postdata(); … … 353 910 $ship_cost = ! empty( $config['shipping_cost'] ) ? $config['shipping_cost'] : '0.00'; 354 911 $ship_curr = ! empty( $config['shipping_currency'] ) ? $config['shipping_currency'] : 'USD'; 912 $enable_labels = ( 'no' !== ( $config['enable_custom_labels'] ?? 'yes' ) ); 355 913 356 914 $xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n"; … … 367 925 $price_val = $item['price']; 368 926 $curr_val = ! empty( $item['currency'] ) ? $item['currency'] : $ship_curr; 369 $item_color = $auto_color ? wipf_detect_color( $title, $desc ) : ''; 370 371 // Pet classification heuristic 927 928 // Color priority: item attribute > heuristic 929 $item_color = ! empty( $item['color'] ) ? $item['color'] : ( $auto_color ? wipf_detect_color( $title, $desc ) : '' ); 930 931 // Category classification heuristic 372 932 $category = $def_category; 933 if ( ! empty( $item['categories'] ) && is_array( $item['categories'] ) ) { 934 $category = implode( ' > ', $item['categories'] ); 935 } 373 936 if ( false !== stripos( $title, 'collar' ) || false !== stripos( $title, 'pet' ) || false !== stripos( $title, 'dog' ) || false !== stripos( $title, 'cat' ) ) { 374 937 $category = 'Animals & Pet Supplies > Pet Supplies > Pet Collars & Harnesses'; … … 377 940 $xml .= " <item>\n"; 378 941 $xml .= ' <g:id>' . wipf_xml_escape( $item['id'] ) . "</g:id>\n"; 942 943 // Variant item_group_id grouping 944 if ( ! empty( $item['item_group_id'] ) ) { 945 $xml .= ' <g:item_group_id>' . wipf_xml_escape( $item['item_group_id'] ) . "</g:item_group_id>\n"; 946 } 947 379 948 $xml .= ' <g:title>' . wipf_xml_escape( $title ) . "</g:title>\n"; 380 949 $xml .= ' <g:description>' . wipf_xml_escape( $desc ) . "</g:description>\n"; … … 400 969 $xml .= ' <g:color>' . wipf_xml_escape( $item_color ) . "</g:color>\n"; 401 970 } 971 if ( ! empty( $item['size'] ) ) { 972 $xml .= ' <g:size>' . wipf_xml_escape( $item['size'] ) . "</g:size>\n"; 973 } 974 if ( ! empty( $item['material'] ) ) { 975 $xml .= ' <g:material>' . wipf_xml_escape( $item['material'] ) . "</g:material>\n"; 976 } 977 if ( ! empty( $item['pattern'] ) ) { 978 $xml .= ' <g:pattern>' . wipf_xml_escape( $item['pattern'] ) . "</g:pattern>\n"; 979 } 980 402 981 if ( ! empty( $def_age_group ) ) { 403 982 $xml .= ' <g:age_group>' . wipf_xml_escape( $def_age_group ) . "</g:age_group>\n"; … … 405 984 if ( ! empty( $def_gender ) ) { 406 985 $xml .= ' <g:gender>' . wipf_xml_escape( $def_gender ) . "</g:gender>\n"; 986 } 987 988 // Inject Custom Labels (custom_label_0 to custom_label_4) 989 if ( $enable_labels ) { 990 for ( $slot = 0; $slot <= 4; $slot++ ) { 991 $label_data = wipf_evaluate_custom_label( $slot, $item, $config ); 992 if ( ! empty( $label_data['value'] ) ) { 993 $xml .= ' <g:custom_label_' . $slot . '>' . wipf_xml_escape( $label_data['value'] ) . '</g:custom_label_' . $slot . ">\n"; 994 } 995 } 407 996 } 408 997 … … 463 1052 464 1053 /** 1054 * Detailed diagnostic inspector for a specific product ID or product data array. 1055 * 1056 * @param int|array $product Product ID or product array. 1057 * @param array|null $config Optional configuration override. 1058 * @return array Diagnostic report. 1059 */ 1060 function wipf_diagnose_product( $product, $config = null ) { 1061 if ( null === $config ) { 1062 $config = wipf_get_settings(); 1063 } 1064 1065 $item = is_array( $product ) ? $product : null; 1066 if ( null === $item && is_numeric( $product ) ) { 1067 $limit = 200; 1068 $all_prods = wipf_collect_products( $limit ); 1069 foreach ( $all_prods as $p ) { 1070 if ( (int) $p['id'] === (int) $product ) { 1071 $item = $p; 1072 break; 1073 } 1074 } 1075 if ( null === $item && function_exists( 'get_post' ) ) { 1076 $post = get_post( (int) $product ); 1077 if ( $post && 'publish' === ( $post->post_status ?? '' ) ) { 1078 $title = get_the_title( $post ); 1079 $content = wp_strip_all_tags( get_the_excerpt( $post ) ?: ( get_the_content( null, false, $post ) ?: $title ) ); 1080 $raw_price = get_post_meta( $post->ID, '_price', true ); 1081 $price = is_numeric( $raw_price ) ? (float) $raw_price : 0.0; 1082 $image = get_the_post_thumbnail_url( $post->ID, 'full' ) ?: ''; 1083 1084 $categories = function_exists( 'wp_get_post_terms' ) ? wp_get_post_terms( $post->ID, 'product_cat', array( 'fields' => 'names' ) ) : array(); 1085 $tags = function_exists( 'wp_get_post_terms' ) ? wp_get_post_terms( $post->ID, 'product_tag', array( 'fields' => 'names' ) ) : array(); 1086 1087 $item = array( 1088 'id' => $post->ID, 1089 'item_group_id' => '', 1090 'title' => $title, 1091 'description' => $content, 1092 'permalink' => get_permalink( $post->ID ), 1093 'image_url' => $image, 1094 'gallery_images' => array(), 1095 'price' => number_format( $price, 2, '.', '' ), 1096 'currency' => $config['shipping_currency'] ?? 'USD', 1097 'availability' => 'in_stock', 1098 'source' => $post->post_type, 1099 'color' => wipf_detect_color( $title, $content ), 1100 'size' => wipf_detect_size( $title, $content ), 1101 'material' => wipf_detect_material( $title, $content ), 1102 'pattern' => wipf_detect_pattern( $title, $content ), 1103 'categories' => is_array( $categories ) ? $categories : array(), 1104 'tags' => is_array( $tags ) ? $tags : array(), 1105 ); 1106 } 1107 } 1108 } 1109 1110 if ( empty( $item ) ) { 1111 return array( 1112 'found' => false, 1113 'error' => __( 'Product not found or not published in catalog.', 'world-inspire-product-feed-for-google-shopping' ), 1114 ); 1115 } 1116 1117 $labels = array(); 1118 for ( $i = 0; $i <= 4; $i++ ) { 1119 $labels[ "custom_label_{$i}" ] = wipf_evaluate_custom_label( $i, $item, $config ); 1120 } 1121 1122 // Generate XML snippet for just this item 1123 $xml_doc = wipf_build_google_shopping_xml( array( $item ), $config ); 1124 $xml_snippet = ''; 1125 if ( preg_match( '/<item>.*?<\/item>/s', $xml_doc, $matches ) ) { 1126 $xml_snippet = trim( $matches[0] ); 1127 } 1128 1129 return array( 1130 'found' => true, 1131 'product' => $item, 1132 'labels' => $labels, 1133 'xml_snippet' => $xml_snippet, 1134 ); 1135 } 1136 1137 /** 465 1138 * Streams XML document with anti-caching reverse-proxy protection. 466 1139 * … … 482 1155 483 1156 /** 484 * Registers REST API endpoints for Google Shopping and Trustmate.1157 * Registers REST API endpoints for Google Shopping, Trustmate, and Status. 485 1158 */ 486 1159 function wipf_register_rest_routes() { … … 525 1198 return new WP_REST_Response( 526 1199 array( 527 'enabled' => true,528 'status' => 'healthy',529 'version' => WIPF_VERSION,530 'surecart_detected' => post_type_exists( 'sc_product' ),1200 'enabled' => true, 1201 'status' => 'healthy', 1202 'version' => WIPF_VERSION, 1203 'surecart_detected' => post_type_exists( 'sc_product' ), 531 1204 'woocommerce_detected' => ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) ), 532 'sample_count' => count( $products ), 1205 'sample_count' => count( $products ), 1206 'custom_labels_active' => ( 'no' !== ( wipf_get_settings()['enable_custom_labels'] ?? 'yes' ) ), 533 1207 ), 534 1208 200 … … 573 1247 $output['identifier_exists'] = ( 'yes' === ( $input['identifier_exists'] ?? 'no' ) ) ? 'yes' : 'no'; 574 1248 $output['auto_detect_color'] = ( 'yes' === ( $input['auto_detect_color'] ?? 'yes' ) ) ? 'yes' : 'no'; 1249 $output['auto_detect_material'] = ( 'yes' === ( $input['auto_detect_material'] ?? 'yes' ) ) ? 'yes' : 'no'; 1250 $output['auto_detect_size'] = ( 'yes' === ( $input['auto_detect_size'] ?? 'yes' ) ) ? 'yes' : 'no'; 1251 $output['auto_detect_pattern'] = ( 'yes' === ( $input['auto_detect_pattern'] ?? 'yes' ) ) ? 'yes' : 'no'; 575 1252 $output['shipping_countries'] = ! empty( $input['shipping_countries'] ) ? sanitize_text_field( $input['shipping_countries'] ) : $defaults['shipping_countries']; 576 1253 $output['shipping_service_name'] = ! empty( $input['shipping_service_name'] ) ? sanitize_text_field( $input['shipping_service_name'] ) : $defaults['shipping_service_name']; … … 580 1257 $output['enable_trustmate_feed'] = ( 'yes' === ( $input['enable_trustmate_feed'] ?? 'yes' ) ) ? 'yes' : 'no'; 581 1258 $output['items_per_feed'] = absint( $input['items_per_feed'] ?? 500 ); 1259 1260 // Custom Labels (custom_label_0 to custom_label_4) 1261 $output['enable_custom_labels'] = ( 'yes' === ( $input['enable_custom_labels'] ?? 'yes' ) ) ? 'yes' : 'no'; 1262 $valid_rule_types = array( 'disabled', 'static', 'price_tier', 'inventory', 'category', 'tag_keywords', 'custom_meta' ); 1263 1264 for ( $i = 0; $i <= 4; $i++ ) { 1265 $type_key = "custom_label_{$i}_type"; 1266 $val_key = "custom_label_{$i}_value"; 1267 $fallback_key = "custom_label_{$i}_fallback"; 1268 1269 $type = isset( $input[ $type_key ] ) && in_array( $input[ $type_key ], $valid_rule_types, true ) ? sanitize_key( $input[ $type_key ] ) : 'disabled'; 1270 $val = isset( $input[ $val_key ] ) ? sanitize_text_field( $input[ $val_key ] ) : ''; 1271 $fb = isset( $input[ $fallback_key ] ) ? sanitize_text_field( $input[ $fallback_key ] ) : ''; 1272 1273 $output[ $type_key ] = $type; 1274 $output[ $val_key ] = $val; 1275 $output[ $fallback_key ] = $fb; 1276 } 582 1277 583 1278 return $output; … … 614 1309 ?> 615 1310 <div class="wrap"> 616 <h1><?php echo esc_html__( 'World Inspire Product Feed for Google Shopping & Trustmate', 'world-inspire-product-feed-for-google-shopping' ); ?> </h1>1311 <h1><?php echo esc_html__( 'World Inspire Product Feed for Google Shopping & Trustmate', 'world-inspire-product-feed-for-google-shopping' ); ?> <span style="font-size: 14px; background: #2271b1; color: #fff; padding: 2px 8px; border-radius: 4px; vertical-align: middle;">v<?php echo esc_html( WIPF_VERSION ); ?></span></h1> 617 1312 <p class="description"> 618 1313 <?php echo esc_html__( 'Zero-configuration, high-performance real-time XML product feeds engineered for Google Merchant Center and Trustmate review synchronization.', 'world-inspire-product-feed-for-google-shopping' ); ?> … … 624 1319 </a> 625 1320 <a href="?page=wipf-settings&tab=google" class="nav-tab <?php echo 'google' === $active_tab ? 'nav-tab-active' : ''; ?>"> 626 <?php echo esc_html__( 'Google Shopping Rules ', 'world-inspire-product-feed-for-google-shopping' ); ?>1321 <?php echo esc_html__( 'Google Shopping Rules & Custom Labels', 'world-inspire-product-feed-for-google-shopping' ); ?> 627 1322 </a> 628 1323 <a href="?page=wipf-settings&tab=shipping" class="nav-tab <?php echo 'shipping' === $active_tab ? 'nav-tab-active' : ''; ?>"> … … 630 1325 </a> 631 1326 <a href="?page=wipf-settings&tab=diagnostic" class="nav-tab <?php echo 'diagnostic' === $active_tab ? 'nav-tab-active' : ''; ?>"> 632 <?php echo esc_html__( 'Live Diagnostics & Preview', 'world-inspire-product-feed-for-google-shopping' ); ?>1327 <?php echo esc_html__( 'Live Diagnostics & Inspector', 'world-inspire-product-feed-for-google-shopping' ); ?> 633 1328 </a> 634 1329 </h2> 635 1330 636 1331 <?php if ( 'endpoints' === $active_tab ) : ?> 637 <div class="card" style="max-width: 9 00px; margin-top: 20px; padding: 20px;">1332 <div class="card" style="max-width: 960px; margin-top: 20px; padding: 20px;"> 638 1333 <h3><?php echo esc_html__( 'Authoritative Feed URLs', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 639 1334 <p><?php echo esc_html__( 'Copy these extension-free dynamic REST endpoints into Google Merchant Center and Trustmate. They automatically bypass reverse-proxy static file caches and update in real-time.', 'world-inspire-product-feed-for-google-shopping' ); ?></p> … … 664 1359 </div> 665 1360 <?php elseif ( 'google' === $active_tab || 'shipping' === $active_tab ) : ?> 666 <form method="post" action="options.php" style="max-width: 9 00px; margin-top: 20px;">1361 <form method="post" action="options.php" style="max-width: 960px; margin-top: 20px;"> 667 1362 <?php 668 1363 settings_fields( 'wipf_settings_group' ); 669 1364 ?> 670 < div class="card" style="padding: 20px;">671 < ?php if ( 'google' === $active_tab ) : ?>1365 <?php if ( 'google' === $active_tab ) : ?> 1366 <div class="card" style="padding: 20px; margin-bottom: 20px;"> 672 1367 <h3><?php echo esc_html__( 'Google Shopping Catalog Standards', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 673 1368 <table class="form-table" role="presentation"> … … 692 1387 <option value="kids" <?php selected( $settings['default_age_group'], 'kids' ); ?>><?php echo esc_html__( 'Kids', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 693 1388 <option value="toddler" <?php selected( $settings['default_age_group'], 'toddler' ); ?>><?php echo esc_html__( 'Toddler', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1389 <option value="infant" <?php selected( $settings['default_age_group'], 'infant' ); ?>><?php echo esc_html__( 'Infant', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1390 <option value="newborn" <?php selected( $settings['default_age_group'], 'newborn' ); ?>><?php echo esc_html__( 'Newborn', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 694 1391 </select> 695 1392 </td> … … 705 1402 </td> 706 1403 </tr> 1404 </table> 1405 </div> 1406 1407 <div class="card" style="padding: 20px; margin-bottom: 20px;"> 1408 <h3><?php echo esc_html__( 'Variant & Attribute Optimization', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 1409 <p class="description"> 1410 <?php echo esc_html__( 'Extract and optimize essential Google Shopping attributes from WooCommerce and SureCart. WooCommerce variable products are automatically exported as individual items linked by <g:item_group_id>.', 'world-inspire-product-feed-for-google-shopping' ); ?> 1411 </p> 1412 <table class="form-table" role="presentation"> 707 1413 <tr> 708 <th scope="row">< label for="wipf_auto_color"><?php echo esc_html__( 'Smart Color Extraction', 'world-inspire-product-feed-for-google-shopping' ); ?></label></th>1414 <th scope="row"><?php echo esc_html__( 'Smart Attribute Detection', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 709 1415 <td> 710 <label> 711 <input type="checkbox" id="wipf_auto_color" name="wipf_feed_settings[auto_detect_color]" value="yes" <?php checked( $settings['auto_detect_color'], 'yes' ); ?> /> 712 <?php echo esc_html__( 'Automatically infer and populate Google standard colors from title and description', 'world-inspire-product-feed-for-google-shopping' ); ?> 1416 <label style="display: block; margin-bottom: 8px;"> 1417 <input type="checkbox" name="wipf_feed_settings[auto_detect_color]" value="yes" <?php checked( $settings['auto_detect_color'], 'yes' ); ?> /> 1418 <strong><?php echo esc_html__( 'Color Extraction (<g:color>)', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> — <?php echo esc_html__( 'Infer Google standard color names from attributes, titles, or descriptions (over 35 jewelry/apparel shades).', 'world-inspire-product-feed-for-google-shopping' ); ?> 1419 </label> 1420 <label style="display: block; margin-bottom: 8px;"> 1421 <input type="checkbox" name="wipf_feed_settings[auto_detect_material]" value="yes" <?php checked( $settings['auto_detect_material'] ?? 'yes', 'yes' ); ?> /> 1422 <strong><?php echo esc_html__( 'Material Extraction (<g:material>)', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> — <?php echo esc_html__( 'Detect materials (Gold, Sterling Silver, Leather, Cotton, Gemstones, Titanium, Stainless Steel).', 'world-inspire-product-feed-for-google-shopping' ); ?> 1423 </label> 1424 <label style="display: block; margin-bottom: 8px;"> 1425 <input type="checkbox" name="wipf_feed_settings[auto_detect_size]" value="yes" <?php checked( $settings['auto_detect_size'] ?? 'yes', 'yes' ); ?> /> 1426 <strong><?php echo esc_html__( 'Size Extraction (<g:size>)', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> — <?php echo esc_html__( 'Extract sizes from variation attributes or titles (e.g. Size 7, One Size, 8mm, S, M, L, XL).', 'world-inspire-product-feed-for-google-shopping' ); ?> 1427 </label> 1428 <label style="display: block; margin-bottom: 8px;"> 1429 <input type="checkbox" name="wipf_feed_settings[auto_detect_pattern]" value="yes" <?php checked( $settings['auto_detect_pattern'] ?? 'yes', 'yes' ); ?> /> 1430 <strong><?php echo esc_html__( 'Pattern Extraction (<g:pattern>)', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> — <?php echo esc_html__( 'Detect patterns (Solid, Striped, Floral, Beaded, Polka Dot, Geometric).', 'world-inspire-product-feed-for-google-shopping' ); ?> 713 1431 </label> 714 1432 </td> 715 1433 </tr> 716 1434 </table> 717 <?php elseif ( 'shipping' === $active_tab ) : ?> 1435 </div> 1436 1437 <div class="card" style="padding: 20px; margin-bottom: 20px;"> 1438 <h3><?php echo esc_html__( 'Google Shopping Custom Labels (0–4)', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 1439 <p class="description"> 1440 <?php echo esc_html__( 'Custom labels allow you to segment and filter products in Google Ads and Performance Max (PMax) campaigns (e.g. by price brackets, inventory status, product category, clearance tags, or margin tiers).', 'world-inspire-product-feed-for-google-shopping' ); ?> 1441 </p> 1442 <p> 1443 <label> 1444 <input type="checkbox" name="wipf_feed_settings[enable_custom_labels]" value="yes" <?php checked( $settings['enable_custom_labels'] ?? 'yes', 'yes' ); ?> /> 1445 <strong><?php echo esc_html__( 'Enable Custom Labels in Feed', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> 1446 </label> 1447 </p> 1448 1449 <table class="widefat striped" style="margin-top: 15px;"> 1450 <thead> 1451 <tr> 1452 <th style="width: 16%;"><?php echo esc_html__( 'Custom Label Slot', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1453 <th style="width: 22%;"><?php echo esc_html__( 'Rule Type', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1454 <th style="width: 38%;"><?php echo esc_html__( 'Rule Value / Configuration', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1455 <th style="width: 24%;"><?php echo esc_html__( 'Fallback Value', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1456 </tr> 1457 </thead> 1458 <tbody> 1459 <?php 1460 $label_recommendations = array( 1461 0 => array( 'name' => 'Custom Label 0', 'rec' => 'Price Tiers (< 25, 25-50, 50-100, 100+)', 'ph_val' => '25,50,100 (Thresholds)', 'ph_fb' => 'Standard' ), 1462 1 => array( 'name' => 'Custom Label 1', 'rec' => 'Inventory Status (In Stock / Out of Stock)', 'ph_val' => 'Optional override: in_stock:Available, out_of_stock:Unavailable', 'ph_fb' => 'Out of Stock' ), 1463 2 => array( 'name' => 'Custom Label 2', 'rec' => 'Category / Collection Name', 'ph_val' => 'Leave empty to use primary category', 'ph_fb' => 'General' ), 1464 3 => array( 'name' => 'Custom Label 3', 'rec' => 'Promotional / Tag Keywords Match', 'ph_val' => 'clearance:Clearance, sale:On Sale, bestseller:Best Seller', 'ph_fb' => 'Regular' ), 1465 4 => array( 'name' => 'Custom Label 4', 'rec' => 'Static Value / Custom Post Meta Key', 'ph_val' => 'Enter static string or post_meta key', 'ph_fb' => '' ), 1466 ); 1467 1468 for ( $slot = 0; $slot <= 4; $slot++ ) : 1469 $slot_type = $settings[ "custom_label_{$slot}_type" ] ?? 'disabled'; 1470 $slot_val = $settings[ "custom_label_{$slot}_value" ] ?? ''; 1471 $slot_fb = $settings[ "custom_label_{$slot}_fallback" ] ?? ''; 1472 $meta_info = $label_recommendations[ $slot ]; 1473 ?> 1474 <tr> 1475 <td> 1476 <strong><?php echo esc_html( $meta_info['name'] ); ?></strong> 1477 <br><span class="description" style="font-size: 11px;"><?php echo esc_html( $meta_info['rec'] ); ?></span> 1478 </td> 1479 <td> 1480 <select name="wipf_feed_settings[custom_label_<?php echo $slot; ?>_type]" style="width: 100%;"> 1481 <option value="disabled" <?php selected( $slot_type, 'disabled' ); ?>><?php echo esc_html__( 'Disabled', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1482 <option value="price_tier" <?php selected( $slot_type, 'price_tier' ); ?>><?php echo esc_html__( 'Price Range Tier', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1483 <option value="inventory" <?php selected( $slot_type, 'inventory' ); ?>><?php echo esc_html__( 'Inventory Status', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1484 <option value="category" <?php selected( $slot_type, 'category' ); ?>><?php echo esc_html__( 'Product Category', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1485 <option value="tag_keywords" <?php selected( $slot_type, 'tag_keywords' ); ?>><?php echo esc_html__( 'Tag & Keyword Matcher', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1486 <option value="static" <?php selected( $slot_type, 'static' ); ?>><?php echo esc_html__( 'Static Custom String', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1487 <option value="custom_meta" <?php selected( $slot_type, 'custom_meta' ); ?>><?php echo esc_html__( 'Custom Post Meta Field', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1488 </select> 1489 </td> 1490 <td> 1491 <input type="text" name="wipf_feed_settings[custom_label_<?php echo $slot; ?>_value]" class="large-text" value="<?php echo esc_attr( $slot_val ); ?>" placeholder="<?php echo esc_attr( $meta_info['ph_val'] ); ?>" /> 1492 </td> 1493 <td> 1494 <input type="text" name="wipf_feed_settings[custom_label_<?php echo $slot; ?>_fallback]" class="regular-text" value="<?php echo esc_attr( $slot_fb ); ?>" placeholder="<?php echo esc_attr( $meta_info['ph_fb'] ); ?>" /> 1495 </td> 1496 </tr> 1497 <?php endfor; ?> 1498 </tbody> 1499 </table> 1500 </div> 1501 <?php elseif ( 'shipping' === $active_tab ) : ?> 1502 <div class="card" style="padding: 20px;"> 718 1503 <h3><?php echo esc_html__( 'Multi-Country Shipping Generator', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 719 1504 <p><?php echo esc_html__( 'Configures embedded shipping rules for international Google Shopping campaigns.', 'world-inspire-product-feed-for-google-shopping' ); ?></p> … … 741 1526 </tr> 742 1527 </table> 743 < ?php endif; ?>744 < /div>1528 </div> 1529 <?php endif; ?> 745 1530 <?php submit_button(); ?> 746 1531 </form> 747 1532 <?php elseif ( 'diagnostic' === $active_tab ) : ?> 748 <div class="card" style="max-width: 900px; margin-top: 20px; padding: 20px;"> 749 <h3><?php echo esc_html__( 'Live Feed Diagnostic Audit', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 750 <?php 751 $sample_products = wipf_collect_products( 5 ); 752 $surecart_ok = post_type_exists( 'sc_product' ); 753 $woocommerce_ok = ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) ); 754 ?> 1533 <?php 1534 $surecart_ok = post_type_exists( 'sc_product' ); 1535 $woocommerce_ok = ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) ); 1536 $sample_products = wipf_collect_products( 5 ); 1537 1538 $inspect_id = isset( $_GET['inspect_id'] ) ? absint( $_GET['inspect_id'] ) : ( ! empty( $sample_products[0]['id'] ) ? (int) $sample_products[0]['id'] : 0 ); 1539 $audit_data = $inspect_id > 0 ? wipf_diagnose_product( $inspect_id, $settings ) : null; 1540 ?> 1541 <div class="card" style="max-width: 960px; margin-top: 20px; padding: 20px;"> 1542 <h3><?php echo esc_html__( 'Live Feed Engine Status', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 755 1543 <p> 756 1544 <strong><?php echo esc_html__( 'SureCart Engine:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> 757 <?php echo $surecart_ok ? '<span style="color:green; ">✔ ' . esc_html__( 'Detected', 'world-inspire-product-feed-for-google-shopping' ) . '</span>' : '<span style="color:#666;">' . esc_html__( 'Not Installed', 'world-inspire-product-feed-for-google-shopping' ) . '</span>'; ?>1545 <?php echo $surecart_ok ? '<span style="color:green; font-weight: bold;">✔ ' . esc_html__( 'Detected', 'world-inspire-product-feed-for-google-shopping' ) . '</span>' : '<span style="color:#666;">' . esc_html__( 'Not Installed', 'world-inspire-product-feed-for-google-shopping' ) . '</span>'; ?> 758 1546 | 759 1547 <strong><?php echo esc_html__( 'WooCommerce Engine:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> 760 <?php echo $woocommerce_ok ? '<span style="color:green;">✔ ' . esc_html__( 'Detected', 'world-inspire-product-feed-for-google-shopping' ) . '</span>' : '<span style="color:#666;">' . esc_html__( 'Not Installed', 'world-inspire-product-feed-for-google-shopping' ) . '</span>'; ?> 1548 <?php echo $woocommerce_ok ? '<span style="color:green; font-weight: bold;">✔ ' . esc_html__( 'Detected', 'world-inspire-product-feed-for-google-shopping' ) . '</span>' : '<span style="color:#666;">' . esc_html__( 'Not Installed', 'world-inspire-product-feed-for-google-shopping' ) . '</span>'; ?> 1549 | 1550 <strong><?php echo esc_html__( 'Catalog Sample Available:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> 1551 <code><?php echo esc_html( count( $sample_products ) ); ?></code> 761 1552 </p> 762 <p> 763 <strong><?php echo esc_html__( 'Products Found in Catalog:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> 764 <?php echo esc_html( count( $sample_products ) ); ?> (<?php echo esc_html__( 'showing first 5', 'world-inspire-product-feed-for-google-shopping' ); ?>) 1553 </div> 1554 1555 <!-- Product Inspector & XML Simulator --> 1556 <div class="card" style="max-width: 960px; margin-top: 20px; padding: 20px;"> 1557 <h3><?php echo esc_html__( 'Product Inspector & Diagnostic Simulator', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 1558 <p class="description"> 1559 <?php echo esc_html__( 'Enter any Product ID to audit its exact extracted attributes, custom labels evaluation logic, and preview the generated Google Shopping <item> XML element.', 'world-inspire-product-feed-for-google-shopping' ); ?> 765 1560 </p> 766 1561 1562 <form method="get" action="" style="margin: 15px 0 20px 0;"> 1563 <input type="hidden" name="page" value="wipf-settings" /> 1564 <input type="hidden" name="tab" value="diagnostic" /> 1565 <label for="inspect_id"><strong><?php echo esc_html__( 'Test Product ID:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong></label> 1566 <input type="number" id="inspect_id" name="inspect_id" value="<?php echo esc_attr( $inspect_id ? (string) $inspect_id : '' ); ?>" placeholder="e.g. 790" style="width: 140px; margin: 0 10px;" /> 1567 <button type="submit" class="button button-primary"><?php echo esc_html__( 'Inspect Product', 'world-inspire-product-feed-for-google-shopping' ); ?></button> 1568 </form> 1569 1570 <?php if ( $audit_data && ! empty( $audit_data['found'] ) ) : ?> 1571 <?php 1572 $p = $audit_data['product']; 1573 ?> 1574 <div style="background: #f0f6fc; border-left: 4px solid #72aee6; padding: 12px 16px; margin-bottom: 20px;"> 1575 <h4 style="margin: 0 0 8px 0;"><?php echo esc_html( $p['title'] ); ?> <span style="font-weight: normal; font-size: 13px;">(ID: <code><?php echo esc_html( $p['id'] ); ?></code> · <?php echo esc_html( $p['price'] . ' ' . $p['currency'] ); ?> · <?php echo esc_html( $p['availability'] ); ?> · Source: <code><?php echo esc_html( $p['source'] ); ?></code>)</span></h4> 1576 <p style="margin: 0; font-size: 13px;"> 1577 <strong><?php echo esc_html__( 'Variant Group ID:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <?php echo ! empty( $p['item_group_id'] ) ? '<code>' . esc_html( $p['item_group_id'] ) . '</code>' : '<em>' . esc_html__( 'None (Simple Product)', 'world-inspire-product-feed-for-google-shopping' ) . '</em>'; ?> 1578 | 1579 <strong><?php echo esc_html__( 'Color:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <code><?php echo esc_html( $p['color'] ?: '—' ); ?></code> 1580 | 1581 <strong><?php echo esc_html__( 'Size:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <code><?php echo esc_html( $p['size'] ?: '—' ); ?></code> 1582 | 1583 <strong><?php echo esc_html__( 'Material:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <code><?php echo esc_html( $p['material'] ?: '—' ); ?></code> 1584 | 1585 <strong><?php echo esc_html__( 'Pattern:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <code><?php echo esc_html( $p['pattern'] ?: '—' ); ?></code> 1586 </p> 1587 </div> 1588 1589 <h4><?php echo esc_html__( 'Custom Labels Evaluation Audit (0–4)', 'world-inspire-product-feed-for-google-shopping' ); ?></h4> 1590 <table class="widefat striped" style="margin-bottom: 20px;"> 1591 <thead> 1592 <tr> 1593 <th style="width: 16%;"><?php echo esc_html__( 'Slot', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1594 <th style="width: 18%;"><?php echo esc_html__( 'Rule Type', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1595 <th style="width: 26%;"><?php echo esc_html__( 'Evaluated Value', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1596 <th style="width: 40%;"><?php echo esc_html__( 'Matching Reason / Trace', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1597 </tr> 1598 </thead> 1599 <tbody> 1600 <?php foreach ( $audit_data['labels'] as $slot_key => $eval ) : ?> 1601 <tr> 1602 <td><code><?php echo esc_html( $slot_key ); ?></code></td> 1603 <td><?php echo esc_html( ucfirst( str_replace( '_', ' ', $eval['type'] ) ) ); ?></td> 1604 <td> 1605 <?php if ( ! empty( $eval['value'] ) ) : ?> 1606 <span style="font-weight: 600; color: #135e96; background: #e7f3fa; padding: 2px 8px; border-radius: 3px;"> 1607 <?php echo esc_html( $eval['value'] ); ?> 1608 </span> 1609 <?php else : ?> 1610 <span style="color: #888;"><em><?php echo esc_html__( 'Empty (Omitted from XML)', 'world-inspire-product-feed-for-google-shopping' ); ?></em></span> 1611 <?php endif; ?> 1612 </td> 1613 <td><small><?php echo esc_html( $eval['reason'] ); ?></small></td> 1614 </tr> 1615 <?php endforeach; ?> 1616 </tbody> 1617 </table> 1618 1619 <h4><?php echo esc_html__( 'Generated XML <item> Snippet Preview', 'world-inspire-product-feed-for-google-shopping' ); ?></h4> 1620 <pre style="background: #2b2b2b; color: #f8f8f2; padding: 15px; border-radius: 4px; overflow-x: auto; font-size: 12px; line-height: 1.5;"><code><?php echo esc_html( $audit_data['xml_snippet'] ); ?></code></pre> 1621 1622 <?php elseif ( $audit_data && empty( $audit_data['found'] ) ) : ?> 1623 <div class="notice notice-warning inline"><p><?php echo esc_html( $audit_data['error'] ); ?></p></div> 1624 <?php endif; ?> 1625 </div> 1626 1627 <!-- Catalog Overview Sample Table --> 1628 <div class="card" style="max-width: 960px; margin-top: 20px; padding: 20px;"> 1629 <h3><?php echo esc_html__( 'Catalog Sample Inspection (First 5 Items)', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 767 1630 <?php if ( ! empty( $sample_products ) ) : ?> 768 1631 <table class="widefat striped" style="margin-top: 15px;"> … … 772 1635 <th><?php echo esc_html__( 'Title', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 773 1636 <th><?php echo esc_html__( 'Price', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 774 <th><?php echo esc_html__( 'Availability', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 775 <th><?php echo esc_html__( 'Detected Color', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 776 <th><?php echo esc_html__( 'Engine', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1637 <th><?php echo esc_html__( 'Group ID', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1638 <th><?php echo esc_html__( 'Color', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1639 <th><?php echo esc_html__( 'Size', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1640 <th><?php echo esc_html__( 'Material', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1641 <th><?php echo esc_html__( 'Custom Labels', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1642 <th><?php echo esc_html__( 'Action', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 777 1643 </tr> 778 1644 </thead> 779 1645 <tbody> 780 1646 <?php foreach ( $sample_products as $p ) : ?> 1647 <?php 1648 $p_eval0 = wipf_evaluate_custom_label( 0, $p, $settings ); 1649 $p_eval1 = wipf_evaluate_custom_label( 1, $p, $settings ); 1650 $label_summary = array(); 1651 if ( ! empty( $p_eval0['value'] ) ) { 1652 $label_summary[] = 'L0: ' . $p_eval0['value']; 1653 } 1654 if ( ! empty( $p_eval1['value'] ) ) { 1655 $label_summary[] = 'L1: ' . $p_eval1['value']; 1656 } 1657 ?> 781 1658 <tr> 782 1659 <td><code><?php echo esc_html( $p['id'] ); ?></code></td> 783 1660 <td><strong><?php echo esc_html( $p['title'] ); ?></strong></td> 784 1661 <td><?php echo esc_html( $p['price'] . ' ' . $p['currency'] ); ?></td> 785 <td><?php echo esc_html( $p['availability'] ); ?></td> 786 <td><?php echo esc_html( wipf_detect_color( $p['title'], $p['description'] ) ?: '—' ); ?></td> 787 <td><code><?php echo esc_html( $p['source'] ); ?></code></td> 1662 <td><code><?php echo esc_html( $p['item_group_id'] ?: '—' ); ?></code></td> 1663 <td><?php echo esc_html( $p['color'] ?: '—' ); ?></td> 1664 <td><?php echo esc_html( $p['size'] ?: '—' ); ?></td> 1665 <td><?php echo esc_html( $p['material'] ?: '—' ); ?></td> 1666 <td><small><?php echo esc_html( ! empty( $label_summary ) ? implode( ' | ', $label_summary ) : '—' ); ?></small></td> 1667 <td><a href="?page=wipf-settings&tab=diagnostic&inspect_id=<?php echo esc_attr( $p['id'] ); ?>" class="button button-small"><?php echo esc_html__( 'Inspect', 'world-inspire-product-feed-for-google-shopping' ); ?></a></td> 788 1668 </tr> 789 1669 <?php endforeach; ?> -
world-inspire-product-feed-for-google-shopping/trunk/readme.txt
r3730435 r3732072 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1. 0.07 Stable tag: 1.1.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 21 21 * **Multi-Engine eCommerce Compatibility**: Native auto-detection and data extraction for both **SureCart** (`sc_product`) and **WooCommerce** (`product`). Zero fatal errors if either or neither is active. 22 22 * **Google Merchant Center Specification**: Built according to official Google RSS 2.0 standards with all essential namespace attributes: `<g:id>`, `<g:title>`, `<g:description>`, `<g:link>`, `<g:image_link>`, `<g:availability>`, `<g:price>`, `<g:brand>`, `<g:condition>`, `<g:identifier_exists>`, `<g:google_product_category>`, `<g:color>`, `<g:age_group>`, and `<g:gender>`. 23 * **Custom Labels 0–4 for Google Ads / PMax**: Full support for `<g:custom_label_0>` through `<g:custom_label_4>` with rules for price tiers (< $25, $25-$50, $50-$100, $100+), inventory status, category mapping, clearance/promotion tag keywords, and custom post meta fields. 24 * **Variant Grouping (`<g:item_group_id>`)**: Automatically expands WooCommerce variable products (`product_variation`) into individual variant items linked by parent `<g:item_group_id>`, enabling swatch selection in Google Shopping results. 25 * **Variant Attribute Optimization**: Smart extraction for `<g:size>`, `<g:material>` (Gold, Silver, Leather, Cotton, Gemstones, etc.), and `<g:pattern>` (Solid, Striped, Floral, Beaded) directly from attributes or intelligent NLP heuristics. 23 26 * **Multi-Country Shipping Generator**: Automatically embeds multi-destination `<g:shipping>` nodes (defaulting to 8 core global markets: US, CA, UK, JP, SG, HK, AE, MX with custom shipping service names and rates). 24 * ** Intelligent Color & Demographic Extraction**: Automatically maps jewelry, apparel, gemstone, and accessory titles/descriptions into recognized Google taxonomy colors (over 35 shades) and adult/unisex demographics.27 * **Live Diagnostics & Product Inspector Simulator**: Test and audit any product ID directly in WordPress Admin to verify extracted attributes, custom labels matching reasons, and preview raw `<item>` XML snippets. 25 28 * **Automatic Pet Category Mapping**: Heuristically detects pet collars, harnesses, and animal accessories and maps them to `Animals & Pet Supplies > Pet Supplies > Pet Collars & Harnesses`. 26 29 * **Trustmate Review Catalog Sync**: Dedicated `<products>` XML feed format ready for instant connection in your Trustmate merchant dashboard. … … 39 42 40 43 = Does this plugin work with SureCart? = 41 Yes! It has full native support for SureCart products, pricing, inventory statuses (`sc_inventory_status`), featured images, and product galleries.44 Yes! It has full native support for SureCart products, pricing, inventory statuses (`sc_inventory_status`), featured images, collections, tags, and product galleries. 42 45 43 46 = Does this plugin work with WooCommerce? = 44 Yes! It automatically detects WooCommerce installations and outputs compliant feeds from published WooCommerce products. 47 Yes! It automatically detects WooCommerce installations and outputs compliant feeds from published WooCommerce products and product variations with `<g:item_group_id>`. 48 49 = How do Custom Labels (0-4) work? = 50 Navigate to **Settings > Product Feeds > Google Shopping Rules & Custom Labels**. You can configure rules for each slot: Price Range Tiers, Inventory Status, Product Category, Tag/Keyword Matching (e.g. `clearance:Clearance, sale:On Sale, bestseller:Best Seller`), or Custom Post Meta fields. 45 51 46 52 = Why does this plugin use extension-free REST URLs? = … … 53 59 54 60 1. Feed URLs & Health Dashboard: Dynamic REST API endpoints and real-time catalog status. 55 2. Google Shopping Rules : Global fallback brand defaults, product taxonomies, and NLP color detection.61 2. Google Shopping Rules & Custom Labels: Global fallback brand defaults, product taxonomies, NLP color/size/material detection, and custom labels 0-4. 56 62 3. Multi-Country Shipping: International destination targeting and custom shipping rates. 57 4. Live Diagnostics : Catalog extraction verification and Google Merchant Center spec compliance audit.63 4. Live Diagnostics & Product Inspector: Catalog extraction audit, custom label rule matching traces, and raw XML simulator. 58 64 59 65 == Changelog == 66 67 = 1.1.0 = 68 * Added Google Shopping Custom Labels (custom_label_0 to custom_label_4) with configurable rules: Price Tiers, Inventory Status, Category Mapping, Promotional Tag Keywords, and Custom Post Meta. 69 * Added WooCommerce variable product expansion with `<g:item_group_id>` linking parent and variant products. 70 * Added smart attribute extraction for `<g:size>`, `<g:material>`, and `<g:pattern>` with jewelry/apparel heuristic engines. 71 * Added Live Product Inspector & XML Simulator to Diagnostics tab with rule matching explanation traces. 72 * Enforced Google Shopping 100-character maximum length limit and XML entity escaping on all custom labels. 73 * Added 87 zero-dependency unit and mock E2E integration tests passing 100% green. 60 74 61 75 = 1.0.0 = -
world-inspire-product-feed-for-google-shopping/trunk/world-inspire-product-feed-for-google-shopping.php
r3730219 r3732072 3 3 * Plugin Name: World Inspire Product Feed for Google Shopping 4 4 * Plugin URI: https://wordpress.org/plugins/world-inspire-product-feed-for-google-shopping/ 5 * Description: High-performance real-time product feed generator for Google Shopping (Merchant Center) and Trustmate reviews sync from WooCommerce and SureCart. Features multi-country shipping, smart color and category detection, and anti-cache streaming architecture.6 * Version: 1. 0.05 * Description: High-performance real-time product feed generator for Google Shopping (Merchant Center) and Trustmate reviews sync from WooCommerce and SureCart. Features multi-country shipping, smart color/size/material detection, custom labels (0-4), variant item_group_id grouping, and anti-cache streaming architecture. 6 * Version: 1.1.0 7 7 * Author: World Inspire LLC, Vec 8 8 * Author URI: https://worldinspirelab.com … … 18 18 } 19 19 20 define( 'WIPF_VERSION', '1. 0.0' );20 define( 'WIPF_VERSION', '1.1.0' ); 21 21 define( 'WIPF_FILE', __FILE__ ); 22 22 define( 'WIPF_PATH', plugin_dir_path( __FILE__ ) ); … … 31 31 function wipf_get_default_settings() { 32 32 return array( 33 'brand_name' => get_bloginfo( 'name' ), 34 'default_category' => 'Apparel & Accessories > Jewelry', 35 'default_age_group' => 'adult', 36 'default_gender' => 'unisex', 37 'default_condition' => 'new', 38 'identifier_exists' => 'no', 39 'auto_detect_color' => 'yes', 40 'shipping_countries' => 'US,CA,GB,JP,SG,HK,AE,MX', 41 'shipping_service_name' => 'Standard Free Shipping', 42 'shipping_cost' => '0.00', 43 'shipping_currency' => 'USD', 44 'enable_google_feed' => 'yes', 45 'enable_trustmate_feed' => 'yes', 46 'items_per_feed' => 500, 33 'brand_name' => get_bloginfo( 'name' ), 34 'default_category' => 'Apparel & Accessories > Jewelry', 35 'default_age_group' => 'adult', 36 'default_gender' => 'unisex', 37 'default_condition' => 'new', 38 'identifier_exists' => 'no', 39 'auto_detect_color' => 'yes', 40 'auto_detect_material' => 'yes', 41 'auto_detect_size' => 'yes', 42 'auto_detect_pattern' => 'yes', 43 'shipping_countries' => 'US,CA,GB,JP,SG,HK,AE,MX', 44 'shipping_service_name' => 'Standard Free Shipping', 45 'shipping_cost' => '0.00', 46 'shipping_currency' => 'USD', 47 'enable_google_feed' => 'yes', 48 'enable_trustmate_feed' => 'yes', 49 'items_per_feed' => 500, 50 'enable_custom_labels' => 'yes', 51 'custom_label_0_type' => 'price_tier', 52 'custom_label_0_value' => '25,50,100', 53 'custom_label_0_fallback' => 'Standard', 54 'custom_label_1_type' => 'inventory', 55 'custom_label_1_value' => '', 56 'custom_label_1_fallback' => 'Out of Stock', 57 'custom_label_2_type' => 'category', 58 'custom_label_2_value' => '', 59 'custom_label_2_fallback' => 'Jewelry', 60 'custom_label_3_type' => 'tag_keywords', 61 'custom_label_3_value' => 'clearance:Clearance, sale:On Sale, bestseller:Best Seller, new:New Arrival', 62 'custom_label_3_fallback' => 'Regular', 63 'custom_label_4_type' => 'static', 64 'custom_label_4_value' => 'World Inspire Catalog', 65 'custom_label_4_fallback' => '', 47 66 ); 48 67 } … … 78 97 // Priority compound phrases checked first 79 98 $compounds = array( 80 'rose gold' => 'Rose Gold',81 'tiger eye' => 'Brown',99 'rose gold' => 'Rose Gold', 100 'tiger eye' => 'Brown', 82 101 'lapis lazuli' => 'Blue', 83 102 ); … … 154 173 155 174 /** 175 * Intelligent material extraction heuristic. 176 * 177 * @param string $title Product title. 178 * @param string $desc Product description. 179 * @return string Extracted material name or empty. 180 */ 181 function wipf_detect_material( $title, $desc = '' ) { 182 $text = strtolower( $title . ' ' . $desc ); 183 184 // Priority compound phrases checked first 185 $compounds = array( 186 'sterling silver' => 'Sterling Silver', 187 '925 silver' => 'Sterling Silver', 188 'rose gold' => 'Rose Gold', 189 'white gold' => 'White Gold', 190 'yellow gold' => 'Yellow Gold', 191 '14k gold' => '14K Gold', 192 '18k gold' => '18K Gold', 193 '24k gold' => '24K Gold', 194 'stainless steel' => 'Stainless Steel', 195 'genuine leather' => 'Leather', 196 'faux leather' => 'Faux Leather', 197 'vegan leather' => 'Faux Leather', 198 'rose quartz' => 'Rose Quartz', 199 'lapis lazuli' => 'Lapis Lazuli', 200 'cubic zirconia' => 'Cubic Zirconia', 201 ); 202 203 $detected = array(); 204 foreach ( $compounds as $phrase => $mat_name ) { 205 if ( false !== strpos( $text, $phrase ) ) { 206 if ( ! in_array( $mat_name, $detected, true ) ) { 207 $detected[] = $mat_name; 208 } 209 $text = str_replace( $phrase, '', $text ); 210 } 211 } 212 213 $single_map = array( 214 'gold' => 'Gold', 215 'silver' => 'Silver', 216 'platinum' => 'Platinum', 217 'titanium' => 'Titanium', 218 'brass' => 'Brass', 219 'copper' => 'Copper', 220 'bronze' => 'Bronze', 221 'steel' => 'Steel', 222 'diamond' => 'Diamond', 223 'pearl' => 'Pearl', 224 'amethyst' => 'Amethyst', 225 'quartz' => 'Quartz', 226 'emerald' => 'Emerald', 227 'ruby' => 'Ruby', 228 'sapphire' => 'Sapphire', 229 'jade' => 'Jade', 230 'turquoise' => 'Turquoise', 231 'obsidian' => 'Obsidian', 232 'onyx' => 'Onyx', 233 'opal' => 'Opal', 234 'moonstone' => 'Moonstone', 235 'topaz' => 'Topaz', 236 'garnet' => 'Garnet', 237 'tourmaline' => 'Tourmaline', 238 'amber' => 'Amber', 239 'carnelian' => 'Carnelian', 240 'citrine' => 'Citrine', 241 'crystal' => 'Crystal', 242 'leather' => 'Leather', 243 'silk' => 'Silk', 244 'velvet' => 'Velvet', 245 'cotton' => 'Cotton', 246 'linen' => 'Linen', 247 'wool' => 'Wool', 248 'cashmere' => 'Cashmere', 249 'satin' => 'Satin', 250 'polyester' => 'Polyester', 251 'canvas' => 'Canvas', 252 'denim' => 'Denim', 253 'ceramic' => 'Ceramic', 254 'glass' => 'Glass', 255 'wood' => 'Wood', 256 'silicone' => 'Silicone', 257 'resin' => 'Resin', 258 ); 259 260 foreach ( $single_map as $needle => $mat_name ) { 261 if ( preg_match( '/\b' . preg_quote( $needle, '/' ) . '\b/', $text ) ) { 262 if ( ! in_array( $mat_name, $detected, true ) ) { 263 $detected[] = $mat_name; 264 } 265 } 266 } 267 268 if ( count( $detected ) > 2 ) { 269 return 'Mixed'; 270 } elseif ( ! empty( $detected ) ) { 271 return implode( '/', $detected ); 272 } 273 274 return ''; 275 } 276 277 /** 278 * Intelligent pattern extraction heuristic. 279 * 280 * @param string $title Product title. 281 * @param string $desc Product description. 282 * @return string Extracted pattern or empty. 283 */ 284 function wipf_detect_pattern( $title, $desc = '' ) { 285 $text = strtolower( $title . ' ' . $desc ); 286 287 $patterns = array( 288 'polka dot' => 'Polka Dot', 289 'polka dots' => 'Polka Dot', 290 'animal print' => 'Animal Print', 291 'striped' => 'Striped', 292 'stripes' => 'Striped', 293 'stripe' => 'Striped', 294 'floral' => 'Floral', 295 'flower' => 'Floral', 296 'geometric' => 'Geometric', 297 'beaded' => 'Beaded', 298 'braided' => 'Braided', 299 'plaid' => 'Plaid', 300 'checkered' => 'Checkered', 301 'leopard' => 'Animal Print', 302 'cheetah' => 'Animal Print', 303 'zebra' => 'Animal Print', 304 'camouflage' => 'Camouflage', 305 'camo' => 'Camouflage', 306 'marble' => 'Marble', 307 'gradient' => 'Gradient', 308 'solid' => 'Solid', 309 ); 310 311 foreach ( $patterns as $needle => $pat_name ) { 312 if ( preg_match( '/\b' . preg_quote( $needle, '/' ) . '\b/', $text ) ) { 313 return $pat_name; 314 } 315 } 316 317 return ''; 318 } 319 320 /** 321 * Intelligent size extraction heuristic. 322 * 323 * @param string $title Product title. 324 * @param string $desc Product description. 325 * @return string Extracted size or empty. 326 */ 327 function wipf_detect_size( $title, $desc = '' ) { 328 $text = $title . ' ' . $desc; 329 330 // 1. One Size / OS 331 if ( preg_match( '/\b(one\s*size|osfm|osfa)\b/i', $text ) ) { 332 return 'One Size'; 333 } 334 335 // 2. Ring or shoe sizes: "Size 7", "Size 7.5", "US 8" 336 if ( preg_match( '/\b(?:size|us|eu)\s*([0-9]+(?:\.[0-9]+)?)\b/i', $text, $matches ) ) { 337 return $matches[1]; 338 } 339 340 // 3. Dimensional bead / diameter sizes: "8mm", "10mm", "12mm" 341 if ( preg_match( '/\b([0-9]+(?:\.[0-9]+)?\s*(?:mm|cm|in|inch|inches))\b/i', $text, $matches ) ) { 342 return trim( $matches[1] ); 343 } 344 345 // 4. Apparel sizes: XS, S, M, L, XL, XXL, 2XL, 3XL 346 if ( preg_match( '/\b(XS|S|M|L|XL|XXL|2XL|3XL)\b/', $text, $matches ) ) { 347 return $matches[1]; 348 } 349 350 return ''; 351 } 352 353 /** 354 * Evaluates a Google Shopping custom label (0-4) according to configured rules. 355 * 356 * @param int $slot Custom label index (0 to 4). 357 * @param array $product Normalized product data. 358 * @param array $config Plugin settings. 359 * @return array Array with keys 'value', 'reason', 'type'. 360 */ 361 function wipf_evaluate_custom_label( $slot, $product, $config ) { 362 $slot = absint( $slot ); 363 if ( $slot < 0 || $slot > 4 ) { 364 return array( 'value' => '', 'reason' => 'Invalid slot', 'type' => 'disabled' ); 365 } 366 367 $type = ! empty( $config["custom_label_{$slot}_type"] ) ? $config["custom_label_{$slot}_type"] : 'disabled'; 368 $rule_val = ! empty( $config["custom_label_{$slot}_value"] ) ? trim( $config["custom_label_{$slot}_value"] ) : ''; 369 $fallback = ! empty( $config["custom_label_{$slot}_fallback"] ) ? trim( $config["custom_label_{$slot}_fallback"] ) : ''; 370 371 $result = ''; 372 $reason = ''; 373 374 switch ( $type ) { 375 case 'disabled': 376 return array( 377 'value' => '', 378 'reason' => 'Slot disabled', 379 'type' => 'disabled', 380 ); 381 382 case 'static': 383 $result = $rule_val; 384 $reason = 'Static configured value: ' . $rule_val; 385 break; 386 387 case 'price_tier': 388 $price = isset( $product['price'] ) ? (float) $product['price'] : 0.0; 389 $curr = ! empty( $product['currency'] ) ? $product['currency'] : ( $config['shipping_currency'] ?? 'USD' ); 390 391 // Check custom thresholds e.g. "50" or "30,80" or "25,50,100" 392 $thresholds = array( 25.0, 50.0, 100.0 ); 393 if ( ! empty( $rule_val ) ) { 394 $parsed = array(); 395 $raw_parts = explode( ',', $rule_val ); 396 foreach ( $raw_parts as $rp ) { 397 $fv = (float) trim( $rp ); 398 if ( $fv > 0 ) { 399 $parsed[] = $fv; 400 } 401 } 402 if ( ! empty( $parsed ) ) { 403 sort( $parsed ); 404 $thresholds = array_values( array_unique( $parsed ) ); 405 } 406 } 407 408 $count = count( $thresholds ); 409 if ( $price < $thresholds[0] ) { 410 $result = 'Under ' . (int) $thresholds[0]; 411 $reason = sprintf( 'Price %0.2f %s < %0.2f', $price, $curr, $thresholds[0] ); 412 } elseif ( $price >= $thresholds[ $count - 1 ] ) { 413 $result = (int) $thresholds[ $count - 1 ] . ' and Above'; 414 $reason = sprintf( 'Price %0.2f %s >= %0.2f', $price, $curr, $thresholds[ $count - 1 ] ); 415 } else { 416 for ( $ti = 0; $ti < $count - 1; $ti++ ) { 417 if ( $price >= $thresholds[ $ti ] && $price < $thresholds[ $ti + 1 ] ) { 418 $result = (int) $thresholds[ $ti ] . ' to ' . (int) $thresholds[ $ti + 1 ]; 419 $reason = sprintf( 'Price %0.2f %s between %0.2f and %0.2f', $price, $curr, $thresholds[ $ti ], $thresholds[ $ti + 1 ] ); 420 break; 421 } 422 } 423 } 424 break; 425 426 case 'inventory': 427 $status = ! empty( $product['availability'] ) ? $product['availability'] : 'in_stock'; 428 $in_label = 'In Stock'; 429 $out_label = 'Out of Stock'; 430 if ( ! empty( $rule_val ) && false !== strpos( $rule_val, ':' ) ) { 431 $pairs = explode( ',', $rule_val ); 432 foreach ( $pairs as $pair ) { 433 $p = array_map( 'trim', explode( ':', $pair, 2 ) ); 434 if ( count( $p ) === 2 ) { 435 if ( 'in_stock' === $p[0] ) { 436 $in_label = $p[1]; 437 } elseif ( 'out_of_stock' === $p[0] ) { 438 $out_label = $p[1]; 439 } 440 } 441 } 442 } 443 444 if ( 'out_of_stock' === $status ) { 445 $result = $out_label; 446 $reason = 'Inventory status is out_of_stock'; 447 } else { 448 $result = $in_label; 449 $reason = 'Inventory status is in_stock'; 450 } 451 break; 452 453 case 'category': 454 if ( ! empty( $product['categories'] ) && is_array( $product['categories'] ) ) { 455 $result = reset( $product['categories'] ); 456 $reason = 'Derived from primary category: ' . $result; 457 } elseif ( ! empty( $product['category'] ) ) { 458 $result = $product['category']; 459 $reason = 'Derived from product category: ' . $result; 460 } else { 461 $result = ! empty( $config['default_category'] ) ? $config['default_category'] : ''; 462 $reason = 'Fallback to default category'; 463 } 464 break; 465 466 case 'tag_keywords': 467 $tags_list = ! empty( $product['tags'] ) && is_array( $product['tags'] ) ? $product['tags'] : array(); 468 $text_to_search = strtolower( ( $product['title'] ?? '' ) . ' ' . implode( ' ', $tags_list ) ); 469 470 if ( ! empty( $rule_val ) ) { 471 $pairs = explode( ',', $rule_val ); 472 foreach ( $pairs as $pair ) { 473 $parts = array_map( 'trim', explode( ':', $pair, 2 ) ); 474 $keyword = strtolower( $parts[0] ); 475 $label = isset( $parts[1] ) && '' !== $parts[1] ? $parts[1] : ucwords( $parts[0] ); 476 477 if ( '' !== $keyword && false !== strpos( $text_to_search, $keyword ) ) { 478 $result = $label; 479 $reason = "Matched keyword '{$keyword}' in title or tags"; 480 break; 481 } 482 } 483 } 484 485 if ( empty( $result ) ) { 486 $reason = 'No matching tag or keyword found'; 487 } 488 break; 489 490 case 'custom_meta': 491 $meta_key = $rule_val; 492 if ( ! empty( $meta_key ) && ! empty( $product['id'] ) && function_exists( 'get_post_meta' ) ) { 493 $meta_val = get_post_meta( $product['id'], $meta_key, true ); 494 if ( empty( $meta_val ) && ! empty( $product['item_group_id'] ) ) { 495 $meta_val = get_post_meta( $product['item_group_id'], $meta_key, true ); 496 } 497 if ( ! empty( $meta_val ) && is_scalar( $meta_val ) ) { 498 $result = (string) $meta_val; 499 $reason = "Retrieved from post meta '{$meta_key}'"; 500 } else { 501 $reason = "Post meta '{$meta_key}' is empty"; 502 } 503 } else { 504 $reason = 'Custom meta key not specified'; 505 } 506 break; 507 508 default: 509 $reason = 'Unknown rule type: ' . $type; 510 break; 511 } 512 513 // Apply fallback if result is empty 514 if ( '' === $result && '' !== $fallback ) { 515 $result = $fallback; 516 $reason .= ' (Fallback applied: ' . $fallback . ')'; 517 } 518 519 // Google Shopping max length for custom_label_0-4 is 100 characters 520 if ( '' !== $result ) { 521 $clean_result = sanitize_text_field( $result ); 522 if ( function_exists( 'mb_strlen' ) ) { 523 if ( mb_strlen( $clean_result, 'UTF-8' ) > 100 ) { 524 $clean_result = mb_substr( $clean_result, 0, 100, 'UTF-8' ); 525 } 526 } else { 527 $clean_result = substr( $clean_result, 0, 100 ); 528 } 529 $result = $clean_result; 530 } 531 532 return array( 533 'value' => $result, 534 'reason' => $reason, 535 'type' => $type, 536 ); 537 } 538 539 /** 156 540 * Safe XML entity escaper. 157 541 * … … 165 549 /** 166 550 * Fetches normalized product catalog from active e-commerce platforms. 167 * Supports SureCart and WooCommerce seamlessly .551 * Supports SureCart and WooCommerce seamlessly including product variations. 168 552 * 169 553 * @param int $limit Maximum number of products to return. … … 176 560 $limit = 500; 177 561 } 562 563 $settings = wipf_get_settings(); 178 564 179 565 // 1. Check SureCart products (sc_product) … … 239 625 } 240 626 627 // Categories & Tags 628 $categories = array(); 629 if ( function_exists( 'wp_get_post_terms' ) ) { 630 $sc_terms = wp_get_post_terms( $post_id, 'sc_collection', array( 'fields' => 'names' ) ); 631 if ( is_array( $sc_terms ) ) { 632 $categories = $sc_terms; 633 } 634 } 635 636 $tags = array(); 637 if ( function_exists( 'wp_get_post_terms' ) ) { 638 $sc_tag_terms = wp_get_post_terms( $post_id, 'sc_tag', array( 'fields' => 'names' ) ); 639 if ( is_array( $sc_tag_terms ) ) { 640 $tags = $sc_tag_terms; 641 } 642 } 643 644 // Variant attributes 645 $parent_id = get_post_meta( $post_id, 'sc_parent_id', true ); 646 647 $sc_color = get_post_meta( $post_id, 'sc_color', true ); 648 if ( empty( $sc_color ) && 'yes' === ( $settings['auto_detect_color'] ?? 'yes' ) ) { 649 $sc_color = wipf_detect_color( $title, $content ); 650 } 651 652 $sc_size = get_post_meta( $post_id, 'sc_size', true ); 653 if ( empty( $sc_size ) && 'yes' === ( $settings['auto_detect_size'] ?? 'yes' ) ) { 654 $sc_size = wipf_detect_size( $title, $content ); 655 } 656 657 $sc_mat = get_post_meta( $post_id, 'sc_material', true ); 658 if ( empty( $sc_mat ) && 'yes' === ( $settings['auto_detect_material'] ?? 'yes' ) ) { 659 $sc_mat = wipf_detect_material( $title, $content ); 660 } 661 662 $sc_pat = get_post_meta( $post_id, 'sc_pattern', true ); 663 if ( empty( $sc_pat ) && 'yes' === ( $settings['auto_detect_pattern'] ?? 'yes' ) ) { 664 $sc_pat = wipf_detect_pattern( $title, $content ); 665 } 666 241 667 $products[] = array( 242 668 'id' => $post_id, 669 'item_group_id' => ! empty( $parent_id ) ? (string) $parent_id : '', 243 670 'title' => $title, 244 671 'description' => $content, … … 250 677 'availability' => $inv_status, 251 678 'source' => 'surecart', 679 'color' => $sc_color, 680 'size' => $sc_size, 681 'material' => $sc_mat, 682 'pattern' => $sc_pat, 683 'categories' => $categories, 684 'tags' => $tags, 252 685 ); 686 687 if ( count( $products ) >= $limit ) { 688 break; 689 } 253 690 } 254 691 wp_reset_postdata(); … … 257 694 258 695 // 2. Check WooCommerce products (product) 259 if ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' )) {696 if ( ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) ) && count( $products ) < $limit ) { 260 697 $wc_args = array( 261 698 'post_type' => 'product', 262 699 'post_status' => 'publish', 263 'posts_per_page' => $limit ,700 'posts_per_page' => $limit - count( $products ), 264 701 'orderby' => 'ID', 265 702 'order' => 'DESC', … … 284 721 $inv_status = 'in_stock'; 285 722 286 if ( function_exists( 'wc_get_product' ) ) { 287 $wc_prod = wc_get_product( $post_id ); 288 if ( $wc_prod ) { 289 $price = $wc_prod->get_price(); 290 $inv_status = $wc_prod->is_in_stock() ? 'in_stock' : 'out_of_stock'; 723 // Categories & Tags 724 $categories = array(); 725 if ( function_exists( 'wp_get_post_terms' ) ) { 726 $wc_terms = wp_get_post_terms( $post_id, 'product_cat', array( 'fields' => 'names' ) ); 727 if ( is_array( $wc_terms ) ) { 728 $categories = $wc_terms; 291 729 } 292 } else { 293 $raw_price = get_post_meta( $post_id, '_price', true ); 294 if ( is_numeric( $raw_price ) ) { 295 $price = (float) $raw_price; 730 } 731 732 $tags = array(); 733 if ( function_exists( 'wp_get_post_terms' ) ) { 734 $wc_tag_terms = wp_get_post_terms( $post_id, 'product_tag', array( 'fields' => 'names' ) ); 735 if ( is_array( $wc_tag_terms ) ) { 736 $tags = $wc_tag_terms; 296 737 } 297 738 } … … 314 755 } 315 756 757 // Check WooCommerce Product object & Variable product handling 758 $wc_prod = function_exists( 'wc_get_product' ) ? wc_get_product( $post_id ) : null; 759 if ( $wc_prod ) { 760 // Variable products: expand into variations with item_group_id 761 if ( method_exists( $wc_prod, 'is_type' ) && $wc_prod->is_type( 'variable' ) ) { 762 $variation_ids = method_exists( $wc_prod, 'get_children' ) ? $wc_prod->get_children() : array(); 763 if ( ! empty( $variation_ids ) ) { 764 foreach ( $variation_ids as $var_id ) { 765 $var_obj = wc_get_product( $var_id ); 766 if ( ! $var_obj ) { 767 continue; 768 } 769 770 $var_price = $var_obj->get_price(); 771 $var_inv = $var_obj->is_in_stock() ? 'in_stock' : 'out_of_stock'; 772 $var_link = $var_obj->get_permalink() ?: $permalink; 773 774 $var_img_id = $var_obj->get_image_id(); 775 $var_image = $var_img_id ? wp_get_attachment_url( $var_img_id ) : $image_url; 776 777 // Variation attributes 778 $var_color = $var_obj->get_attribute( 'pa_color' ) ?: $var_obj->get_attribute( 'color' ); 779 if ( empty( $var_color ) && 'yes' === ( $settings['auto_detect_color'] ?? 'yes' ) ) { 780 $var_color = wipf_detect_color( $var_obj->get_name() . ' ' . $title, $content ); 781 } 782 783 $var_size = $var_obj->get_attribute( 'pa_size' ) ?: $var_obj->get_attribute( 'size' ); 784 if ( empty( $var_size ) && 'yes' === ( $settings['auto_detect_size'] ?? 'yes' ) ) { 785 $var_size = wipf_detect_size( $var_obj->get_name() . ' ' . $title, $content ); 786 } 787 788 $var_mat = $var_obj->get_attribute( 'pa_material' ) ?: $var_obj->get_attribute( 'material' ); 789 if ( empty( $var_mat ) && 'yes' === ( $settings['auto_detect_material'] ?? 'yes' ) ) { 790 $var_mat = wipf_detect_material( $var_obj->get_name() . ' ' . $title, $content ); 791 } 792 793 $var_pat = $var_obj->get_attribute( 'pa_pattern' ) ?: $var_obj->get_attribute( 'pattern' ); 794 if ( empty( $var_pat ) && 'yes' === ( $settings['auto_detect_pattern'] ?? 'yes' ) ) { 795 $var_pat = wipf_detect_pattern( $var_obj->get_name() . ' ' . $title, $content ); 796 } 797 798 $products[] = array( 799 'id' => $var_id, 800 'item_group_id' => (string) $post_id, 801 'title' => $var_obj->get_name() ?: $title, 802 'description' => $content, 803 'permalink' => $var_link, 804 'image_url' => $var_image, 805 'gallery_images' => $gallery_images, 806 'price' => number_format( (float) $var_price, 2, '.', '' ), 807 'currency' => $currency, 808 'availability' => $var_inv, 809 'source' => 'woocommerce', 810 'color' => $var_color, 811 'size' => $var_size, 812 'material' => $var_mat, 813 'pattern' => $var_pat, 814 'categories' => $categories, 815 'tags' => $tags, 816 ); 817 818 if ( count( $products ) >= $limit ) { 819 break 2; 820 } 821 } 822 continue; // Variations added, skip parent product 823 } 824 } 825 826 // Simple product 827 $price = $wc_prod->get_price(); 828 $inv_status = $wc_prod->is_in_stock() ? 'in_stock' : 'out_of_stock'; 829 } else { 830 $raw_price = get_post_meta( $post_id, '_price', true ); 831 if ( is_numeric( $raw_price ) ) { 832 $price = (float) $raw_price; 833 } 834 } 835 836 // Simple product attributes 837 $prod_color = ''; 838 $prod_size = ''; 839 $prod_mat = ''; 840 $prod_pat = ''; 841 842 if ( $wc_prod && method_exists( $wc_prod, 'get_attribute' ) ) { 843 $prod_color = $wc_prod->get_attribute( 'pa_color' ) ?: $wc_prod->get_attribute( 'color' ); 844 $prod_size = $wc_prod->get_attribute( 'pa_size' ) ?: $wc_prod->get_attribute( 'size' ); 845 $prod_mat = $wc_prod->get_attribute( 'pa_material' ) ?: $wc_prod->get_attribute( 'material' ); 846 $prod_pat = $wc_prod->get_attribute( 'pa_pattern' ) ?: $wc_prod->get_attribute( 'pattern' ); 847 } 848 849 if ( empty( $prod_color ) && 'yes' === ( $settings['auto_detect_color'] ?? 'yes' ) ) { 850 $prod_color = wipf_detect_color( $title, $content ); 851 } 852 if ( empty( $prod_size ) && 'yes' === ( $settings['auto_detect_size'] ?? 'yes' ) ) { 853 $prod_size = wipf_detect_size( $title, $content ); 854 } 855 if ( empty( $prod_mat ) && 'yes' === ( $settings['auto_detect_material'] ?? 'yes' ) ) { 856 $prod_mat = wipf_detect_material( $title, $content ); 857 } 858 if ( empty( $prod_pat ) && 'yes' === ( $settings['auto_detect_pattern'] ?? 'yes' ) ) { 859 $prod_pat = wipf_detect_pattern( $title, $content ); 860 } 861 316 862 $products[] = array( 317 863 'id' => $post_id, 864 'item_group_id' => '', 318 865 'title' => $title, 319 866 'description' => $content, … … 325 872 'availability' => $inv_status, 326 873 'source' => 'woocommerce', 874 'color' => $prod_color, 875 'size' => $prod_size, 876 'material' => $prod_mat, 877 'pattern' => $prod_pat, 878 'categories' => $categories, 879 'tags' => $tags, 327 880 ); 881 882 if ( count( $products ) >= $limit ) { 883 break; 884 } 328 885 } 329 886 wp_reset_postdata(); … … 353 910 $ship_cost = ! empty( $config['shipping_cost'] ) ? $config['shipping_cost'] : '0.00'; 354 911 $ship_curr = ! empty( $config['shipping_currency'] ) ? $config['shipping_currency'] : 'USD'; 912 $enable_labels = ( 'no' !== ( $config['enable_custom_labels'] ?? 'yes' ) ); 355 913 356 914 $xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n"; … … 367 925 $price_val = $item['price']; 368 926 $curr_val = ! empty( $item['currency'] ) ? $item['currency'] : $ship_curr; 369 $item_color = $auto_color ? wipf_detect_color( $title, $desc ) : ''; 370 371 // Pet classification heuristic 927 928 // Color priority: item attribute > heuristic 929 $item_color = ! empty( $item['color'] ) ? $item['color'] : ( $auto_color ? wipf_detect_color( $title, $desc ) : '' ); 930 931 // Category classification heuristic 372 932 $category = $def_category; 933 if ( ! empty( $item['categories'] ) && is_array( $item['categories'] ) ) { 934 $category = implode( ' > ', $item['categories'] ); 935 } 373 936 if ( false !== stripos( $title, 'collar' ) || false !== stripos( $title, 'pet' ) || false !== stripos( $title, 'dog' ) || false !== stripos( $title, 'cat' ) ) { 374 937 $category = 'Animals & Pet Supplies > Pet Supplies > Pet Collars & Harnesses'; … … 377 940 $xml .= " <item>\n"; 378 941 $xml .= ' <g:id>' . wipf_xml_escape( $item['id'] ) . "</g:id>\n"; 942 943 // Variant item_group_id grouping 944 if ( ! empty( $item['item_group_id'] ) ) { 945 $xml .= ' <g:item_group_id>' . wipf_xml_escape( $item['item_group_id'] ) . "</g:item_group_id>\n"; 946 } 947 379 948 $xml .= ' <g:title>' . wipf_xml_escape( $title ) . "</g:title>\n"; 380 949 $xml .= ' <g:description>' . wipf_xml_escape( $desc ) . "</g:description>\n"; … … 400 969 $xml .= ' <g:color>' . wipf_xml_escape( $item_color ) . "</g:color>\n"; 401 970 } 971 if ( ! empty( $item['size'] ) ) { 972 $xml .= ' <g:size>' . wipf_xml_escape( $item['size'] ) . "</g:size>\n"; 973 } 974 if ( ! empty( $item['material'] ) ) { 975 $xml .= ' <g:material>' . wipf_xml_escape( $item['material'] ) . "</g:material>\n"; 976 } 977 if ( ! empty( $item['pattern'] ) ) { 978 $xml .= ' <g:pattern>' . wipf_xml_escape( $item['pattern'] ) . "</g:pattern>\n"; 979 } 980 402 981 if ( ! empty( $def_age_group ) ) { 403 982 $xml .= ' <g:age_group>' . wipf_xml_escape( $def_age_group ) . "</g:age_group>\n"; … … 405 984 if ( ! empty( $def_gender ) ) { 406 985 $xml .= ' <g:gender>' . wipf_xml_escape( $def_gender ) . "</g:gender>\n"; 986 } 987 988 // Inject Custom Labels (custom_label_0 to custom_label_4) 989 if ( $enable_labels ) { 990 for ( $slot = 0; $slot <= 4; $slot++ ) { 991 $label_data = wipf_evaluate_custom_label( $slot, $item, $config ); 992 if ( ! empty( $label_data['value'] ) ) { 993 $xml .= ' <g:custom_label_' . $slot . '>' . wipf_xml_escape( $label_data['value'] ) . '</g:custom_label_' . $slot . ">\n"; 994 } 995 } 407 996 } 408 997 … … 463 1052 464 1053 /** 1054 * Detailed diagnostic inspector for a specific product ID or product data array. 1055 * 1056 * @param int|array $product Product ID or product array. 1057 * @param array|null $config Optional configuration override. 1058 * @return array Diagnostic report. 1059 */ 1060 function wipf_diagnose_product( $product, $config = null ) { 1061 if ( null === $config ) { 1062 $config = wipf_get_settings(); 1063 } 1064 1065 $item = is_array( $product ) ? $product : null; 1066 if ( null === $item && is_numeric( $product ) ) { 1067 $limit = 200; 1068 $all_prods = wipf_collect_products( $limit ); 1069 foreach ( $all_prods as $p ) { 1070 if ( (int) $p['id'] === (int) $product ) { 1071 $item = $p; 1072 break; 1073 } 1074 } 1075 if ( null === $item && function_exists( 'get_post' ) ) { 1076 $post = get_post( (int) $product ); 1077 if ( $post && 'publish' === ( $post->post_status ?? '' ) ) { 1078 $title = get_the_title( $post ); 1079 $content = wp_strip_all_tags( get_the_excerpt( $post ) ?: ( get_the_content( null, false, $post ) ?: $title ) ); 1080 $raw_price = get_post_meta( $post->ID, '_price', true ); 1081 $price = is_numeric( $raw_price ) ? (float) $raw_price : 0.0; 1082 $image = get_the_post_thumbnail_url( $post->ID, 'full' ) ?: ''; 1083 1084 $categories = function_exists( 'wp_get_post_terms' ) ? wp_get_post_terms( $post->ID, 'product_cat', array( 'fields' => 'names' ) ) : array(); 1085 $tags = function_exists( 'wp_get_post_terms' ) ? wp_get_post_terms( $post->ID, 'product_tag', array( 'fields' => 'names' ) ) : array(); 1086 1087 $item = array( 1088 'id' => $post->ID, 1089 'item_group_id' => '', 1090 'title' => $title, 1091 'description' => $content, 1092 'permalink' => get_permalink( $post->ID ), 1093 'image_url' => $image, 1094 'gallery_images' => array(), 1095 'price' => number_format( $price, 2, '.', '' ), 1096 'currency' => $config['shipping_currency'] ?? 'USD', 1097 'availability' => 'in_stock', 1098 'source' => $post->post_type, 1099 'color' => wipf_detect_color( $title, $content ), 1100 'size' => wipf_detect_size( $title, $content ), 1101 'material' => wipf_detect_material( $title, $content ), 1102 'pattern' => wipf_detect_pattern( $title, $content ), 1103 'categories' => is_array( $categories ) ? $categories : array(), 1104 'tags' => is_array( $tags ) ? $tags : array(), 1105 ); 1106 } 1107 } 1108 } 1109 1110 if ( empty( $item ) ) { 1111 return array( 1112 'found' => false, 1113 'error' => __( 'Product not found or not published in catalog.', 'world-inspire-product-feed-for-google-shopping' ), 1114 ); 1115 } 1116 1117 $labels = array(); 1118 for ( $i = 0; $i <= 4; $i++ ) { 1119 $labels[ "custom_label_{$i}" ] = wipf_evaluate_custom_label( $i, $item, $config ); 1120 } 1121 1122 // Generate XML snippet for just this item 1123 $xml_doc = wipf_build_google_shopping_xml( array( $item ), $config ); 1124 $xml_snippet = ''; 1125 if ( preg_match( '/<item>.*?<\/item>/s', $xml_doc, $matches ) ) { 1126 $xml_snippet = trim( $matches[0] ); 1127 } 1128 1129 return array( 1130 'found' => true, 1131 'product' => $item, 1132 'labels' => $labels, 1133 'xml_snippet' => $xml_snippet, 1134 ); 1135 } 1136 1137 /** 465 1138 * Streams XML document with anti-caching reverse-proxy protection. 466 1139 * … … 482 1155 483 1156 /** 484 * Registers REST API endpoints for Google Shopping and Trustmate.1157 * Registers REST API endpoints for Google Shopping, Trustmate, and Status. 485 1158 */ 486 1159 function wipf_register_rest_routes() { … … 525 1198 return new WP_REST_Response( 526 1199 array( 527 'enabled' => true,528 'status' => 'healthy',529 'version' => WIPF_VERSION,530 'surecart_detected' => post_type_exists( 'sc_product' ),1200 'enabled' => true, 1201 'status' => 'healthy', 1202 'version' => WIPF_VERSION, 1203 'surecart_detected' => post_type_exists( 'sc_product' ), 531 1204 'woocommerce_detected' => ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) ), 532 'sample_count' => count( $products ), 1205 'sample_count' => count( $products ), 1206 'custom_labels_active' => ( 'no' !== ( wipf_get_settings()['enable_custom_labels'] ?? 'yes' ) ), 533 1207 ), 534 1208 200 … … 573 1247 $output['identifier_exists'] = ( 'yes' === ( $input['identifier_exists'] ?? 'no' ) ) ? 'yes' : 'no'; 574 1248 $output['auto_detect_color'] = ( 'yes' === ( $input['auto_detect_color'] ?? 'yes' ) ) ? 'yes' : 'no'; 1249 $output['auto_detect_material'] = ( 'yes' === ( $input['auto_detect_material'] ?? 'yes' ) ) ? 'yes' : 'no'; 1250 $output['auto_detect_size'] = ( 'yes' === ( $input['auto_detect_size'] ?? 'yes' ) ) ? 'yes' : 'no'; 1251 $output['auto_detect_pattern'] = ( 'yes' === ( $input['auto_detect_pattern'] ?? 'yes' ) ) ? 'yes' : 'no'; 575 1252 $output['shipping_countries'] = ! empty( $input['shipping_countries'] ) ? sanitize_text_field( $input['shipping_countries'] ) : $defaults['shipping_countries']; 576 1253 $output['shipping_service_name'] = ! empty( $input['shipping_service_name'] ) ? sanitize_text_field( $input['shipping_service_name'] ) : $defaults['shipping_service_name']; … … 580 1257 $output['enable_trustmate_feed'] = ( 'yes' === ( $input['enable_trustmate_feed'] ?? 'yes' ) ) ? 'yes' : 'no'; 581 1258 $output['items_per_feed'] = absint( $input['items_per_feed'] ?? 500 ); 1259 1260 // Custom Labels (custom_label_0 to custom_label_4) 1261 $output['enable_custom_labels'] = ( 'yes' === ( $input['enable_custom_labels'] ?? 'yes' ) ) ? 'yes' : 'no'; 1262 $valid_rule_types = array( 'disabled', 'static', 'price_tier', 'inventory', 'category', 'tag_keywords', 'custom_meta' ); 1263 1264 for ( $i = 0; $i <= 4; $i++ ) { 1265 $type_key = "custom_label_{$i}_type"; 1266 $val_key = "custom_label_{$i}_value"; 1267 $fallback_key = "custom_label_{$i}_fallback"; 1268 1269 $type = isset( $input[ $type_key ] ) && in_array( $input[ $type_key ], $valid_rule_types, true ) ? sanitize_key( $input[ $type_key ] ) : 'disabled'; 1270 $val = isset( $input[ $val_key ] ) ? sanitize_text_field( $input[ $val_key ] ) : ''; 1271 $fb = isset( $input[ $fallback_key ] ) ? sanitize_text_field( $input[ $fallback_key ] ) : ''; 1272 1273 $output[ $type_key ] = $type; 1274 $output[ $val_key ] = $val; 1275 $output[ $fallback_key ] = $fb; 1276 } 582 1277 583 1278 return $output; … … 614 1309 ?> 615 1310 <div class="wrap"> 616 <h1><?php echo esc_html__( 'World Inspire Product Feed for Google Shopping & Trustmate', 'world-inspire-product-feed-for-google-shopping' ); ?> </h1>1311 <h1><?php echo esc_html__( 'World Inspire Product Feed for Google Shopping & Trustmate', 'world-inspire-product-feed-for-google-shopping' ); ?> <span style="font-size: 14px; background: #2271b1; color: #fff; padding: 2px 8px; border-radius: 4px; vertical-align: middle;">v<?php echo esc_html( WIPF_VERSION ); ?></span></h1> 617 1312 <p class="description"> 618 1313 <?php echo esc_html__( 'Zero-configuration, high-performance real-time XML product feeds engineered for Google Merchant Center and Trustmate review synchronization.', 'world-inspire-product-feed-for-google-shopping' ); ?> … … 624 1319 </a> 625 1320 <a href="?page=wipf-settings&tab=google" class="nav-tab <?php echo 'google' === $active_tab ? 'nav-tab-active' : ''; ?>"> 626 <?php echo esc_html__( 'Google Shopping Rules ', 'world-inspire-product-feed-for-google-shopping' ); ?>1321 <?php echo esc_html__( 'Google Shopping Rules & Custom Labels', 'world-inspire-product-feed-for-google-shopping' ); ?> 627 1322 </a> 628 1323 <a href="?page=wipf-settings&tab=shipping" class="nav-tab <?php echo 'shipping' === $active_tab ? 'nav-tab-active' : ''; ?>"> … … 630 1325 </a> 631 1326 <a href="?page=wipf-settings&tab=diagnostic" class="nav-tab <?php echo 'diagnostic' === $active_tab ? 'nav-tab-active' : ''; ?>"> 632 <?php echo esc_html__( 'Live Diagnostics & Preview', 'world-inspire-product-feed-for-google-shopping' ); ?>1327 <?php echo esc_html__( 'Live Diagnostics & Inspector', 'world-inspire-product-feed-for-google-shopping' ); ?> 633 1328 </a> 634 1329 </h2> 635 1330 636 1331 <?php if ( 'endpoints' === $active_tab ) : ?> 637 <div class="card" style="max-width: 9 00px; margin-top: 20px; padding: 20px;">1332 <div class="card" style="max-width: 960px; margin-top: 20px; padding: 20px;"> 638 1333 <h3><?php echo esc_html__( 'Authoritative Feed URLs', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 639 1334 <p><?php echo esc_html__( 'Copy these extension-free dynamic REST endpoints into Google Merchant Center and Trustmate. They automatically bypass reverse-proxy static file caches and update in real-time.', 'world-inspire-product-feed-for-google-shopping' ); ?></p> … … 664 1359 </div> 665 1360 <?php elseif ( 'google' === $active_tab || 'shipping' === $active_tab ) : ?> 666 <form method="post" action="options.php" style="max-width: 9 00px; margin-top: 20px;">1361 <form method="post" action="options.php" style="max-width: 960px; margin-top: 20px;"> 667 1362 <?php 668 1363 settings_fields( 'wipf_settings_group' ); 669 1364 ?> 670 < div class="card" style="padding: 20px;">671 < ?php if ( 'google' === $active_tab ) : ?>1365 <?php if ( 'google' === $active_tab ) : ?> 1366 <div class="card" style="padding: 20px; margin-bottom: 20px;"> 672 1367 <h3><?php echo esc_html__( 'Google Shopping Catalog Standards', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 673 1368 <table class="form-table" role="presentation"> … … 692 1387 <option value="kids" <?php selected( $settings['default_age_group'], 'kids' ); ?>><?php echo esc_html__( 'Kids', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 693 1388 <option value="toddler" <?php selected( $settings['default_age_group'], 'toddler' ); ?>><?php echo esc_html__( 'Toddler', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1389 <option value="infant" <?php selected( $settings['default_age_group'], 'infant' ); ?>><?php echo esc_html__( 'Infant', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1390 <option value="newborn" <?php selected( $settings['default_age_group'], 'newborn' ); ?>><?php echo esc_html__( 'Newborn', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 694 1391 </select> 695 1392 </td> … … 705 1402 </td> 706 1403 </tr> 1404 </table> 1405 </div> 1406 1407 <div class="card" style="padding: 20px; margin-bottom: 20px;"> 1408 <h3><?php echo esc_html__( 'Variant & Attribute Optimization', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 1409 <p class="description"> 1410 <?php echo esc_html__( 'Extract and optimize essential Google Shopping attributes from WooCommerce and SureCart. WooCommerce variable products are automatically exported as individual items linked by <g:item_group_id>.', 'world-inspire-product-feed-for-google-shopping' ); ?> 1411 </p> 1412 <table class="form-table" role="presentation"> 707 1413 <tr> 708 <th scope="row">< label for="wipf_auto_color"><?php echo esc_html__( 'Smart Color Extraction', 'world-inspire-product-feed-for-google-shopping' ); ?></label></th>1414 <th scope="row"><?php echo esc_html__( 'Smart Attribute Detection', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 709 1415 <td> 710 <label> 711 <input type="checkbox" id="wipf_auto_color" name="wipf_feed_settings[auto_detect_color]" value="yes" <?php checked( $settings['auto_detect_color'], 'yes' ); ?> /> 712 <?php echo esc_html__( 'Automatically infer and populate Google standard colors from title and description', 'world-inspire-product-feed-for-google-shopping' ); ?> 1416 <label style="display: block; margin-bottom: 8px;"> 1417 <input type="checkbox" name="wipf_feed_settings[auto_detect_color]" value="yes" <?php checked( $settings['auto_detect_color'], 'yes' ); ?> /> 1418 <strong><?php echo esc_html__( 'Color Extraction (<g:color>)', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> — <?php echo esc_html__( 'Infer Google standard color names from attributes, titles, or descriptions (over 35 jewelry/apparel shades).', 'world-inspire-product-feed-for-google-shopping' ); ?> 1419 </label> 1420 <label style="display: block; margin-bottom: 8px;"> 1421 <input type="checkbox" name="wipf_feed_settings[auto_detect_material]" value="yes" <?php checked( $settings['auto_detect_material'] ?? 'yes', 'yes' ); ?> /> 1422 <strong><?php echo esc_html__( 'Material Extraction (<g:material>)', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> — <?php echo esc_html__( 'Detect materials (Gold, Sterling Silver, Leather, Cotton, Gemstones, Titanium, Stainless Steel).', 'world-inspire-product-feed-for-google-shopping' ); ?> 1423 </label> 1424 <label style="display: block; margin-bottom: 8px;"> 1425 <input type="checkbox" name="wipf_feed_settings[auto_detect_size]" value="yes" <?php checked( $settings['auto_detect_size'] ?? 'yes', 'yes' ); ?> /> 1426 <strong><?php echo esc_html__( 'Size Extraction (<g:size>)', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> — <?php echo esc_html__( 'Extract sizes from variation attributes or titles (e.g. Size 7, One Size, 8mm, S, M, L, XL).', 'world-inspire-product-feed-for-google-shopping' ); ?> 1427 </label> 1428 <label style="display: block; margin-bottom: 8px;"> 1429 <input type="checkbox" name="wipf_feed_settings[auto_detect_pattern]" value="yes" <?php checked( $settings['auto_detect_pattern'] ?? 'yes', 'yes' ); ?> /> 1430 <strong><?php echo esc_html__( 'Pattern Extraction (<g:pattern>)', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> — <?php echo esc_html__( 'Detect patterns (Solid, Striped, Floral, Beaded, Polka Dot, Geometric).', 'world-inspire-product-feed-for-google-shopping' ); ?> 713 1431 </label> 714 1432 </td> 715 1433 </tr> 716 1434 </table> 717 <?php elseif ( 'shipping' === $active_tab ) : ?> 1435 </div> 1436 1437 <div class="card" style="padding: 20px; margin-bottom: 20px;"> 1438 <h3><?php echo esc_html__( 'Google Shopping Custom Labels (0–4)', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 1439 <p class="description"> 1440 <?php echo esc_html__( 'Custom labels allow you to segment and filter products in Google Ads and Performance Max (PMax) campaigns (e.g. by price brackets, inventory status, product category, clearance tags, or margin tiers).', 'world-inspire-product-feed-for-google-shopping' ); ?> 1441 </p> 1442 <p> 1443 <label> 1444 <input type="checkbox" name="wipf_feed_settings[enable_custom_labels]" value="yes" <?php checked( $settings['enable_custom_labels'] ?? 'yes', 'yes' ); ?> /> 1445 <strong><?php echo esc_html__( 'Enable Custom Labels in Feed', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> 1446 </label> 1447 </p> 1448 1449 <table class="widefat striped" style="margin-top: 15px;"> 1450 <thead> 1451 <tr> 1452 <th style="width: 16%;"><?php echo esc_html__( 'Custom Label Slot', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1453 <th style="width: 22%;"><?php echo esc_html__( 'Rule Type', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1454 <th style="width: 38%;"><?php echo esc_html__( 'Rule Value / Configuration', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1455 <th style="width: 24%;"><?php echo esc_html__( 'Fallback Value', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1456 </tr> 1457 </thead> 1458 <tbody> 1459 <?php 1460 $label_recommendations = array( 1461 0 => array( 'name' => 'Custom Label 0', 'rec' => 'Price Tiers (< 25, 25-50, 50-100, 100+)', 'ph_val' => '25,50,100 (Thresholds)', 'ph_fb' => 'Standard' ), 1462 1 => array( 'name' => 'Custom Label 1', 'rec' => 'Inventory Status (In Stock / Out of Stock)', 'ph_val' => 'Optional override: in_stock:Available, out_of_stock:Unavailable', 'ph_fb' => 'Out of Stock' ), 1463 2 => array( 'name' => 'Custom Label 2', 'rec' => 'Category / Collection Name', 'ph_val' => 'Leave empty to use primary category', 'ph_fb' => 'General' ), 1464 3 => array( 'name' => 'Custom Label 3', 'rec' => 'Promotional / Tag Keywords Match', 'ph_val' => 'clearance:Clearance, sale:On Sale, bestseller:Best Seller', 'ph_fb' => 'Regular' ), 1465 4 => array( 'name' => 'Custom Label 4', 'rec' => 'Static Value / Custom Post Meta Key', 'ph_val' => 'Enter static string or post_meta key', 'ph_fb' => '' ), 1466 ); 1467 1468 for ( $slot = 0; $slot <= 4; $slot++ ) : 1469 $slot_type = $settings[ "custom_label_{$slot}_type" ] ?? 'disabled'; 1470 $slot_val = $settings[ "custom_label_{$slot}_value" ] ?? ''; 1471 $slot_fb = $settings[ "custom_label_{$slot}_fallback" ] ?? ''; 1472 $meta_info = $label_recommendations[ $slot ]; 1473 ?> 1474 <tr> 1475 <td> 1476 <strong><?php echo esc_html( $meta_info['name'] ); ?></strong> 1477 <br><span class="description" style="font-size: 11px;"><?php echo esc_html( $meta_info['rec'] ); ?></span> 1478 </td> 1479 <td> 1480 <select name="wipf_feed_settings[custom_label_<?php echo $slot; ?>_type]" style="width: 100%;"> 1481 <option value="disabled" <?php selected( $slot_type, 'disabled' ); ?>><?php echo esc_html__( 'Disabled', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1482 <option value="price_tier" <?php selected( $slot_type, 'price_tier' ); ?>><?php echo esc_html__( 'Price Range Tier', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1483 <option value="inventory" <?php selected( $slot_type, 'inventory' ); ?>><?php echo esc_html__( 'Inventory Status', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1484 <option value="category" <?php selected( $slot_type, 'category' ); ?>><?php echo esc_html__( 'Product Category', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1485 <option value="tag_keywords" <?php selected( $slot_type, 'tag_keywords' ); ?>><?php echo esc_html__( 'Tag & Keyword Matcher', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1486 <option value="static" <?php selected( $slot_type, 'static' ); ?>><?php echo esc_html__( 'Static Custom String', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1487 <option value="custom_meta" <?php selected( $slot_type, 'custom_meta' ); ?>><?php echo esc_html__( 'Custom Post Meta Field', 'world-inspire-product-feed-for-google-shopping' ); ?></option> 1488 </select> 1489 </td> 1490 <td> 1491 <input type="text" name="wipf_feed_settings[custom_label_<?php echo $slot; ?>_value]" class="large-text" value="<?php echo esc_attr( $slot_val ); ?>" placeholder="<?php echo esc_attr( $meta_info['ph_val'] ); ?>" /> 1492 </td> 1493 <td> 1494 <input type="text" name="wipf_feed_settings[custom_label_<?php echo $slot; ?>_fallback]" class="regular-text" value="<?php echo esc_attr( $slot_fb ); ?>" placeholder="<?php echo esc_attr( $meta_info['ph_fb'] ); ?>" /> 1495 </td> 1496 </tr> 1497 <?php endfor; ?> 1498 </tbody> 1499 </table> 1500 </div> 1501 <?php elseif ( 'shipping' === $active_tab ) : ?> 1502 <div class="card" style="padding: 20px;"> 718 1503 <h3><?php echo esc_html__( 'Multi-Country Shipping Generator', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 719 1504 <p><?php echo esc_html__( 'Configures embedded shipping rules for international Google Shopping campaigns.', 'world-inspire-product-feed-for-google-shopping' ); ?></p> … … 741 1526 </tr> 742 1527 </table> 743 < ?php endif; ?>744 < /div>1528 </div> 1529 <?php endif; ?> 745 1530 <?php submit_button(); ?> 746 1531 </form> 747 1532 <?php elseif ( 'diagnostic' === $active_tab ) : ?> 748 <div class="card" style="max-width: 900px; margin-top: 20px; padding: 20px;"> 749 <h3><?php echo esc_html__( 'Live Feed Diagnostic Audit', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 750 <?php 751 $sample_products = wipf_collect_products( 5 ); 752 $surecart_ok = post_type_exists( 'sc_product' ); 753 $woocommerce_ok = ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) ); 754 ?> 1533 <?php 1534 $surecart_ok = post_type_exists( 'sc_product' ); 1535 $woocommerce_ok = ( function_exists( 'wc_get_products' ) || post_type_exists( 'product' ) ); 1536 $sample_products = wipf_collect_products( 5 ); 1537 1538 $inspect_id = isset( $_GET['inspect_id'] ) ? absint( $_GET['inspect_id'] ) : ( ! empty( $sample_products[0]['id'] ) ? (int) $sample_products[0]['id'] : 0 ); 1539 $audit_data = $inspect_id > 0 ? wipf_diagnose_product( $inspect_id, $settings ) : null; 1540 ?> 1541 <div class="card" style="max-width: 960px; margin-top: 20px; padding: 20px;"> 1542 <h3><?php echo esc_html__( 'Live Feed Engine Status', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 755 1543 <p> 756 1544 <strong><?php echo esc_html__( 'SureCart Engine:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> 757 <?php echo $surecart_ok ? '<span style="color:green; ">✔ ' . esc_html__( 'Detected', 'world-inspire-product-feed-for-google-shopping' ) . '</span>' : '<span style="color:#666;">' . esc_html__( 'Not Installed', 'world-inspire-product-feed-for-google-shopping' ) . '</span>'; ?>1545 <?php echo $surecart_ok ? '<span style="color:green; font-weight: bold;">✔ ' . esc_html__( 'Detected', 'world-inspire-product-feed-for-google-shopping' ) . '</span>' : '<span style="color:#666;">' . esc_html__( 'Not Installed', 'world-inspire-product-feed-for-google-shopping' ) . '</span>'; ?> 758 1546 | 759 1547 <strong><?php echo esc_html__( 'WooCommerce Engine:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> 760 <?php echo $woocommerce_ok ? '<span style="color:green;">✔ ' . esc_html__( 'Detected', 'world-inspire-product-feed-for-google-shopping' ) . '</span>' : '<span style="color:#666;">' . esc_html__( 'Not Installed', 'world-inspire-product-feed-for-google-shopping' ) . '</span>'; ?> 1548 <?php echo $woocommerce_ok ? '<span style="color:green; font-weight: bold;">✔ ' . esc_html__( 'Detected', 'world-inspire-product-feed-for-google-shopping' ) . '</span>' : '<span style="color:#666;">' . esc_html__( 'Not Installed', 'world-inspire-product-feed-for-google-shopping' ) . '</span>'; ?> 1549 | 1550 <strong><?php echo esc_html__( 'Catalog Sample Available:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> 1551 <code><?php echo esc_html( count( $sample_products ) ); ?></code> 761 1552 </p> 762 <p> 763 <strong><?php echo esc_html__( 'Products Found in Catalog:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> 764 <?php echo esc_html( count( $sample_products ) ); ?> (<?php echo esc_html__( 'showing first 5', 'world-inspire-product-feed-for-google-shopping' ); ?>) 1553 </div> 1554 1555 <!-- Product Inspector & XML Simulator --> 1556 <div class="card" style="max-width: 960px; margin-top: 20px; padding: 20px;"> 1557 <h3><?php echo esc_html__( 'Product Inspector & Diagnostic Simulator', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 1558 <p class="description"> 1559 <?php echo esc_html__( 'Enter any Product ID to audit its exact extracted attributes, custom labels evaluation logic, and preview the generated Google Shopping <item> XML element.', 'world-inspire-product-feed-for-google-shopping' ); ?> 765 1560 </p> 766 1561 1562 <form method="get" action="" style="margin: 15px 0 20px 0;"> 1563 <input type="hidden" name="page" value="wipf-settings" /> 1564 <input type="hidden" name="tab" value="diagnostic" /> 1565 <label for="inspect_id"><strong><?php echo esc_html__( 'Test Product ID:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong></label> 1566 <input type="number" id="inspect_id" name="inspect_id" value="<?php echo esc_attr( $inspect_id ? (string) $inspect_id : '' ); ?>" placeholder="e.g. 790" style="width: 140px; margin: 0 10px;" /> 1567 <button type="submit" class="button button-primary"><?php echo esc_html__( 'Inspect Product', 'world-inspire-product-feed-for-google-shopping' ); ?></button> 1568 </form> 1569 1570 <?php if ( $audit_data && ! empty( $audit_data['found'] ) ) : ?> 1571 <?php 1572 $p = $audit_data['product']; 1573 ?> 1574 <div style="background: #f0f6fc; border-left: 4px solid #72aee6; padding: 12px 16px; margin-bottom: 20px;"> 1575 <h4 style="margin: 0 0 8px 0;"><?php echo esc_html( $p['title'] ); ?> <span style="font-weight: normal; font-size: 13px;">(ID: <code><?php echo esc_html( $p['id'] ); ?></code> · <?php echo esc_html( $p['price'] . ' ' . $p['currency'] ); ?> · <?php echo esc_html( $p['availability'] ); ?> · Source: <code><?php echo esc_html( $p['source'] ); ?></code>)</span></h4> 1576 <p style="margin: 0; font-size: 13px;"> 1577 <strong><?php echo esc_html__( 'Variant Group ID:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <?php echo ! empty( $p['item_group_id'] ) ? '<code>' . esc_html( $p['item_group_id'] ) . '</code>' : '<em>' . esc_html__( 'None (Simple Product)', 'world-inspire-product-feed-for-google-shopping' ) . '</em>'; ?> 1578 | 1579 <strong><?php echo esc_html__( 'Color:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <code><?php echo esc_html( $p['color'] ?: '—' ); ?></code> 1580 | 1581 <strong><?php echo esc_html__( 'Size:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <code><?php echo esc_html( $p['size'] ?: '—' ); ?></code> 1582 | 1583 <strong><?php echo esc_html__( 'Material:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <code><?php echo esc_html( $p['material'] ?: '—' ); ?></code> 1584 | 1585 <strong><?php echo esc_html__( 'Pattern:', 'world-inspire-product-feed-for-google-shopping' ); ?></strong> <code><?php echo esc_html( $p['pattern'] ?: '—' ); ?></code> 1586 </p> 1587 </div> 1588 1589 <h4><?php echo esc_html__( 'Custom Labels Evaluation Audit (0–4)', 'world-inspire-product-feed-for-google-shopping' ); ?></h4> 1590 <table class="widefat striped" style="margin-bottom: 20px;"> 1591 <thead> 1592 <tr> 1593 <th style="width: 16%;"><?php echo esc_html__( 'Slot', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1594 <th style="width: 18%;"><?php echo esc_html__( 'Rule Type', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1595 <th style="width: 26%;"><?php echo esc_html__( 'Evaluated Value', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1596 <th style="width: 40%;"><?php echo esc_html__( 'Matching Reason / Trace', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1597 </tr> 1598 </thead> 1599 <tbody> 1600 <?php foreach ( $audit_data['labels'] as $slot_key => $eval ) : ?> 1601 <tr> 1602 <td><code><?php echo esc_html( $slot_key ); ?></code></td> 1603 <td><?php echo esc_html( ucfirst( str_replace( '_', ' ', $eval['type'] ) ) ); ?></td> 1604 <td> 1605 <?php if ( ! empty( $eval['value'] ) ) : ?> 1606 <span style="font-weight: 600; color: #135e96; background: #e7f3fa; padding: 2px 8px; border-radius: 3px;"> 1607 <?php echo esc_html( $eval['value'] ); ?> 1608 </span> 1609 <?php else : ?> 1610 <span style="color: #888;"><em><?php echo esc_html__( 'Empty (Omitted from XML)', 'world-inspire-product-feed-for-google-shopping' ); ?></em></span> 1611 <?php endif; ?> 1612 </td> 1613 <td><small><?php echo esc_html( $eval['reason'] ); ?></small></td> 1614 </tr> 1615 <?php endforeach; ?> 1616 </tbody> 1617 </table> 1618 1619 <h4><?php echo esc_html__( 'Generated XML <item> Snippet Preview', 'world-inspire-product-feed-for-google-shopping' ); ?></h4> 1620 <pre style="background: #2b2b2b; color: #f8f8f2; padding: 15px; border-radius: 4px; overflow-x: auto; font-size: 12px; line-height: 1.5;"><code><?php echo esc_html( $audit_data['xml_snippet'] ); ?></code></pre> 1621 1622 <?php elseif ( $audit_data && empty( $audit_data['found'] ) ) : ?> 1623 <div class="notice notice-warning inline"><p><?php echo esc_html( $audit_data['error'] ); ?></p></div> 1624 <?php endif; ?> 1625 </div> 1626 1627 <!-- Catalog Overview Sample Table --> 1628 <div class="card" style="max-width: 960px; margin-top: 20px; padding: 20px;"> 1629 <h3><?php echo esc_html__( 'Catalog Sample Inspection (First 5 Items)', 'world-inspire-product-feed-for-google-shopping' ); ?></h3> 767 1630 <?php if ( ! empty( $sample_products ) ) : ?> 768 1631 <table class="widefat striped" style="margin-top: 15px;"> … … 772 1635 <th><?php echo esc_html__( 'Title', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 773 1636 <th><?php echo esc_html__( 'Price', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 774 <th><?php echo esc_html__( 'Availability', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 775 <th><?php echo esc_html__( 'Detected Color', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 776 <th><?php echo esc_html__( 'Engine', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1637 <th><?php echo esc_html__( 'Group ID', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1638 <th><?php echo esc_html__( 'Color', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1639 <th><?php echo esc_html__( 'Size', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1640 <th><?php echo esc_html__( 'Material', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1641 <th><?php echo esc_html__( 'Custom Labels', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 1642 <th><?php echo esc_html__( 'Action', 'world-inspire-product-feed-for-google-shopping' ); ?></th> 777 1643 </tr> 778 1644 </thead> 779 1645 <tbody> 780 1646 <?php foreach ( $sample_products as $p ) : ?> 1647 <?php 1648 $p_eval0 = wipf_evaluate_custom_label( 0, $p, $settings ); 1649 $p_eval1 = wipf_evaluate_custom_label( 1, $p, $settings ); 1650 $label_summary = array(); 1651 if ( ! empty( $p_eval0['value'] ) ) { 1652 $label_summary[] = 'L0: ' . $p_eval0['value']; 1653 } 1654 if ( ! empty( $p_eval1['value'] ) ) { 1655 $label_summary[] = 'L1: ' . $p_eval1['value']; 1656 } 1657 ?> 781 1658 <tr> 782 1659 <td><code><?php echo esc_html( $p['id'] ); ?></code></td> 783 1660 <td><strong><?php echo esc_html( $p['title'] ); ?></strong></td> 784 1661 <td><?php echo esc_html( $p['price'] . ' ' . $p['currency'] ); ?></td> 785 <td><?php echo esc_html( $p['availability'] ); ?></td> 786 <td><?php echo esc_html( wipf_detect_color( $p['title'], $p['description'] ) ?: '—' ); ?></td> 787 <td><code><?php echo esc_html( $p['source'] ); ?></code></td> 1662 <td><code><?php echo esc_html( $p['item_group_id'] ?: '—' ); ?></code></td> 1663 <td><?php echo esc_html( $p['color'] ?: '—' ); ?></td> 1664 <td><?php echo esc_html( $p['size'] ?: '—' ); ?></td> 1665 <td><?php echo esc_html( $p['material'] ?: '—' ); ?></td> 1666 <td><small><?php echo esc_html( ! empty( $label_summary ) ? implode( ' | ', $label_summary ) : '—' ); ?></small></td> 1667 <td><a href="?page=wipf-settings&tab=diagnostic&inspect_id=<?php echo esc_attr( $p['id'] ); ?>" class="button button-small"><?php echo esc_html__( 'Inspect', 'world-inspire-product-feed-for-google-shopping' ); ?></a></td> 788 1668 </tr> 789 1669 <?php endforeach; ?>
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)