post_status === 'auto-draft') return; // Recalculate and update postmeta cache $this->get_cached_word_count($post_id, true); } /** * Handle Elementor saves to recalculate and cache word count */ public function on_elementor_save($document) { if ($document && method_exists($document, 'get_main_id')) { $post_id = $document->get_main_id(); if ($post_id) { $this->get_cached_word_count($post_id, true); } } } /** * Detect the content type of a post * * @param int $post_id Post ID * @return string Content type constant */ public function detect($post_id) { // SureCart products are API-first: their translatable content is the product // name (post_title) and description (post_excerpt), mirrored locally — exactly // the same shape as a WooCommerce product. Route them through the product path // so the review captures title + description, regardless of any block markup // the product-page template leaves in post_content. $sc_post = get_post($post_id); if ($sc_post && $sc_post->post_type === 'sc_product') { return self::TYPE_WOOCOMMERCE; } $form_post_types = ['wpcf7_contact_form', 'sureforms_form', 'sf_form', 'wpforms', 'jet-form-builder', 'fluentform', 'formidable', 'ninja_forms']; $form_post = get_post($post_id); if ($form_post && in_array($form_post->post_type, $form_post_types, true)) { return self::TYPE_FORM; } $has_elementor = $this->is_elementor_page($post_id); $has_gutenberg = $this->is_gutenberg_page($post_id); // When both Elementor data AND Gutenberg blocks exist, the user likely // switched from Elementor to a block-based editor (e.g. Greenshift). // Stale _elementor_data remains but post_content now drives rendering. // Prefer Gutenberg in this case; pure Elementor pages have no block markers. if ($has_elementor && $has_gutenberg) { return self::TYPE_GUTENBERG; } // Check for Elementor (page only has Elementor data, no Gutenberg blocks) if ($has_elementor) { return self::TYPE_ELEMENTOR; } // Check for Gutenberg blocks if ($has_gutenberg) { return self::TYPE_GUTENBERG; } // Check if it has any content at all $post = get_post($post_id); if ($post && !empty(trim($post->post_content))) { return self::TYPE_CLASSIC; } // WooCommerce product with no editor content but has translatable data // (title, short description, meta, attributes, etc.) if ($post && $post->post_type === 'product') { return self::TYPE_WOOCOMMERCE; } // Pages with a title but no body (e.g. WooCommerce shop/cart/checkout/my-account // pages) are valid classic pages — the classic path will translate just the title // and create a translated shell page needed by the language switcher. if ($post && !empty(trim($post->post_title))) { return self::TYPE_CLASSIC; } return self::TYPE_UNKNOWN; } /** * Check if page uses Elementor * * @param int $post_id Post ID * @return bool */ public function is_elementor_page($post_id) { // Check for Elementor data meta $elementor_data = get_post_meta($post_id, '_elementor_data', true); if (empty($elementor_data)) { return false; } // Verify it's valid JSON with content $data = json_decode($elementor_data, true); if (json_last_error() !== JSON_ERROR_NONE || empty($data)) { return false; } // Check for Elementor edit mode meta $edit_mode = get_post_meta($post_id, '_elementor_edit_mode', true); return $edit_mode === 'builder' || !empty($data); } /** * Check if page uses Gutenberg blocks * * @param int $post_id Post ID * @return bool */ public function is_gutenberg_page($post_id) { $post = get_post($post_id); if (!$post || empty($post->post_content)) { return false; } // Check for Gutenberg block markers () return $this->has_blocks($post->post_content); } /** * Check if content contains Gutenberg blocks * Uses WordPress function if available, falls back to regex * * @param string $content Post content * @return bool */ public function has_blocks($content) { // Use WordPress built-in function if available (WP 5.0+) if (function_exists('has_blocks')) { return has_blocks($content); } // Fallback: Check for block comment markers return preg_match('/