Plugin Directory

Changeset 3498103


Ignore:
Timestamp:
04/03/2026 09:51:48 AM (12 hours ago)
Author:
soroseo
Message:

Release 1.4.0: Enhanced publishing reliability, smart duplicate prevention, slug conflict detection

Location:
soro-seo
Files:
4 edited
1 copied

Legend:

Unmodified
Added
Removed
  • soro-seo/tags/1.4.0/readme.txt

    r3493781 r3498103  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.3.6
     7Stable tag: 1.4.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    9191== Changelog ==
    9292
     93= 1.4.0 =
     94* Improved: Enhanced publishing reliability on slower hosting environments
     95* New: Added smart duplicate prevention to handle interrupted connections gracefully
     96* New: Intelligent slug detection prevents accidental conflicts with your existing pages
     97* Tweak: Cleaner Soro settings interface with streamlined notifications
     98
    9399= 1.3.6 =
    94100* Fixed connection failures on sites where the REST API is restricted to authenticated users
     
    137143== Upgrade Notice ==
    138144
     145= 1.4.0 =
     146Important reliability update for the automated publishing system. Recommended for all users.
     147
    139148= 1.3.6 =
    140149Fixes "invalid API key" errors on sites that restrict the REST API. Recommended for all users.
  • soro-seo/tags/1.4.0/soro-seo.php

    r3493781 r3498103  
    44 * Plugin URI: https://trysoro.com/wordpress
    55 * Description: Connect your WordPress site to Soro for automatic AI-powered article publishing.
    6  * Version: 1.3.6
     6 * Version: 1.4.0
    77 * Author: Soro
    88 * Author URI: https://trysoro.com
     
    1919}
    2020
    21 define('SORO_CONNECTOR_VERSION', '1.3.6');
     21define('SORO_CONNECTOR_VERSION', '1.4.0');
    2222define('SORO_CONNECTOR_PLUGIN_DIR', plugin_dir_path(__FILE__));
    2323define('SORO_CONNECTOR_PLUGIN_URL', plugin_dir_url(__FILE__));
     
    3838       
    3939        add_filter('rest_authentication_errors', array($this, 'bypass_rest_restriction_for_soro'), 999);
     40        add_action('in_admin_header', array($this, 'hide_admin_notices_on_soro_page'));
    4041       
    4142        register_activation_hook(__FILE__, array($this, 'activate'));
     
    6465       
    6566        return $result;
     67    }
     68   
     69    /**
     70     * Hide all admin notices (update nags, plugin promos, etc.) on the Soro settings page
     71     */
     72    public function hide_admin_notices_on_soro_page() {
     73        $screen = get_current_screen();
     74        if ($screen && $screen->id === 'settings_page_soro-seo') {
     75            remove_all_actions('admin_notices');
     76            remove_all_actions('all_admin_notices');
     77        }
    6678    }
    6779   
     
    663675        }
    664676       
     677        // Idempotency: if this exact Soro article was already published, return the existing post
     678        if (!empty($params['soro_article_id'])) {
     679            $existing = get_posts(array(
     680                'post_type'   => 'post',
     681                'post_status' => array('publish', 'draft', 'pending', 'private'),
     682                'meta_key'    => '_soro_article_id',
     683                'meta_value'  => sanitize_text_field($params['soro_article_id']),
     684                'numberposts' => 1,
     685                'fields'      => 'ids',
     686            ));
     687           
     688            if (!empty($existing)) {
     689                $existing_id = $existing[0];
     690                return new WP_REST_Response(array(
     691                    'success' => true,
     692                    'post_id' => $existing_id,
     693                    'post_url' => get_permalink($existing_id),
     694                    'edit_url' => get_edit_post_link($existing_id, 'raw'),
     695                    'duplicate' => true,
     696                ), 200);
     697            }
     698        }
     699       
     700        // Prevent duplicate slugs: if any post or page with this slug already exists,
     701        // still create the article as draft (so Soro stops retrying) but don't publish
     702        // to avoid SEO cannibalization.
     703        $slug_conflict = false;
     704        if (!empty($params['slug'])) {
     705            $existing_by_slug = get_posts(array(
     706                'post_type'   => array('post', 'page'),
     707                'post_status' => array('publish', 'draft', 'pending', 'private'),
     708                'name'        => sanitize_title($params['slug']),
     709                'numberposts' => 1,
     710                'fields'      => 'ids',
     711            ));
     712            if (!empty($existing_by_slug)) {
     713                $slug_conflict = true;
     714            }
     715        }
     716       
    665717        $allowed_statuses = array('draft', 'publish', 'pending', 'private');
    666718        $status = isset($params['status']) ? sanitize_text_field($params['status']) : 'draft';
     
    669721        }
    670722       
     723        if ($slug_conflict) {
     724            $status = 'draft';
     725        }
     726       
    671727        // Build meta_input so SEO meta is set DURING wp_insert_post, before
    672728        // Yoast/RankMath/AIOSEO fire their save_post hooks to build indexables.
    673         // Previously we used update_post_meta after wp_insert_post, which meant
    674         // Yoast would build its indexable without the focus keyword, causing
    675         // the SEO score to not appear until the user re-entered the keyphrase.
    676729        $meta_input = array(
    677730            '_soro_published_at' => current_time('mysql'),
     
    741794        }
    742795       
    743         // Trigger Yoast indexable rebuild as a safety net.
    744         // Even though meta_input sets the data before save_post fires,
    745         // some Yoast versions may need an explicit rebuild to pick up
    746         // the focus keyword in their indexables table.
    747796        $this->rebuild_yoast_indexable($post_id);
    748797       
     798        // Featured image is non-critical — if it crashes, we still return the post_id
     799        // so Soro records the publish and never retries (which would create duplicates).
    749800        $featured_image_id = null;
    750801        if (!empty($params['featured_image_url'])) {
    751             $featured_image_id = $this->attach_featured_image($post_id, $params['featured_image_url'], $params['title']);
    752         }
    753        
    754         return new WP_REST_Response(array(
     802            try {
     803                $featured_image_id = $this->attach_featured_image($post_id, $params['featured_image_url'], $params['title']);
     804            } catch (\Throwable $e) {
     805                // Log but don't fail — the post is already created
     806            }
     807        }
     808       
     809        $response_data = array(
    755810            'success' => true,
    756811            'post_id' => $post_id,
     
    758813            'edit_url' => get_edit_post_link($post_id, 'raw'),
    759814            'featured_image_id' => $featured_image_id,
    760         ), 201);
     815        );
     816       
     817        if ($slug_conflict) {
     818            $response_data['slug_conflict'] = true;
     819            $response_data['saved_as_draft'] = true;
     820        }
     821       
     822        return new WP_REST_Response($response_data, 201);
    761823    }
    762824   
  • soro-seo/trunk/readme.txt

    r3493781 r3498103  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.3.6
     7Stable tag: 1.4.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    9191== Changelog ==
    9292
     93= 1.4.0 =
     94* Improved: Enhanced publishing reliability on slower hosting environments
     95* New: Added smart duplicate prevention to handle interrupted connections gracefully
     96* New: Intelligent slug detection prevents accidental conflicts with your existing pages
     97* Tweak: Cleaner Soro settings interface with streamlined notifications
     98
    9399= 1.3.6 =
    94100* Fixed connection failures on sites where the REST API is restricted to authenticated users
     
    137143== Upgrade Notice ==
    138144
     145= 1.4.0 =
     146Important reliability update for the automated publishing system. Recommended for all users.
     147
    139148= 1.3.6 =
    140149Fixes "invalid API key" errors on sites that restrict the REST API. Recommended for all users.
  • soro-seo/trunk/soro-seo.php

    r3493781 r3498103  
    44 * Plugin URI: https://trysoro.com/wordpress
    55 * Description: Connect your WordPress site to Soro for automatic AI-powered article publishing.
    6  * Version: 1.3.6
     6 * Version: 1.4.0
    77 * Author: Soro
    88 * Author URI: https://trysoro.com
     
    1919}
    2020
    21 define('SORO_CONNECTOR_VERSION', '1.3.6');
     21define('SORO_CONNECTOR_VERSION', '1.4.0');
    2222define('SORO_CONNECTOR_PLUGIN_DIR', plugin_dir_path(__FILE__));
    2323define('SORO_CONNECTOR_PLUGIN_URL', plugin_dir_url(__FILE__));
     
    3838       
    3939        add_filter('rest_authentication_errors', array($this, 'bypass_rest_restriction_for_soro'), 999);
     40        add_action('in_admin_header', array($this, 'hide_admin_notices_on_soro_page'));
    4041       
    4142        register_activation_hook(__FILE__, array($this, 'activate'));
     
    6465       
    6566        return $result;
     67    }
     68   
     69    /**
     70     * Hide all admin notices (update nags, plugin promos, etc.) on the Soro settings page
     71     */
     72    public function hide_admin_notices_on_soro_page() {
     73        $screen = get_current_screen();
     74        if ($screen && $screen->id === 'settings_page_soro-seo') {
     75            remove_all_actions('admin_notices');
     76            remove_all_actions('all_admin_notices');
     77        }
    6678    }
    6779   
     
    663675        }
    664676       
     677        // Idempotency: if this exact Soro article was already published, return the existing post
     678        if (!empty($params['soro_article_id'])) {
     679            $existing = get_posts(array(
     680                'post_type'   => 'post',
     681                'post_status' => array('publish', 'draft', 'pending', 'private'),
     682                'meta_key'    => '_soro_article_id',
     683                'meta_value'  => sanitize_text_field($params['soro_article_id']),
     684                'numberposts' => 1,
     685                'fields'      => 'ids',
     686            ));
     687           
     688            if (!empty($existing)) {
     689                $existing_id = $existing[0];
     690                return new WP_REST_Response(array(
     691                    'success' => true,
     692                    'post_id' => $existing_id,
     693                    'post_url' => get_permalink($existing_id),
     694                    'edit_url' => get_edit_post_link($existing_id, 'raw'),
     695                    'duplicate' => true,
     696                ), 200);
     697            }
     698        }
     699       
     700        // Prevent duplicate slugs: if any post or page with this slug already exists,
     701        // still create the article as draft (so Soro stops retrying) but don't publish
     702        // to avoid SEO cannibalization.
     703        $slug_conflict = false;
     704        if (!empty($params['slug'])) {
     705            $existing_by_slug = get_posts(array(
     706                'post_type'   => array('post', 'page'),
     707                'post_status' => array('publish', 'draft', 'pending', 'private'),
     708                'name'        => sanitize_title($params['slug']),
     709                'numberposts' => 1,
     710                'fields'      => 'ids',
     711            ));
     712            if (!empty($existing_by_slug)) {
     713                $slug_conflict = true;
     714            }
     715        }
     716       
    665717        $allowed_statuses = array('draft', 'publish', 'pending', 'private');
    666718        $status = isset($params['status']) ? sanitize_text_field($params['status']) : 'draft';
     
    669721        }
    670722       
     723        if ($slug_conflict) {
     724            $status = 'draft';
     725        }
     726       
    671727        // Build meta_input so SEO meta is set DURING wp_insert_post, before
    672728        // Yoast/RankMath/AIOSEO fire their save_post hooks to build indexables.
    673         // Previously we used update_post_meta after wp_insert_post, which meant
    674         // Yoast would build its indexable without the focus keyword, causing
    675         // the SEO score to not appear until the user re-entered the keyphrase.
    676729        $meta_input = array(
    677730            '_soro_published_at' => current_time('mysql'),
     
    741794        }
    742795       
    743         // Trigger Yoast indexable rebuild as a safety net.
    744         // Even though meta_input sets the data before save_post fires,
    745         // some Yoast versions may need an explicit rebuild to pick up
    746         // the focus keyword in their indexables table.
    747796        $this->rebuild_yoast_indexable($post_id);
    748797       
     798        // Featured image is non-critical — if it crashes, we still return the post_id
     799        // so Soro records the publish and never retries (which would create duplicates).
    749800        $featured_image_id = null;
    750801        if (!empty($params['featured_image_url'])) {
    751             $featured_image_id = $this->attach_featured_image($post_id, $params['featured_image_url'], $params['title']);
    752         }
    753        
    754         return new WP_REST_Response(array(
     802            try {
     803                $featured_image_id = $this->attach_featured_image($post_id, $params['featured_image_url'], $params['title']);
     804            } catch (\Throwable $e) {
     805                // Log but don't fail — the post is already created
     806            }
     807        }
     808       
     809        $response_data = array(
    755810            'success' => true,
    756811            'post_id' => $post_id,
     
    758813            'edit_url' => get_edit_post_link($post_id, 'raw'),
    759814            'featured_image_id' => $featured_image_id,
    760         ), 201);
     815        );
     816       
     817        if ($slug_conflict) {
     818            $response_data['slug_conflict'] = true;
     819            $response_data['saved_as_draft'] = true;
     820        }
     821       
     822        return new WP_REST_Response($response_data, 201);
    761823    }
    762824   
Note: See TracChangeset for help on using the changeset viewer.