true, 'default_card_type' => 'summary_large_image', 'site_username' => '', 'creator_username' => '', 'default_image' => '', 'image_alt_text' => '', 'enable_creator_tags' => true, 'enable_fallback_to_og' => true, 'card_title_length' => 70, 'card_description_length' => 200, 'optimize_for_engagement' => true ); /** * Available Twitter Card types */ private $card_types = array( 'summary' => 'Summary Card', 'summary_large_image' => 'Summary Card with Large Image', 'app' => 'App Card', 'player' => 'Player Card' ); /** * Get single instance */ public static function instance() { if (is_null(self::$instance)) { self::$instance = new self(); } return self::$instance; } /** * Constructor */ private function __construct() { $this->init(); } /** * Initialize the module */ private function init() { // Frontend hooks add_action('wp_head', array($this, 'output_twitter_card_tags'), 6); // Admin hooks if (is_admin()) { add_action('admin_init', array($this, 'register_settings')); } // REST API endpoints for testing add_action('rest_api_init', array($this, 'register_rest_routes')); } /** * Get Twitter settings */ public function get_settings() { $settings = get_option($this->option_name, array()); return wp_parse_args($settings, $this->defaults); } /** * Update Twitter settings */ public function update_settings($new_settings) { $settings = $this->get_settings(); $settings = wp_parse_args($new_settings, $settings); return update_option($this->option_name, $settings); } /** * Output Twitter Card meta tags */ public function output_twitter_card_tags() { $settings = $this->get_settings(); if (!$settings['enable_twitter_cards']) { return; } // Get Twitter Card data $card_data = $this->get_twitter_card_data(); // Output Twitter Card tags foreach ($card_data as $name => $content) { if (!empty($content)) { printf( '' . "\n", esc_attr($name), esc_attr($content) ); } } } /** * Get Twitter Card data for current page */ private function get_twitter_card_data() { $settings = $this->get_settings(); $card_data = array(); // Basic required tags $card_data['twitter:card'] = $this->get_card_type(); $card_data['twitter:site'] = $this->get_site_username(); // Content tags $card_data['twitter:title'] = $this->get_twitter_title(); $card_data['twitter:description'] = $this->get_twitter_description(); // Image tags $image_data = $this->get_twitter_image(); if ($image_data) { $card_data['twitter:image'] = $image_data['url']; if (!empty($image_data['alt'])) { $card_data['twitter:image:alt'] = $image_data['alt']; } } // Creator tag if ($settings['enable_creator_tags']) { $creator = $this->get_creator_username(); if ($creator) { $card_data['twitter:creator'] = $creator; } } // App card specific tags if ($card_data['twitter:card'] === 'app') { $app_data = $this->get_app_card_data(); $card_data = array_merge($card_data, $app_data); } // Player card specific tags if ($card_data['twitter:card'] === 'player') { $player_data = $this->get_player_card_data(); $card_data = array_merge($card_data, $player_data); } return apply_filters('tigerstyle_heat_twitter_card_data', $card_data); } /** * Get Twitter Card type */ private function get_card_type() { $settings = $this->get_settings(); // Check for post-specific card type if (is_singular()) { $post_card_type = get_post_meta(get_the_ID(), '_twitter_card_type', true); if ($post_card_type && isset($this->card_types[$post_card_type])) { return $post_card_type; } } return $settings['default_card_type']; } /** * Get site username (with @ prefix) */ private function get_site_username() { $settings = $this->get_settings(); $username = $settings['site_username']; if (!empty($username)) { // Ensure @ prefix return (strpos($username, '@') === 0) ? $username : '@' . $username; } return ''; } /** * Get creator username (with @ prefix) */ private function get_creator_username() { $settings = $this->get_settings(); // Check for post-specific creator if (is_singular()) { $post_creator = get_post_meta(get_the_ID(), '_twitter_creator', true); if ($post_creator) { return (strpos($post_creator, '@') === 0) ? $post_creator : '@' . $post_creator; } // Try to get author's Twitter handle $author_twitter = get_the_author_meta('twitter'); if ($author_twitter) { return (strpos($author_twitter, '@') === 0) ? $author_twitter : '@' . $author_twitter; } } $username = $settings['creator_username']; if (!empty($username)) { return (strpos($username, '@') === 0) ? $username : '@' . $username; } return ''; } /** * Get Twitter title */ private function get_twitter_title() { $settings = $this->get_settings(); $max_length = $settings['card_title_length']; // Check for custom Twitter title if (is_singular()) { $custom_title = get_post_meta(get_the_ID(), '_twitter_title', true); if ($custom_title) { return $this->truncate_text($custom_title, $max_length); } } // Fallback to OpenGraph or default title logic if (is_single() || is_page()) { $title = get_the_title(); } elseif (is_category()) { $title = single_cat_title('', false); } elseif (is_tag()) { $title = single_tag_title('', false); } elseif (is_author()) { $title = get_the_author(); } elseif (is_search()) { $title = sprintf(__('Search Results for: %s', 'tigerstyle-heat'), get_search_query()); } elseif (is_archive()) { $title = get_the_archive_title(); } else { $title = get_bloginfo('name'); } return $this->truncate_text($title, $max_length); } /** * Get Twitter description */ private function get_twitter_description() { $settings = $this->get_settings(); $max_length = $settings['card_description_length']; // Check for custom Twitter description if (is_singular()) { $custom_description = get_post_meta(get_the_ID(), '_twitter_description', true); if ($custom_description) { return $this->truncate_text($custom_description, $max_length); } } // Fallback logic if (is_single() || is_page()) { $excerpt = get_the_excerpt(); if ($excerpt) { return $this->truncate_text($excerpt, $max_length); } } elseif (is_category()) { $description = category_description(); if ($description) { return $this->truncate_text(strip_tags($description), $max_length); } } elseif (is_tag()) { $description = tag_description(); if ($description) { return $this->truncate_text(strip_tags($description), $max_length); } } $site_description = get_bloginfo('description'); return $this->truncate_text($site_description, $max_length); } /** * Get Twitter image data */ private function get_twitter_image() { $settings = $this->get_settings(); // Check for custom Twitter image if (is_singular()) { $custom_image = get_post_meta(get_the_ID(), '_twitter_image', true); if ($custom_image) { return array( 'url' => $custom_image, 'alt' => get_post_meta(get_the_ID(), '_twitter_image_alt', true) ?: $settings['image_alt_text'] ); } } // Try featured image if (is_singular() && has_post_thumbnail()) { $image_id = get_post_thumbnail_id(); $image = wp_get_attachment_image_src($image_id, 'full'); if ($image) { return array( 'url' => $image[0], 'alt' => get_post_meta($image_id, '_wp_attachment_image_alt', true) ?: get_the_title() ); } } // Fallback to default image if (!empty($settings['default_image'])) { return array( 'url' => $settings['default_image'], 'alt' => $settings['image_alt_text'] ?: get_bloginfo('name') . ' - Default Image' ); } return false; } /** * Get App Card specific data */ private function get_app_card_data() { $app_data = array(); if (is_singular()) { $post_id = get_the_ID(); // iOS app data $ios_id = get_post_meta($post_id, '_twitter_app_id_iphone', true); if ($ios_id) { $app_data['twitter:app:id:iphone'] = $ios_id; $app_data['twitter:app:id:ipad'] = $ios_id; $ios_url = get_post_meta($post_id, '_twitter_app_url_iphone', true); if ($ios_url) { $app_data['twitter:app:url:iphone'] = $ios_url; $app_data['twitter:app:url:ipad'] = $ios_url; } $ios_name = get_post_meta($post_id, '_twitter_app_name_iphone', true); if ($ios_name) { $app_data['twitter:app:name:iphone'] = $ios_name; $app_data['twitter:app:name:ipad'] = $ios_name; } } // Android app data $android_id = get_post_meta($post_id, '_twitter_app_id_googleplay', true); if ($android_id) { $app_data['twitter:app:id:googleplay'] = $android_id; $android_url = get_post_meta($post_id, '_twitter_app_url_googleplay', true); if ($android_url) { $app_data['twitter:app:url:googleplay'] = $android_url; } $android_name = get_post_meta($post_id, '_twitter_app_name_googleplay', true); if ($android_name) { $app_data['twitter:app:name:googleplay'] = $android_name; } } } return $app_data; } /** * Get Player Card specific data */ private function get_player_card_data() { $player_data = array(); if (is_singular()) { $post_id = get_the_ID(); $player_url = get_post_meta($post_id, '_twitter_player_url', true); if ($player_url) { $player_data['twitter:player'] = $player_url; $player_width = get_post_meta($post_id, '_twitter_player_width', true); if ($player_width) { $player_data['twitter:player:width'] = $player_width; } $player_height = get_post_meta($post_id, '_twitter_player_height', true); if ($player_height) { $player_data['twitter:player:height'] = $player_height; } $player_stream = get_post_meta($post_id, '_twitter_player_stream', true); if ($player_stream) { $player_data['twitter:player:stream'] = $player_stream; } } } return $player_data; } /** * Truncate text to specified length */ private function truncate_text($text, $max_length) { if (strlen($text) <= $max_length) { return $text; } return substr($text, 0, $max_length - 3) . '...'; } /** * Validate Twitter image requirements */ public function validate_image($image_url) { $errors = array(); // Check if image exists and get dimensions $image_info = getimagesize($image_url); if (!$image_info) { $errors[] = __('Unable to access image or invalid image format.', 'tigerstyle-heat'); return $errors; } $width = $image_info[0]; $height = $image_info[1]; $mime_type = $image_info['mime']; // Get file size $headers = get_headers($image_url, 1); $size = isset($headers['Content-Length']) ? $headers['Content-Length'] : 0; // Check file size (5MB limit) if ($size > 5 * 1024 * 1024) { $errors[] = __('Image file size must be under 5MB for Twitter Cards.', 'tigerstyle-heat'); } // Check supported formats $supported_formats = array('image/jpeg', 'image/png', 'image/webp', 'image/gif'); if (!in_array($mime_type, $supported_formats)) { $errors[] = __('Image format not supported. Use JPG, PNG, WEBP, or GIF.', 'tigerstyle-heat'); } // Check minimum dimensions for summary_large_image if ($width < 300 || $height < 157) { $errors[] = __('Warning: For best results, images should be at least 300x157 pixels.', 'tigerstyle-heat'); } // Check recommended dimensions if ($width < 1200 || $height < 628) { $errors[] = __('Recommendation: Use 1200x628 pixels for optimal display.', 'tigerstyle-heat'); } // Check aspect ratio for large image cards $ratio = $width / $height; if ($ratio < 1.8 || $ratio > 2.1) { $errors[] = __('Warning: Recommended aspect ratio is 1.91:1 for large image cards.', 'tigerstyle-heat'); } return $errors; } /** * Register admin settings */ public function register_settings() { register_setting( 'tigerstyle_heat_twitter', $this->option_name, array( 'sanitize_callback' => array($this, 'sanitize_settings') ) ); } /** * Sanitize settings */ public function sanitize_settings($settings) { $clean_settings = array(); // Boolean settings $boolean_fields = array('enable_twitter_cards', 'enable_creator_tags', 'enable_fallback_to_og', 'optimize_for_engagement'); foreach ($boolean_fields as $field) { $clean_settings[$field] = !empty($settings[$field]); } // Text settings $clean_settings['default_card_type'] = sanitize_text_field($settings['default_card_type'] ?? 'summary_large_image'); $clean_settings['site_username'] = sanitize_text_field($settings['site_username'] ?? ''); $clean_settings['creator_username'] = sanitize_text_field($settings['creator_username'] ?? ''); $clean_settings['default_image'] = esc_url_raw($settings['default_image'] ?? ''); $clean_settings['image_alt_text'] = sanitize_text_field($settings['image_alt_text'] ?? ''); // Numeric settings $clean_settings['card_title_length'] = min(70, max(10, absint($settings['card_title_length'] ?? 70))); $clean_settings['card_description_length'] = min(200, max(50, absint($settings['card_description_length'] ?? 200))); return $clean_settings; } /** * Register REST API routes for testing */ public function register_rest_routes() { register_rest_route('tigerstyle-heat/v1', '/twitter/debug', array( 'methods' => 'GET', 'callback' => array($this, 'debug_twitter_cards'), 'permission_callback' => function() { return current_user_can('manage_options'); } )); } /** * Debug Twitter Card data (REST endpoint) */ public function debug_twitter_cards($request) { $url = $request->get_param('url'); if (empty($url)) { $url = home_url(); } // Get Twitter Card data for the URL $card_data = $this->get_twitter_card_data(); return rest_ensure_response(array( 'url' => $url, 'twitter_card_data' => $card_data, 'card_validator_urls' => $this->get_card_validator_urls(), 'validation_notes' => $this->get_validation_notes($card_data) )); } /** * Get validation notes for Twitter Card data */ private function get_validation_notes($card_data) { $notes = array(); // Check required fields $required_fields = array('twitter:card', 'twitter:title', 'twitter:description'); foreach ($required_fields as $field) { if (empty($card_data[$field])) { $notes[] = sprintf(__('Missing required field: %s', 'tigerstyle-heat'), $field); } } // Check site username if (empty($card_data['twitter:site'])) { $notes[] = __('Missing twitter:site - highly recommended for attribution', 'tigerstyle-heat'); } // Check image if (!empty($card_data['twitter:image'])) { $image_errors = $this->validate_image($card_data['twitter:image']); $notes = array_merge($notes, $image_errors); } else { $notes[] = __('No Twitter image specified - cards may not display optimally', 'tigerstyle-heat'); } return $notes; } /** * Get Twitter Card validator URLs */ public function get_card_validator_urls() { return array( 'twitter_official' => 'https://cards-dev.twitter.com/validator', 'threadcreator' => 'https://threadcreator.com/tools/twitter-card-validator', 'tweetpik' => 'https://tweethunter.io/tweetpik/twitter-card-validator', 'boilerplate' => 'https://boilerplatehq.com/tools/twitter-card-validator', 'brandbird' => 'https://www.brandbird.app/tools/twitter-card-validator', 'typefully' => 'https://typefully.com/tools/twitter-card-validator' ); } /** * Get Twitter/X developer documentation URLs */ public function get_twitter_docs_urls() { return array( 'cards_overview' => 'https://developer.x.com/en/docs/x-for-websites/cards/overview/markup', 'troubleshooting' => 'https://developer.x.com/en/docs/x-for-websites/cards/guides/troubleshooting-cards', 'getting_started' => 'https://developer.x.com/en/docs/x-for-websites/cards/guides/getting-started', 'card_types' => 'https://developer.x.com/en/docs/x-for-websites/cards/overview/abouts-cards' ); } /** * Render admin page */ public function render_admin_page() { // Handle form submission if (isset($_POST['submit_twitter_settings']) && wp_verify_nonce($_POST['twitter_nonce'], 'twitter_settings')) { $this->handle_settings_update(); } $settings = $this->get_settings(); $validator_urls = $this->get_card_validator_urls(); $docs_urls = $this->get_twitter_docs_urls(); ?>
validate_image($new_settings['default_image']); if (!empty($image_errors)) { // Show validation warnings add_action('admin_notices', function() use ($image_errors) { echo '' . __('Image Validation Warnings:', 'tigerstyle-heat') . '