id, 'active-twist-seo') === false) { return; } wp_enqueue_media(); wp_enqueue_style('active-twist-seo-admin', plugin_dir_url(dirname(__DIR__)) . 'assets/css/admin.css', [], '1.0.0'); wp_enqueue_script('active-twist-seo-admin', plugin_dir_url(dirname(__DIR__)) . 'assets/js/admin.js', ['jquery'], '1.0.0', true); } // add_settings_page removed in favor of AdminMenu public function register_settings() { register_setting( 'active_twist_seo_group', Options::OPTION_NAME, [$this, 'sanitize_options'] ); // -- Tab: General (Page Slug: active-twist-seo-general) -- $page_general = 'active-twist-seo-general'; add_settings_section('at_seo_general', esc_html__('General Defaults', 'active-twist-seo-json-ld'), '__return_empty_string', $page_general); add_settings_section('at_seo_social', esc_html__('Social Defaults', 'active-twist-seo-json-ld'), '__return_empty_string', $page_general); add_settings_section('at_seo_schema', esc_html__('Structured Data', 'active-twist-seo-json-ld'), '__return_empty_string', $page_general); add_settings_section('at_seo_media', esc_html__('Media', 'active-twist-seo-json-ld'), '__return_empty_string', $page_general); // Fields - General add_settings_field('seo_title_suffix', esc_html__('Title Suffix', 'active-twist-seo-json-ld'), [$this, 'render_text_field'], $page_general, 'at_seo_general', [ 'label_for' => 'seo_title_suffix', 'description' => esc_html__('Appended to auto-generated titles (e.g. " - Site Name").', 'active-twist-seo-json-ld') ]); add_settings_field('default_description', esc_html__('Default Description', 'active-twist-seo-json-ld'), [$this, 'render_textarea_field'], $page_general, 'at_seo_general', [ 'label_for' => 'default_description', 'description' => esc_html__('Fallback description for pages with no manual description or excerpt.', 'active-twist-seo-json-ld') ]); // Fields - Social add_settings_field('default_twitter_card', esc_html__('Twitter Card Type', 'active-twist-seo-json-ld'), [$this, 'render_select_field'], $page_general, 'at_seo_social', [ 'label_for' => 'default_twitter_card', 'options' => [ 'summary_large_image' => esc_html__('Summary Large Image', 'active-twist-seo-json-ld'), 'summary' => esc_html__('Summary', 'active-twist-seo-json-ld') ], 'description' => esc_html__('Default Twitter card type.', 'active-twist-seo-json-ld') ]); add_settings_field('social_urls', esc_html__('Social Profiles', 'active-twist-seo-json-ld'), [$this, 'render_social_repeater'], $page_general, 'at_seo_social', [ 'label_for' => 'social_urls', 'description' => esc_html__('Enter your social profile URLs. These will be added to your Organization\'s "sameAs" schema property, helping search engines associate these profiles with your site.', 'active-twist-seo-json-ld') ]); // Fields - Schema add_settings_field('org_name', esc_html__('Organization Name', 'active-twist-seo-json-ld'), [$this, 'render_text_field'], $page_general, 'at_seo_schema', [ 'label_for' => 'org_name', 'description' => esc_html__('Name of the organization/website.', 'active-twist-seo-json-ld') ]); add_settings_field('org_url', esc_html__('Organization URL', 'active-twist-seo-json-ld'), [$this, 'render_text_field'], $page_general, 'at_seo_schema', [ 'label_for' => 'org_url', 'description' => esc_html__('URL of the organization/website.', 'active-twist-seo-json-ld') ]); add_settings_field('org_logo', esc_html__('Organization Logo', 'active-twist-seo-json-ld'), [$this, 'render_image_field'], $page_general, 'at_seo_schema', [ 'label_for' => 'org_logo', 'description' => esc_html__('URL of the organization logo.', 'active-twist-seo-json-ld') ]); // Fields - Media add_settings_field('default_og_image', esc_html__('Default Social Image', 'active-twist-seo-json-ld'), [$this, 'render_image_field'], $page_general, 'at_seo_media', [ 'label_for' => 'default_og_image', 'description' => esc_html__('Fallback image when no featured image or manual OG image is set. Recommended size: 1200x630 pixels.', 'active-twist-seo-json-ld') ]); // -- Tab: Business (Page Slug: active-twist-seo-business) -- $page_business = 'active-twist-seo-business'; add_settings_section('at_seo_local', esc_html__('Local Business (Schema)', 'active-twist-seo-json-ld'), '__return_empty_string', $page_business); // Fields - Local Business add_settings_field('local_business_enabled', esc_html__('Enable Local Business', 'active-twist-seo-json-ld'), [$this, 'render_checkbox_field'], $page_business, 'at_seo_local', [ 'label_for' => 'local_business_enabled', 'description' => esc_html__('Output LocalBusiness schema on organization/website pages.', 'active-twist-seo-json-ld') ]); add_settings_field('local_business_name', esc_html__('Business Name', 'active-twist-seo-json-ld'), [$this, 'render_text_field'], $page_business, 'at_seo_local', [ 'label_for' => 'local_business_name', 'description' => esc_html__('Defaults to Organization Name if empty.', 'active-twist-seo-json-ld') ]); add_settings_field('local_business_address', esc_html__('Address', 'active-twist-seo-json-ld'), [$this, 'render_textarea_field'], $page_business, 'at_seo_local', [ 'label_for' => 'local_business_address', 'description' => esc_html__('Full address. Can include newlines.', 'active-twist-seo-json-ld') ]); add_settings_field('local_business_phone', esc_html__('Telephone', 'active-twist-seo-json-ld'), [$this, 'render_text_field'], $page_business, 'at_seo_local', [ 'label_for' => 'local_business_phone', 'description' => esc_html__('International format preferred (e.g. +1-555-555-5555).', 'active-twist-seo-json-ld') ]); add_settings_field('local_business_hours', esc_html__('Opening Hours', 'active-twist-seo-json-ld'), [$this, 'render_text_field'], $page_business, 'at_seo_local', [ 'label_for' => 'local_business_hours', 'description' => esc_html__('Simple text description (e.g. "Mo-Fr 09:00-17:00"). For complex hours, Schema syntax is managed manually.', 'active-twist-seo-json-ld') ]); add_settings_field('local_business_image', esc_html__('Business Image', 'active-twist-seo-json-ld'), [$this, 'render_image_field'], $page_business, 'at_seo_local', [ 'label_for' => 'local_business_image', 'description' => esc_html__('Representative image of the business location.', 'active-twist-seo-json-ld') ]); // -- Tab: Advanced (Page Slug: active-twist-seo-advanced) -- $page_advanced = 'active-twist-seo-advanced'; add_settings_section('at_seo_toggles', esc_html__('Output Control (Advanced)', 'active-twist-seo-json-ld'), '__return_empty_string', $page_advanced); add_settings_section('at_seo_danger', esc_html__('Danger Zone', 'active-twist-seo-json-ld'), [$this, 'render_danger_section'], $page_advanced); // Fields - Output Toggles add_settings_field('enable_canonical_output', esc_html__('Canonical Tags', 'active-twist-seo-json-ld'), [$this, 'render_checkbox_field'], $page_advanced, 'at_seo_toggles', [ 'label_for' => 'enable_canonical_output', /* translators: %s: HTML code tag. */ 'description' => sprintf(esc_html__('Output %s tags.', 'active-twist-seo-json-ld'), 'rel="canonical"') ]); add_settings_field('enable_json_ld_output', esc_html__('JSON-LD Schema', 'active-twist-seo-json-ld'), [$this, 'render_checkbox_field'], $page_advanced, 'at_seo_toggles', [ 'label_for' => 'enable_json_ld_output', 'description' => esc_html__('Output structured data script blocks.', 'active-twist-seo-json-ld') ]); add_settings_field('enable_social_output', esc_html__('Social Metadata', 'active-twist-seo-json-ld'), [$this, 'render_checkbox_field'], $page_advanced, 'at_seo_toggles', [ 'label_for' => 'enable_social_output', 'description' => esc_html__('Output Open Graph and Twitter Card meta tags.', 'active-twist-seo-json-ld') ]); add_settings_field('enable_robots_output', esc_html__('Robots Meta', 'active-twist-seo-json-ld'), [$this, 'render_checkbox_field'], $page_advanced, 'at_seo_toggles', [ 'label_for' => 'enable_robots_output', /* translators: %s: HTML code tag. */ 'description' => sprintf(esc_html__('Output %s and other crawler directives.', 'active-twist-seo-json-ld'), 'robots') ]); } public function render_danger_section() { ?>

