Plugin Directory

Changeset 3514908


Ignore:
Timestamp:
04/24/2026 07:44:25 PM (6 months ago)
Author:
ertiqah
Message:

feat: add dual REST auth, auto post export, credentials storage, rate limiting, and default auto-sync with full SEO + hreflang support

Location:
hydori-seo-ai-powered-seo-automation
Files:
29 added
9 edited

Legend:

Unmodified
Added
Removed
  • hydori-seo-ai-powered-seo-automation/trunk/admin/class-hydori-sync-admin.php

    r3488505 r3514908  
    167167        $key_last_used        = Hydori_API_Key_Manager::get_key_last_used_date();
    168168
     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
    169181        // Include the partial
    170182        include HYDORI_SYNC_PLUGIN_DIR . 'admin/partials/hydori-sync-admin-display.php';
    … …  
    294306        update_option( HYDORI_OPTION_DEBUG_MODE, $debug_mode );
    295307
    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';
    298312        if ( ! in_array( $sync_mode, array( 'manual', 'auto' ), true ) ) {
    299             $sync_mode = 'manual';
     313            $sync_mode = 'auto';
    300314        }
    301315        update_option( HYDORI_OPTION_SYNC_MODE, $sync_mode );
     316        update_option( HYDORI_OPTION_SYNC_MODE_USER_SET, 'yes', 'yes' );
    302317
    303318        // Sanitize and save featured images toggle
  • hydori-seo-ai-powered-seo-automation/trunk/admin/css/hydori-sync-admin.css

    r3488505 r3514908  
    542542    }
    543543}
     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  
    4343            $('#hydori-export-btn').on('click', this.exportToHydori.bind(this));
    4444
    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 ) {
    4849                    $('#hydori-export-section').show();
    4950                } else {
    5051                    $('#hydori-export-section').hide();
    5152                }
     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() );
    5259            });
    5360
    … …  
    242249            $btn.html('<span class="hydori-spinner"></span> ' + hydoriSync.strings.savingSettings);
    243250
    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
    245269            var formData = {
    246270                action: 'hydori_save_settings',
    247271                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',
    252276                hydori_api_token: $('#hydori-api-token').val(),
    253277                hydori_site_id: $('#hydori-site-id').val()
  • hydori-seo-ai-powered-seo-automation/trunk/admin/partials/hydori-sync-admin-display.php

    r3491852 r3514908  
    33 * Admin settings page display template.
    44 *
     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 *
    511 * @package    Hydori_Sync
    612 * @subpackage Hydori_Sync/admin/partials
    7  * @since      2.0.0
     13 * @since      2.5.0
    814 */
    915
    … …  
    1824    $debug_info = Hydori_API_Key_Manager::get_debug_info();
    1925}
     26
     27// Connection state fallback (provided by display_settings_page()).
     28if ( ! isset( $connection_state ) ) {
     29    $connection_state = 'disconnected';
     30}
     31if ( ! isset( $connected_to_hydori ) ) {
     32    $connected_to_hydori = false;
     33}
    2034?>
    2135
    22 <div class="wrap hydori-sync-admin">
    23     <h1>
     36<div class="wrap hydori-sync-admin hydori-v25">
     37    <h1 class="hydori-page-title">
    2438        <span class="hydori-logo">
    2539            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
    … …  
    2842            </svg>
    2943        </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' ); ?>
    3145        <span class="hydori-version"><?php echo esc_html( 'v' . $this->version ); ?></span>
    3246    </h1>
    3347
    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">
    3854                <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 ' &middot; ';
     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
    41102        <?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>
    45118            </div>
    46119        <?php endif; ?>
    47120    </div>
    48121
    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>
    53270            <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' ); ?>
    55272            </p>
    56273
    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>
    61314                    <input
    62315                        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 ); ?>"
    68319                    />
    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>
    274323                    <button type="button" id="hydori-export-btn" class="button button-primary">
    275324                        <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' ); ?>
    277326                    </button>
    278327                </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
    283331            <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>
    479373            </div>
    480374        </div>
    481375    </div>
    482376</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  
    33 * Plugin Name:       Hydori SEO - AI-Powered SEO Automation
    44 * 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.1
     5 * 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
    77 * Requires at least: 5.0
    88 * Requires PHP:      7.4
    … …  
    2121 * Current plugin version.
    2222 */
    23 define( 'HYDORI_SYNC_VERSION', '2.3.1' );
     23define( 'HYDORI_SYNC_VERSION', '2.5.0' );
    2424
    2525/**
    … …  
    5252define( 'HYDORI_OPTION_HYDORI_SITE_ID', 'hydori_sync_hydori_site_id' );
    5353
     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.
     58define( 'HYDORI_OPTION_SYNC_MODE_USER_SET', 'hydori_sync_mode_user_set' );
     59define( 'HYDORI_OPTION_DB_VERSION', 'hydori_sync_db_version' );
     60
    5461/**
    5562 * The core plugin class.
    … …  
    8390    }
    8491
    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.
    8694    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' );
    8896    }
    8997
    … …  
    93101    }
    94102
    95     // Set sync mode default
     103    // Set sync mode default. v2.4.0: auto for new installs.
    96104    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' );
    98106    }
    99107
    … …  
    103111    }
    104112
     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
    105118    // Flush rewrite rules for REST API
    106119    flush_rewrite_rules();
    107120}
    108121register_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 */
     134function 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}
     157add_action( 'plugins_loaded', 'hydori_sync_maybe_upgrade' );
    109158
    110159/**
  • hydori-seo-ai-powered-seo-automation/trunk/includes/class-hydori-post-handler.php

    r3488505 r3514908  
    9999        }
    100100
    101         // Handle featured image
     101        // Handle featured image + alt text. Alt text comes from $data['meta']['featured_image_alt'].
    102102        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 );
    104107        }
    105108
    … …  
    195198        }
    196199
    197         // Handle featured image
     200        // Handle featured image + alt text. Alt text comes from $data['meta']['featured_image_alt'].
    198201        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 );
    200206        }
    201207
    … …  
    307313     * Set SEO metadata for various SEO plugins.
    308314     *
     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     *
    309318     * @since 2.0.0
    310319     * @param int   $post_id The post ID.
    … …  
    312321     */
    313322    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';
    318337
    319338        // Yoast SEO
    … …  
    328347                update_post_meta( $post_id, '_yoast_wpseo_focuskw', $focus_keyword );
    329348            }
    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 );
    331371        }
    332372
    … …  
    342382                update_post_meta( $post_id, 'rank_math_focus_keyword', $focus_keyword );
    343383            }
    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 );
    345403        }
    346404
    … …  
    349407            if ( $meta_title ) {
    350408                update_post_meta( $post_id, '_aioseop_title', $meta_title );
    351                 // New AIOSEO format
    352409                update_post_meta( $post_id, '_aioseo_title', $meta_title );
    353410            }
    354411            if ( $meta_description ) {
    355412                update_post_meta( $post_id, '_aioseop_description', $meta_description );
    356                 // New AIOSEO format
    357413                update_post_meta( $post_id, '_aioseo_description', $meta_description );
    358414            }
    359415            if ( $focus_keyword ) {
    360416                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 );
    363435        }
    364436
    … …  
    374446                update_post_meta( $post_id, '_seopress_analysis_target_kw', $focus_keyword );
    375447            }
    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.
    380468        if ( $meta_title ) {
    381469            update_post_meta( $post_id, 'hydori_meta_title', $meta_title );
    … …  
    386474        if ( ! empty( $keywords ) ) {
    387475            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 );
    388494        }
    389495    }
    … …  
    493599     * @param string $image_url The URL of the image to sideload.
    494600     */
    495     private function set_featured_image( $post_id, $image_url ) {
     601    private function set_featured_image( $post_id, $image_url, $alt_text = '' ) {
    496602        if ( empty( $image_url ) ) {
    497603            return;
    … …  
    499605
    500606        // 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).
    501608        $stored_url = get_post_meta( $post_id, '_hydori_featured_image_url', true );
    502609        if ( $stored_url && $stored_url === $image_url && has_post_thumbnail( $post_id ) ) {
    503610            $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            }
    504617            return;
    505618        }
    … …  
    520633        set_post_thumbnail( $post_id, $attachment_id );
    521634
     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
    522641        // Persist the URL so future syncs can skip re-downloading.
    523642        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  
    8282                'methods'             => WP_REST_Server::READABLE,
    8383                'callback'            => array( $this, 'get_status' ),
    84                 'permission_callback' => array( $this, 'check_api_key' ),
     84                'permission_callback' => array( $this, 'check_permission' ),
    8585            )
    8686        );
    … …  
    9393                'methods'             => WP_REST_Server::CREATABLE,
    9494                'callback'            => array( $this, 'create_post' ),
    95                 'permission_callback' => array( $this, 'check_api_key' ),
     95                'permission_callback' => array( $this, 'check_permission' ),
    9696                'args'                => $this->get_sync_args(),
    9797            )
    … …  
    105105                'methods'             => WP_REST_Server::EDITABLE,
    106106                'callback'            => array( $this, 'update_post' ),
    107                 'permission_callback' => array( $this, 'check_api_key' ),
     107                'permission_callback' => array( $this, 'check_permission' ),
    108108                'args'                => array_merge(
    109109                    array(
    … …  
    129129                'methods'             => WP_REST_Server::DELETABLE,
    130130                'callback'            => array( $this, 'delete_post' ),
    131                 'permission_callback' => array( $this, 'check_api_key' ),
     131                'permission_callback' => array( $this, 'check_permission' ),
    132132                'args'                => array(
    133133                    'id' => array(
    … …  
    155155                'methods'             => WP_REST_Server::READABLE,
    156156                'callback'            => array( $this, 'lookup_post' ),
    157                 'permission_callback' => array( $this, 'check_api_key' ),
     157                'permission_callback' => array( $this, 'check_permission' ),
    158158                'args'                => array(
    159159                    'hydori_id' => array(
    … …  
    173173                'methods'             => WP_REST_Server::READABLE,
    174174                'callback'            => array( $this, 'get_debug_info' ),
    175                 'permission_callback' => array( $this, 'check_api_key' ),
     175                'permission_callback' => array( $this, 'check_permission' ),
    176176            )
    177177        );
    178178
     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
    179204        $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        ) );
    180240    }
    181241
    … …  
    350410                'default'     => array(),
    351411                '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' ),
    354417                    ),
    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' ),
    364430                ),
    365431            ),
    … …  
    504570
    505571    /**
    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).
    507613     *
    508614     * @since 2.0.0
    … …  
    511617     */
    512618    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            }
    517628        }
    518629
  • hydori-seo-ai-powered-seo-automation/trunk/includes/class-hydori-sync.php

    r3488505 r3514908  
    6363        $this->define_api_hooks();
    6464        $this->define_export_hooks();
     65        $this->define_seo_head_hooks();
    6566    }
    6667
    … …  
    101102         */
    102103        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';
    103110
    104111        $this->loader = new Hydori_Sync_Loader();
    … …  
    159166
    160167    /**
     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    /**
    161178     * Run the loader to execute all of the hooks with WordPress.
    162179     *
  • hydori-seo-ai-powered-seo-automation/trunk/readme.txt

    r3491852 r3514908  
    44Requires at least: 5.0
    55Tested up to: 6.9
    6 Stable tag: 2.3.1
     6Stable tag: 2.5.0
    77Requires PHP: 7.4
    88License: GPLv2 or later
    … …  
    241241
    242242== 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.
    243258
    244259= 2.3.1 =
Note: See TracChangeset for help on using the changeset viewer.