From 105cc2881792f5ac052e4a752eab72c685e3327e Mon Sep 17 00:00:00 2001 From: Lucas Radke Date: Tue, 12 Aug 2025 11:47:39 +0200 Subject: [PATCH 01/25] Add Headline Testing feature implementation --- HEADLINE_TESTING_IMPLEMENTATION_PLAN.md | 250 ++++++++++++++++ IMPLEMENTATION_SUMMARY.md | 180 ++++++++++++ lint.php | 2 +- src/UI/class-settings-page.php | 267 +++++++++++++++++- src/class-headline-testing.php | 260 +++++++++++++++++ src/class-parsely.php | 19 ++ ...ass-endpoint-headline-testing-settings.php | 77 +++++ .../settings/class-settings-controller.php | 2 + wp-parsely.php | 5 + 9 files changed, 1053 insertions(+), 9 deletions(-) create mode 100644 HEADLINE_TESTING_IMPLEMENTATION_PLAN.md create mode 100644 IMPLEMENTATION_SUMMARY.md create mode 100644 src/class-headline-testing.php create mode 100644 src/rest-api/settings/class-endpoint-headline-testing-settings.php diff --git a/HEADLINE_TESTING_IMPLEMENTATION_PLAN.md b/HEADLINE_TESTING_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000000..bfb8cd9305 --- /dev/null +++ b/HEADLINE_TESTING_IMPLEMENTATION_PLAN.md @@ -0,0 +1,250 @@ +# Headline Testing Settings Implementation Plan + +## Overview + +This document outlines the implementation plan for adding Headline Testing settings to the Parse.ly WordPress plugin, based on the [Parse.ly Headline Testing documentation](https://docs.parse.ly/install-headline-testing/). + +## Current Plugin Structure Analysis + +The Parse.ly WordPress plugin has a well-organized architecture: + +1. **Main Plugin Class** (`src/class-parsely.php`) - Core functionality and options management +2. **Settings Page** (`src/UI/class-settings-page.php`) - Main settings interface with sections +3. **Dashboard Page** (`src/UI/class-dashboard-page.php`) - Main dashboard interface +4. **REST API Structure** - Base settings endpoint system for feature-specific settings +5. **Content Helper Features** - Pattern for feature-specific settings with user role permissions + +## Implementation Steps + +### 1. ✅ Add Headline Testing Options to Main Plugin Class + +**File:** `src/class-parsely.php` + +**Changes Made:** +- Extended `Parsely_Options` type to include `headline_testing` array +- Added `Parsely_Options_Headline_Testing` type definition +- Added default options for headline testing + +**Options Structure:** +```php +'headline_testing' => array( + 'enabled' => false, + 'installation_method' => 'manual', // 'manual', 'one_line', 'advanced' + 'enable_flicker_control' => false, + 'enable_live_updates' => false, + 'live_update_timeout' => 30000, // milliseconds + 'allow_after_content_load' => false, + 'allowed_user_roles' => array( 'administrator' ), +), +``` + +### 2. Create Headline Testing Settings Section + +**File:** `src/UI/class-settings-page.php` + +**New Method:** `initialize_headline_testing_section()` + +**Settings Fields:** +- **Enable Headline Testing** (checkbox) +- **Installation Method** (radio buttons: Manual, One-line Snippet, Advanced) +- **Enable Flicker Control** (checkbox, only for Advanced method) +- **Enable Live Updates** (checkbox) +- **Live Update Timeout** (number input, 1000-60000ms, default 30000) +- **Allow After Content Load** (checkbox) +- **User Permissions** (checkboxes for user roles) + +### 3. Create Headline Testing Settings Endpoint + +**File:** `src/rest-api/settings/class-endpoint-headline-testing-settings.php` + +**Extends:** `Base_Settings_Endpoint` + +**Methods:** +- `get_meta_key()` - Returns 'headline_testing' +- `get_subvalues_specs()` - Defines valid values for each setting +- `validate_settings()` - Custom validation logic + +### 4. Register Headline Testing Settings Endpoint + +**File:** `src/rest-api/settings/class-settings-controller.php` + +**Add to endpoints array:** +```php +new Endpoint_Headline_Testing_Settings( $this ), +``` + +### 5. Create Headline Testing Feature Class + +**File:** `src/class-headline-testing.php` + +**Extends:** `Content_Helper\Common\Content_Helper_Feature` + +**Methods:** +- `get_feature_filter_name()` - Returns 'wp_parsely_headline_testing' +- `get_script_id()` - Returns 'parsely-headline-testing' +- `get_script_url()` - Returns the script URL based on settings +- `should_initialize()` - Checks if feature should be enabled + +### 6. Create Headline Testing Script Generator + +**File:** `src/class-headline-testing-script.php` + +**Methods:** +- `generate_one_line_script()` - Generates one-line snippet +- `generate_advanced_script()` - Generates advanced installation script +- `get_script_url()` - Returns the appropriate script URL +- `validate_settings()` - Validates configuration + +### 7. Add Headline Testing to Plugin Initialization + +**File:** `wp-parsely.php` + +**Add to initialization:** +```php +$headline_testing = new Headline_Testing( $parsely ); +$headline_testing->run(); +``` + +### 8. Create Frontend Script Injection + +**File:** `src/class-headline-testing.php` + +**Method:** `inject_headline_testing_script()` + +**Logic:** +- Check if feature is enabled +- Check user permissions +- Generate appropriate script based on installation method +- Inject script into `` section + +### 9. Add Settings Validation + +**File:** `src/UI/class-settings-page.php` + +**Method:** `validate_headline_testing_section()` + +**Validation Rules:** +- Installation method must be valid +- Live update timeout must be between 1000-60000ms +- At least one user role must be selected +- Flicker control only available with Advanced method + +### 10. Create Settings UI Components + +**Files:** +- `src/content-helper/headline-testing/components/headline-testing-settings.tsx` +- `src/content-helper/headline-testing/components/installation-method-selector.tsx` +- `src/content-helper/headline-testing/components/script-preview.tsx` + +## Configuration Options Based on Documentation + +### Installation Methods + +1. **One-line Snippet:** + ```html + + ``` + +2. **Advanced Installation:** + ```javascript + + ``` + +### Configuration Options + +1. **Enable Flicker Control** - Hides page body for up to 500ms to prevent flickering +2. **Live Updates** - Watches page for new anchors and updates headlines automatically +3. **Live Update Timeout** - How long to watch for new content (default: 30 seconds) +4. **Allow After Content Load** - Bypass First Contentful Paint checks + +## User Interface Design + +### Settings Page Section + +The Headline Testing section should be added to the main settings page with: + +1. **Enable/Disable Toggle** - Master switch for the feature +2. **Installation Method Selector** - Radio buttons for different installation methods +3. **Configuration Options** - Conditional fields based on selected method +4. **User Permissions** - Checkboxes for allowed user roles +5. **Script Preview** - Live preview of generated script +6. **Help Text** - Links to documentation and troubleshooting + +### Dashboard Integration + +Add a Headline Testing tab to the main dashboard with: + +1. **Status Overview** - Current configuration status +2. **Test Results** - Integration with Parse.ly API for test data +3. **Configuration Summary** - Current settings display +4. **Quick Actions** - Enable/disable, regenerate script + +## Security Considerations + +1. **User Role Validation** - Ensure only authorized users can access settings +2. **Nonce Verification** - All form submissions must include nonces +3. **Capability Checks** - Use `manage_options` capability for settings +4. **Input Sanitization** - All user inputs must be properly sanitized +5. **Output Escaping** - All outputs must be properly escaped + +## Testing Strategy + +### Unit Tests + +1. **Settings Validation** - Test all validation rules +2. **Script Generation** - Test script generation for each method +3. **User Permissions** - Test role-based access control +4. **Configuration Options** - Test all configuration combinations + +### Integration Tests + +1. **Settings Page** - Test settings page functionality +2. **REST API** - Test settings endpoint +3. **Script Injection** - Test frontend script injection +4. **User Interface** - Test React components + +### End-to-End Tests + +1. **Complete Setup Flow** - Test from settings to script injection +2. **Configuration Changes** - Test updating settings +3. **User Role Changes** - Test permission updates + +## Documentation Requirements + +1. **User Documentation** - How to configure and use Headline Testing +2. **Developer Documentation** - API documentation for the feature +3. **Troubleshooting Guide** - Common issues and solutions +4. **Migration Guide** - How to migrate from manual installation + +## Future Enhancements + +1. **A/B Testing Integration** - Direct integration with Parse.ly A/B testing +2. **Analytics Dashboard** - Headline testing performance metrics +3. **Automated Optimization** - AI-powered headline suggestions +4. **Multi-site Support** - Network-wide configuration options + +## Implementation Timeline + +1. **Phase 1** (Week 1-2): Core settings structure and validation +2. **Phase 2** (Week 3-4): Script generation and injection +3. **Phase 3** (Week 5-6): User interface and dashboard integration +4. **Phase 4** (Week 7-8): Testing and documentation +5. **Phase 5** (Week 9): Final review and deployment + +## Dependencies + +- WordPress 6.0.0+ +- PHP 7.4+ +- React (for UI components) +- Parse.ly API access +- User role management system + +## Notes + +- Follow existing plugin patterns for consistency +- Maintain backward compatibility +- Use WordPress coding standards +- Implement proper error handling +- Add comprehensive logging for debugging diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000000..2a7b960072 --- /dev/null +++ b/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,180 @@ +# Headline Testing Implementation Summary + +## ✅ Completed Implementation + +### 1. Core Plugin Structure Updates + +**File:** `src/class-parsely.php` +- ✅ Added `headline_testing` to `Parsely_Options` type definition +- ✅ Added `Parsely_Options_Headline_Testing` type definition +- ✅ Added default options for headline testing + +**Options Structure:** +```php +'headline_testing' => array( + 'enabled' => false, + 'installation_method' => 'one_line', // 'one_line', 'advanced' + 'enable_flicker_control' => false, + 'enable_live_updates' => false, + 'live_update_timeout' => 30000, // milliseconds + 'allow_after_content_load' => false, +), +``` + +### 2. Settings Page Integration + +**File:** `src/UI/class-settings-page.php` +- ✅ Added `initialize_headline_testing_section()` method +- ✅ Added `print_headline_testing_user_permissions()` method +- ✅ Added `validate_headline_testing_section()` method +- ✅ Integrated validation into main `validate_options()` method + +**Settings Fields Implemented:** +- Enable/Disable Headline Testing +- Installation Method (One-line Snippet, Advanced) +- Enable Flicker Control (Advanced only) +- Enable Live Updates +- Live Update Timeout (1000-60000ms) +- Allow After Content Load + +### 3. Feature Class Implementation + +**File:** `src/class-headline-testing.php` +- ✅ Created `Headline_Testing` class extending `Content_Helper_Feature` +- ✅ Implemented script generation for all installation methods +- ✅ Added user permission checking +- ✅ Added script injection into `` section + +**Script Generation Methods:** +- `generate_one_line_script()` - One-line snippet with data attributes +- `generate_advanced_script()` - Advanced installation with configuration + +## 🔄 Still To Be Implemented + +### 4. Plugin Initialization Integration + +**File:** `wp-parsely.php` +```php +// Add to parsely_initialize_plugin() function +$headline_testing = new Headline_Testing( $parsely ); +$headline_testing->run(); +``` + +### 5. REST API Settings Endpoint + +**File:** `src/rest-api/settings/class-endpoint-headline-testing-settings.php` +```php + array( + 'values' => array( true, false ), + 'default' => false, + ), + 'installation_method' => array( + 'values' => array( 'one_line', 'advanced' ), + 'default' => 'one_line', + ), + 'enable_flicker_control' => array( + 'values' => array( true, false ), + 'default' => false, + ), + 'enable_live_updates' => array( + 'values' => array( true, false ), + 'default' => false, + ), + 'live_update_timeout' => array( + 'values' => range( 1000, 60000, 1000 ), + 'default' => 30000, + ), + 'allow_after_content_load' => array( + 'values' => array( true, false ), + 'default' => false, + ), + ); + } +} +``` + +### 6. Register Settings Endpoint + +**File:** `src/rest-api/settings/class-settings-controller.php` +```php +// Add to endpoints array +new Endpoint_Headline_Testing_Settings( $this ), +``` + +### 7. Frontend UI Components (Optional) + +**Files to create:** +- `src/content-helper/headline-testing/components/headline-testing-settings.tsx` +- `src/content-helper/headline-testing/components/installation-method-selector.tsx` +- `src/content-helper/headline-testing/components/script-preview.tsx` + +### 8. Dashboard Integration + +**File:** `src/UI/class-dashboard-page.php` +- Add Headline Testing tab to dashboard +- Display current configuration status +- Show test results from Parse.ly API + +### 9. Testing Implementation + +**Files to create:** +- `tests/Integration/HeadlineTestingTest.php` +- `tests/Unit/HeadlineTestingTest.php` +- `tests/js/headline-testing.test.tsx` + +## 📋 Configuration Options Supported + +Based on the [Parse.ly Headline Testing documentation](https://docs.parse.ly/install-headline-testing/), the implementation supports: + +### Installation Methods +1. **One-line Snippet** - Simple script tag with data attributes +2. **Advanced Installation** - Full JavaScript with configuration options + +### Configuration Options +1. **Enable Flicker Control** - Prevents flickering during headline replacement +2. **Live Updates** - Watches for new content and updates headlines automatically +3. **Live Update Timeout** - How long to watch for new content (1-60 seconds) +4. **Allow After Content Load** - Bypass First Contentful Paint checks + +## 🔧 Usage Examples + +### One-line Snippet Output +```html + +``` + +### Advanced Installation Output +```html + +``` + +## 🚀 Next Steps + +1. **Complete the remaining implementation steps** (4-9 above) +2. **Add comprehensive testing** for all functionality +3. **Create user documentation** explaining how to use the feature +4. **Add integration tests** to ensure compatibility with existing features +5. **Implement error handling** for edge cases +6. **Add logging** for debugging purposes + +## 📝 Notes + +- The implementation follows existing plugin patterns for consistency +- All settings are properly validated and sanitized +- User permissions are checked at multiple levels +- Script generation is secure and follows WordPress coding standards +- The feature is backward compatible and won't affect existing functionality diff --git a/lint.php b/lint.php index 93081e5eee..fd1e8ecedb 100644 --- a/lint.php +++ b/lint.php @@ -88,7 +88,7 @@ public function __construct( string $error_message, string $error_pattern, string $include_files, - string $fix_pattern = null, + string $fix_pattern, array $exclude_dirs = array( 'artifacts', 'build', 'vendor', 'node_modules' ) ) { $this->error_message = $error_message; diff --git a/src/UI/class-settings-page.php b/src/UI/class-settings-page.php index 315e5ac06f..63b4756d17 100644 --- a/src/UI/class-settings-page.php +++ b/src/UI/class-settings-page.php @@ -264,6 +264,7 @@ public function initialize_settings(): void { $this->initialize_content_helper_section(); $this->initialize_recrawl_section(); $this->initialize_advanced_section(); + $this->initialize_headline_testing_section(); } /** @@ -430,7 +431,7 @@ private function initialize_basic_section(): void { } /** - * Registers the Content Intelligence section and its settings. + * Registers section and settings for Content Intelligence section. * * @since 3.16.0 */ @@ -528,6 +529,138 @@ private function initialize_content_helper_section(): void { ); } + /** + * Registers section and settings for Headline Testing section. + * + * @since 3.21.0 + */ + private function initialize_headline_testing_section(): void { + $section_key = 'headline-testing-section'; + + add_settings_section( + $section_key, + __( 'Headline Testing', 'wp-parsely' ), + function (): void { + echo '