delete all Active Twist SEO data, including settings, custom meta, and scan logs. It will also reset the "First Run" state.', 'active-twist-seo-json-ld')); ?>


Newline String) if (isset($input['social_urls']) && is_array($input['social_urls'])) { $urls = array_map('sanitize_url', $input['social_urls']); $urls = array_filter($urls); // Remove empty $new_input['social_urls'] = implode("\n", $urls); } else { $new_input['social_urls'] = sanitize_textarea_field($input['social_urls'] ?? ''); } $new_input['org_name'] = sanitize_text_field($input['org_name'] ?? ''); $new_input['org_url'] = esc_url_raw($input['org_url'] ?? ''); $new_input['org_logo'] = esc_url_raw($input['org_logo'] ?? ''); $new_input['default_og_image'] = esc_url_raw($input['default_og_image'] ?? ''); // Phase 4: Local Business Sanitize // Note: When saving from "General" tab, Business fields might be missing from $_POST, // causing them to be unset if we just use $input to rebuild $new_input completely? // register_setting usually handles merging if we return the full array, OR WP replaces the whole option. // WP replaces the WHOLE option. // DANGER: If we submit General form, Business fields are missing from $_POST. // We MUST merge with existing options for fields not present in current tab submission. // Let's get existing options to merge. $existing = Options::get_all(); // We can't easily know which tab submitted, but we can check existence of keys? // Actually, checkboxes are tricky (unchecked = missing). // Solution: Hidden field with tab name? OR just merge carefully. // Better: register_setting callback receives the *whole* new input from the form. // If a field is missing from the form data, it's missing from $input. // Strategy: // 1. Start $new_input as $existing. // 2. Overwrite with sanitize($input) keys that are expected for the *current context*. // But sanitize callback doesn't know context easily. // Alternative: Use a hidden field 'active_twist_seo_tab' in the form, and use it here. // $_POST['active_twist_seo_tab'] available? Yes, likely. $current_tab = sanitize_key($posted_tab); if ($current_tab === '') { $current_tab = 'general'; } if ($current_tab === 'general') { // We are saving General tab. Keep Business settings from $existing. $new_input['local_business_enabled'] = $existing['local_business_enabled'] ?? false; $new_input['local_business_name'] = $existing['local_business_name'] ?? ''; $new_input['local_business_address'] = $existing['local_business_address'] ?? ''; $new_input['local_business_phone'] = $existing['local_business_phone'] ?? ''; $new_input['local_business_hours'] = $existing['local_business_hours'] ?? ''; $new_input['local_business_image'] = $existing['local_business_image'] ?? ''; // Process General fields $new_input['seo_title_suffix'] = sanitize_text_field($input['seo_title_suffix'] ?? ''); $new_input['default_description'] = sanitize_textarea_field($input['default_description'] ?? ''); $new_input['default_twitter_card'] = sanitize_text_field($input['default_twitter_card'] ?? 'summary_large_image'); $new_input['org_name'] = sanitize_text_field($input['org_name'] ?? ''); $new_input['org_url'] = esc_url_raw($input['org_url'] ?? ''); $new_input['org_logo'] = esc_url_raw($input['org_logo'] ?? ''); $new_input['default_og_image'] = esc_url_raw($input['default_og_image'] ?? ''); if (isset($input['social_urls']) && is_array($input['social_urls'])) { $urls = array_map('sanitize_url', $input['social_urls']); $urls = array_filter($urls); $new_input['social_urls'] = implode("\n", $urls); } else { $new_input['social_urls'] = sanitize_textarea_field($input['social_urls'] ?? ''); } } elseif ($current_tab === 'business') { // Saving Business tab. Keep General settings. $new_input['seo_title_suffix'] = $existing['seo_title_suffix'] ?? ''; $new_input['default_description'] = $existing['default_description'] ?? ''; $new_input['default_twitter_card'] = $existing['default_twitter_card'] ?? 'summary_large_image'; $new_input['social_urls'] = $existing['social_urls'] ?? ''; $new_input['org_name'] = $existing['org_name'] ?? ''; $new_input['org_url'] = $existing['org_url'] ?? ''; $new_input['org_logo'] = $existing['org_logo'] ?? ''; $new_input['default_og_image'] = $existing['default_og_image'] ?? ''; // Process Business fields $new_input['local_business_enabled'] = isset($input['local_business_enabled']); // Checkbox works here because we know we are in Business tab $new_input['local_business_name'] = sanitize_text_field($input['local_business_name'] ?? ''); $new_input['local_business_address'] = sanitize_textarea_field($input['local_business_address'] ?? ''); $new_input['local_business_phone'] = sanitize_text_field($input['local_business_phone'] ?? ''); $new_input['local_business_hours'] = sanitize_text_field($input['local_business_hours'] ?? ''); $new_input['local_business_image'] = esc_url_raw($input['local_business_image'] ?? ''); } elseif ($current_tab === 'advanced') { // Saving Advanced tab. Keep General and Business settings. $new_input = array_merge($existing, $new_input); // Start with existing // Process Advanced fields (Checkboxes) // If key exists in input, it's true. If not, it's false (because uncheck = missing from POST). $new_input['enable_canonical_output'] = isset($input['enable_canonical_output']); $new_input['enable_json_ld_output'] = isset($input['enable_json_ld_output']); $new_input['enable_social_output'] = isset($input['enable_social_output']); $new_input['enable_robots_output'] = isset($input['enable_robots_output']); } // Validation for images (General tab only? Or always check if changing?) if (!empty($new_input['default_og_image']) && $current_tab === 'general') { $validation = Validation::validate_image_dimensions($new_input['default_og_image']); if (!$validation['valid']) { add_settings_error(Options::OPTION_NAME, 'default_og_image_error', 'Default Social Image: ' . $validation['message']); } elseif ($validation['message'] !== 'OK') { add_settings_error(Options::OPTION_NAME, 'default_og_image_warn', 'Default Social Image: ' . $validation['message'], 'updated'); } } // Check local business image if desired, but not strictly required by prompt. return $new_input; } public function render_page() { $requested_tab = filter_input(INPUT_GET, 'tab', FILTER_SANITIZE_FULL_SPECIAL_CHARS); $requested_tab = is_string($requested_tab) ? sanitize_key($requested_tab) : ''; $active_tab = in_array($requested_tab, ['general', 'business', 'advanced'], true) ? $requested_tab : 'general'; ?>

SEO Settings

'; if (!empty($args['description'])) { echo '

' . esc_html($args['description']) . '

'; } } public function render_textarea_field($args) { $options = Options::get_all(); $key = $args['label_for']; $value = isset($options[$key]) ? $options[$key] : ''; echo ''; if (!empty($args['description'])) { echo '

' . esc_html($args['description']) . '

'; } } public function render_social_repeater($args) { $options = Options::get_all(); $key = $args['label_for']; $stored = isset($options[$key]) ? $options[$key] : ''; // Parse stored string into array $current_urls = []; if (is_string($stored)) { $current_urls = array_filter(array_map('trim', explode("\n", $stored))); } elseif (is_array($stored)) { $current_urls = $stored; } $platforms = [ 'linkedin.com' => 'LinkedIn', 'twitter.com' => 'X (Twitter)', 'x.com' => 'X (Twitter)', 'facebook.com' => 'Facebook', 'instagram.com' => 'Instagram', 'youtube.com' => 'YouTube', 'github.com' => 'GitHub' ]; // Map current URLs to platforms $matched_urls = []; $custom_urls = []; foreach ($current_urls as $url) { $matched = false; foreach ($platforms as $domain => $name) { if (strpos($url, $domain) !== false) { // Avoid duplicate matches for same platform in this simple logic (or allow multiple?) // Let's allow multiple, but typically 1 per platform. // For UI simplicity, we pre-fill the "known platform" row. // If multiple URLs exist for same platform, only the LAST one effectively wins in this simple UI (key collision in visual map), // unless we use $matched_urls as array of ['platform' => ..., 'url' => ...]. // But we want fixed rows. // Let's populate the fixed row with the FIRST match found. if (!isset($matched_urls[$domain])) { $matched_urls[$domain] = $url; $matched = true; break; } } } if (!$matched) { $custom_urls[] = $url; } } // Render Known Platforms // Using `[]` in name allows multiple values. echo '
'; $displayed_platforms = [ 'LinkedIn' => ['linkedin.com'], 'X (Twitter)' => ['twitter.com', 'x.com'], 'Facebook' => ['facebook.com'], 'Instagram' => ['instagram.com'], 'YouTube' => ['youtube.com'], 'GitHub' => ['github.com'] ]; foreach ($displayed_platforms as $label => $domains) { $val = ''; foreach ($domains as $d) { if (isset($matched_urls[$d])) { $val = $matched_urls[$d]; break; } } echo '
'; echo ''; echo '
'; echo ''; echo '
'; echo '
'; } // Render Custom URLs (Existing + 1 Empty) if (empty($custom_urls)) { $custom_urls[] = ''; // One empty slot } else { $custom_urls[] = ''; // Always add one empty slot at end? } foreach ($custom_urls as $url) { echo '
'; echo ''; echo '
'; echo ''; echo '
'; echo '
'; } echo '
'; // close repeater echo '

' . esc_html($args['description']) . '

'; } public function render_select_field($args) { $options = Options::get_all(); $key = $args['label_for']; $value = isset($options[$key]) ? $options[$key] : ''; echo ''; if (!empty($args['description'])) { echo '

' . esc_html($args['description']) . '

'; } } public function render_image_field($args) { $options = Options::get_all(); $key = $args['label_for']; $value = isset($options[$key]) ? $options[$key] : ''; echo '
'; echo ''; echo ' '; echo ' '; echo '
'; echo '

' . esc_html($args['description']) . '

'; } public function render_checkbox_field($args) { $options = Options::get_all(); $key = $args['label_for']; $checked = !empty($options[$key]); echo ''; if (!empty($args['description'])) { echo '

' . wp_kses_post($args['description']) . '

'; } } public function handle_nuclear_reset() { check_admin_referer('at_seo_nuclear_reset', 'at_seo_reset_nonce'); $confirm = isset($_POST['reset_confirm']) ? sanitize_text_field(wp_unslash($_POST['reset_confirm'])) : ''; if ($confirm !== 'reset') { wp_die('Confirmation failed.'); } // 1. Delete Option delete_option(Options::OPTION_NAME); // 2. Clear 404 Log (and other transients/custom tables if any) delete_option('at_seo_404_monitor_log'); // 3. Reset First Run State (Actually done by deleting valid option, but ensure default returns false) // Deleting the option means next get() returns defaults, which has first_run_complete = false. // 4. Redirect to First Run (or Status which triggers First Run logic) // Since first_run_complete will be false, AdminMenu will show the First Run page. // Let's redirect to main dashboard or settings to restart flow. wp_safe_redirect(admin_url('admin.php?page=active-twist-seo-first-run')); exit; } }