Changeset 3514908
- Timestamp:
- 04/24/2026 07:44:25 PM (6 months ago)
- Location:
- hydori-seo-ai-powered-seo-automation
- Files:
-
- 29 added
- 9 edited
-
tags/2.5.0 (added)
-
tags/2.5.0/admin (added)
-
tags/2.5.0/admin/class-hydori-sync-admin.php (added)
-
tags/2.5.0/admin/css (added)
-
tags/2.5.0/admin/css/hydori-sync-admin.css (added)
-
tags/2.5.0/admin/css/index.php (added)
-
tags/2.5.0/admin/index.php (added)
-
tags/2.5.0/admin/js (added)
-
tags/2.5.0/admin/js/hydori-sync-admin.js (added)
-
tags/2.5.0/admin/js/index.php (added)
-
tags/2.5.0/admin/partials (added)
-
tags/2.5.0/admin/partials/hydori-sync-admin-display.php (added)
-
tags/2.5.0/admin/partials/index.php (added)
-
tags/2.5.0/hydori-sync.php (added)
-
tags/2.5.0/includes (added)
-
tags/2.5.0/includes/class-hydori-api-key-manager.php (added)
-
tags/2.5.0/includes/class-hydori-export-manager.php (added)
-
tags/2.5.0/includes/class-hydori-post-handler.php (added)
-
tags/2.5.0/includes/class-hydori-rest-api.php (added)
-
tags/2.5.0/includes/class-hydori-seo-head.php (added)
-
tags/2.5.0/includes/class-hydori-sync-loader.php (added)
-
tags/2.5.0/includes/class-hydori-sync.php (added)
-
tags/2.5.0/includes/index.php (added)
-
tags/2.5.0/index.php (added)
-
tags/2.5.0/languages (added)
-
tags/2.5.0/languages/index.php (added)
-
tags/2.5.0/readme.txt (added)
-
tags/2.5.0/uninstall.php (added)
-
trunk/admin/class-hydori-sync-admin.php (modified) (2 diffs)
-
trunk/admin/css/hydori-sync-admin.css (modified) (1 diff)
-
trunk/admin/js/hydori-sync-admin.js (modified) (2 diffs)
-
trunk/admin/partials/hydori-sync-admin-display.php (modified) (3 diffs)
-
trunk/hydori-sync.php (modified) (6 diffs)
-
trunk/includes/class-hydori-post-handler.php (modified) (12 diffs)
-
trunk/includes/class-hydori-rest-api.php (modified) (9 diffs)
-
trunk/includes/class-hydori-seo-head.php (added)
-
trunk/includes/class-hydori-sync.php (modified) (3 diffs)
-
trunk/readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
hydori-seo-ai-powered-seo-automation/trunk/admin/class-hydori-sync-admin.php
r3488505 r3514908 167 167 $key_last_used = Hydori_API_Key_Manager::get_key_last_used_date(); 168 168 169 // Derive the connection status shown in the new focused layout. 170 // A site counts as "connected to Hydori" when it has a Hydori Site 171 // ID stored (the value Hydori writes back when a user finishes 172 // onboarding). Auto-sync gates the amber "sync off" state. 173 $connected_to_hydori = ! empty( $hydori_site_id ); 174 $connection_state = 'disconnected'; 175 if ( $connected_to_hydori && 'auto' === $sync_mode ) { 176 $connection_state = 'connected'; 177 } elseif ( $connected_to_hydori && 'manual' === $sync_mode ) { 178 $connection_state = 'sync_off'; 179 } 180 169 181 // Include the partial 170 182 include HYDORI_SYNC_PLUGIN_DIR . 'admin/partials/hydori-sync-admin-display.php'; … … 294 306 update_option( HYDORI_OPTION_DEBUG_MODE, $debug_mode ); 295 307 296 // Sanitize and save sync mode 297 $sync_mode = isset( $_POST['sync_mode'] ) ? sanitize_text_field( wp_unslash( $_POST['sync_mode'] ) ) : 'manual'; 308 // Sanitize and save sync mode. v2.4.0: default is auto for new installs; 309 // we also record that the user explicitly interacted with this setting 310 // so the 2.4 upgrade path never flips an explicit choice. 311 $sync_mode = isset( $_POST['sync_mode'] ) ? sanitize_text_field( wp_unslash( $_POST['sync_mode'] ) ) : 'auto'; 298 312 if ( ! in_array( $sync_mode, array( 'manual', 'auto' ), true ) ) { 299 $sync_mode = ' manual';313 $sync_mode = 'auto'; 300 314 } 301 315 update_option( HYDORI_OPTION_SYNC_MODE, $sync_mode ); 316 update_option( HYDORI_OPTION_SYNC_MODE_USER_SET, 'yes', 'yes' ); 302 317 303 318 // Sanitize and save featured images toggle -
hydori-seo-ai-powered-seo-automation/trunk/admin/css/hydori-sync-admin.css
r3488505 r3514908 542 542 } 543 543 } 544 545 /* ======================================================================== 546 * Phase 16 / v2.5.0 focused settings layout. 547 * Three cards stacked vertically: status, sync settings, diagnostics. 548 * Everything vestigial (legacy key, export, compatibility matrix) lives 549 * behind the "Advanced" disclosure at the bottom. 550 * ==================================================================== */ 551 552 .hydori-sync-admin.hydori-v25 { 553 max-width: 860px; 554 } 555 556 .hydori-v25 .hydori-page-title { 557 display: flex; 558 align-items: center; 559 gap: 10px; 560 margin: 0 0 20px; 561 font-size: 22px; 562 line-height: 1; 563 } 564 565 .hydori-v25 .hydori-version { 566 font-size: 12px; 567 font-weight: 500; 568 color: #646970; 569 background: #f0f0f1; 570 padding: 3px 8px; 571 border-radius: 999px; 572 margin-left: auto; 573 } 574 575 .hydori-v25 .hydori-card { 576 background: #fff; 577 border: 1px solid #e5e7eb; 578 border-radius: 10px; 579 padding: 20px 22px; 580 margin-bottom: 16px; 581 box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03); 582 } 583 584 /* Status card. */ 585 .hydori-v25 .hydori-card-status { 586 display: flex; 587 align-items: center; 588 gap: 16px; 589 border-left-width: 4px; 590 border-left-style: solid; 591 padding: 18px 22px; 592 } 593 594 .hydori-v25 .hydori-status-connected { border-left-color: #0b8a4a; background: #f5fbf7; } 595 .hydori-v25 .hydori-status-sync_off { border-left-color: #c67b00; background: #fff9ed; } 596 .hydori-v25 .hydori-status-disconnected { border-left-color: #b32d2e; background: #fdf5f5; } 597 598 .hydori-v25 .hydori-status-icon .dashicons { 599 font-size: 28px; 600 width: 28px; 601 height: 28px; 602 } 603 604 .hydori-v25 .hydori-status-connected .hydori-status-icon .dashicons { color: #0b8a4a; } 605 .hydori-v25 .hydori-status-sync_off .hydori-status-icon .dashicons { color: #c67b00; } 606 .hydori-v25 .hydori-status-disconnected .hydori-status-icon .dashicons { color: #b32d2e; } 607 608 .hydori-v25 .hydori-status-body { flex: 1 1 auto; min-width: 0; } 609 610 .hydori-v25 .hydori-status-title { 611 font-size: 15px; 612 font-weight: 600; 613 color: #1d2327; 614 margin-bottom: 2px; 615 } 616 617 .hydori-v25 .hydori-status-meta { 618 font-size: 13px; 619 color: #50575e; 620 line-height: 1.5; 621 } 622 623 .hydori-v25 .hydori-status-actions { flex-shrink: 0; } 624 625 /* Sync settings card. */ 626 .hydori-v25 .hydori-card-title { 627 margin: 0 0 14px; 628 font-size: 15px; 629 font-weight: 600; 630 } 631 632 .hydori-v25 .hydori-form .hydori-field { 633 border: 0; 634 padding: 0; 635 margin: 0 0 18px; 636 } 637 638 .hydori-v25 .hydori-field > legend { 639 font-size: 12px; 640 font-weight: 600; 641 text-transform: uppercase; 642 letter-spacing: 0.06em; 643 color: #1d2327; 644 margin-bottom: 8px; 645 padding: 0; 646 } 647 648 .hydori-v25 .hydori-radio, 649 .hydori-v25 .hydori-toggle { 650 display: flex; 651 align-items: flex-start; 652 gap: 10px; 653 padding: 6px 0; 654 font-size: 13px; 655 cursor: pointer; 656 } 657 658 .hydori-v25 .hydori-radio input, 659 .hydori-v25 .hydori-toggle input { 660 margin-top: 2px; 661 flex-shrink: 0; 662 } 663 664 .hydori-v25 .hydori-radio span, 665 .hydori-v25 .hydori-toggle span { 666 color: #1d2327; 667 font-weight: 500; 668 } 669 670 .hydori-v25 .hydori-radio small { 671 display: block; 672 color: #646970; 673 font-size: 12px; 674 font-weight: 400; 675 margin-top: 2px; 676 } 677 678 .hydori-v25 .hydori-radio { 679 flex-wrap: wrap; 680 } 681 .hydori-v25 .hydori-radio small { 682 flex: 1 0 100%; 683 margin-left: 26px; 684 } 685 686 .hydori-v25 .hydori-field-help { 687 font-size: 12px; 688 color: #646970; 689 margin: 0; 690 } 691 692 .hydori-v25 .hydori-form-submit { 693 margin: 16px 0 0; 694 } 695 696 /* Disclosure button (diagnostics, advanced). */ 697 .hydori-v25 .hydori-disclosure { 698 background: none; 699 border: 0; 700 padding: 0; 701 margin: 0; 702 display: inline-flex; 703 align-items: center; 704 gap: 6px; 705 color: #1d2327; 706 font-size: 14px; 707 font-weight: 600; 708 cursor: pointer; 709 } 710 711 .hydori-v25 .hydori-disclosure .dashicons { 712 transition: transform 0.15s ease; 713 } 714 715 .hydori-v25 .hydori-disclosure-open .dashicons { 716 transform: rotate(90deg); 717 } 718 719 .hydori-v25 .hydori-diag-panel, 720 .hydori-v25 .hydori-advanced-panel { 721 padding-top: 16px; 722 } 723 724 .hydori-v25 .hydori-diag-row { 725 display: flex; 726 align-items: center; 727 gap: 10px; 728 padding: 8px 0; 729 border-bottom: 1px solid #f0f0f1; 730 font-size: 13px; 731 } 732 733 .hydori-v25 .hydori-diag-label { 734 color: #50575e; 735 width: 160px; 736 flex-shrink: 0; 737 } 738 739 .hydori-v25 .hydori-diag-value { 740 color: #1d2327; 741 } 742 743 .hydori-v25 .hydori-diag-toggle-row { 744 flex-direction: column; 745 align-items: flex-start; 746 gap: 4px; 747 } 748 749 .hydori-v25 .hydori-diag-toggle-row small { 750 color: #646970; 751 font-size: 12px; 752 margin-left: 26px; 753 } 754 755 .hydori-v25 .hydori-diag-actions { 756 display: flex; 757 flex-wrap: wrap; 758 gap: 10px; 759 margin-top: 14px; 760 } 761 762 .hydori-v25 .hydori-debug-details { 763 margin-top: 16px; 764 padding: 12px; 765 background: #f9fafb; 766 border-radius: 6px; 767 } 768 769 .hydori-v25 .hydori-debug-details summary { 770 cursor: pointer; 771 font-weight: 600; 772 color: #1d2327; 773 } 774 775 .hydori-v25 .hydori-debug-list { 776 list-style: none; 777 padding: 8px 0 0; 778 margin: 0; 779 font-size: 12px; 780 } 781 782 .hydori-v25 .hydori-debug-list li { 783 padding: 2px 0; 784 } 785 786 /* Advanced panel internals. */ 787 .hydori-v25 .hydori-card-advanced h3 { 788 font-size: 13px; 789 font-weight: 600; 790 margin: 16px 0 6px; 791 text-transform: uppercase; 792 letter-spacing: 0.04em; 793 color: #1d2327; 794 } 795 796 .hydori-v25 .hydori-card-advanced h3:first-of-type { 797 margin-top: 0; 798 } 799 800 .hydori-v25 .hydori-card-advanced .hydori-description { 801 color: #50575e; 802 font-size: 13px; 803 margin: 0 0 10px; 804 line-height: 1.5; 805 } 806 807 .hydori-v25 .hydori-api-key-wrapper { 808 display: flex; 809 gap: 8px; 810 margin-bottom: 10px; 811 } 812 813 .hydori-v25 .hydori-api-key-input { 814 flex: 1 1 auto; 815 font-family: ui-monospace, SFMono-Regular, Menlo, monospace; 816 font-size: 12px; 817 min-width: 0; 818 } 819 820 .hydori-v25 .hydori-inline-field { 821 display: flex; 822 flex-direction: column; 823 gap: 4px; 824 margin-bottom: 12px; 825 font-size: 12px; 826 color: #50575e; 827 } 828 829 .hydori-v25 .hydori-inline-field input { 830 font-size: 13px; 831 } 832 833 .hydori-v25 .hydori-compat-grid { 834 display: grid; 835 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); 836 gap: 16px; 837 margin-top: 8px; 838 } 839 840 .hydori-v25 .hydori-compat-grid h4 { 841 font-size: 12px; 842 font-weight: 600; 843 margin: 0 0 6px; 844 text-transform: uppercase; 845 letter-spacing: 0.04em; 846 color: #50575e; 847 } 848 849 .hydori-v25 .hydori-compat-list { 850 list-style: none; 851 padding: 0; 852 margin: 0; 853 } 854 855 .hydori-v25 .hydori-compat-list li { 856 display: flex; 857 align-items: center; 858 gap: 6px; 859 padding: 4px 0; 860 font-size: 13px; 861 color: #646970; 862 } 863 864 .hydori-v25 .hydori-compat-list li.active { 865 color: #1d2327; 866 font-weight: 500; 867 } 868 869 .hydori-v25 .hydori-test-result { 870 margin-top: 12px; 871 padding: 10px 12px; 872 border-radius: 6px; 873 font-size: 13px; 874 line-height: 1.5; 875 } 876 877 @media (max-width: 640px) { 878 .hydori-v25 .hydori-card-status { 879 flex-direction: column; 880 align-items: flex-start; 881 } 882 883 .hydori-v25 .hydori-status-actions { width: 100%; } 884 885 .hydori-v25 .hydori-diag-row { 886 flex-direction: column; 887 align-items: flex-start; 888 gap: 2px; 889 } 890 891 .hydori-v25 .hydori-diag-label { width: auto; } 892 } -
hydori-seo-ai-powered-seo-automation/trunk/admin/js/hydori-sync-admin.js
r3488505 r3514908 43 43 $('#hydori-export-btn').on('click', this.exportToHydori.bind(this)); 44 44 45 // Show/hide export section when sync mode changes 46 $('#hydori-sync-mode').on('change', function() { 47 if ('manual' === $(this).val()) { 45 // v2.4 select + v2.5 radios both feed this listener so the 46 // legacy #hydori-export-section toggles on both layouts. 47 var handleSyncModeChange = function( value ) { 48 if ( 'manual' === value ) { 48 49 $('#hydori-export-section').show(); 49 50 } else { 50 51 $('#hydori-export-section').hide(); 51 52 } 53 }; 54 $('#hydori-sync-mode').on('change', function() { 55 handleSyncModeChange( $(this).val() ); 56 }); 57 $('input[name="sync_mode"]').on('change', function() { 58 handleSyncModeChange( $(this).val() ); 52 59 }); 53 60 … … 242 249 $btn.html('<span class="hydori-spinner"></span> ' + hydoriSync.strings.savingSettings); 243 250 244 // Gather form data 251 // Gather form data. v2.5 replaced sync_mode / post_status 252 // <select> elements with radio groups and moved the debug 253 // toggle into the diagnostics card, so we read by name / 254 // the new ID and fall back to the legacy selectors so users 255 // bouncing between plugin versions still save cleanly. 256 var syncMode = $('input[name="sync_mode"]:checked').val(); 257 if ( ! syncMode ) { 258 syncMode = $('#hydori-sync-mode').val() || 'auto'; 259 } 260 var postStatus = $('input[name="post_status"]:checked').val(); 261 if ( ! postStatus ) { 262 postStatus = $('#hydori-post-status').val() || 'publish'; 263 } 264 var debugMode = $('#hydori-debug-mode-toggle').is(':checked') 265 || $('#hydori-debug-mode').is(':checked'); 266 var featuredImages = $('input[name="sync_featured_images"]').is(':checked') 267 || $('#hydori-sync-featured-images').is(':checked'); 268 245 269 var formData = { 246 270 action: 'hydori_save_settings', 247 271 nonce: hydoriSync.nonce, 248 sync_mode: $('#hydori-sync-mode').val(),249 post_status: $('#hydori-post-status').val(),250 sync_featured_images: $('#hydori-sync-featured-images').is(':checked')? 'yes' : 'no',251 debug_mode: $('#hydori-debug-mode').is(':checked')? 'yes' : 'no',272 sync_mode: syncMode, 273 post_status: postStatus, 274 sync_featured_images: featuredImages ? 'yes' : 'no', 275 debug_mode: debugMode ? 'yes' : 'no', 252 276 hydori_api_token: $('#hydori-api-token').val(), 253 277 hydori_site_id: $('#hydori-site-id').val() -
hydori-seo-ai-powered-seo-automation/trunk/admin/partials/hydori-sync-admin-display.php
r3491852 r3514908 3 3 * Admin settings page display template. 4 4 * 5 * Phase 16 rewrite. Three focused zones: 6 * 1. Connection status card (top) - green / amber / red state. 7 * 2. Sync settings (middle) - only when connected. 8 * 3. Diagnostics (bottom, collapsed by default) - API endpoint, debug, 9 * test connection, help links, legacy API key under Advanced. 10 * 5 11 * @package Hydori_Sync 6 12 * @subpackage Hydori_Sync/admin/partials 7 * @since 2. 0.013 * @since 2.5.0 8 14 */ 9 15 … … 18 24 $debug_info = Hydori_API_Key_Manager::get_debug_info(); 19 25 } 26 27 // Connection state fallback (provided by display_settings_page()). 28 if ( ! isset( $connection_state ) ) { 29 $connection_state = 'disconnected'; 30 } 31 if ( ! isset( $connected_to_hydori ) ) { 32 $connected_to_hydori = false; 33 } 20 34 ?> 21 35 22 <div class="wrap hydori-sync-admin ">23 <h1 >36 <div class="wrap hydori-sync-admin hydori-v25"> 37 <h1 class="hydori-page-title"> 24 38 <span class="hydori-logo"> 25 39 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> … … 28 42 </svg> 29 43 </span> 30 <?php esc_html_e( 'Hydori Settings', 'hydori-seo-ai-powered-seo-automation' ); ?>44 <?php esc_html_e( 'Hydori', 'hydori-seo-ai-powered-seo-automation' ); ?> 31 45 <span class="hydori-version"><?php echo esc_html( 'v' . $this->version ); ?></span> 32 46 </h1> 33 47 34 <!-- Connection Status Banner --> 35 <div id="hydori-status-banner" class="hydori-status-banner"> 36 <?php if ( $api_key ) : ?> 37 <div class="hydori-status hydori-status-ready"> 48 <?php /* ============================================================ 49 * ZONE 1. Connection status card. 50 * ============================================================ */ ?> 51 <div class="hydori-card hydori-card-status hydori-status-<?php echo esc_attr( $connection_state ); ?>"> 52 <?php if ( 'connected' === $connection_state ) : ?> 53 <div class="hydori-status-icon"> 38 54 <span class="dashicons dashicons-yes-alt"></span> 39 <span class="hydori-status-text"><?php esc_html_e( 'Ready to receive content from Hydori', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 40 </div> 55 </div> 56 <div class="hydori-status-body"> 57 <div class="hydori-status-title"> 58 <?php esc_html_e( 'Connected to Hydori', 'hydori-seo-ai-powered-seo-automation' ); ?> 59 </div> 60 <div class="hydori-status-meta"> 61 <?php 62 printf( 63 /* translators: %s: Hydori site ID */ 64 esc_html__( 'Site ID: %s', 'hydori-seo-ai-powered-seo-automation' ), 65 '<code>' . esc_html( $hydori_site_id ) . '</code>' 66 ); 67 if ( $key_created ) { 68 echo ' · '; 69 printf( 70 /* translators: %s: date */ 71 esc_html__( 'Connected since %s', 'hydori-seo-ai-powered-seo-automation' ), 72 esc_html( date_i18n( get_option( 'date_format' ), strtotime( $key_created ) ) ) 73 ); 74 } 75 ?> 76 </div> 77 </div> 78 <div class="hydori-status-actions"> 79 <a href="https://app.hydori.io/" target="_blank" rel="noopener" class="button button-secondary"> 80 <?php esc_html_e( 'Open Hydori', 'hydori-seo-ai-powered-seo-automation' ); ?> 81 </a> 82 </div> 83 84 <?php elseif ( 'sync_off' === $connection_state ) : ?> 85 <div class="hydori-status-icon"> 86 <span class="dashicons dashicons-warning"></span> 87 </div> 88 <div class="hydori-status-body"> 89 <div class="hydori-status-title"> 90 <?php esc_html_e( 'Auto-sync is OFF', 'hydori-seo-ai-powered-seo-automation' ); ?> 91 </div> 92 <div class="hydori-status-meta"> 93 <?php esc_html_e( 'New posts will not translate automatically. Flip on auto-sync to have Hydori translate every new post the moment it publishes.', 'hydori-seo-ai-powered-seo-automation' ); ?> 94 </div> 95 </div> 96 <div class="hydori-status-actions"> 97 <button type="button" id="hydori-enable-autosync" class="button button-primary"> 98 <?php esc_html_e( 'Enable auto-sync', 'hydori-seo-ai-powered-seo-automation' ); ?> 99 </button> 100 </div> 101 41 102 <?php else : ?> 42 <div class="hydori-status hydori-status-error"> 43 <span class="dashicons dashicons-warning"></span> 44 <span class="hydori-status-text"><?php esc_html_e( 'API key missing - click "Regenerate API Key" to create one', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 103 <div class="hydori-status-icon"> 104 <span class="dashicons dashicons-minus"></span> 105 </div> 106 <div class="hydori-status-body"> 107 <div class="hydori-status-title"> 108 <?php esc_html_e( 'Not connected to Hydori', 'hydori-seo-ai-powered-seo-automation' ); ?> 109 </div> 110 <div class="hydori-status-meta"> 111 <?php esc_html_e( 'Open Hydori in your browser and follow the one-click connect flow. You do not need to copy anything from this page.', 'hydori-seo-ai-powered-seo-automation' ); ?> 112 </div> 113 </div> 114 <div class="hydori-status-actions"> 115 <a href="https://app.hydori.io/onboarding/wp-connect" target="_blank" rel="noopener" class="button button-primary"> 116 <?php esc_html_e( 'Open Hydori', 'hydori-seo-ai-powered-seo-automation' ); ?> 117 </a> 45 118 </div> 46 119 <?php endif; ?> 47 120 </div> 48 121 49 <div class="hydori-grid"> 50 <!-- Main Settings Panel --> 51 <div class="hydori-panel hydori-panel-main"> 52 <h2><?php esc_html_e( 'API Key', 'hydori-seo-ai-powered-seo-automation' ); ?></h2> 122 <?php /* ============================================================ 123 * ZONE 2. Sync settings. Only shown when connected to Hydori. 124 * ============================================================ */ ?> 125 <?php if ( $connected_to_hydori ) : ?> 126 <div class="hydori-card hydori-card-settings"> 127 <h2 class="hydori-card-title"><?php esc_html_e( 'Sync settings', 'hydori-seo-ai-powered-seo-automation' ); ?></h2> 128 <form id="hydori-settings-form" class="hydori-form"> 129 <?php wp_nonce_field( 'hydori_sync_nonce', 'nonce' ); ?> 130 131 <fieldset class="hydori-field"> 132 <legend><?php esc_html_e( 'Sync mode', 'hydori-seo-ai-powered-seo-automation' ); ?></legend> 133 <label class="hydori-radio"> 134 <input type="radio" name="sync_mode" value="auto" <?php checked( $sync_mode, 'auto' ); ?> /> 135 <span><?php esc_html_e( 'Automatic', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 136 <small><?php esc_html_e( 'Every new post triggers a translation + publish to Hydori. Recommended.', 'hydori-seo-ai-powered-seo-automation' ); ?></small> 137 </label> 138 <label class="hydori-radio"> 139 <input type="radio" name="sync_mode" value="manual" <?php checked( $sync_mode, 'manual' ); ?> /> 140 <span><?php esc_html_e( 'Manual', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 141 <small><?php esc_html_e( 'Only trigger a sync when you explicitly export posts.', 'hydori-seo-ai-powered-seo-automation' ); ?></small> 142 </label> 143 </fieldset> 144 145 <fieldset class="hydori-field"> 146 <legend><?php esc_html_e( 'Default post status', 'hydori-seo-ai-powered-seo-automation' ); ?></legend> 147 <label class="hydori-radio"> 148 <input type="radio" name="post_status" value="publish" <?php checked( $post_status, 'publish' ); ?> /> 149 <span><?php esc_html_e( 'Published', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 150 <small><?php esc_html_e( 'Translations go live the moment they arrive.', 'hydori-seo-ai-powered-seo-automation' ); ?></small> 151 </label> 152 <label class="hydori-radio"> 153 <input type="radio" name="post_status" value="draft" <?php checked( $post_status, 'draft' ); ?> /> 154 <span><?php esc_html_e( 'Draft', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 155 <small><?php esc_html_e( 'Translations arrive as drafts for you to review before publishing.', 'hydori-seo-ai-powered-seo-automation' ); ?></small> 156 </label> 157 </fieldset> 158 159 <fieldset class="hydori-field"> 160 <legend><?php esc_html_e( 'Featured images', 'hydori-seo-ai-powered-seo-automation' ); ?></legend> 161 <label class="hydori-toggle"> 162 <input type="checkbox" name="sync_featured_images" value="yes" <?php checked( $sync_featured_images, 'yes' ); ?> /> 163 <span><?php esc_html_e( 'Sync featured images to the WordPress media library', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 164 </label> 165 </fieldset> 166 167 <fieldset class="hydori-field"> 168 <legend><?php esc_html_e( 'Review before publishing', 'hydori-seo-ai-powered-seo-automation' ); ?></legend> 169 <p class="hydori-field-help"> 170 <?php esc_html_e( 'To require review on every translation, set the default post status to Draft above. This plugin mirrors the Review toggle you set in Hydori.', 'hydori-seo-ai-powered-seo-automation' ); ?> 171 </p> 172 </fieldset> 173 174 <p class="hydori-form-submit"> 175 <button type="submit" id="hydori-save-settings" class="button button-primary"> 176 <?php esc_html_e( 'Save settings', 'hydori-seo-ai-powered-seo-automation' ); ?> 177 </button> 178 </p> 179 </form> 180 </div> 181 <?php endif; ?> 182 183 <?php /* ============================================================ 184 * ZONE 3. Diagnostics. Collapsed by default. 185 * ============================================================ */ ?> 186 <div class="hydori-card hydori-card-diagnostics"> 187 <button type="button" class="hydori-disclosure" aria-expanded="false" aria-controls="hydori-diag-panel"> 188 <span class="dashicons dashicons-arrow-right-alt2"></span> 189 <?php esc_html_e( 'Diagnostics', 'hydori-seo-ai-powered-seo-automation' ); ?> 190 </button> 191 <div id="hydori-diag-panel" class="hydori-diag-panel" hidden> 192 <div class="hydori-diag-row"> 193 <span class="hydori-diag-label"><?php esc_html_e( 'API endpoint', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 194 <code class="hydori-diag-value"><?php echo esc_html( rest_url( 'hydori/v1/' ) ); ?></code> 195 </div> 196 <div class="hydori-diag-row"> 197 <span class="hydori-diag-label"><?php esc_html_e( 'Plugin version', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 198 <span class="hydori-diag-value"><?php echo esc_html( $this->version ); ?></span> 199 </div> 200 <div class="hydori-diag-row"> 201 <span class="hydori-diag-label"><?php esc_html_e( 'WordPress version', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 202 <span class="hydori-diag-value"><?php echo esc_html( get_bloginfo( 'version' ) ); ?></span> 203 </div> 204 <div class="hydori-diag-row"> 205 <span class="hydori-diag-label"><?php esc_html_e( 'PHP version', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 206 <span class="hydori-diag-value"><?php echo esc_html( phpversion() ); ?></span> 207 </div> 208 209 <div class="hydori-diag-row hydori-diag-toggle-row"> 210 <label class="hydori-toggle"> 211 <input type="checkbox" id="hydori-debug-mode-toggle" name="debug_mode" value="yes" <?php checked( $debug_mode, 'yes' ); ?> /> 212 <span><?php esc_html_e( 'Debug logging', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 213 </label> 214 <small> 215 <?php esc_html_e( 'Writes verbose auth + sync logs to wp-content/debug.log. Leave off unless troubleshooting.', 'hydori-seo-ai-powered-seo-automation' ); ?> 216 </small> 217 </div> 218 219 <div class="hydori-diag-actions"> 220 <button type="button" id="hydori-test-connection" class="button"> 221 <?php esc_html_e( 'Test connection', 'hydori-seo-ai-powered-seo-automation' ); ?> 222 </button> 223 <a href="https://hydori.io/help" target="_blank" rel="noopener" class="button button-link"> 224 <?php esc_html_e( 'Help center', 'hydori-seo-ai-powered-seo-automation' ); ?> 225 </a> 226 <a href="https://hydori.io/contact" target="_blank" rel="noopener" class="button button-link"> 227 <?php esc_html_e( 'Contact support', 'hydori-seo-ai-powered-seo-automation' ); ?> 228 </a> 229 </div> 230 <div id="hydori-test-result" class="hydori-test-result" style="display:none;"></div> 231 232 <?php if ( $debug_info ) : ?> 233 <details class="hydori-debug-details"> 234 <summary><?php esc_html_e( 'Raw debug information', 'hydori-seo-ai-powered-seo-automation' ); ?></summary> 235 <ul class="hydori-debug-list"> 236 <li> 237 <strong><?php esc_html_e( 'Legacy API key present:', 'hydori-seo-ai-powered-seo-automation' ); ?></strong> 238 <?php echo $debug_info['key_exists'] ? esc_html__( 'Yes', 'hydori-seo-ai-powered-seo-automation' ) : esc_html__( 'No', 'hydori-seo-ai-powered-seo-automation' ); ?> 239 </li> 240 <li> 241 <strong><?php esc_html_e( 'Legacy API key valid format:', 'hydori-seo-ai-powered-seo-automation' ); ?></strong> 242 <?php echo $debug_info['key_valid'] ? esc_html__( 'Yes', 'hydori-seo-ai-powered-seo-automation' ) : esc_html__( 'No', 'hydori-seo-ai-powered-seo-automation' ); ?> 243 </li> 244 <li> 245 <strong><?php esc_html_e( 'Key prefix:', 'hydori-seo-ai-powered-seo-automation' ); ?></strong> 246 <code><?php echo esc_html( $debug_info['key_prefix'] ); ?></code> 247 </li> 248 <li> 249 <strong><?php esc_html_e( 'Key length:', 'hydori-seo-ai-powered-seo-automation' ); ?></strong> 250 <?php echo esc_html( $debug_info['key_length'] ); ?> 251 </li> 252 </ul> 253 </details> 254 <?php endif; ?> 255 </div> 256 </div> 257 258 <?php /* ============================================================ 259 * Advanced panel. Legacy Hydori API key + the Export-to-Hydori and 260 * "Connect to Hydori" credential fields. Kept for power users and 261 * sites migrating off the v2.4 copy-paste flow. 262 * ============================================================ */ ?> 263 <div class="hydori-card hydori-card-advanced"> 264 <button type="button" class="hydori-disclosure" aria-expanded="false" aria-controls="hydori-advanced-panel"> 265 <span class="dashicons dashicons-arrow-right-alt2"></span> 266 <?php esc_html_e( 'Advanced', 'hydori-seo-ai-powered-seo-automation' ); ?> 267 </button> 268 <div id="hydori-advanced-panel" class="hydori-advanced-panel" hidden> 269 <h3><?php esc_html_e( 'Legacy WordPress API key', 'hydori-seo-ai-powered-seo-automation' ); ?></h3> 53 270 <p class="hydori-description"> 54 <?php esc_html_e( ' Copy this API key and paste it in your Hydori dashboard to connect your WordPress site.', 'hydori-seo-ai-powered-seo-automation' ); ?>271 <?php esc_html_e( 'Hydori v2.5 onboards sites over the built-in WordPress Application Password flow, so this key is optional. Keep it if you were connected on v2.4 or earlier.', 'hydori-seo-ai-powered-seo-automation' ); ?> 55 272 </p> 56 273 57 <!-- API Key Display --> 58 <div class="hydori-api-key-box"> 59 <label for="hydori-api-key"><?php esc_html_e( 'Your WordPress API Key', 'hydori-seo-ai-powered-seo-automation' ); ?></label> 60 <div class="hydori-api-key-wrapper"> 274 <div class="hydori-api-key-wrapper"> 275 <input 276 type="text" 277 id="hydori-api-key" 278 class="hydori-api-key-input" 279 value="<?php echo esc_attr( $api_key ); ?>" 280 readonly 281 onclick="this.select();" 282 /> 283 <button type="button" id="hydori-copy-key" class="button"> 284 <span class="dashicons dashicons-clipboard"></span> 285 <?php esc_html_e( 'Copy', 'hydori-seo-ai-powered-seo-automation' ); ?> 286 </button> 287 <button type="button" id="hydori-regenerate-key" class="button"> 288 <?php esc_html_e( 'Regenerate', 'hydori-seo-ai-powered-seo-automation' ); ?> 289 </button> 290 </div> 291 292 <hr> 293 294 <h3><?php esc_html_e( 'Export posts to Hydori', 'hydori-seo-ai-powered-seo-automation' ); ?></h3> 295 <p class="hydori-description"> 296 <?php esc_html_e( 'Push every published WordPress post into Hydori in one shot. Useful when backfilling a site that already had content before you installed the plugin.', 'hydori-seo-ai-powered-seo-automation' ); ?> 297 </p> 298 299 <form id="hydori-export-credentials-form" class="hydori-form"> 300 <?php wp_nonce_field( 'hydori_sync_nonce', 'export_nonce' ); ?> 301 <label class="hydori-inline-field"> 302 <span><?php esc_html_e( 'Hydori API token', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 303 <input 304 type="password" 305 id="hydori-api-token" 306 name="hydori_api_token" 307 value="" 308 placeholder="<?php echo $hydori_api_token ? esc_attr__( 'Token saved. Enter a new one to replace it.', 'hydori-seo-ai-powered-seo-automation' ) : esc_attr__( 'Paste your Hydori API token', 'hydori-seo-ai-powered-seo-automation' ); ?>" 309 autocomplete="new-password" 310 /> 311 </label> 312 <label class="hydori-inline-field"> 313 <span><?php esc_html_e( 'Hydori site ID', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 61 314 <input 62 315 type="text" 63 id="hydori-api-key" 64 class="hydori-api-key-input" 65 value="<?php echo esc_attr( $api_key ); ?>" 66 readonly 67 onclick="this.select();" 316 id="hydori-site-id" 317 name="hydori_site_id" 318 value="<?php echo esc_attr( $hydori_site_id ); ?>" 68 319 /> 69 <button type="button" id="hydori-copy-key" class="button hydori-copy-btn"> 70 <span class="dashicons dashicons-clipboard"></span> 71 <?php esc_html_e( 'Copy', 'hydori-seo-ai-powered-seo-automation' ); ?> 72 </button> 73 </div> 74 <?php if ( $api_key ) : ?> 75 <p class="hydori-key-meta"> 76 <strong><?php esc_html_e( 'Key format:', 'hydori-seo-ai-powered-seo-automation' ); ?></strong> 77 <?php echo esc_html( substr( $api_key, 0, 4 ) . '_...' ); ?> (<?php echo esc_html( strlen( $api_key ) ); ?> <?php esc_html_e( 'characters', 'hydori-seo-ai-powered-seo-automation' ); ?>) 78 <?php if ( $key_created ) : ?> 79 <br> 80 <?php 81 printf( 82 /* translators: %s: date string */ 83 esc_html__( 'Created: %s', 'hydori-seo-ai-powered-seo-automation' ), 84 esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $key_created ) ) ) 85 ); 86 ?> 87 <?php endif; ?> 88 <?php if ( $key_last_used ) : ?> 89 <br> 90 <?php 91 printf( 92 /* translators: %s: date string */ 93 esc_html__( 'Last used: %s', 'hydori-seo-ai-powered-seo-automation' ), 94 esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $key_last_used ) ) ) 95 ); 96 ?> 97 <?php endif; ?> 98 </p> 99 <?php endif; ?> 100 </div> 101 102 <!-- Action Buttons --> 103 <div class="hydori-actions"> 104 <button type="button" id="hydori-test-connection" class="button button-primary"> 105 <span class="dashicons dashicons-update"></span> 106 <?php esc_html_e( 'Test Connection', 'hydori-seo-ai-powered-seo-automation' ); ?> 107 </button> 108 <button type="button" id="hydori-regenerate-key" class="button"> 109 <span class="dashicons dashicons-image-rotate"></span> 110 <?php esc_html_e( 'Regenerate API Key', 'hydori-seo-ai-powered-seo-automation' ); ?> 111 </button> 112 </div> 113 114 <!-- Test Result Area --> 115 <div id="hydori-test-result" class="hydori-test-result" style="display: none;"></div> 116 117 <hr> 118 119 <!-- Settings Form --> 120 <h2><?php esc_html_e( 'Import Settings', 'hydori-seo-ai-powered-seo-automation' ); ?></h2> 121 <form id="hydori-settings-form"> 122 <?php wp_nonce_field( 'hydori_sync_nonce', 'nonce' ); ?> 123 <table class="form-table" role="presentation"> 124 <tbody> 125 <tr> 126 <th scope="row"> 127 <label for="hydori-sync-mode"><?php esc_html_e( 'Sync Mode', 'hydori-seo-ai-powered-seo-automation' ); ?></label> 128 </th> 129 <td> 130 <select id="hydori-sync-mode" name="sync_mode"> 131 <option value="manual" <?php selected( $sync_mode, 'manual' ); ?>> 132 <?php esc_html_e( 'Manual', 'hydori-seo-ai-powered-seo-automation' ); ?> 133 </option> 134 <option value="auto" <?php selected( $sync_mode, 'auto' ); ?>> 135 <?php esc_html_e( 'Automatic', 'hydori-seo-ai-powered-seo-automation' ); ?> 136 </option> 137 </select> 138 <p class="description"> 139 <?php esc_html_e( 'Manual: Export posts to Hydori on demand. Automatic: Export posts to Hydori automatically', 'hydori-seo-ai-powered-seo-automation' ); ?> 140 </p> 141 </td> 142 </tr> 143 <tr> 144 <th scope="row"> 145 <label for="hydori-post-status"><?php esc_html_e( 'Default Post Status', 'hydori-seo-ai-powered-seo-automation' ); ?></label> 146 </th> 147 <td> 148 <select id="hydori-post-status" name="post_status"> 149 <option value="draft" <?php selected( $post_status, 'draft' ); ?>> 150 <?php esc_html_e( 'Draft (Recommended)', 'hydori-seo-ai-powered-seo-automation' ); ?> 151 </option> 152 <option value="publish" <?php selected( $post_status, 'publish' ); ?>> 153 <?php esc_html_e( 'Publish Immediately', 'hydori-seo-ai-powered-seo-automation' ); ?> 154 </option> 155 </select> 156 <p class="description"> 157 <?php esc_html_e( 'The status for newly synced posts. Draft lets you review before publishing.', 'hydori-seo-ai-powered-seo-automation' ); ?> 158 </p> 159 </td> 160 </tr> 161 <tr> 162 <th scope="row"> 163 <label for="hydori-sync-featured-images"><?php esc_html_e( 'Featured Images', 'hydori-seo-ai-powered-seo-automation' ); ?></label> 164 </th> 165 <td> 166 <label> 167 <input 168 type="checkbox" 169 id="hydori-sync-featured-images" 170 name="sync_featured_images" 171 value="yes" 172 <?php checked( $sync_featured_images, 'yes' ); ?> 173 /> 174 <?php esc_html_e( 'Sync featured images to WordPress media library', 'hydori-seo-ai-powered-seo-automation' ); ?> 175 </label> 176 <p class="description"> 177 <?php esc_html_e( 'When enabled, Hydori article featured images are downloaded and attached as featured images to WordPress posts.', 'hydori-seo-ai-powered-seo-automation' ); ?> 178 </p> 179 </td> 180 </tr> 181 <tr> 182 <th scope="row"> 183 <label for="hydori-debug-mode"><?php esc_html_e( 'Debug Mode', 'hydori-seo-ai-powered-seo-automation' ); ?></label> 184 </th> 185 <td> 186 <label> 187 <input 188 type="checkbox" 189 id="hydori-debug-mode" 190 name="debug_mode" 191 value="yes" 192 <?php checked( $debug_mode, 'yes' ); ?> 193 /> 194 <?php esc_html_e( 'Enable debug logging', 'hydori-seo-ai-powered-seo-automation' ); ?> 195 </label> 196 <p class="description"> 197 <?php esc_html_e( 'Logs detailed information to the WordPress debug log. Enable when troubleshooting connection issues.', 'hydori-seo-ai-powered-seo-automation' ); ?> 198 </p> 199 </td> 200 </tr> 201 </tbody> 202 </table> 203 204 <hr> 205 206 <!-- Hydori Connection (for Export to Hydori) --> 207 <h2><?php esc_html_e( 'Connect to Hydori', 'hydori-seo-ai-powered-seo-automation' ); ?></h2> 208 <p class="hydori-description"> 209 <?php esc_html_e( 'Enter your Hydori credentials to enable exporting posts from WordPress to Hydori.', 'hydori-seo-ai-powered-seo-automation' ); ?> 210 <?php 211 printf( 212 /* translators: %s: link to Hydori developer settings page */ 213 esc_html__( 'You can find these credentials on the %s in your account settings', 'hydori-seo-ai-powered-seo-automation' ), 214 '<a href="https://app.hydori.io/account/developer" target="_blank" rel="noopener">' . esc_html__( 'Developer', 'hydori-seo-ai-powered-seo-automation' ) . '</a>' 215 ); 216 ?> 217 </p> 218 <table class="form-table" role="presentation"> 219 <tbody> 220 <tr> 221 <th scope="row"> 222 <label for="hydori-api-token"><?php esc_html_e( 'Hydori API Token', 'hydori-seo-ai-powered-seo-automation' ); ?></label> 223 </th> 224 <td> 225 <input 226 type="password" 227 id="hydori-api-token" 228 name="hydori_api_token" 229 class="regular-text" 230 value="" 231 placeholder="<?php echo $hydori_api_token ? esc_attr__( 'Token saved — enter a new one to replace it', 'hydori-seo-ai-powered-seo-automation' ) : esc_attr__( 'Enter your Hydori API token', 'hydori-seo-ai-powered-seo-automation' ); ?>" 232 autocomplete="new-password" 233 /> 234 <p class="description"> 235 <?php esc_html_e( 'Your Hydori account API token. Leave blank to keep the current token.', 'hydori-seo-ai-powered-seo-automation' ); ?> 236 </p> 237 </td> 238 </tr> 239 <tr> 240 <th scope="row"> 241 <label for="hydori-site-id"><?php esc_html_e( 'Hydori Site ID', 'hydori-seo-ai-powered-seo-automation' ); ?></label> 242 </th> 243 <td> 244 <input 245 type="text" 246 id="hydori-site-id" 247 name="hydori_site_id" 248 class="regular-text" 249 value="<?php echo esc_attr( $hydori_site_id ); ?>" 250 /> 251 <p class="description"> 252 <?php esc_html_e( 'The ID of your Hydori site to export posts into.', 'hydori-seo-ai-powered-seo-automation' ); ?> 253 </p> 254 </td> 255 </tr> 256 </tbody> 257 </table> 258 259 <p class="submit"> 260 <button type="submit" id="hydori-save-settings" class="button button-primary"> 261 <?php esc_html_e( 'Save Settings', 'hydori-seo-ai-powered-seo-automation' ); ?> 262 </button> 263 </p> 264 </form> 265 266 <!-- Export Posts to Hydori (manual mode only) --> 267 <div id="hydori-export-section" style="<?php echo 'manual' === $sync_mode ? '' : 'display:none;'; ?>"> 268 <hr> 269 <h2><?php esc_html_e( 'Export Posts to Hydori', 'hydori-seo-ai-powered-seo-automation' ); ?></h2> 270 <p class="hydori-description"> 271 <?php esc_html_e( 'Export all published WordPress posts to your Hydori site. Requires Hydori API Token and Site ID to be saved above.', 'hydori-seo-ai-powered-seo-automation' ); ?> 272 </p> 273 <div class="hydori-actions"> 320 </label> 321 <div class="hydori-diag-actions"> 322 <button type="submit" class="button"><?php esc_html_e( 'Save export credentials', 'hydori-seo-ai-powered-seo-automation' ); ?></button> 274 323 <button type="button" id="hydori-export-btn" class="button button-primary"> 275 324 <span class="dashicons dashicons-upload"></span> 276 <?php esc_html_e( 'Export Posts to Hydori', 'hydori-seo-ai-powered-seo-automation' ); ?>325 <?php esc_html_e( 'Export all posts to Hydori', 'hydori-seo-ai-powered-seo-automation' ); ?> 277 326 </button> 278 327 </div> 279 <div id="hydori-export-result" class="hydori-test-result" style="display: none;"></div> 280 </div> 281 282 <?php if ( $debug_info ) : ?> 328 <div id="hydori-export-result" class="hydori-test-result" style="display:none;"></div> 329 </form> 330 283 331 <hr> 284 <!-- Debug Information (only shown when debug mode is enabled) --> 285 <h2><?php esc_html_e( 'Debug Information', 'hydori-seo-ai-powered-seo-automation' ); ?></h2> 286 <div class="hydori-debug-info"> 287 <table class="widefat striped"> 288 <tbody> 289 <tr> 290 <th><?php esc_html_e( 'API Key Exists', 'hydori-seo-ai-powered-seo-automation' ); ?></th> 291 <td> 292 <?php if ( $debug_info['key_exists'] ) : ?> 293 <span style="color:green;"><?php esc_html_e( 'Yes', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 294 <?php else : ?> 295 <span style="color:red;"><?php esc_html_e( 'No', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 296 <?php endif; ?> 297 </td> 298 </tr> 299 <tr> 300 <th><?php esc_html_e( 'API Key Valid Format', 'hydori-seo-ai-powered-seo-automation' ); ?></th> 301 <td> 302 <?php if ( $debug_info['key_valid'] ) : ?> 303 <span style="color:green;"><?php esc_html_e( 'Yes', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 304 <?php else : ?> 305 <span style="color:red;"><?php esc_html_e( 'No', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 306 <?php endif; ?> 307 </td> 308 </tr> 309 <tr> 310 <th><?php esc_html_e( 'Key Prefix', 'hydori-seo-ai-powered-seo-automation' ); ?></th> 311 <td><code><?php echo esc_html( $debug_info['key_prefix'] ); ?></code></td> 312 </tr> 313 <tr> 314 <th><?php esc_html_e( 'Key Length', 'hydori-seo-ai-powered-seo-automation' ); ?></th> 315 <td><?php echo esc_html( $debug_info['key_length'] ); ?> <?php esc_html_e( 'characters', 'hydori-seo-ai-powered-seo-automation' ); ?></td> 316 </tr> 317 <tr> 318 <th><?php esc_html_e( 'Database Direct Check', 'hydori-seo-ai-powered-seo-automation' ); ?></th> 319 <td> 320 <?php if ( $debug_info['db_direct_check'] ) : ?> 321 <span style="color:green;"><?php esc_html_e( 'Found', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 322 <?php else : ?> 323 <span style="color:red;"><?php esc_html_e( 'Not Found', 'hydori-seo-ai-powered-seo-automation' ); ?></span> 324 <?php endif; ?> 325 </td> 326 </tr> 327 <tr> 328 <th><?php esc_html_e( 'Option Autoload', 'hydori-seo-ai-powered-seo-automation' ); ?></th> 329 <td><code><?php echo esc_html( $debug_info['option_autoload'] ); ?></code></td> 330 </tr> 331 <tr> 332 <th><?php esc_html_e( 'Object Cache', 'hydori-seo-ai-powered-seo-automation' ); ?></th> 333 <td> 334 <?php if ( $debug_info['cache_check'] ) : ?> 335 <?php esc_html_e( 'Cached', 'hydori-seo-ai-powered-seo-automation' ); ?> 336 <?php else : ?> 337 <?php esc_html_e( 'Not Cached', 'hydori-seo-ai-powered-seo-automation' ); ?> 338 <?php endif; ?> 339 </td> 340 </tr> 341 <tr> 342 <th><?php esc_html_e( 'REST API URL', 'hydori-seo-ai-powered-seo-automation' ); ?></th> 343 <td><code><?php echo esc_html( rest_url( 'hydori/v1/' ) ); ?></code></td> 344 </tr> 345 <tr> 346 <th><?php esc_html_e( 'PHP Version', 'hydori-seo-ai-powered-seo-automation' ); ?></th> 347 <td><?php echo esc_html( phpversion() ); ?></td> 348 </tr> 349 <tr> 350 <th><?php esc_html_e( 'WordPress Version', 'hydori-seo-ai-powered-seo-automation' ); ?></th> 351 <td><?php echo esc_html( get_bloginfo( 'version' ) ); ?></td> 352 </tr> 353 </tbody> 354 </table> 355 <p class="description" style="margin-top: 10px;"> 356 <?php esc_html_e( 'Debug log location:', 'hydori-seo-ai-powered-seo-automation' ); ?> 357 <code><?php echo esc_html( WP_CONTENT_DIR . '/debug.log' ); ?></code> 358 </p> 359 </div> 360 <?php endif; ?> 361 </div> 362 363 <!-- Sidebar --> 364 <div class="hydori-sidebar"> 365 <!-- How to Use Panel --> 366 <div class="hydori-panel hydori-panel-help"> 367 <h3><?php esc_html_e( 'How to Connect', 'hydori-seo-ai-powered-seo-automation' ); ?></h3> 368 <ol class="hydori-steps"> 369 <li><?php esc_html_e( 'Copy your WordPress API key on the left', 'hydori-seo-ai-powered-seo-automation' ); ?></li> 370 <li><?php esc_html_e( 'Navigate to Settings > Integrations on Hydori', 'hydori-seo-ai-powered-seo-automation' ); ?></li> 371 <li><?php esc_html_e( 'Select the WordPress option and paste your API key', 'hydori-seo-ai-powered-seo-automation' ); ?></li> 372 <li><?php esc_html_e( 'Start syncing content!', 'hydori-seo-ai-powered-seo-automation' ); ?></li> 373 </ol> 374 </div> 375 376 <!-- Troubleshooting Panel --> 377 <div class="hydori-panel hydori-panel-trouble"> 378 <h3><?php esc_html_e( 'Troubleshooting', 'hydori-seo-ai-powered-seo-automation' ); ?></h3> 379 <p class="hydori-description"><?php esc_html_e( 'Getting 401 errors? Try these:', 'hydori-seo-ai-powered-seo-automation' ); ?></p> 380 <ol class="hydori-steps" style="font-size: 13px;"> 381 <li><?php esc_html_e( 'Copy the full API key from above', 'hydori-seo-ai-powered-seo-automation' ); ?></li> 382 <li><?php esc_html_e( 'Update it in your Hydori dashboard', 'hydori-seo-ai-powered-seo-automation' ); ?></li> 383 <li><?php esc_html_e( 'Enable Debug Mode above', 'hydori-seo-ai-powered-seo-automation' ); ?></li> 384 <li><?php esc_html_e( 'Click Test Connection', 'hydori-seo-ai-powered-seo-automation' ); ?></li> 385 <li><?php esc_html_e( 'Check wp-content/debug.log', 'hydori-seo-ai-powered-seo-automation' ); ?></li> 386 </ol> 387 </div> 388 389 <!-- API Endpoint Info --> 390 <div class="hydori-panel hydori-panel-info"> 391 <h3><?php esc_html_e( 'API Endpoint', 'hydori-seo-ai-powered-seo-automation' ); ?></h3> 392 <p class="hydori-description"><?php esc_html_e( 'Your site\'s Hydori API endpoint:', 'hydori-seo-ai-powered-seo-automation' ); ?></p> 393 <code class="hydori-endpoint"><?php echo esc_html( rest_url( 'hydori/v1/' ) ); ?></code> 394 </div> 395 396 <!-- Features Panel --> 397 <div class="hydori-panel hydori-panel-features"> 398 <h3><?php esc_html_e( 'Features', 'hydori-seo-ai-powered-seo-automation' ); ?></h3> 399 <ul class="hydori-features-list"> 400 <li> 401 <span class="dashicons dashicons-yes"></span> 402 <?php esc_html_e( 'AI-powered content generation', 'hydori-seo-ai-powered-seo-automation' ); ?> 403 </li> 404 <li> 405 <span class="dashicons dashicons-yes"></span> 406 <?php esc_html_e( 'Multilingual translations (19+ languages)', 'hydori-seo-ai-powered-seo-automation' ); ?> 407 </li> 408 <li> 409 <span class="dashicons dashicons-yes"></span> 410 <?php esc_html_e( 'SEO metadata sync', 'hydori-seo-ai-powered-seo-automation' ); ?> 411 </li> 412 <li> 413 <span class="dashicons dashicons-yes"></span> 414 <?php esc_html_e( 'Category & tag mapping', 'hydori-seo-ai-powered-seo-automation' ); ?> 415 </li> 416 <li> 417 <span class="dashicons dashicons-yes"></span> 418 <?php esc_html_e( 'Draft mode for review', 'hydori-seo-ai-powered-seo-automation' ); ?> 419 </li> 420 </ul> 421 </div> 422 423 <!-- Compatible Plugins --> 424 <div class="hydori-panel hydori-panel-compat"> 425 <h3><?php esc_html_e( 'Compatible Plugins', 'hydori-seo-ai-powered-seo-automation' ); ?></h3> 426 427 <h4><?php esc_html_e( 'SEO Plugins', 'hydori-seo-ai-powered-seo-automation' ); ?></h4> 428 <ul class="hydori-compat-list"> 429 <li class="<?php echo esc_attr( class_exists( 'WPSEO_Meta' ) ? 'active' : '' ); ?>"> 430 <span class="dashicons <?php echo esc_attr( class_exists( 'WPSEO_Meta' ) ? 'dashicons-yes' : 'dashicons-minus' ); ?>"></span> 431 Yoast SEO 432 </li> 433 <li class="<?php echo esc_attr( class_exists( 'RankMath' ) ? 'active' : '' ); ?>"> 434 <span class="dashicons <?php echo esc_attr( class_exists( 'RankMath' ) ? 'dashicons-yes' : 'dashicons-minus' ); ?>"></span> 435 Rank Math 436 </li> 437 <li class="<?php echo esc_attr( ( class_exists( 'All_in_One_SEO_Pack' ) || defined( 'AIOSEO_VERSION' ) ) ? 'active' : '' ); ?>"> 438 <span class="dashicons <?php echo esc_attr( ( class_exists( 'All_in_One_SEO_Pack' ) || defined( 'AIOSEO_VERSION' ) ) ? 'dashicons-yes' : 'dashicons-minus' ); ?>"></span> 439 All in One SEO 440 </li> 441 <li class="<?php echo esc_attr( ( function_exists( 'seopress_init' ) || defined( 'SEOPRESS_VERSION' ) ) ? 'active' : '' ); ?>"> 442 <span class="dashicons <?php echo esc_attr( ( function_exists( 'seopress_init' ) || defined( 'SEOPRESS_VERSION' ) ) ? 'dashicons-yes' : 'dashicons-minus' ); ?>"></span> 443 SEOPress 444 </li> 445 </ul> 446 447 <h4><?php esc_html_e( 'Multilingual Plugins', 'hydori-seo-ai-powered-seo-automation' ); ?></h4> 448 <ul class="hydori-compat-list"> 449 <li class="<?php echo esc_attr( class_exists( 'SitePress' ) ? 'active' : '' ); ?>"> 450 <span class="dashicons <?php echo esc_attr( class_exists( 'SitePress' ) ? 'dashicons-yes' : 'dashicons-minus' ); ?>"></span> 451 WPML 452 </li> 453 <li class="<?php echo esc_attr( function_exists( 'pll_current_language' ) ? 'active' : '' ); ?>"> 454 <span class="dashicons <?php echo esc_attr( function_exists( 'pll_current_language' ) ? 'dashicons-yes' : 'dashicons-minus' ); ?>"></span> 455 Polylang 456 </li> 457 <li class="<?php echo esc_attr( class_exists( 'TRP_Translate_Press' ) ? 'active' : '' ); ?>"> 458 <span class="dashicons <?php echo esc_attr( class_exists( 'TRP_Translate_Press' ) ? 'dashicons-yes' : 'dashicons-minus' ); ?>"></span> 459 TranslatePress 460 </li> 461 </ul> 462 </div> 463 464 <!-- Support Panel --> 465 <div class="hydori-panel hydori-panel-support"> 466 <h3><?php esc_html_e( 'Need Help?', 'hydori-seo-ai-powered-seo-automation' ); ?></h3> 467 <p> 468 <a href="https://hydori.io/help" target="_blank" rel="noopener"> 469 <span class="dashicons dashicons-book"></span> 470 <?php esc_html_e( 'Documentation', 'hydori-seo-ai-powered-seo-automation' ); ?> 471 </a> 472 </p> 473 <p> 474 <a href="https://hydori.io/contact" target="_blank" rel="noopener"> 475 <span class="dashicons dashicons-sos"></span> 476 <?php esc_html_e( 'Contact Support', 'hydori-seo-ai-powered-seo-automation' ); ?> 477 </a> 478 </p> 332 333 <h3><?php esc_html_e( 'Plugin compatibility', 'hydori-seo-ai-powered-seo-automation' ); ?></h3> 334 <div class="hydori-compat-grid"> 335 <div> 336 <h4><?php esc_html_e( 'SEO', 'hydori-seo-ai-powered-seo-automation' ); ?></h4> 337 <ul class="hydori-compat-list"> 338 <li class="<?php echo esc_attr( class_exists( 'WPSEO_Meta' ) ? 'active' : '' ); ?>"> 339 <span class="dashicons <?php echo esc_attr( class_exists( 'WPSEO_Meta' ) ? 'dashicons-yes' : 'dashicons-minus' ); ?>"></span> 340 Yoast SEO 341 </li> 342 <li class="<?php echo esc_attr( class_exists( 'RankMath' ) ? 'active' : '' ); ?>"> 343 <span class="dashicons <?php echo esc_attr( class_exists( 'RankMath' ) ? 'dashicons-yes' : 'dashicons-minus' ); ?>"></span> 344 Rank Math 345 </li> 346 <li class="<?php echo esc_attr( ( class_exists( 'All_in_One_SEO_Pack' ) || defined( 'AIOSEO_VERSION' ) ) ? 'active' : '' ); ?>"> 347 <span class="dashicons <?php echo esc_attr( ( class_exists( 'All_in_One_SEO_Pack' ) || defined( 'AIOSEO_VERSION' ) ) ? 'dashicons-yes' : 'dashicons-minus' ); ?>"></span> 348 All in One SEO 349 </li> 350 <li class="<?php echo esc_attr( ( function_exists( 'seopress_init' ) || defined( 'SEOPRESS_VERSION' ) ) ? 'active' : '' ); ?>"> 351 <span class="dashicons <?php echo esc_attr( ( function_exists( 'seopress_init' ) || defined( 'SEOPRESS_VERSION' ) ) ? 'dashicons-yes' : 'dashicons-minus' ); ?>"></span> 352 SEOPress 353 </li> 354 </ul> 355 </div> 356 <div> 357 <h4><?php esc_html_e( 'Multilingual', 'hydori-seo-ai-powered-seo-automation' ); ?></h4> 358 <ul class="hydori-compat-list"> 359 <li class="<?php echo esc_attr( class_exists( 'SitePress' ) ? 'active' : '' ); ?>"> 360 <span class="dashicons <?php echo esc_attr( class_exists( 'SitePress' ) ? 'dashicons-yes' : 'dashicons-minus' ); ?>"></span> 361 WPML 362 </li> 363 <li class="<?php echo esc_attr( function_exists( 'pll_current_language' ) ? 'active' : '' ); ?>"> 364 <span class="dashicons <?php echo esc_attr( function_exists( 'pll_current_language' ) ? 'dashicons-yes' : 'dashicons-minus' ); ?>"></span> 365 Polylang 366 </li> 367 <li class="<?php echo esc_attr( class_exists( 'TRP_Translate_Press' ) ? 'active' : '' ); ?>"> 368 <span class="dashicons <?php echo esc_attr( class_exists( 'TRP_Translate_Press' ) ? 'dashicons-yes' : 'dashicons-minus' ); ?>"></span> 369 TranslatePress 370 </li> 371 </ul> 372 </div> 479 373 </div> 480 374 </div> 481 375 </div> 482 376 </div> 377 378 <script> 379 // Minimal inline disclosure handler. Full interactivity lives in 380 // admin/js/hydori-sync-admin.js (Test Connection, Save, etc.). 381 (function(){ 382 document.querySelectorAll('.hydori-disclosure').forEach(function(btn){ 383 btn.addEventListener('click', function(){ 384 var expanded = btn.getAttribute('aria-expanded') === 'true'; 385 btn.setAttribute('aria-expanded', expanded ? 'false' : 'true'); 386 var panelId = btn.getAttribute('aria-controls'); 387 var panel = panelId ? document.getElementById(panelId) : null; 388 if ( panel ) { 389 if ( expanded ) { 390 panel.setAttribute('hidden', 'hidden'); 391 } else { 392 panel.removeAttribute('hidden'); 393 } 394 } 395 btn.classList.toggle('hydori-disclosure-open', !expanded); 396 }); 397 }); 398 399 var enableBtn = document.getElementById('hydori-enable-autosync'); 400 if ( enableBtn ) { 401 enableBtn.addEventListener('click', function(){ 402 var autoRadio = document.querySelector('input[name="sync_mode"][value="auto"]'); 403 if ( autoRadio ) { 404 autoRadio.checked = true; 405 } 406 var saveBtn = document.getElementById('hydori-save-settings'); 407 if ( saveBtn ) { 408 saveBtn.click(); 409 } 410 }); 411 } 412 })(); 413 </script> -
hydori-seo-ai-powered-seo-automation/trunk/hydori-sync.php
r3491852 r3514908 3 3 * Plugin Name: Hydori SEO - AI-Powered SEO Automation 4 4 * Plugin URI: https://hydori.io 5 * Description: Autonomous SEO platform — detect ranking drops, fix with AI, publish to WordPress, and prove impact.6 * Version: 2. 3.15 * Description: Autonomous SEO platform. Detect ranking drops, fix with AI, publish to WordPress, and prove impact. Auto-translate posts with full SEO metadata in one click. 6 * Version: 2.5.0 7 7 * Requires at least: 5.0 8 8 * Requires PHP: 7.4 … … 21 21 * Current plugin version. 22 22 */ 23 define( 'HYDORI_SYNC_VERSION', '2. 3.1' );23 define( 'HYDORI_SYNC_VERSION', '2.5.0' ); 24 24 25 25 /** … … 52 52 define( 'HYDORI_OPTION_HYDORI_SITE_ID', 'hydori_sync_hydori_site_id' ); 53 53 54 // Phase 15 additions (v2.4.0). Tracks whether the user explicitly picked a 55 // sync mode (vs having inherited the activation default). Used by the one-time 56 // 2.3.x -> 2.4 upgrade path to flip inherited "manual" to "auto" without 57 // overwriting an explicit user choice. 58 define( 'HYDORI_OPTION_SYNC_MODE_USER_SET', 'hydori_sync_mode_user_set' ); 59 define( 'HYDORI_OPTION_DB_VERSION', 'hydori_sync_db_version' ); 60 54 61 /** 55 62 * The core plugin class. … … 83 90 } 84 91 85 // Set default post status if not set 92 // Set default post status if not set. v2.4.0: new installs default to 93 // publish so translations go live automatically. Upgrade path handled below. 86 94 if ( false === get_option( HYDORI_OPTION_POST_STATUS ) ) { 87 update_option( HYDORI_OPTION_POST_STATUS, ' draft', 'yes' );95 update_option( HYDORI_OPTION_POST_STATUS, 'publish', 'yes' ); 88 96 } 89 97 … … 93 101 } 94 102 95 // Set sync mode default 103 // Set sync mode default. v2.4.0: auto for new installs. 96 104 if ( false === get_option( HYDORI_OPTION_SYNC_MODE ) ) { 97 update_option( HYDORI_OPTION_SYNC_MODE, ' manual', 'yes' );105 update_option( HYDORI_OPTION_SYNC_MODE, 'auto', 'yes' ); 98 106 } 99 107 … … 103 111 } 104 112 113 // Mark the DB schema version for upgrade-path detection. 114 if ( false === get_option( HYDORI_OPTION_DB_VERSION ) ) { 115 update_option( HYDORI_OPTION_DB_VERSION, HYDORI_SYNC_VERSION, 'yes' ); 116 } 117 105 118 // Flush rewrite rules for REST API 106 119 flush_rewrite_rules(); 107 120 } 108 121 register_activation_hook( __FILE__, 'hydori_sync_activate' ); 122 123 /** 124 * One-time 2.3.x -> 2.4.0 upgrade migration. 125 * 126 * Existing v2.3.x sites will have HYDORI_OPTION_SYNC_MODE set to 'manual' (the 127 * old activation default). We only flip those where the user never toggled the 128 * setting themselves, tracked via HYDORI_OPTION_SYNC_MODE_USER_SET. This keeps 129 * intentional manual-mode users on manual while moving everyone else to the 130 * new automatic behaviour. 131 * 132 * @since 2.4.0 133 */ 134 function hydori_sync_maybe_upgrade() { 135 $stored_version = get_option( HYDORI_OPTION_DB_VERSION, '0' ); 136 if ( version_compare( $stored_version, '2.4.0', '>=' ) ) { 137 return; 138 } 139 140 // Flip inherited manual -> auto for sites that never explicitly chose manual. 141 $user_set = get_option( HYDORI_OPTION_SYNC_MODE_USER_SET, 'no' ); 142 $sync_mode = get_option( HYDORI_OPTION_SYNC_MODE, 'manual' ); 143 if ( 'yes' !== $user_set && 'manual' === $sync_mode ) { 144 update_option( HYDORI_OPTION_SYNC_MODE, 'auto', 'yes' ); 145 error_log( 'Hydori Sync: v2.4 upgrade flipped inherited sync_mode=manual -> auto' ); 146 } 147 148 // Same idea for post_status: inherited draft becomes publish. 149 $post_status = get_option( HYDORI_OPTION_POST_STATUS, 'draft' ); 150 if ( 'draft' === $post_status ) { 151 update_option( HYDORI_OPTION_POST_STATUS, 'publish', 'yes' ); 152 error_log( 'Hydori Sync: v2.4 upgrade flipped inherited post_status=draft -> publish' ); 153 } 154 155 update_option( HYDORI_OPTION_DB_VERSION, HYDORI_SYNC_VERSION, 'yes' ); 156 } 157 add_action( 'plugins_loaded', 'hydori_sync_maybe_upgrade' ); 109 158 110 159 /** -
hydori-seo-ai-powered-seo-automation/trunk/includes/class-hydori-post-handler.php
r3488505 r3514908 99 99 } 100 100 101 // Handle featured image 101 // Handle featured image + alt text. Alt text comes from $data['meta']['featured_image_alt']. 102 102 if ( ! empty( $data['featured_image_url'] ) ) { 103 $this->set_featured_image( $post_id, $data['featured_image_url'] ); 103 $alt_text = ! empty( $data['meta']['featured_image_alt'] ) 104 ? sanitize_text_field( $data['meta']['featured_image_alt'] ) 105 : ''; 106 $this->set_featured_image( $post_id, $data['featured_image_url'], $alt_text ); 104 107 } 105 108 … … 195 198 } 196 199 197 // Handle featured image 200 // Handle featured image + alt text. Alt text comes from $data['meta']['featured_image_alt']. 198 201 if ( ! empty( $data['featured_image_url'] ) ) { 199 $this->set_featured_image( $post_id, $data['featured_image_url'] ); 202 $alt_text = ! empty( $data['meta']['featured_image_alt'] ) 203 ? sanitize_text_field( $data['meta']['featured_image_alt'] ) 204 : ''; 205 $this->set_featured_image( $post_id, $data['featured_image_url'], $alt_text ); 200 206 } 201 207 … … 307 313 * Set SEO metadata for various SEO plugins. 308 314 * 315 * v2.4.0: writes OG/Twitter/canonical/robots postmeta for each detected 316 * plugin in addition to the core meta title / description / focus keyword. 317 * 309 318 * @since 2.0.0 310 319 * @param int $post_id The post ID. … … 312 321 */ 313 322 private function set_seo_meta( $post_id, $meta ) { 314 $meta_title = ! empty( $meta['meta_title'] ) ? sanitize_text_field( $meta['meta_title'] ) : ''; 315 $meta_description = ! empty( $meta['meta_description'] ) ? sanitize_textarea_field( $meta['meta_description'] ) : ''; 316 $keywords = ! empty( $meta['keywords'] ) && is_array( $meta['keywords'] ) ? $meta['keywords'] : array(); 317 $focus_keyword = ! empty( $keywords ) ? sanitize_text_field( $keywords[0] ) : ''; 323 $meta_title = ! empty( $meta['meta_title'] ) ? sanitize_text_field( $meta['meta_title'] ) : ''; 324 $meta_description = ! empty( $meta['meta_description'] ) ? sanitize_textarea_field( $meta['meta_description'] ) : ''; 325 $keywords = ! empty( $meta['keywords'] ) && is_array( $meta['keywords'] ) ? $meta['keywords'] : array(); 326 $focus_keyword = ! empty( $meta['focus_keyword'] ) 327 ? sanitize_text_field( $meta['focus_keyword'] ) 328 : ( ! empty( $keywords ) ? sanitize_text_field( $keywords[0] ) : '' ); 329 $og_title = ! empty( $meta['og_title'] ) ? sanitize_text_field( $meta['og_title'] ) : $meta_title; 330 $og_description = ! empty( $meta['og_description'] ) ? sanitize_textarea_field( $meta['og_description'] ) : $meta_description; 331 $twitter_title = ! empty( $meta['twitter_title'] ) ? sanitize_text_field( $meta['twitter_title'] ) : $meta_title; 332 $twitter_description = ! empty( $meta['twitter_description'] ) ? sanitize_textarea_field( $meta['twitter_description'] ) : $meta_description; 333 $canonical_url = ! empty( $meta['canonical_url'] ) ? esc_url_raw( $meta['canonical_url'] ) : ''; 334 $noindex = ! empty( $meta['robots_noindex'] ); 335 $nofollow = ! empty( $meta['robots_nofollow'] ); 336 $schema_type = ! empty( $meta['schema_type'] ) ? sanitize_text_field( $meta['schema_type'] ) : 'BlogPosting'; 318 337 319 338 // Yoast SEO … … 328 347 update_post_meta( $post_id, '_yoast_wpseo_focuskw', $focus_keyword ); 329 348 } 330 $this->log( 'Set Yoast SEO metadata for post ' . $post_id ); 349 if ( $og_title ) { 350 update_post_meta( $post_id, '_yoast_wpseo_opengraph-title', $og_title ); 351 } 352 if ( $og_description ) { 353 update_post_meta( $post_id, '_yoast_wpseo_opengraph-description', $og_description ); 354 } 355 if ( $twitter_title ) { 356 update_post_meta( $post_id, '_yoast_wpseo_twitter-title', $twitter_title ); 357 } 358 if ( $twitter_description ) { 359 update_post_meta( $post_id, '_yoast_wpseo_twitter-description', $twitter_description ); 360 } 361 if ( $canonical_url ) { 362 update_post_meta( $post_id, '_yoast_wpseo_canonical', $canonical_url ); 363 } 364 if ( $noindex ) { 365 update_post_meta( $post_id, '_yoast_wpseo_meta-robots-noindex', '1' ); 366 } 367 if ( $nofollow ) { 368 update_post_meta( $post_id, '_yoast_wpseo_meta-robots-nofollow', '1' ); 369 } 370 $this->log( 'Set Yoast SEO metadata (incl. OG/Twitter/canonical) for post ' . $post_id ); 331 371 } 332 372 … … 342 382 update_post_meta( $post_id, 'rank_math_focus_keyword', $focus_keyword ); 343 383 } 344 $this->log( 'Set Rank Math metadata for post ' . $post_id ); 384 if ( $og_title ) { 385 update_post_meta( $post_id, 'rank_math_facebook_title', $og_title ); 386 } 387 if ( $og_description ) { 388 update_post_meta( $post_id, 'rank_math_facebook_description', $og_description ); 389 } 390 if ( $twitter_title ) { 391 update_post_meta( $post_id, 'rank_math_twitter_title', $twitter_title ); 392 } 393 if ( $twitter_description ) { 394 update_post_meta( $post_id, 'rank_math_twitter_description', $twitter_description ); 395 } 396 if ( $canonical_url ) { 397 update_post_meta( $post_id, 'rank_math_canonical_url', $canonical_url ); 398 } 399 if ( $noindex ) { 400 update_post_meta( $post_id, 'rank_math_robots', array( 'noindex' ) ); 401 } 402 $this->log( 'Set Rank Math metadata (incl. OG/Twitter/canonical) for post ' . $post_id ); 345 403 } 346 404 … … 349 407 if ( $meta_title ) { 350 408 update_post_meta( $post_id, '_aioseop_title', $meta_title ); 351 // New AIOSEO format352 409 update_post_meta( $post_id, '_aioseo_title', $meta_title ); 353 410 } 354 411 if ( $meta_description ) { 355 412 update_post_meta( $post_id, '_aioseop_description', $meta_description ); 356 // New AIOSEO format357 413 update_post_meta( $post_id, '_aioseo_description', $meta_description ); 358 414 } 359 415 if ( $focus_keyword ) { 360 416 update_post_meta( $post_id, '_aioseop_keywords', $focus_keyword ); 361 } 362 $this->log( 'Set All in One SEO metadata for post ' . $post_id ); 417 update_post_meta( $post_id, '_aioseo_keywords', $focus_keyword ); 418 } 419 if ( $og_title ) { 420 update_post_meta( $post_id, '_aioseo_og_title', $og_title ); 421 } 422 if ( $og_description ) { 423 update_post_meta( $post_id, '_aioseo_og_description', $og_description ); 424 } 425 if ( $twitter_title ) { 426 update_post_meta( $post_id, '_aioseo_twitter_title', $twitter_title ); 427 } 428 if ( $twitter_description ) { 429 update_post_meta( $post_id, '_aioseo_twitter_description', $twitter_description ); 430 } 431 if ( $canonical_url ) { 432 update_post_meta( $post_id, '_aioseo_canonical_url', $canonical_url ); 433 } 434 $this->log( 'Set All in One SEO metadata (incl. OG/Twitter/canonical) for post ' . $post_id ); 363 435 } 364 436 … … 374 446 update_post_meta( $post_id, '_seopress_analysis_target_kw', $focus_keyword ); 375 447 } 376 $this->log( 'Set SEOPress metadata for post ' . $post_id ); 377 } 378 379 // Store generic meta for other plugins or custom use 448 if ( $og_title ) { 449 update_post_meta( $post_id, '_seopress_social_fb_title', $og_title ); 450 } 451 if ( $og_description ) { 452 update_post_meta( $post_id, '_seopress_social_fb_desc', $og_description ); 453 } 454 if ( $twitter_title ) { 455 update_post_meta( $post_id, '_seopress_social_twitter_title', $twitter_title ); 456 } 457 if ( $twitter_description ) { 458 update_post_meta( $post_id, '_seopress_social_twitter_desc', $twitter_description ); 459 } 460 if ( $canonical_url ) { 461 update_post_meta( $post_id, '_seopress_robots_canonical', $canonical_url ); 462 } 463 $this->log( 'Set SEOPress metadata (incl. OG/Twitter/canonical) for post ' . $post_id ); 464 } 465 466 // Store generic meta for the Hydori fallback (reader plugin, hreflang 467 // injector, custom themes) and for partners that read hydori_* postmeta. 380 468 if ( $meta_title ) { 381 469 update_post_meta( $post_id, 'hydori_meta_title', $meta_title ); … … 386 474 if ( ! empty( $keywords ) ) { 387 475 update_post_meta( $post_id, 'hydori_keywords', implode( ', ', array_map( 'sanitize_text_field', $keywords ) ) ); 476 } 477 if ( $og_title ) { 478 update_post_meta( $post_id, 'hydori_og_title', $og_title ); 479 } 480 if ( $og_description ) { 481 update_post_meta( $post_id, 'hydori_og_description', $og_description ); 482 } 483 if ( $twitter_title ) { 484 update_post_meta( $post_id, 'hydori_twitter_title', $twitter_title ); 485 } 486 if ( $twitter_description ) { 487 update_post_meta( $post_id, 'hydori_twitter_description', $twitter_description ); 488 } 489 if ( $canonical_url ) { 490 update_post_meta( $post_id, 'hydori_canonical_url', $canonical_url ); 491 } 492 if ( $schema_type ) { 493 update_post_meta( $post_id, 'hydori_schema_type', $schema_type ); 388 494 } 389 495 } … … 493 599 * @param string $image_url The URL of the image to sideload. 494 600 */ 495 private function set_featured_image( $post_id, $image_url ) {601 private function set_featured_image( $post_id, $image_url, $alt_text = '' ) { 496 602 if ( empty( $image_url ) ) { 497 603 return; … … 499 605 500 606 // Skip re-downloading if the image URL has not changed AND the post already has a thumbnail. 607 // We still update alt text even when skipping download (cheap, translation-sensitive). 501 608 $stored_url = get_post_meta( $post_id, '_hydori_featured_image_url', true ); 502 609 if ( $stored_url && $stored_url === $image_url && has_post_thumbnail( $post_id ) ) { 503 610 $this->log( 'Featured image unchanged and exists, skipping download for post ' . $post_id ); 611 if ( ! empty( $alt_text ) ) { 612 $thumb_id = get_post_thumbnail_id( $post_id ); 613 if ( $thumb_id ) { 614 update_post_meta( $thumb_id, '_wp_attachment_image_alt', $alt_text ); 615 } 616 } 504 617 return; 505 618 } … … 520 633 set_post_thumbnail( $post_id, $attachment_id ); 521 634 635 // v2.4.0: set the translated alt text on the attachment so images carry 636 // the correct alt per language. This is read by SEO plugins + frontends. 637 if ( ! empty( $alt_text ) ) { 638 update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt_text ); 639 } 640 522 641 // Persist the URL so future syncs can skip re-downloading. 523 642 update_post_meta( $post_id, '_hydori_featured_image_url', $image_url ); -
hydori-seo-ai-powered-seo-automation/trunk/includes/class-hydori-rest-api.php
r3488505 r3514908 82 82 'methods' => WP_REST_Server::READABLE, 83 83 'callback' => array( $this, 'get_status' ), 84 'permission_callback' => array( $this, 'check_ api_key' ),84 'permission_callback' => array( $this, 'check_permission' ), 85 85 ) 86 86 ); … … 93 93 'methods' => WP_REST_Server::CREATABLE, 94 94 'callback' => array( $this, 'create_post' ), 95 'permission_callback' => array( $this, 'check_ api_key' ),95 'permission_callback' => array( $this, 'check_permission' ), 96 96 'args' => $this->get_sync_args(), 97 97 ) … … 105 105 'methods' => WP_REST_Server::EDITABLE, 106 106 'callback' => array( $this, 'update_post' ), 107 'permission_callback' => array( $this, 'check_ api_key' ),107 'permission_callback' => array( $this, 'check_permission' ), 108 108 'args' => array_merge( 109 109 array( … … 129 129 'methods' => WP_REST_Server::DELETABLE, 130 130 'callback' => array( $this, 'delete_post' ), 131 'permission_callback' => array( $this, 'check_ api_key' ),131 'permission_callback' => array( $this, 'check_permission' ), 132 132 'args' => array( 133 133 'id' => array( … … 155 155 'methods' => WP_REST_Server::READABLE, 156 156 'callback' => array( $this, 'lookup_post' ), 157 'permission_callback' => array( $this, 'check_ api_key' ),157 'permission_callback' => array( $this, 'check_permission' ), 158 158 'args' => array( 159 159 'hydori_id' => array( … … 173 173 'methods' => WP_REST_Server::READABLE, 174 174 'callback' => array( $this, 'get_debug_info' ), 175 'permission_callback' => array( $this, 'check_ api_key' ),175 'permission_callback' => array( $this, 'check_permission' ), 176 176 ) 177 177 ); 178 178 179 // Settings endpoint - POST /wp-json/hydori/v1/settings (since 2.4.0). 180 // Lets Hydori flip sync_mode / post_status remotely for users who 181 // enable auto-sync from the Hydori dashboard instead of WP admin. 182 register_rest_route( 183 $this->namespace, 184 '/settings', 185 array( 186 'methods' => WP_REST_Server::CREATABLE, 187 'callback' => array( $this, 'update_settings' ), 188 'permission_callback' => array( $this, 'check_permission' ), 189 'args' => array( 190 'sync_mode' => array( 191 'type' => 'string', 192 'enum' => array( 'auto', 'manual' ), 193 'sanitize_callback' => 'sanitize_text_field', 194 ), 195 'post_status' => array( 196 'type' => 'string', 197 'enum' => array( 'draft', 'publish' ), 198 'sanitize_callback' => 'sanitize_text_field', 199 ), 200 ), 201 ) 202 ); 203 179 204 $this->log( 'REST API routes registered.' ); 205 } 206 207 /** 208 * Update plugin settings remotely. 209 * 210 * @since 2.4.0 211 * @param WP_REST_Request $request Full details about the request. 212 * @return WP_REST_Response Response object. 213 */ 214 public function update_settings( $request ) { 215 $sync_mode = $request->get_param( 'sync_mode' ); 216 $post_status = $request->get_param( 'post_status' ); 217 218 $updated = array(); 219 if ( ! empty( $sync_mode ) ) { 220 update_option( HYDORI_OPTION_SYNC_MODE, $sync_mode, 'yes' ); 221 // Remote flips count as "user set" so the upgrade path won't override them. 222 update_option( HYDORI_OPTION_SYNC_MODE_USER_SET, 'yes', 'yes' ); 223 $updated['sync_mode'] = $sync_mode; 224 } 225 if ( ! empty( $post_status ) ) { 226 update_option( HYDORI_OPTION_POST_STATUS, $post_status, 'yes' ); 227 $updated['post_status'] = $post_status; 228 } 229 230 $this->log( 'Remote settings update: ' . wp_json_encode( $updated ) ); 231 232 return rest_ensure_response( array( 233 'success' => true, 234 'updated' => $updated, 235 'settings' => array( 236 'sync_mode' => get_option( HYDORI_OPTION_SYNC_MODE, 'auto' ), 237 'post_status' => get_option( HYDORI_OPTION_POST_STATUS, 'publish' ), 238 ), 239 ) ); 180 240 } 181 241 … … 350 410 'default' => array(), 351 411 'properties' => array( 352 'meta_title' => array( 353 'type' => 'string', 412 'meta_title' => array( 'type' => 'string' ), 413 'meta_description' => array( 'type' => 'string' ), 414 'keywords' => array( 415 'type' => 'array', 416 'items' => array( 'type' => 'string' ), 354 417 ), 355 'meta_description' => array( 356 'type' => 'string', 357 ), 358 'keywords' => array( 359 'type' => 'array', 360 'items' => array( 361 'type' => 'string', 362 ), 363 ), 418 // v2.4.0 additions — expanded SEO fields written to Yoast / 419 // Rank Math / AIOSEO / SEOPress OG/Twitter/canonical meta. 420 'focus_keyword' => array( 'type' => 'string' ), 421 'og_title' => array( 'type' => 'string' ), 422 'og_description' => array( 'type' => 'string' ), 423 'twitter_title' => array( 'type' => 'string' ), 424 'twitter_description' => array( 'type' => 'string' ), 425 'canonical_url' => array( 'type' => 'string' ), 426 'robots_noindex' => array( 'type' => 'boolean' ), 427 'robots_nofollow' => array( 'type' => 'boolean' ), 428 'featured_image_alt' => array( 'type' => 'string' ), 429 'schema_type' => array( 'type' => 'string' ), 364 430 ), 365 431 ), … … 504 570 505 571 /** 506 * Check API key permission callback. 572 * Dual-path permission callback. Accepts native WordPress auth 573 * (cookie, Application Password, Basic Auth) when the current user 574 * has manage_options, and falls back to the legacy Hydori 575 * `hyd_*` API key otherwise. 576 * 577 * WordPress core runs its authentication layer before this callback 578 * fires, so by the time we get here `wp_get_current_user()` already 579 * reflects any valid Application Password the request carried. Using 580 * `current_user_can('manage_options')` is how Jetpack and other 581 * modern WP SaaS plugins authorize their REST endpoints against the 582 * Application Password flow. 583 * 584 * @since 2.5.0 585 * @param WP_REST_Request $request Full details about the request. 586 * @return bool|WP_Error True if authorized, WP_Error otherwise. 587 */ 588 public function check_permission( $request ) { 589 // Rate limit applies to every auth path. 590 $rate_check = $this->check_rate_limit(); 591 if ( is_wp_error( $rate_check ) ) { 592 return $rate_check; 593 } 594 595 // Native WordPress auth (cookie, Application Password, Basic Auth). 596 // When WP core accepts the credentials it populates the current 597 // user, so `current_user_can` reports the right capabilities. 598 if ( is_user_logged_in() && current_user_can( 'manage_options' ) ) { 599 $this->log( 'Auth accepted via native WordPress auth (user: ' . wp_get_current_user()->user_login . ').' ); 600 return true; 601 } 602 603 // Legacy Hydori API key path. Kept for backward compatibility 604 // with v2.4 and earlier installs that onboarded via the copy- 605 // paste `hyd_*` key flow before Phase 16. 606 // Mark the request so check_api_key() skips its own rate check. 607 $request->set_header( 'x-hydori-rate-checked', '1' ); 608 return $this->check_api_key( $request ); 609 } 610 611 /** 612 * Check API key permission callback (legacy path). 507 613 * 508 614 * @since 2.0.0 … … 511 617 */ 512 618 public function check_api_key( $request ) { 513 // Check rate limit before authentication 514 $rate_check = $this->check_rate_limit(); 515 if ( is_wp_error( $rate_check ) ) { 516 return $rate_check; 619 // Rate limit when invoked directly. When called via 620 // check_permission() the limiter already ran, but we stash a 621 // flag on the request object (not static state) so a fresh 622 // request does not inherit the skip. 623 if ( ! $request->get_header( 'x-hydori-rate-checked' ) ) { 624 $rate_check = $this->check_rate_limit(); 625 if ( is_wp_error( $rate_check ) ) { 626 return $rate_check; 627 } 517 628 } 518 629 -
hydori-seo-ai-powered-seo-automation/trunk/includes/class-hydori-sync.php
r3488505 r3514908 63 63 $this->define_api_hooks(); 64 64 $this->define_export_hooks(); 65 $this->define_seo_head_hooks(); 65 66 } 66 67 … … 101 102 */ 102 103 require_once HYDORI_SYNC_PLUGIN_DIR . 'includes/class-hydori-export-manager.php'; 104 105 /** 106 * The class responsible for frontend SEO <head> output (hreflang + canonical 107 * fallback when no multilingual plugin is active). @since 2.4.0 108 */ 109 require_once HYDORI_SYNC_PLUGIN_DIR . 'includes/class-hydori-seo-head.php'; 103 110 104 111 $this->loader = new Hydori_Sync_Loader(); … … 159 166 160 167 /** 168 * Register frontend hreflang + canonical output hooks. 169 * 170 * @since 2.4.0 171 * @access private 172 */ 173 private function define_seo_head_hooks() { 174 Hydori_SEO_Head::init(); 175 } 176 177 /** 161 178 * Run the loader to execute all of the hooks with WordPress. 162 179 * -
hydori-seo-ai-powered-seo-automation/trunk/readme.txt
r3491852 r3514908 4 4 Requires at least: 5.0 5 5 Tested up to: 6.9 6 Stable tag: 2. 3.16 Stable tag: 2.5.0 7 7 Requires PHP: 7.4 8 8 License: GPLv2 or later … … 241 241 242 242 == Changelog == 243 244 = 2.5.0 = 245 * New: Dual-path REST API authentication — endpoints now accept WordPress Application Passwords, Basic Auth, and cookie-based auth (for users with manage_options) alongside the legacy Hydori API key, which remains fully supported for backward compatibility 246 * New: Automatic post export to Hydori — when auto-sync is enabled, published posts are pushed to the Hydori platform automatically, enabling bi-directional WordPress ↔ Hydori sync 247 * New: Hydori credentials storage - plugin now stores a Hydori API token and site ID to support the post export feature 248 * New: Per-IP rate limiting (60 requests/minute) applied uniformly across all REST endpoints and authentication paths 249 250 = 2.4.0 = 251 * New: Automatic sync mode is now the default for both new installs and upgraded sites (sites that never explicitly chose manual). Publish once in WordPress, Hydori translates and republishes as live posts. Existing users who explicitly picked "manual" keep their choice. 252 * New: Translated posts now carry the full SEO metadata set. Open Graph title/description, Twitter Card title/description, canonical URL, focus keyword, featured-image alt text, and per-post robots flags are written to Yoast, Rank Math, All in One SEO, and SEOPress in every supported language. 253 * New: hreflang fallback. When no multilingual plugin (WPML, Polylang, TranslatePress) is active, Hydori emits rel="alternate" hreflang tags in <head> linking every translated version of a post, plus an x-default fallback. 254 * New: Self-referencing canonical URL on translated posts when no SEO plugin is active. 255 * New: REST endpoint POST /wp-json/hydori/v1/settings so the Hydori dashboard can flip sync mode and default post status remotely. 256 * New: Featured image alt text is now translated per language on the attachment (via _wp_attachment_image_alt). 257 * Note: Upgrading from 2.3.x flips inherited "manual + draft" defaults to "auto + publish" only on sites that never toggled the setting themselves. Explicit choices are preserved. 243 258 244 259 = 2.3.1 =
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)