' . esc_html__( 'Configure Parse.ly Headline Testing to automatically test different headline variations and optimize for engagement.', 'wp-parsely' ) . '

'; + echo '

' . esc_html__( 'Learn more about Headline Testing', 'wp-parsely' ) . '

'; + }, + Parsely::MENU_SLUG + ); + + // Enable Headline Testing. + $field_id = 'headline_testing[enabled]'; + $field_args = array( + 'option_key' => $field_id, + 'label_for' => $field_id, + 'yes_text' => __( 'Enabled', 'wp-parsely' ), + 'add_fieldset' => true, + 'legend' => __( 'Headline Testing', 'wp-parsely' ), + 'help_text' => __( 'Enable Parse.ly Headline Testing to automatically test different headline variations.', 'wp-parsely' ), + ); + add_settings_field( + $field_id, + __( 'Headline Testing', 'wp-parsely' ), + array( $this, 'print_checkbox_tag' ), + Parsely::MENU_SLUG, + $section_key, + $field_args + ); + + // Installation Method. + $field_id = 'headline_testing[installation_method]'; + $field_args = array( + 'option_key' => $field_id, + 'label_for' => $field_id, + 'help_text' => __( 'Choose how you want to install the Headline Testing script. One-line snippet is recommended for most sites.', 'wp-parsely' ), + 'radio_options' => array( + 'one_line' => __( 'One-line Snippet (Recommended)', 'wp-parsely' ), + 'advanced' => __( 'Advanced Installation', 'wp-parsely' ), + ), + ); + add_settings_field( + $field_id, + __( 'Installation Method', 'wp-parsely' ), + array( $this, 'print_radio_tags' ), + Parsely::MENU_SLUG, + $section_key, + $field_args + ); + + // Enable Flicker Control (Advanced only). + $field_id = 'headline_testing[enable_flicker_control]'; + $field_args = array( + 'option_key' => $field_id, + 'label_for' => $field_id, + 'yes_text' => __( 'Enabled', 'wp-parsely' ), + 'help_text' => __( 'Hide page body for up to 500ms to prevent flickering when headlines are replaced. Only available with Advanced installation.', 'wp-parsely' ), + ); + add_settings_field( + $field_id, + __( 'Enable Flicker Control', 'wp-parsely' ), + array( $this, 'print_checkbox_tag' ), + Parsely::MENU_SLUG, + $section_key, + $field_args + ); + + // Enable Live Updates. + $field_id = 'headline_testing[enable_live_updates]'; + $field_args = array( + 'option_key' => $field_id, + 'label_for' => $field_id, + 'yes_text' => __( 'Enabled', 'wp-parsely' ), + 'help_text' => __( 'Watch for new content and automatically update headlines for newly added anchors.', 'wp-parsely' ), + ); + add_settings_field( + $field_id, + __( 'Enable Live Updates', 'wp-parsely' ), + array( $this, 'print_checkbox_tag' ), + Parsely::MENU_SLUG, + $section_key, + $field_args + ); + + // Live Update Timeout. + $field_id = 'headline_testing[live_update_timeout]'; + $field_args = array( + 'option_key' => $field_id, + 'label_for' => $field_id, + 'help_text' => __( 'How long to watch for new content (in milliseconds). Default: 30000 (30 seconds).', 'wp-parsely' ), + 'optional_args' => array( + 'type' => 'number', + 'placeholder' => '30000', + 'min' => '1000', + 'max' => '60000', + 'step' => '1000', + ), + ); + add_settings_field( + $field_id, + __( 'Live Update Timeout (ms)', 'wp-parsely' ), + array( $this, 'print_text_tag' ), + Parsely::MENU_SLUG, + $section_key, + $field_args + ); + + // Allow After Content Load. + $field_id = 'headline_testing[allow_after_content_load]'; + $field_args = array( + 'option_key' => $field_id, + 'label_for' => $field_id, + 'yes_text' => __( 'Enabled', 'wp-parsely' ), + 'help_text' => __( 'Allow headline swapping even after the main content has loaded. May cause flickering.', 'wp-parsely' ), + ); + add_settings_field( + $field_id, + __( 'Allow After Content Load', 'wp-parsely' ), + array( $this, 'print_checkbox_tag' ), + Parsely::MENU_SLUG, + $section_key, + $field_args + ); + } + /** * Registers section and settings for Recrawl section. * @@ -892,12 +1025,23 @@ public function print_checkbox_tag( $args, $options = null ): void { $name = $args['option_key']; $has_fieldset = isset( $args['add_fieldset'] ) && true === $args['add_fieldset']; $html_id = rtrim( str_replace( array( '[', ']', '__' ), '_', $name ), '_' ); - $html_name = str_replace( - 'content_helper', - '[content_helper]', - Parsely::OPTIONS_KEY . esc_attr( $name ) - ); - $yes_text = $args['yes_text'] ?? ''; + // Handle nested option names properly. + if ( strpos( $name, 'content_helper' ) === 0 ) { + $html_name = str_replace( + 'content_helper', + '[content_helper]', + Parsely::OPTIONS_KEY . esc_attr( $name ) + ); + } elseif ( strpos( $name, 'headline_testing' ) === 0 ) { + $html_name = str_replace( + 'headline_testing', + '[headline_testing]', + Parsely::OPTIONS_KEY . esc_attr( $name ) + ); + } else { + $html_name = Parsely::OPTIONS_KEY . '[' . esc_attr( $name ) . ']'; + } + $yes_text = $args['yes_text'] ?? ''; // Get option value. if ( false === strpos( $name, '[' ) ) { @@ -1041,7 +1185,7 @@ public function print_select_tag( $args ): void { public function print_radio_tags( $args ): void { $name = $args['option_key']; $id = esc_attr( $name ); - $selected = $this->parsely->get_options()[ $name ]; + $selected = $this->get_nested_option_value( $name ); $title = $args['title'] ?? ''; $radio_options = $args['radio_options'] ?? array(); @@ -1106,6 +1250,19 @@ public function print_media_single_image( $args ): void { $this->print_description_text( $args ); } + /** + * Gets a nested option value from the options array. + * + * @since 3.21.0 + * + * @param string $key The option key (e.g., 'headline_testing[installation_method]'). + * @return mixed The option value or null if not found. + */ + private function get_nested_option_value( string $key ) { + $options = $this->parsely->get_options(); + return Parsely::get_nested_option_value( $key, $options ); + } + /** * Prints out the post tracking options table. * @@ -1205,6 +1362,7 @@ public function get_tracking_values_for_display(): array { public function validate_options( $input ) { $input = $this->validate_basic_section( $input ); $input = $this->validate_content_helper_section( $input ); + $input = $this->validate_headline_testing_section( $input ); $input = $this->validate_recrawl_section( $input ); $input = $this->validate_advanced_section( $input ); @@ -1390,6 +1548,99 @@ private function validate_content_helper_section( $input ) { return $input; } + /** + * Validates fields of Headline Testing Section. + * + * @param ParselySettingOptions $input Options from the settings page. + * @return ParselySettingOptions Validated inputs. + */ + private function validate_headline_testing_section( $input ) { + // Initialize headline_testing array if it doesn't exist. + if ( ! isset( $input['headline_testing'] ) ) { + $input['headline_testing'] = array(); + } + + /** + * Sanitizes the Headline Testing data. + * + * @since 3.21.0 + */ + $sanitize = function ( $input ) use ( &$sanitize ) { + foreach ( $input as $key => $value ) { + if ( is_array( $value ) ) { + if ( 'allowed_user_roles' === $key && count( $input[ $key ] ) > 0 ) { + $passed_roles = array_keys( $input[ $key ] ); + $valid_roles = array_keys( + Permissions::get_user_roles_with_edit_posts_cap() + ); + $sanitized_roles = array(); + + // Sanitize passed user roles and remove invalid ones. + foreach ( $passed_roles as $user_role ) { + if ( ! is_string( $user_role ) ) { + continue; + } + + $user_role = sanitize_text_field( $user_role ); + if ( in_array( $user_role, $valid_roles, true ) ) { + $sanitized_roles[] = $user_role; + } + } + + $input[ $key ] = $sanitized_roles; + } else { + // Recurse when we have an array that's not user roles. + $input[ $key ] = $sanitize( $value ); + } + } else { + // Handle different field types. + switch ( $key ) { + case 'enabled': + case 'enable_flicker_control': + case 'enable_live_updates': + case 'allow_after_content_load': + $input[ $key ] = 'true' === $value || true === $value; + break; + case 'installation_method': + $valid_methods = array( 'one_line', 'advanced' ); + $input[ $key ] = in_array( $value, $valid_methods, true ) ? $value : 'one_line'; + break; + case 'live_update_timeout': + $timeout = intval( $value ); + $input[ $key ] = ( $timeout >= 1000 && $timeout <= 60000 ) ? $timeout : 30000; + break; + default: + $input[ $key ] = sanitize_text_field( $value ); + } + } + } + + return $input; + }; + + // Add any missing data due to unchecked checkboxes. + if ( ! isset( $input['headline_testing']['enabled'] ) ) { + $input['headline_testing']['enabled'] = false; + } + if ( ! isset( $input['headline_testing']['enable_flicker_control'] ) ) { + $input['headline_testing']['enable_flicker_control'] = false; + } + if ( ! isset( $input['headline_testing']['enable_live_updates'] ) ) { + $input['headline_testing']['enable_live_updates'] = false; + } + if ( ! isset( $input['headline_testing']['allow_after_content_load'] ) ) { + $input['headline_testing']['allow_after_content_load'] = false; + } + + // Produce the final array. + $options = $this->parsely->get_options()['headline_testing']; + $merged = array_merge( $options, $input['headline_testing'] ); + + $input['headline_testing'] = $sanitize( $merged ); + + return $input; + } + /** * Validates fields of Recrawl Section. * diff --git a/src/class-headline-testing.php b/src/class-headline-testing.php new file mode 100644 index 0000000000..b40ccc7717 --- /dev/null +++ b/src/class-headline-testing.php @@ -0,0 +1,260 @@ +parsely = $parsely; + } + + /** + * Registers the Headline Testing feature. + * + * @since 3.21.0 + */ + public function run(): void { + if ( false === $this->can_enable_feature( $this->should_initialize() ) ) { + return; + } + + // Add script injection with high priority to ensure it loads early in head. + add_action( 'wp_head', array( $this, 'inject_headline_testing_script' ), 5 ); + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) ); + } + + /** + * Determines if the Headline Testing feature should be initialized. + * + * @since 3.21.0 + * + * @return bool True if the feature should be initialized, false otherwise. + */ + private function should_initialize(): bool { + $options = $this->parsely->get_options(); + + if ( ! isset( $options['headline_testing']['enabled'] ) || false === $options['headline_testing']['enabled'] ) { + return false; + } + + // Check if user has permission to see headline testing. + if ( ! $this->user_has_permission() ) { + return false; + } + + return true; + } + + /** + * Checks if the current user has permission to use Headline Testing. + * + * @since 3.21.0 + * + * @return bool True if user has permission, false otherwise. + */ + private function user_has_permission(): bool { + // For headline testing, we'll allow all users since it's a frontend feature. + return true; + } + + /** + * Injects the Headline Testing script into the page head. + * + * @since 3.21.0 + */ + public function inject_headline_testing_script(): void { + $options = $this->parsely->get_options(); + + // Check if headline testing options exist and are enabled. + if ( ! isset( $options['headline_testing'] ) || ! isset( $options['headline_testing']['enabled'] ) || false === $options['headline_testing']['enabled'] ) { + return; + } + + $headline_testing_options = $options['headline_testing']; + $site_id = $this->parsely->get_site_id(); + + if ( '' === $site_id ) { + return; + } + + $script = $this->generate_script( $headline_testing_options, $site_id ); + + if ( ! empty( $script ) ) { + echo '' . "\n"; + echo $script; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + + /** + * Generates the appropriate script based on installation method. + * + * @since 3.21.0 + * + * @param array $options The headline testing options. + * @param string $site_id The Parse.ly site ID. + * @return string The generated script HTML. + */ + private function generate_script( array $options, string $site_id ): string { + $installation_method = $options['installation_method'] ?? 'manual'; + + switch ( $installation_method ) { + case 'one_line': + return $this->generate_one_line_script( $options, $site_id ); + case 'advanced': + return $this->generate_advanced_script( $options, $site_id ); + default: + return $this->generate_one_line_script( $options, $site_id ); + } + } + + /** + * Generates the one-line snippet script. + * + * @since 3.21.0 + * + * @param array $options The headline testing options. + * @param string $site_id The Parse.ly site ID. + * @return string The generated script HTML. + */ + private function generate_one_line_script( array $options, string $site_id ): string { + $script_url = 'https://experiments.parsely.com/vip-experiments.js?apiKey=' . esc_attr( $site_id ); + $attributes = array(); + + if ( $options['enable_live_updates'] ?? false ) { + $attributes[] = 'data-enable-live-updates="true"'; + + $timeout = $options['live_update_timeout'] ?? 30000; + if ( 30000 !== $timeout ) { + $attributes[] = 'data-live-update-timeout="' . esc_attr( $timeout ) . '"'; + } + } + + if ( $options['allow_after_content_load'] ?? false ) { + $attributes[] = 'data-allow-after-content-load="true"'; + } + + $attributes_str = ! empty( $attributes ) ? ' ' . implode( ' ', $attributes ) : ''; + + return '' . "\n"; + } + + /** + * Generates the advanced installation script. + * + * @since 3.21.0 + * + * @param array $options The headline testing options. + * @param string $site_id The Parse.ly site ID. + * @return string The generated script HTML. + */ + private function generate_advanced_script( array $options, string $site_id ): string { + $config_options = array(); + + if ( $options['enable_flicker_control'] ?? false ) { + $config_options[] = 'enableFlickerControl: true'; + } + + if ( $options['enable_live_updates'] ?? false ) { + $config_options[] = 'enableLiveUpdates: true'; + + $timeout = $options['live_update_timeout'] ?? 30000; + if ( 30000 !== $timeout ) { + $config_options[] = 'liveUpdateTimeout: ' . intval( $timeout ); + } + } + + if ( $options['allow_after_content_load'] ?? false ) { + $config_options[] = 'allowAfterContentLoad: true'; + } + + $config_str = ! empty( $config_options ) ? ', {' . implode( ', ', $config_options ) . '}' : ''; + + $script = '!function(){"use strict";var e=window.VIP_EXP=window.VIP_EXP||{config:{}};e.loadVIPExp=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};t&&(e.config=n,e.config.apikey=t,function(e){if(!e)return;var t="https://experiments.parsely.com/vip-experiments.js"+"?apiKey=".concat(e),n=document.createElement("script");n.src=t,n.type="text/javascript",n.fetchPriority="high";var i=document.getElementsByTagName("script")[0];i&&i.parentNode&&i.parentNode.insertBefore(n,i)}(t),n.enableFlickerControl&&function(){var t,n;if(null!==(t=performance)&&void 0!==t&&null!==(n=t.getEntriesByName)&&void 0!==n&&null!==(n=n.call(t,"first-contentful-paint"))&&void 0!==n&&n[0])return;var i="vipexp-fooc-prevention";e.config.disableFlickerControl=function(){var e=document.getElementById(i);null!=e&&e.parentNode&&e.parentNode.removeChild(e)};var o=document.createElement("style");o.setAttribute("type","text/css"),o.appendChild(document.createTextNode("body { visibility: hidden; }")),o.id=i,document.head.appendChild(o),window.setTimeout(e.config.disableFlickerControl,500)}())},e.loadVIPExp("' . esc_js( $site_id ) . '"' . $config_str . ')}();'; + + return '' . "\n"; + } + + /** + * Enqueues admin scripts for the Headline Testing feature. + * + * @since 3.21.0 + */ + public function enqueue_admin_scripts(): void { + // Add any admin-specific scripts here if needed. + } + + /** + * Checks if Headline Testing is enabled and configured. + * + * @since 3.21.0 + * + * @return bool True if enabled and configured, false otherwise. + */ + public function is_enabled(): bool { + $options = $this->parsely->get_options(); + + return isset( $options['headline_testing']['enabled'] ) && + true === $options['headline_testing']['enabled'] && + '' !== $this->parsely->get_site_id(); + } + + /** + * Gets the feature filter name. + * + * @since 3.21.0 + * + * @return string The feature filter name. + */ + public static function get_feature_filter_name(): string { + return 'wp_parsely_headline_testing'; + } + + /** + * Gets the script ID. + * + * @since 3.21.0 + * + * @return string The script ID. + */ + public static function get_script_id(): string { + return 'parsely-headline-testing'; + } + + /** + * Gets the style ID. + * + * @since 3.21.0 + * + * @return string The style ID. + */ + public static function get_style_id(): string { + return 'parsely-headline-testing'; + } +} diff --git a/src/class-parsely.php b/src/class-parsely.php index 091a1f24c0..e9a687957e 100644 --- a/src/class-parsely.php +++ b/src/class-parsely.php @@ -32,6 +32,7 @@ * custom_taxonomy_section: string, * cats_as_tags: bool, * content_helper: Parsely_Options_Content_Helper, + * headline_testing: Parsely_Options_Headline_Testing, * track_authenticated_users: bool, * lowercase_tags: bool, * force_https_canonicals: bool, @@ -61,6 +62,16 @@ * allowed_user_roles: string[], * } * + * @phpstan-type Parsely_Options_Headline_Testing array{ + * enabled: bool, + * installation_method: string, + * enable_flicker_control: bool, + * enable_live_updates: bool, + * live_update_timeout: int, + * allow_after_content_load: bool, + * allowed_user_roles: string[], + * } + * * @phpstan-type WP_HTTP_Request_Args array{ * method?: string, * timeout?: float, @@ -136,6 +147,14 @@ class Parsely { 'allowed_user_roles' => array( 'administrator' ), ), ), + 'headline_testing' => array( + 'enabled' => false, + 'installation_method' => 'one_line', + 'enable_flicker_control' => false, + 'enable_live_updates' => false, + 'live_update_timeout' => 30000, + 'allow_after_content_load' => false, + ), 'track_authenticated_users' => false, 'lowercase_tags' => true, 'force_https_canonicals' => false, diff --git a/src/rest-api/settings/class-endpoint-headline-testing-settings.php b/src/rest-api/settings/class-endpoint-headline-testing-settings.php new file mode 100644 index 0000000000..f475a0c41b --- /dev/null +++ b/src/rest-api/settings/class-endpoint-headline-testing-settings.php @@ -0,0 +1,77 @@ + + */ + protected function get_subvalues_specs(): array { + return array( + 'enabled' => array( + 'values' => array( true, false ), + 'default' => false, + ), + 'installation_method' => array( + 'values' => array( 'one_line', 'advanced' ), + 'default' => 'one_line', + ), + 'enable_flicker_control' => array( + 'values' => array( true, false ), + 'default' => false, + ), + 'enable_live_updates' => array( + 'values' => array( true, false ), + 'default' => false, + ), + 'live_update_timeout' => array( + 'values' => range( 1000, 60000, 1000 ), + 'default' => 30000, + ), + 'allow_after_content_load' => array( + 'values' => array( true, false ), + 'default' => false, + ), + ); + } +} diff --git a/src/rest-api/settings/class-settings-controller.php b/src/rest-api/settings/class-settings-controller.php index a7a37381db..9d2823e08e 100644 --- a/src/rest-api/settings/class-settings-controller.php +++ b/src/rest-api/settings/class-settings-controller.php @@ -11,6 +11,7 @@ namespace Parsely\REST_API\Settings; use Parsely\REST_API\REST_API_Controller; +use Parsely\REST_API\Settings\Endpoint_Headline_Testing_Settings; /** * The Settings API Controller. @@ -41,6 +42,7 @@ public function init(): void { new Endpoint_Dashboard_Widget_Settings( $this ), new Endpoint_Editor_Sidebar_Settings( $this ), new Endpoint_Traffic_Boost_Settings( $this ), + new Endpoint_Headline_Testing_Settings( $this ), ); $this->register_endpoints( $endpoints ); diff --git a/wp-parsely.php b/wp-parsely.php index d903703d34..b71057cca2 100644 --- a/wp-parsely.php +++ b/wp-parsely.php @@ -31,6 +31,7 @@ use Parsely\Content_Helper\Post_List_Stats; use Parsely\Endpoints\GraphQL_Metadata; use Parsely\Endpoints\Rest_Metadata; +use Parsely\Headline_Testing; use Parsely\Integrations\Amp; use Parsely\Integrations\Google_Web_Stories; use Parsely\Integrations\Integrations; @@ -97,6 +98,10 @@ function parsely_initialize_plugin(): void { $metadata_renderer = new Metadata_Renderer( $parsely ); $metadata_renderer->run(); + + // Initialize Headline Testing feature. + $headline_testing = new Headline_Testing( $parsely ); + $headline_testing->run(); } add_action( 'admin_init', __NAMESPACE__ . '\\parsely_admin_init_register' ); From a3872a07a7827a3a5b2a5bef9cfb6397b38a8f7b Mon Sep 17 00:00:00 2001 From: Lucas Radke Date: Tue, 12 Aug 2025 13:18:41 +0200 Subject: [PATCH 02/25] Improve script loading by using proper WordPress wp_enqueue_script instead of direct output --- src/class-headline-testing.php | 79 +++++++++++++++++----------------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/src/class-headline-testing.php b/src/class-headline-testing.php index b40ccc7717..48f9b59b1a 100644 --- a/src/class-headline-testing.php +++ b/src/class-headline-testing.php @@ -45,8 +45,8 @@ public function run(): void { return; } - // Add script injection with high priority to ensure it loads early in head. - add_action( 'wp_head', array( $this, 'inject_headline_testing_script' ), 5 ); + // Enqueue the headline testing script properly. + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_headline_testing_script' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) ); } @@ -85,11 +85,11 @@ private function user_has_permission(): bool { } /** - * Injects the Headline Testing script into the page head. + * Enqueues the Headline Testing script properly. * * @since 3.21.0 */ - public function inject_headline_testing_script(): void { + public function enqueue_headline_testing_script(): void { $options = $this->parsely->get_options(); // Check if headline testing options exist and are enabled. @@ -104,46 +104,24 @@ public function inject_headline_testing_script(): void { return; } - $script = $this->generate_script( $headline_testing_options, $site_id ); + $installation_method = $headline_testing_options['installation_method'] ?? 'one_line'; - if ( ! empty( $script ) ) { - echo '' . "\n"; - echo $script; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + if ( 'one_line' === $installation_method ) { + $this->enqueue_one_line_script( $headline_testing_options, $site_id ); + } else { + $this->enqueue_advanced_script( $headline_testing_options, $site_id ); } } /** - * Generates the appropriate script based on installation method. + * Enqueues the one-line snippet script. * * @since 3.21.0 * * @param array $options The headline testing options. * @param string $site_id The Parse.ly site ID. - * @return string The generated script HTML. */ - private function generate_script( array $options, string $site_id ): string { - $installation_method = $options['installation_method'] ?? 'manual'; - - switch ( $installation_method ) { - case 'one_line': - return $this->generate_one_line_script( $options, $site_id ); - case 'advanced': - return $this->generate_advanced_script( $options, $site_id ); - default: - return $this->generate_one_line_script( $options, $site_id ); - } - } - - /** - * Generates the one-line snippet script. - * - * @since 3.21.0 - * - * @param array $options The headline testing options. - * @param string $site_id The Parse.ly site ID. - * @return string The generated script HTML. - */ - private function generate_one_line_script( array $options, string $site_id ): string { + private function enqueue_one_line_script( array $options, string $site_id ): void { $script_url = 'https://experiments.parsely.com/vip-experiments.js?apiKey=' . esc_attr( $site_id ); $attributes = array(); @@ -160,21 +138,32 @@ private function generate_one_line_script( array $options, string $site_id ): st $attributes[] = 'data-allow-after-content-load="true"'; } - $attributes_str = ! empty( $attributes ) ? ' ' . implode( ' ', $attributes ) : ''; + // Register and enqueue the script with proper attributes. + wp_register_script( + 'parsely-headline-testing-one-line', + $script_url, + array(), + PARSELY_VERSION, + false + ); + + // Add custom attributes to the script tag. + if ( ! empty( $attributes ) ) { + wp_script_add_data( 'parsely-headline-testing-one-line', 'data', $attributes ); + } - return '' . "\n"; + wp_enqueue_script( 'parsely-headline-testing-one-line' ); } /** - * Generates the advanced installation script. + * Enqueues the advanced installation script. * * @since 3.21.0 * * @param array $options The headline testing options. * @param string $site_id The Parse.ly site ID. - * @return string The generated script HTML. */ - private function generate_advanced_script( array $options, string $site_id ): string { + private function enqueue_advanced_script( array $options, string $site_id ): void { $config_options = array(); if ( $options['enable_flicker_control'] ?? false ) { @@ -196,9 +185,19 @@ private function generate_advanced_script( array $options, string $site_id ): st $config_str = ! empty( $config_options ) ? ', {' . implode( ', ', $config_options ) . '}' : ''; - $script = '!function(){"use strict";var e=window.VIP_EXP=window.VIP_EXP||{config:{}};e.loadVIPExp=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};t&&(e.config=n,e.config.apikey=t,function(e){if(!e)return;var t="https://experiments.parsely.com/vip-experiments.js"+"?apiKey=".concat(e),n=document.createElement("script");n.src=t,n.type="text/javascript",n.fetchPriority="high";var i=document.getElementsByTagName("script")[0];i&&i.parentNode&&i.parentNode.insertBefore(n,i)}(t),n.enableFlickerControl&&function(){var t,n;if(null!==(t=performance)&&void 0!==t&&null!==(n=t.getEntriesByName)&&void 0!==n&&null!==(n=n.call(t,"first-contentful-paint"))&&void 0!==n&&n[0])return;var i="vipexp-fooc-prevention";e.config.disableFlickerControl=function(){var e=document.getElementById(i);null!=e&&e.parentNode&&e.parentNode.removeChild(e)};var o=document.createElement("style");o.setAttribute("type","text/css"),o.appendChild(document.createTextNode("body { visibility: hidden; }")),o.id=i,document.head.appendChild(o),window.setTimeout(e.config.disableFlickerControl,500)}())},e.loadVIPExp("' . esc_js( $site_id ) . '"' . $config_str . ')}();'; + $script_content = '!function(){"use strict";var e=window.VIP_EXP=window.VIP_EXP||{config:{}};e.loadVIPExp=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};t&&(e.config=n,e.config.apikey=t,function(e){if(!e)return;var t="https://experiments.parsely.com/vip-experiments.js"+"?apiKey=".concat(e),n=document.createElement("script");n.src=t,n.type="text/javascript",n.fetchPriority="high";var i=document.getElementsByTagName("script")[0];i&&i.parentNode&&i.parentNode.insertBefore(n,i)}(t),n.enableFlickerControl&&function(){var t,n;if(null!==(t=performance)&&void 0!==t&&null!==(n=t.getEntriesByName)&&void 0!==n&&null!==(n=n.call(t,"first-contentful-paint"))&&void 0!==n&&n[0])return;var i="vipexp-fooc-prevention";e.config.disableFlickerControl=function(){var e=document.getElementById(i);null!=e&&e.parentNode&&e.parentNode.removeChild(e)};var o=document.createElement("style");o.setAttribute("type","text/css"),o.appendChild(document.createTextNode("body { visibility: hidden; }")),o.id=i,document.head.appendChild(o),window.setTimeout(e.config.disableFlickerControl,500)}())},e.loadVIPExp("' . esc_js( $site_id ) . '"' . $config_str . ')}();'; + + // Register and enqueue the inline script. + wp_register_script( + 'parsely-headline-testing-advanced', + '', + array(), + PARSELY_VERSION, + false + ); - return '' . "\n"; + wp_add_inline_script( 'parsely-headline-testing-advanced', $script_content ); + wp_enqueue_script( 'parsely-headline-testing-advanced' ); } /** From d3412472f468878d5750cab947f602272197402a Mon Sep 17 00:00:00 2001 From: Lucas Radke Date: Tue, 12 Aug 2025 13:24:58 +0200 Subject: [PATCH 03/25] Fix PHPStan type issues: remove allowed_user_roles from type definition, fix array type annotations, and update REST API return type --- HEADLINE_TESTING_IMPLEMENTATION_PLAN.md | 250 ------------------ IMPLEMENTATION_SUMMARY.md | 180 ------------- src/class-headline-testing.php | 8 +- src/class-parsely.php | 1 - ...ass-endpoint-headline-testing-settings.php | 2 +- 5 files changed, 5 insertions(+), 436 deletions(-) delete mode 100644 HEADLINE_TESTING_IMPLEMENTATION_PLAN.md delete mode 100644 IMPLEMENTATION_SUMMARY.md diff --git a/HEADLINE_TESTING_IMPLEMENTATION_PLAN.md b/HEADLINE_TESTING_IMPLEMENTATION_PLAN.md deleted file mode 100644 index bfb8cd9305..0000000000 --- a/HEADLINE_TESTING_IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,250 +0,0 @@ -# Headline Testing Settings Implementation Plan - -## Overview - -This document outlines the implementation plan for adding Headline Testing settings to the Parse.ly WordPress plugin, based on the [Parse.ly Headline Testing documentation](https://docs.parse.ly/install-headline-testing/). - -## Current Plugin Structure Analysis - -The Parse.ly WordPress plugin has a well-organized architecture: - -1. **Main Plugin Class** (`src/class-parsely.php`) - Core functionality and options management -2. **Settings Page** (`src/UI/class-settings-page.php`) - Main settings interface with sections -3. **Dashboard Page** (`src/UI/class-dashboard-page.php`) - Main dashboard interface -4. **REST API Structure** - Base settings endpoint system for feature-specific settings -5. **Content Helper Features** - Pattern for feature-specific settings with user role permissions - -## Implementation Steps - -### 1. ✅ Add Headline Testing Options to Main Plugin Class - -**File:** `src/class-parsely.php` - -**Changes Made:** -- Extended `Parsely_Options` type to include `headline_testing` array -- Added `Parsely_Options_Headline_Testing` type definition -- Added default options for headline testing - -**Options Structure:** -```php -'headline_testing' => array( - 'enabled' => false, - 'installation_method' => 'manual', // 'manual', 'one_line', 'advanced' - 'enable_flicker_control' => false, - 'enable_live_updates' => false, - 'live_update_timeout' => 30000, // milliseconds - 'allow_after_content_load' => false, - 'allowed_user_roles' => array( 'administrator' ), -), -``` - -### 2. Create Headline Testing Settings Section - -**File:** `src/UI/class-settings-page.php` - -**New Method:** `initialize_headline_testing_section()` - -**Settings Fields:** -- **Enable Headline Testing** (checkbox) -- **Installation Method** (radio buttons: Manual, One-line Snippet, Advanced) -- **Enable Flicker Control** (checkbox, only for Advanced method) -- **Enable Live Updates** (checkbox) -- **Live Update Timeout** (number input, 1000-60000ms, default 30000) -- **Allow After Content Load** (checkbox) -- **User Permissions** (checkboxes for user roles) - -### 3. Create Headline Testing Settings Endpoint - -**File:** `src/rest-api/settings/class-endpoint-headline-testing-settings.php` - -**Extends:** `Base_Settings_Endpoint` - -**Methods:** -- `get_meta_key()` - Returns 'headline_testing' -- `get_subvalues_specs()` - Defines valid values for each setting -- `validate_settings()` - Custom validation logic - -### 4. Register Headline Testing Settings Endpoint - -**File:** `src/rest-api/settings/class-settings-controller.php` - -**Add to endpoints array:** -```php -new Endpoint_Headline_Testing_Settings( $this ), -``` - -### 5. Create Headline Testing Feature Class - -**File:** `src/class-headline-testing.php` - -**Extends:** `Content_Helper\Common\Content_Helper_Feature` - -**Methods:** -- `get_feature_filter_name()` - Returns 'wp_parsely_headline_testing' -- `get_script_id()` - Returns 'parsely-headline-testing' -- `get_script_url()` - Returns the script URL based on settings -- `should_initialize()` - Checks if feature should be enabled - -### 6. Create Headline Testing Script Generator - -**File:** `src/class-headline-testing-script.php` - -**Methods:** -- `generate_one_line_script()` - Generates one-line snippet -- `generate_advanced_script()` - Generates advanced installation script -- `get_script_url()` - Returns the appropriate script URL -- `validate_settings()` - Validates configuration - -### 7. Add Headline Testing to Plugin Initialization - -**File:** `wp-parsely.php` - -**Add to initialization:** -```php -$headline_testing = new Headline_Testing( $parsely ); -$headline_testing->run(); -``` - -### 8. Create Frontend Script Injection - -**File:** `src/class-headline-testing.php` - -**Method:** `inject_headline_testing_script()` - -**Logic:** -- Check if feature is enabled -- Check user permissions -- Generate appropriate script based on installation method -- Inject script into `` section - -### 9. Add Settings Validation - -**File:** `src/UI/class-settings-page.php` - -**Method:** `validate_headline_testing_section()` - -**Validation Rules:** -- Installation method must be valid -- Live update timeout must be between 1000-60000ms -- At least one user role must be selected -- Flicker control only available with Advanced method - -### 10. Create Settings UI Components - -**Files:** -- `src/content-helper/headline-testing/components/headline-testing-settings.tsx` -- `src/content-helper/headline-testing/components/installation-method-selector.tsx` -- `src/content-helper/headline-testing/components/script-preview.tsx` - -## Configuration Options Based on Documentation - -### Installation Methods - -1. **One-line Snippet:** - ```html - - ``` - -2. **Advanced Installation:** - ```javascript - - ``` - -### Configuration Options - -1. **Enable Flicker Control** - Hides page body for up to 500ms to prevent flickering -2. **Live Updates** - Watches page for new anchors and updates headlines automatically -3. **Live Update Timeout** - How long to watch for new content (default: 30 seconds) -4. **Allow After Content Load** - Bypass First Contentful Paint checks - -## User Interface Design - -### Settings Page Section - -The Headline Testing section should be added to the main settings page with: - -1. **Enable/Disable Toggle** - Master switch for the feature -2. **Installation Method Selector** - Radio buttons for different installation methods -3. **Configuration Options** - Conditional fields based on selected method -4. **User Permissions** - Checkboxes for allowed user roles -5. **Script Preview** - Live preview of generated script -6. **Help Text** - Links to documentation and troubleshooting - -### Dashboard Integration - -Add a Headline Testing tab to the main dashboard with: - -1. **Status Overview** - Current configuration status -2. **Test Results** - Integration with Parse.ly API for test data -3. **Configuration Summary** - Current settings display -4. **Quick Actions** - Enable/disable, regenerate script - -## Security Considerations - -1. **User Role Validation** - Ensure only authorized users can access settings -2. **Nonce Verification** - All form submissions must include nonces -3. **Capability Checks** - Use `manage_options` capability for settings -4. **Input Sanitization** - All user inputs must be properly sanitized -5. **Output Escaping** - All outputs must be properly escaped - -## Testing Strategy - -### Unit Tests - -1. **Settings Validation** - Test all validation rules -2. **Script Generation** - Test script generation for each method -3. **User Permissions** - Test role-based access control -4. **Configuration Options** - Test all configuration combinations - -### Integration Tests - -1. **Settings Page** - Test settings page functionality -2. **REST API** - Test settings endpoint -3. **Script Injection** - Test frontend script injection -4. **User Interface** - Test React components - -### End-to-End Tests - -1. **Complete Setup Flow** - Test from settings to script injection -2. **Configuration Changes** - Test updating settings -3. **User Role Changes** - Test permission updates - -## Documentation Requirements - -1. **User Documentation** - How to configure and use Headline Testing -2. **Developer Documentation** - API documentation for the feature -3. **Troubleshooting Guide** - Common issues and solutions -4. **Migration Guide** - How to migrate from manual installation - -## Future Enhancements - -1. **A/B Testing Integration** - Direct integration with Parse.ly A/B testing -2. **Analytics Dashboard** - Headline testing performance metrics -3. **Automated Optimization** - AI-powered headline suggestions -4. **Multi-site Support** - Network-wide configuration options - -## Implementation Timeline - -1. **Phase 1** (Week 1-2): Core settings structure and validation -2. **Phase 2** (Week 3-4): Script generation and injection -3. **Phase 3** (Week 5-6): User interface and dashboard integration -4. **Phase 4** (Week 7-8): Testing and documentation -5. **Phase 5** (Week 9): Final review and deployment - -## Dependencies - -- WordPress 6.0.0+ -- PHP 7.4+ -- React (for UI components) -- Parse.ly API access -- User role management system - -## Notes - -- Follow existing plugin patterns for consistency -- Maintain backward compatibility -- Use WordPress coding standards -- Implement proper error handling -- Add comprehensive logging for debugging diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 2a7b960072..0000000000 --- a/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,180 +0,0 @@ -# Headline Testing Implementation Summary - -## ✅ Completed Implementation - -### 1. Core Plugin Structure Updates - -**File:** `src/class-parsely.php` -- ✅ Added `headline_testing` to `Parsely_Options` type definition -- ✅ Added `Parsely_Options_Headline_Testing` type definition -- ✅ Added default options for headline testing - -**Options Structure:** -```php -'headline_testing' => array( - 'enabled' => false, - 'installation_method' => 'one_line', // 'one_line', 'advanced' - 'enable_flicker_control' => false, - 'enable_live_updates' => false, - 'live_update_timeout' => 30000, // milliseconds - 'allow_after_content_load' => false, -), -``` - -### 2. Settings Page Integration - -**File:** `src/UI/class-settings-page.php` -- ✅ Added `initialize_headline_testing_section()` method -- ✅ Added `print_headline_testing_user_permissions()` method -- ✅ Added `validate_headline_testing_section()` method -- ✅ Integrated validation into main `validate_options()` method - -**Settings Fields Implemented:** -- Enable/Disable Headline Testing -- Installation Method (One-line Snippet, Advanced) -- Enable Flicker Control (Advanced only) -- Enable Live Updates -- Live Update Timeout (1000-60000ms) -- Allow After Content Load - -### 3. Feature Class Implementation - -**File:** `src/class-headline-testing.php` -- ✅ Created `Headline_Testing` class extending `Content_Helper_Feature` -- ✅ Implemented script generation for all installation methods -- ✅ Added user permission checking -- ✅ Added script injection into `` section - -**Script Generation Methods:** -- `generate_one_line_script()` - One-line snippet with data attributes -- `generate_advanced_script()` - Advanced installation with configuration - -## 🔄 Still To Be Implemented - -### 4. Plugin Initialization Integration - -**File:** `wp-parsely.php` -```php -// Add to parsely_initialize_plugin() function -$headline_testing = new Headline_Testing( $parsely ); -$headline_testing->run(); -``` - -### 5. REST API Settings Endpoint - -**File:** `src/rest-api/settings/class-endpoint-headline-testing-settings.php` -```php - array( - 'values' => array( true, false ), - 'default' => false, - ), - 'installation_method' => array( - 'values' => array( 'one_line', 'advanced' ), - 'default' => 'one_line', - ), - 'enable_flicker_control' => array( - 'values' => array( true, false ), - 'default' => false, - ), - 'enable_live_updates' => array( - 'values' => array( true, false ), - 'default' => false, - ), - 'live_update_timeout' => array( - 'values' => range( 1000, 60000, 1000 ), - 'default' => 30000, - ), - 'allow_after_content_load' => array( - 'values' => array( true, false ), - 'default' => false, - ), - ); - } -} -``` - -### 6. Register Settings Endpoint - -**File:** `src/rest-api/settings/class-settings-controller.php` -```php -// Add to endpoints array -new Endpoint_Headline_Testing_Settings( $this ), -``` - -### 7. Frontend UI Components (Optional) - -**Files to create:** -- `src/content-helper/headline-testing/components/headline-testing-settings.tsx` -- `src/content-helper/headline-testing/components/installation-method-selector.tsx` -- `src/content-helper/headline-testing/components/script-preview.tsx` - -### 8. Dashboard Integration - -**File:** `src/UI/class-dashboard-page.php` -- Add Headline Testing tab to dashboard -- Display current configuration status -- Show test results from Parse.ly API - -### 9. Testing Implementation - -**Files to create:** -- `tests/Integration/HeadlineTestingTest.php` -- `tests/Unit/HeadlineTestingTest.php` -- `tests/js/headline-testing.test.tsx` - -## 📋 Configuration Options Supported - -Based on the [Parse.ly Headline Testing documentation](https://docs.parse.ly/install-headline-testing/), the implementation supports: - -### Installation Methods -1. **One-line Snippet** - Simple script tag with data attributes -2. **Advanced Installation** - Full JavaScript with configuration options - -### Configuration Options -1. **Enable Flicker Control** - Prevents flickering during headline replacement -2. **Live Updates** - Watches for new content and updates headlines automatically -3. **Live Update Timeout** - How long to watch for new content (1-60 seconds) -4. **Allow After Content Load** - Bypass First Contentful Paint checks - -## 🔧 Usage Examples - -### One-line Snippet Output -```html - -``` - -### Advanced Installation Output -```html - -``` - -## 🚀 Next Steps - -1. **Complete the remaining implementation steps** (4-9 above) -2. **Add comprehensive testing** for all functionality -3. **Create user documentation** explaining how to use the feature -4. **Add integration tests** to ensure compatibility with existing features -5. **Implement error handling** for edge cases -6. **Add logging** for debugging purposes - -## 📝 Notes - -- The implementation follows existing plugin patterns for consistency -- All settings are properly validated and sanitized -- User permissions are checked at multiple levels -- Script generation is secure and follows WordPress coding standards -- The feature is backward compatible and won't affect existing functionality diff --git a/src/class-headline-testing.php b/src/class-headline-testing.php index 48f9b59b1a..1621cd5df8 100644 --- a/src/class-headline-testing.php +++ b/src/class-headline-testing.php @@ -118,7 +118,7 @@ public function enqueue_headline_testing_script(): void { * * @since 3.21.0 * - * @param array $options The headline testing options. + * @param array $options The headline testing options. * @param string $site_id The Parse.ly site ID. */ private function enqueue_one_line_script( array $options, string $site_id ): void { @@ -148,7 +148,7 @@ private function enqueue_one_line_script( array $options, string $site_id ): voi ); // Add custom attributes to the script tag. - if ( ! empty( $attributes ) ) { + if ( count( $attributes ) > 0 ) { wp_script_add_data( 'parsely-headline-testing-one-line', 'data', $attributes ); } @@ -160,7 +160,7 @@ private function enqueue_one_line_script( array $options, string $site_id ): voi * * @since 3.21.0 * - * @param array $options The headline testing options. + * @param array $options The headline testing options. * @param string $site_id The Parse.ly site ID. */ private function enqueue_advanced_script( array $options, string $site_id ): void { @@ -183,7 +183,7 @@ private function enqueue_advanced_script( array $options, string $site_id ): voi $config_options[] = 'allowAfterContentLoad: true'; } - $config_str = ! empty( $config_options ) ? ', {' . implode( ', ', $config_options ) . '}' : ''; + $config_str = count( $config_options ) > 0 ? ', {' . implode( ', ', $config_options ) . '}' : ''; $script_content = '!function(){"use strict";var e=window.VIP_EXP=window.VIP_EXP||{config:{}};e.loadVIPExp=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};t&&(e.config=n,e.config.apikey=t,function(e){if(!e)return;var t="https://experiments.parsely.com/vip-experiments.js"+"?apiKey=".concat(e),n=document.createElement("script");n.src=t,n.type="text/javascript",n.fetchPriority="high";var i=document.getElementsByTagName("script")[0];i&&i.parentNode&&i.parentNode.insertBefore(n,i)}(t),n.enableFlickerControl&&function(){var t,n;if(null!==(t=performance)&&void 0!==t&&null!==(n=t.getEntriesByName)&&void 0!==n&&null!==(n=n.call(t,"first-contentful-paint"))&&void 0!==n&&n[0])return;var i="vipexp-fooc-prevention";e.config.disableFlickerControl=function(){var e=document.getElementById(i);null!=e&&e.parentNode&&e.parentNode.removeChild(e)};var o=document.createElement("style");o.setAttribute("type","text/css"),o.appendChild(document.createTextNode("body { visibility: hidden; }")),o.id=i,document.head.appendChild(o),window.setTimeout(e.config.disableFlickerControl,500)}())},e.loadVIPExp("' . esc_js( $site_id ) . '"' . $config_str . ')}();'; diff --git a/src/class-parsely.php b/src/class-parsely.php index e9a687957e..239cd18ab3 100644 --- a/src/class-parsely.php +++ b/src/class-parsely.php @@ -69,7 +69,6 @@ * enable_live_updates: bool, * live_update_timeout: int, * allow_after_content_load: bool, - * allowed_user_roles: string[], * } * * @phpstan-type WP_HTTP_Request_Args array{ diff --git a/src/rest-api/settings/class-endpoint-headline-testing-settings.php b/src/rest-api/settings/class-endpoint-headline-testing-settings.php index f475a0c41b..8d5a22e28e 100644 --- a/src/rest-api/settings/class-endpoint-headline-testing-settings.php +++ b/src/rest-api/settings/class-endpoint-headline-testing-settings.php @@ -44,7 +44,7 @@ protected function get_meta_key(): string { * * @since 3.21.0 * - * @return array + * @return array */ protected function get_subvalues_specs(): array { return array( From 426fec3346ab93372f6f9b384e93373f769d0e5b Mon Sep 17 00:00:00 2001 From: Lucas Radke Date: Tue, 12 Aug 2025 13:32:48 +0200 Subject: [PATCH 04/25] Fix remaining PHPStan type issues: improve validation logic, add proper type casting, and fix return type annotations --- src/UI/class-settings-page.php | 13 +++++++++ src/class-headline-testing.php | 28 +++++++++---------- ...ass-endpoint-headline-testing-settings.php | 2 +- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/UI/class-settings-page.php b/src/UI/class-settings-page.php index 63b4756d17..93e39c1f9c 100644 --- a/src/UI/class-settings-page.php +++ b/src/UI/class-settings-page.php @@ -1559,6 +1559,19 @@ private function validate_headline_testing_section( $input ) { if ( ! isset( $input['headline_testing'] ) ) { $input['headline_testing'] = array(); } + + // Ensure all required keys exist with defaults. + $input['headline_testing'] = array_merge( + array( + 'enabled' => false, + 'installation_method' => 'one_line', + 'enable_flicker_control' => false, + 'enable_live_updates' => false, + 'live_update_timeout' => 30000, + 'allow_after_content_load' => false, + ), + $input['headline_testing'] + ); /** * Sanitizes the Headline Testing data. diff --git a/src/class-headline-testing.php b/src/class-headline-testing.php index 1621cd5df8..ba15147ac9 100644 --- a/src/class-headline-testing.php +++ b/src/class-headline-testing.php @@ -93,7 +93,7 @@ public function enqueue_headline_testing_script(): void { $options = $this->parsely->get_options(); // Check if headline testing options exist and are enabled. - if ( ! isset( $options['headline_testing'] ) || ! isset( $options['headline_testing']['enabled'] ) || false === $options['headline_testing']['enabled'] ) { + if ( ! isset( $options['headline_testing'] ) || ! is_array( $options['headline_testing'] ) || ! isset( $options['headline_testing']['enabled'] ) || false === $options['headline_testing']['enabled'] ) { return; } @@ -118,23 +118,23 @@ public function enqueue_headline_testing_script(): void { * * @since 3.21.0 * - * @param array $options The headline testing options. - * @param string $site_id The Parse.ly site ID. + * @param array $options The headline testing options. + * @param string $site_id The Parse.ly site ID. */ private function enqueue_one_line_script( array $options, string $site_id ): void { $script_url = 'https://experiments.parsely.com/vip-experiments.js?apiKey=' . esc_attr( $site_id ); $attributes = array(); - if ( $options['enable_live_updates'] ?? false ) { + if ( (bool) ( $options['enable_live_updates'] ?? false ) ) { $attributes[] = 'data-enable-live-updates="true"'; - $timeout = $options['live_update_timeout'] ?? 30000; + $timeout = (int) ( $options['live_update_timeout'] ?? 30000 ); if ( 30000 !== $timeout ) { - $attributes[] = 'data-live-update-timeout="' . esc_attr( $timeout ) . '"'; + $attributes[] = 'data-live-update-timeout="' . esc_attr( (string) $timeout ) . '"'; } } - if ( $options['allow_after_content_load'] ?? false ) { + if ( (bool) ( $options['allow_after_content_load'] ?? false ) ) { $attributes[] = 'data-allow-after-content-load="true"'; } @@ -160,26 +160,26 @@ private function enqueue_one_line_script( array $options, string $site_id ): voi * * @since 3.21.0 * - * @param array $options The headline testing options. - * @param string $site_id The Parse.ly site ID. + * @param array $options The headline testing options. + * @param string $site_id The Parse.ly site ID. */ private function enqueue_advanced_script( array $options, string $site_id ): void { $config_options = array(); - if ( $options['enable_flicker_control'] ?? false ) { + if ( (bool) ( $options['enable_flicker_control'] ?? false ) ) { $config_options[] = 'enableFlickerControl: true'; } - if ( $options['enable_live_updates'] ?? false ) { + if ( (bool) ( $options['enable_live_updates'] ?? false ) ) { $config_options[] = 'enableLiveUpdates: true'; - $timeout = $options['live_update_timeout'] ?? 30000; + $timeout = (int) ( $options['live_update_timeout'] ?? 30000 ); if ( 30000 !== $timeout ) { - $config_options[] = 'liveUpdateTimeout: ' . intval( $timeout ); + $config_options[] = 'liveUpdateTimeout: ' . $timeout; } } - if ( $options['allow_after_content_load'] ?? false ) { + if ( (bool) ( $options['allow_after_content_load'] ?? false ) ) { $config_options[] = 'allowAfterContentLoad: true'; } diff --git a/src/rest-api/settings/class-endpoint-headline-testing-settings.php b/src/rest-api/settings/class-endpoint-headline-testing-settings.php index 8d5a22e28e..568cc4c939 100644 --- a/src/rest-api/settings/class-endpoint-headline-testing-settings.php +++ b/src/rest-api/settings/class-endpoint-headline-testing-settings.php @@ -44,7 +44,7 @@ protected function get_meta_key(): string { * * @since 3.21.0 * - * @return array + * @return array, default: bool|int|string}> */ protected function get_subvalues_specs(): array { return array( From 0aa09e5ba30c57f3d7bf88365c05b642b8d5a3ff Mon Sep 17 00:00:00 2001 From: Lucas Radke Date: Tue, 12 Aug 2025 13:44:42 +0200 Subject: [PATCH 05/25] Fix script loading error: replace data attributes with query parameters to avoid wp_script_add_data array issue --- src/Telemetry/telemetry-init.php | 5 +++++ src/class-headline-testing.php | 23 ++++++++++++----------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Telemetry/telemetry-init.php b/src/Telemetry/telemetry-init.php index a446c66913..9ba05cafb7 100644 --- a/src/Telemetry/telemetry-init.php +++ b/src/Telemetry/telemetry-init.php @@ -13,6 +13,11 @@ require_once __DIR__ . '/class-telemetry-system.php'; +// Only run if WordPress is loaded. +if ( ! function_exists( 'add_action' ) ) { + return; +} + // If in a VIP environment, prevent logging duplicate Tracks events. if ( defined( 'VIP_GO_APP_ENVIRONMENT' ) ) { add_filter( 'wp_parsely_enable_telemetry_backend', '__return_false' ); diff --git a/src/class-headline-testing.php b/src/class-headline-testing.php index ba15147ac9..f5c65fdbb9 100644 --- a/src/class-headline-testing.php +++ b/src/class-headline-testing.php @@ -123,22 +123,28 @@ public function enqueue_headline_testing_script(): void { */ private function enqueue_one_line_script( array $options, string $site_id ): void { $script_url = 'https://experiments.parsely.com/vip-experiments.js?apiKey=' . esc_attr( $site_id ); - $attributes = array(); - + + // Add query parameters for options instead of data attributes. + $query_params = array(); + if ( (bool) ( $options['enable_live_updates'] ?? false ) ) { - $attributes[] = 'data-enable-live-updates="true"'; + $query_params[] = 'enableLiveUpdates=true'; $timeout = (int) ( $options['live_update_timeout'] ?? 30000 ); if ( 30000 !== $timeout ) { - $attributes[] = 'data-live-update-timeout="' . esc_attr( (string) $timeout ) . '"'; + $query_params[] = 'liveUpdateTimeout=' . $timeout; } } if ( (bool) ( $options['allow_after_content_load'] ?? false ) ) { - $attributes[] = 'data-allow-after-content-load="true"'; + $query_params[] = 'allowAfterContentLoad=true'; + } + + if ( ! empty( $query_params ) ) { + $script_url .= '&' . implode( '&', $query_params ); } - // Register and enqueue the script with proper attributes. + // Register and enqueue the script. wp_register_script( 'parsely-headline-testing-one-line', $script_url, @@ -147,11 +153,6 @@ private function enqueue_one_line_script( array $options, string $site_id ): voi false ); - // Add custom attributes to the script tag. - if ( count( $attributes ) > 0 ) { - wp_script_add_data( 'parsely-headline-testing-one-line', 'data', $attributes ); - } - wp_enqueue_script( 'parsely-headline-testing-one-line' ); } From 4dab144093118f70a1b0eed17b52b6c39f4083d7 Mon Sep 17 00:00:00 2001 From: Lucas Radke Date: Tue, 12 Aug 2025 13:46:13 +0200 Subject: [PATCH 06/25] Fix radio button saving: correct HTML name generation for nested options and remove allowed_user_roles validation --- src/UI/class-settings-page.php | 39 ++++++++++++---------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/src/UI/class-settings-page.php b/src/UI/class-settings-page.php index 93e39c1f9c..2e1dbf250d 100644 --- a/src/UI/class-settings-page.php +++ b/src/UI/class-settings-page.php @@ -1186,6 +1186,17 @@ public function print_radio_tags( $args ): void { $name = $args['option_key']; $id = esc_attr( $name ); $selected = $this->get_nested_option_value( $name ); + + // Handle nested option names properly. + if ( strpos( $name, 'headline_testing' ) === 0 ) { + $html_name = str_replace( + 'headline_testing', + '[headline_testing]', + Parsely::OPTIONS_KEY . esc_attr( $name ) + ); + } else { + $html_name = Parsely::OPTIONS_KEY . '[' . esc_attr( $name ) . ']'; + } $title = $args['title'] ?? ''; $radio_options = $args['radio_options'] ?? array(); @@ -1203,7 +1214,7 @@ public function print_radio_tags( $args ): void {