diff --git a/ansico-wp-basic/ansico-wp-basic.php b/ansico-wp-basic/ansico-wp-basic.php new file mode 100644 index 0000000..df5e36d --- /dev/null +++ b/ansico-wp-basic/ansico-wp-basic.php @@ -0,0 +1,1346 @@ +register_sitemap_rewrites(); + flush_rewrite_rules(); + } + + public static function deactivate() { + flush_rewrite_rules(); + } + + public function __construct() { + add_action('admin_menu', [$this, 'register_admin_menu']); + add_action('admin_init', [$this, 'register_settings']); + + add_action('add_meta_boxes', [$this, 'register_meta_boxes']); + add_action('save_post', [$this, 'save_meta_box']); + add_filter('wp_insert_post_data', [$this, 'maybe_switch_post_type'], 10, 2); + + add_action('show_user_profile', [$this, 'render_user_fields']); + add_action('edit_user_profile', [$this, 'render_user_fields']); + add_action('personal_options_update', [$this, 'save_user_fields']); + add_action('edit_user_profile_update', [$this, 'save_user_fields']); + + add_action('created_term', [$this, 'save_term_fields'], 10, 3); + add_action('edited_term', [$this, 'save_term_fields'], 10, 3); + + add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']); + add_filter('pre_get_document_title', [$this, 'filter_document_title'], 20); + add_action('wp_head', [$this, 'output_meta_description'], 1); + add_filter('the_title', [$this, 'maybe_hide_page_title'], 10, 2); + add_filter('single_post_title', [$this, 'maybe_hide_single_post_title'], 10, 2); + add_action('admin_notices', [$this, 'settings_notice_if_no_types']); + add_action('init', [$this, 'register_taxonomy_hooks']); + add_action('init', [$this, 'register_sitemap_rewrites']); + add_action('template_redirect', [$this, 'maybe_render_sitemap']); + add_filter('query_vars', [$this, 'register_query_vars']); + } + + public function get_settings() { + $defaults = [ + 'enable_meta_module' => 1, + 'enable_sitemap_module' => 1, + 'enabled_post_types' => $this->get_default_post_types(), + 'enable_author_fields' => 1, + 'enable_taxonomy_fields'=> 1, + 'special_pages' => [ + 'date' => ['title' => '', 'description' => ''], + 'search'=> ['title' => '', 'description' => ''], + '404' => ['title' => '', 'description' => ''], + 'home' => ['title' => '', 'description' => ''], + ], + 'post_type_archives' => [], + ]; + + $settings = get_option(self::OPTION_KEY, []); + if (!is_array($settings)) { + $settings = []; + } + + $settings = wp_parse_args($settings, $defaults); + $settings['enable_meta_module'] = empty($settings['enable_meta_module']) ? 0 : 1; + $settings['enable_sitemap_module'] = empty($settings['enable_sitemap_module']) ? 0 : 1; + $settings['enabled_post_types'] = array_values(array_filter(array_map('sanitize_key', (array) $settings['enabled_post_types']))); + $settings['enable_author_fields'] = empty($settings['enable_author_fields']) ? 0 : 1; + $settings['enable_taxonomy_fields'] = empty($settings['enable_taxonomy_fields']) ? 0 : 1; + $settings['special_pages'] = $this->sanitize_special_pages($settings['special_pages']); + $settings['post_type_archives'] = $this->sanitize_post_type_archive_settings($settings['post_type_archives']); + + return $settings; + } + + private function get_public_post_types() { + $post_types = get_post_types([ + 'public' => true, + 'show_ui' => true, + ], 'objects'); + + unset($post_types['attachment']); + + return $post_types; + } + + private function get_default_post_types() { + return array_keys($this->get_public_post_types()); + } + + private function get_public_taxonomies() { + return get_taxonomies([ + 'public' => true, + 'show_ui' => true, + ], 'objects'); + } + + private function get_archive_post_types() { + $post_types = $this->get_public_post_types(); + foreach ($post_types as $key => $post_type) { + if (empty($post_type->has_archive)) { + unset($post_types[$key]); + } + } + return $post_types; + } + + private function sanitize_special_pages($special_pages) { + $defaults = [ + 'date' => ['title' => '', 'description' => ''], + 'search' => ['title' => '', 'description' => ''], + '404' => ['title' => '', 'description' => ''], + 'home' => ['title' => '', 'description' => ''], + ]; + + $special_pages = is_array($special_pages) ? $special_pages : []; + + foreach ($defaults as $key => $value) { + $page = isset($special_pages[$key]) && is_array($special_pages[$key]) ? $special_pages[$key] : []; + $defaults[$key] = [ + 'title' => isset($page['title']) ? sanitize_text_field($page['title']) : '', + 'description' => isset($page['description']) ? sanitize_textarea_field($page['description']) : '', + ]; + } + + return $defaults; + } + + private function sanitize_post_type_archive_settings($archives) { + $clean = []; + $allowed = array_keys($this->get_archive_post_types()); + $archives = is_array($archives) ? $archives : []; + + foreach ($allowed as $post_type) { + $archive = isset($archives[$post_type]) && is_array($archives[$post_type]) ? $archives[$post_type] : []; + $clean[$post_type] = [ + 'title' => isset($archive['title']) ? sanitize_text_field($archive['title']) : '', + 'description' => isset($archive['description']) ? sanitize_textarea_field($archive['description']) : '', + ]; + } + + return $clean; + } + + public function register_admin_menu() { + add_menu_page( + __('Ansico WP Basic', 'ansico-wp-basic'), + __('Ansico WP Basic', 'ansico-wp-basic'), + 'manage_options', + 'ansico-wp-basic', + [$this, 'render_settings_page'], + 'dashicons-search', + 81 + ); + + add_submenu_page( + 'ansico-wp-basic', + __('Settings', 'ansico-wp-basic'), + __('Settings', 'ansico-wp-basic'), + 'manage_options', + 'ansico-wp-basic', + [$this, 'render_settings_page'] + ); + } + + public function register_settings() { + register_setting( + 'ansico_wp_basic_settings_group', + self::OPTION_KEY, + [$this, 'sanitize_settings'] + ); + + add_settings_section( + 'ansico_wp_basic_main_section', + __('General settings', 'ansico-wp-basic'), + function () { + echo '

' . esc_html__('Choose where the SEO fields should be available.', 'ansico-wp-basic') . '

'; + }, + 'ansico-wp-basic' + ); + + add_settings_field( + 'enable_meta_module', + __('SEO module', 'ansico-wp-basic'), + [$this, 'render_meta_module_toggle'], + 'ansico-wp-basic', + 'ansico_wp_basic_main_section' + ); + + add_settings_field( + 'enable_sitemap_module', + __('XML sitemap module', 'ansico-wp-basic'), + [$this, 'render_sitemap_module_toggle'], + 'ansico-wp-basic', + 'ansico_wp_basic_main_section' + ); + + add_settings_field( + 'enabled_post_types', + __('Enable for post types', 'ansico-wp-basic'), + [$this, 'render_post_types_field'], + 'ansico-wp-basic', + 'ansico_wp_basic_main_section' + ); + + add_settings_field( + 'enable_author_fields', + __('Author archive SEO fields', 'ansico-wp-basic'), + [$this, 'render_author_field_toggle'], + 'ansico-wp-basic', + 'ansico_wp_basic_main_section' + ); + + add_settings_field( + 'enable_taxonomy_fields', + __('Taxonomy archive SEO fields', 'ansico-wp-basic'), + [$this, 'render_taxonomy_field_toggle'], + 'ansico-wp-basic', + 'ansico_wp_basic_main_section' + ); + + add_settings_section( + 'ansico_wp_basic_archives_section', + __('Archives and special pages', 'ansico-wp-basic'), + function () { + echo '

' . esc_html__('Set SEO title and description for archive-style pages that do not have a normal editor screen.', 'ansico-wp-basic') . '

'; + }, + 'ansico-wp-basic' + ); + + add_settings_field( + 'special_pages', + __('Global archive pages', 'ansico-wp-basic'), + [$this, 'render_special_pages_fields'], + 'ansico-wp-basic', + 'ansico_wp_basic_archives_section' + ); + + add_settings_field( + 'post_type_archives', + __('Post type archives', 'ansico-wp-basic'), + [$this, 'render_post_type_archive_fields'], + 'ansico-wp-basic', + 'ansico_wp_basic_archives_section' + ); + } + + public function sanitize_settings($input) { + $public_post_types = array_keys($this->get_public_post_types()); + + $enabled_post_types = []; + if (!empty($input['enabled_post_types']) && is_array($input['enabled_post_types'])) { + foreach ($input['enabled_post_types'] as $post_type) { + $post_type = sanitize_key($post_type); + if (in_array($post_type, $public_post_types, true)) { + $enabled_post_types[] = $post_type; + } + } + } + + return [ + 'enable_meta_module' => empty($input['enable_meta_module']) ? 0 : 1, + 'enable_sitemap_module' => empty($input['enable_sitemap_module']) ? 0 : 1, + 'enabled_post_types' => array_values(array_unique($enabled_post_types)), + 'enable_author_fields' => empty($input['enable_author_fields']) ? 0 : 1, + 'enable_taxonomy_fields' => empty($input['enable_taxonomy_fields']) ? 0 : 1, + 'special_pages' => $this->sanitize_special_pages(isset($input['special_pages']) ? $input['special_pages'] : []), + 'post_type_archives' => $this->sanitize_post_type_archive_settings(isset($input['post_type_archives']) ? $input['post_type_archives'] : []), + ]; + } + + + public function render_meta_module_toggle() { + $settings = $this->get_settings(); + printf( + '

%4$s

', + esc_attr(self::OPTION_KEY), + checked($settings['enable_meta_module'], 1, false), + esc_html__('Enable Meta title and Meta description fields and frontend output.', 'ansico-wp-basic'), + esc_html__('Turn this off to hide the SEO editor UI and stop custom SEO output on the frontend, while preserving saved values.', 'ansico-wp-basic') + ); + } + + public function render_sitemap_module_toggle() { + $settings = $this->get_settings(); + printf( + '

%4$s

', + esc_attr(self::OPTION_KEY), + checked($settings['enable_sitemap_module'], 1, false), + esc_html__('Enable XML sitemap generation.', 'ansico-wp-basic'), + esc_html__('When enabled, the sitemap index is available at /ansico-sitemap.xml.', 'ansico-wp-basic') + ); + } + + public function render_post_types_field() { + $settings = $this->get_settings(); + $public_post_types = $this->get_public_post_types(); + + echo '
'; + foreach ($public_post_types as $post_type) { + printf( + '', + esc_attr(self::OPTION_KEY), + esc_attr($post_type->name), + checked(in_array($post_type->name, $settings['enabled_post_types'], true), true, false), + esc_html($post_type->labels->singular_name) + ); + } + echo '
'; + } + + public function render_author_field_toggle() { + $settings = $this->get_settings(); + printf( + '

%4$s

', + esc_attr(self::OPTION_KEY), + checked($settings['enable_author_fields'], 1, false), + esc_html__('Allow SEO fields on user profile pages for author archives.', 'ansico-wp-basic'), + esc_html__('When enabled, each user profile gets Meta title and Meta description fields for that author archive.', 'ansico-wp-basic') + ); + } + + public function render_taxonomy_field_toggle() { + $settings = $this->get_settings(); + printf( + '

%4$s

', + esc_attr(self::OPTION_KEY), + checked($settings['enable_taxonomy_fields'], 1, false), + esc_html__('Allow SEO fields on taxonomy term pages.', 'ansico-wp-basic'), + esc_html__('Works for categories, tags, and public custom taxonomies.', 'ansico-wp-basic') + ); + } + + public function render_special_pages_fields() { + $settings = $this->get_settings(); + if (empty($settings['enable_meta_module'])) { + echo '

' . esc_html__('The SEO module is currently disabled.', 'ansico-wp-basic') . '

'; + return; + } + $pages = [ + 'home' => __('Blog home / posts page', 'ansico-wp-basic'), + 'date' => __('Date archives', 'ansico-wp-basic'), + 'search' => __('Search results pages', 'ansico-wp-basic'), + '404' => __('404 page', 'ansico-wp-basic'), + ]; + + echo '
'; + foreach ($pages as $key => $label) { + $title = $settings['special_pages'][$key]['title'] ?? ''; + $description = $settings['special_pages'][$key]['description'] ?? ''; + printf( + '

%1$s

', + esc_html($label), + esc_html__('Meta title', 'ansico-wp-basic'), + esc_attr(self::OPTION_KEY), + esc_attr($key), + esc_attr($title), + esc_attr__('Enter a custom meta title', 'ansico-wp-basic'), + esc_html__('Meta description', 'ansico-wp-basic'), + esc_attr__('Enter a custom meta description', 'ansico-wp-basic'), + esc_textarea($description) + ); + } + echo '
'; + } + + public function render_post_type_archive_fields() { + $settings = $this->get_settings(); + if (empty($settings['enable_meta_module'])) { + echo '

' . esc_html__('The SEO module is currently disabled.', 'ansico-wp-basic') . '

'; + return; + } + $archives = $this->get_archive_post_types(); + + if (empty($archives)) { + echo '

' . esc_html__('No public post type archives were found.', 'ansico-wp-basic') . '

'; + return; + } + + echo '
'; + foreach ($archives as $post_type) { + $title = $settings['post_type_archives'][$post_type->name]['title'] ?? ''; + $description = $settings['post_type_archives'][$post_type->name]['description'] ?? ''; + printf( + '

%1$s (%2$s)

', + esc_html($post_type->labels->name), + esc_attr($post_type->name), + esc_html__('Meta title', 'ansico-wp-basic'), + esc_attr(self::OPTION_KEY), + esc_attr($title), + esc_attr__('Enter a custom meta title', 'ansico-wp-basic'), + esc_html__('Meta description', 'ansico-wp-basic'), + esc_attr__('Enter a custom meta description', 'ansico-wp-basic'), + esc_textarea($description) + ); + } + echo '
'; + } + + public function render_settings_page() { + if (!current_user_can('manage_options')) { + return; + } + ?> +
+

+

+ get_settings(); ?> +

' . esc_html(home_url('/ansico-sitemap.xml')) . '' : esc_html__('Disabled', 'ansico-wp-basic'); ?>

+
+ +
+
+ get_settings(); + foreach ($settings['enabled_post_types'] as $post_type) { + if (!empty($settings['enable_meta_module'])) { + add_meta_box( + 'ansico_wp_basic_seo', + __('Ansico WP Basic SEO', 'ansico-wp-basic'), + [$this, 'render_meta_box'], + $post_type, + 'normal', + 'default' + ); + } + + add_meta_box( + 'ansico_wp_basic_tools', + __('Ansico WP Basic Tools', 'ansico-wp-basic'), + [$this, 'render_tools_meta_box'], + $post_type, + 'side', + 'default' + ); + } + } + + + private function get_switchable_post_types() { + $post_types = $this->get_public_post_types(); + return array_filter($post_types, function($post_type) { + return !empty($post_type->show_ui); + }); + } + + public function render_tools_meta_box($post) { + wp_nonce_field('ansico_wp_basic_save_meta_box', self::NONCE_KEY); + $post_type = get_post_type($post); + $switchable_types = $this->get_switchable_post_types(); + $hide_title = (int) get_post_meta($post->ID, self::HIDE_TITLE_KEY, true); + echo '
'; + if ($post_type === 'page') { + echo '

'; + } + echo '

'; + echo ''; + echo '

'; + } + + private function render_snippet_box($args) { + $meta_title = isset($args['meta_title']) ? (string) $args['meta_title'] : ''; + $meta_description = isset($args['meta_description']) ? (string) $args['meta_description'] : ''; + $fallback_title = isset($args['fallback_title']) ? (string) $args['fallback_title'] : ''; + $permalink = isset($args['permalink']) ? (string) $args['permalink'] : home_url('/'); + $site_name = get_bloginfo('name'); + ?> +
+

+ + + + +

+

+ + + + +

+ +
+ +
+
+
+
+
+
+

+
+
+ (int) floor($limit * 0.6)) { + $trimmed = mb_substr($trimmed, 0, $last_space); + } else { + $trimmed = mb_substr($trimmed, 0, $limit); + } + return rtrim($trimmed, " + + .,;:-"); + } + + if (strlen($text) <= $limit) { + return $text; + } + $trimmed = substr($text, 0, $limit + 1); + $last_space = strrpos($trimmed, ' '); + if ($last_space !== false && $last_space > (int) floor($limit * 0.6)) { + $trimmed = substr($trimmed, 0, $last_space); + } else { + $trimmed = substr($trimmed, 0, $limit); + } + return rtrim($trimmed, " + + .,;:-"); + } + + private function get_default_meta_description_for_post($post) { + $content = isset($post->post_content) ? (string) $post->post_content : ''; + $first_paragraph = ''; + + if (preg_match('/]*>(.*?)<\/p>/is', $content, $matches)) { + $first_paragraph = wp_strip_all_tags($matches[1]); + } + + if ($first_paragraph === '') { + $blocks = preg_split('/ +\s* +/', wp_strip_all_tags($content)); + if (is_array($blocks)) { + foreach ($blocks as $block) { + $block = trim((string) $block); + if ($block !== '') { + $first_paragraph = $block; + break; + } + } + } + } + + if ($first_paragraph === '') { + $first_paragraph = has_excerpt($post) ? $post->post_excerpt : wp_trim_words(wp_strip_all_tags($content), 30, ''); + } + + return $this->get_trimmed_text_excerpt($first_paragraph, 150); + } + + private function build_document_title_from_meta($meta_title) { + $meta_title = trim((string) $meta_title); + if ($meta_title === '') { + return ''; + } + + $site_name = trim((string) get_bloginfo('name')); + if ($site_name === '') { + return wp_strip_all_tags($meta_title); + } + + $title_plain = wp_strip_all_tags($meta_title); + if (stripos($title_plain, $site_name) !== false) { + return $title_plain; + } + + return $title_plain . ' - ' . $site_name; + } + + public function render_meta_box($post) { + wp_nonce_field('ansico_wp_basic_save_meta_box', self::NONCE_KEY); + + $saved_meta_title = get_post_meta($post->ID, self::META_TITLE_KEY, true); + $saved_meta_description = get_post_meta($post->ID, self::META_DESC_KEY, true); + $fallback_title = get_the_title($post); + $meta_title = $saved_meta_title !== '' ? $saved_meta_title : $fallback_title; + $meta_description = $saved_meta_description !== '' ? $saved_meta_description : $this->get_default_meta_description_for_post($post); + $permalink = get_permalink($post) ?: home_url('/'); + + $this->render_snippet_box([ + 'meta_title' => $meta_title, + 'meta_description' => $meta_description, + 'fallback_title' => $fallback_title, + 'permalink' => $permalink, + ]); + } + + public function save_meta_box($post_id) { + if (!isset($_POST[self::NONCE_KEY]) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST[self::NONCE_KEY])), 'ansico_wp_basic_save_meta_box')) { + return; + } + + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { + return; + } + + if (!current_user_can('edit_post', $post_id)) { + return; + } + + $post_type = get_post_type($post_id); + $settings = $this->get_settings(); + if (!in_array($post_type, $settings['enabled_post_types'], true)) { + return; + } + + if ($post_type === 'page') { + $hide_title = isset($_POST['ansico_wp_basic_hide_title']) ? 1 : 0; + if ($hide_title) { + update_post_meta($post_id, self::HIDE_TITLE_KEY, 1); + } else { + delete_post_meta($post_id, self::HIDE_TITLE_KEY); + } + } + + if (empty($settings['enable_meta_module'])) { + return; + } + + $meta_title = isset($_POST['ansico_wp_basic_meta_title']) ? sanitize_text_field(wp_unslash($_POST['ansico_wp_basic_meta_title'])) : ''; + $meta_description = isset($_POST['ansico_wp_basic_meta_description']) ? sanitize_textarea_field(wp_unslash($_POST['ansico_wp_basic_meta_description'])) : ''; + + if ($meta_title === '') { + delete_post_meta($post_id, self::META_TITLE_KEY); + } else { + update_post_meta($post_id, self::META_TITLE_KEY, $meta_title); + } + + if ($meta_description === '') { + delete_post_meta($post_id, self::META_DESC_KEY); + } else { + update_post_meta($post_id, self::META_DESC_KEY, $meta_description); + } + } + + public function maybe_switch_post_type($data, $postarr) { + if (is_admin() && isset($_POST[self::NONCE_KEY]) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST[self::NONCE_KEY])), 'ansico_wp_basic_save_meta_box')) { + if (!empty($postarr['ID']) && current_user_can('edit_post', (int) $postarr['ID']) && !empty($_POST['ansico_wp_basic_change_post_type'])) { + $new_post_type = sanitize_key(wp_unslash($_POST['ansico_wp_basic_change_post_type'])); + $allowed_types = array_keys($this->get_switchable_post_types()); + if (in_array($new_post_type, $allowed_types, true)) { + $data['post_type'] = $new_post_type; + } + } + } + return $data; + } + + public function maybe_hide_page_title($title, $post_id = 0) { + if (is_admin() || !is_singular('page')) { + return $title; + } + + $queried_id = get_queried_object_id(); + if (!$queried_id || (int) $post_id !== (int) $queried_id) { + return $title; + } + + if ((int) get_post_meta($queried_id, self::HIDE_TITLE_KEY, true) === 1) { + return ''; + } + + return $title; + } + + public function maybe_hide_single_post_title($title, $post = null) { + if (is_admin() || !is_singular('page')) { + return $title; + } + + $queried_id = get_queried_object_id(); + if ($queried_id && (int) get_post_meta($queried_id, self::HIDE_TITLE_KEY, true) === 1) { + return ''; + } + + return $title; + } + + public function register_taxonomy_hooks() { + foreach ($this->get_public_taxonomies() as $taxonomy) { + add_action($taxonomy->name . '_add_form_fields', [$this, 'render_term_add_fields']); + add_action($taxonomy->name . '_edit_form_fields', [$this, 'render_term_edit_fields']); + } + } + + public function render_term_add_fields($taxonomy) { + $settings = $this->get_settings(); + if (empty($settings['enable_meta_module']) || empty($settings['enable_taxonomy_fields'])) { + return; + } + + $taxonomy_name = is_string($taxonomy) ? $taxonomy : ''; + $example_url = home_url('/' . $taxonomy_name . '/example/'); + wp_nonce_field('ansico_wp_basic_save_term_fields', self::TERM_NONCE_KEY); + ?> +
+

+ render_snippet_box([ + 'meta_title' => '', + 'meta_description' => '', + 'fallback_title' => ucfirst($taxonomy_name) . ' archive', + 'permalink' => $example_url, + ]); ?> +
+ get_settings(); + if (empty($settings['enable_meta_module']) || empty($settings['enable_taxonomy_fields'])) { + return; + } + + $meta_title = get_term_meta($term->term_id, self::META_TITLE_KEY, true); + $meta_description = get_term_meta($term->term_id, self::META_DESC_KEY, true); + wp_nonce_field('ansico_wp_basic_save_term_fields', self::TERM_NONCE_KEY); + ?> + + + + render_snippet_box([ + 'meta_title' => $meta_title, + 'meta_description' => $meta_description, + 'fallback_title' => $term->name, + 'permalink' => (!is_wp_error(get_term_link($term)) ? get_term_link($term) : home_url('/')), + ]); ?> + + + get_settings(); + if (empty($settings['enable_meta_module']) || empty($settings['enable_taxonomy_fields'])) { + return; + } + + $taxonomy_object = $taxonomy ? get_taxonomy($taxonomy) : null; + if ($taxonomy_object && (!is_object($taxonomy_object) || empty($taxonomy_object->public) || empty($taxonomy_object->show_ui))) { + return; + } + + if (!isset($_POST[self::TERM_NONCE_KEY]) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST[self::TERM_NONCE_KEY])), 'ansico_wp_basic_save_term_fields')) { + return; + } + + if (!current_user_can('manage_categories')) { + return; + } + + $meta_title = isset($_POST['ansico_wp_basic_meta_title']) ? sanitize_text_field(wp_unslash($_POST['ansico_wp_basic_meta_title'])) : ''; + $meta_description = isset($_POST['ansico_wp_basic_meta_description']) ? sanitize_textarea_field(wp_unslash($_POST['ansico_wp_basic_meta_description'])) : ''; + + if ($meta_title === '') { + delete_term_meta($term_id, self::META_TITLE_KEY); + } else { + update_term_meta($term_id, self::META_TITLE_KEY, $meta_title); + } + + if ($meta_description === '') { + delete_term_meta($term_id, self::META_DESC_KEY); + } else { + update_term_meta($term_id, self::META_DESC_KEY, $meta_description); + } + } + + public function render_user_fields($user) { + $settings = $this->get_settings(); + if (empty($settings['enable_meta_module']) || empty($settings['enable_author_fields'])) { + return; + } + + wp_nonce_field('ansico_wp_basic_save_user_fields', self::USER_NONCE_KEY); + $meta_title = get_user_meta($user->ID, self::META_TITLE_KEY, true); + $meta_description = get_user_meta($user->ID, self::META_DESC_KEY, true); + ?> +

+ render_snippet_box([ + 'meta_title' => $meta_title, + 'meta_description' => $meta_description, + 'fallback_title' => $user->display_name, + 'permalink' => get_author_posts_url($user->ID), + ]); ?> + get_settings(); + if (empty($settings['enable_meta_module']) || empty($settings['enable_author_fields'])) { + return; + } + + if (!current_user_can('edit_user', $user_id)) { + return; + } + + if (!isset($_POST[self::USER_NONCE_KEY]) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST[self::USER_NONCE_KEY])), 'ansico_wp_basic_save_user_fields')) { + return; + } + + $meta_title = isset($_POST['ansico_wp_basic_meta_title']) ? sanitize_text_field(wp_unslash($_POST['ansico_wp_basic_meta_title'])) : ''; + $meta_description = isset($_POST['ansico_wp_basic_meta_description']) ? sanitize_textarea_field(wp_unslash($_POST['ansico_wp_basic_meta_description'])) : ''; + + if ($meta_title === '') { + delete_user_meta($user_id, self::META_TITLE_KEY); + } else { + update_user_meta($user_id, self::META_TITLE_KEY, $meta_title); + } + + if ($meta_description === '') { + delete_user_meta($user_id, self::META_DESC_KEY); + } else { + update_user_meta($user_id, self::META_DESC_KEY, $meta_description); + } + } + + public function enqueue_admin_assets($hook) { + $allowed_hooks = [ + 'post.php', + 'post-new.php', + 'profile.php', + 'user-edit.php', + 'edit-tags.php', + 'term.php', + 'toplevel_page_ansico-wp-basic', + ]; + + if (!in_array($hook, $allowed_hooks, true)) { + return; + } + + wp_enqueue_style( + 'ansico-wp-basic-admin', + plugin_dir_url(__FILE__) . 'assets/admin.css', + [], + '0.0.0.2' + ); + + wp_enqueue_script( + 'ansico-wp-basic-admin', + plugin_dir_url(__FILE__) . 'assets/admin.js', + [], + '0.0.0.2', + true + ); + } + + private function get_context_meta_title() { + $settings = $this->get_settings(); + if (empty($settings['enable_meta_module'])) { + return ''; + } + + if (is_singular()) { + $post_id = get_queried_object_id(); + $post_type = get_post_type($post_id); + if ($post_id && in_array($post_type, $settings['enabled_post_types'], true)) { + return get_post_meta($post_id, self::META_TITLE_KEY, true); + } + } + + if (is_author() && !empty($settings['enable_author_fields'])) { + $author = get_queried_object(); + if ($author instanceof WP_User || (is_object($author) && isset($author->ID))) { + return get_user_meta((int) $author->ID, self::META_TITLE_KEY, true); + } + } + + if ((is_category() || is_tag() || is_tax()) && !empty($settings['enable_taxonomy_fields'])) { + $term = get_queried_object(); + if ($term instanceof WP_Term || (is_object($term) && isset($term->term_id))) { + return get_term_meta((int) $term->term_id, self::META_TITLE_KEY, true); + } + } + + if (is_post_type_archive()) { + $post_type = get_query_var('post_type'); + $post_type = is_array($post_type) ? reset($post_type) : $post_type; + return $settings['post_type_archives'][$post_type]['title'] ?? ''; + } + + if (is_home()) { + return $settings['special_pages']['home']['title'] ?? ''; + } + + if (is_date()) { + return $settings['special_pages']['date']['title'] ?? ''; + } + + if (is_search()) { + return $settings['special_pages']['search']['title'] ?? ''; + } + + if (is_404()) { + return $settings['special_pages']['404']['title'] ?? ''; + } + + return ''; + } + + private function get_context_meta_description() { + $settings = $this->get_settings(); + if (empty($settings['enable_meta_module'])) { + return ''; + } + + if (is_singular()) { + $post_id = get_queried_object_id(); + $post_type = get_post_type($post_id); + if ($post_id && in_array($post_type, $settings['enabled_post_types'], true)) { + return get_post_meta($post_id, self::META_DESC_KEY, true); + } + } + + if (is_author() && !empty($settings['enable_author_fields'])) { + $author = get_queried_object(); + if ($author instanceof WP_User || (is_object($author) && isset($author->ID))) { + return get_user_meta((int) $author->ID, self::META_DESC_KEY, true); + } + } + + if ((is_category() || is_tag() || is_tax()) && !empty($settings['enable_taxonomy_fields'])) { + $term = get_queried_object(); + if ($term instanceof WP_Term || (is_object($term) && isset($term->term_id))) { + return get_term_meta((int) $term->term_id, self::META_DESC_KEY, true); + } + } + + if (is_post_type_archive()) { + $post_type = get_query_var('post_type'); + $post_type = is_array($post_type) ? reset($post_type) : $post_type; + return $settings['post_type_archives'][$post_type]['description'] ?? ''; + } + + if (is_home()) { + return $settings['special_pages']['home']['description'] ?? ''; + } + + if (is_date()) { + return $settings['special_pages']['date']['description'] ?? ''; + } + + if (is_search()) { + return $settings['special_pages']['search']['description'] ?? ''; + } + + if (is_404()) { + return $settings['special_pages']['404']['description'] ?? ''; + } + + return ''; + } + + public function filter_document_title($title) { + if (is_admin() || is_feed()) { + return $title; + } + + $meta_title = $this->get_context_meta_title(); + if (!empty($meta_title)) { + return $this->build_document_title_from_meta($meta_title); + } + + return $title; + } + + public function output_meta_description() { + if (is_admin() || is_feed()) { + return; + } + + $meta_description = $this->get_context_meta_description(); + if (!empty($meta_description)) { + echo "\n" . '' . "\n"; + } + } + + + public function register_query_vars($vars) { + $vars[] = 'ansico_wp_basic_sitemap'; + $vars[] = 'ansico_wp_basic_sitemap_name'; + return $vars; + } + + public function register_sitemap_rewrites() { + $settings = $this->get_settings(); + if (empty($settings['enable_sitemap_module'])) { + return; + } + add_rewrite_rule('^ansico-sitemap\.xml$', 'index.php?ansico_wp_basic_sitemap=index', 'top'); + add_rewrite_rule('^ansico-sitemap-([a-z0-9_-]+)-([a-z0-9_-]+)\.xml$', 'index.php?ansico_wp_basic_sitemap=$matches[1]&ansico_wp_basic_sitemap_name=$matches[2]', 'top'); + } + + public function maybe_render_sitemap() { + $settings = $this->get_settings(); + if (empty($settings['enable_sitemap_module'])) { + return; + } + $type = get_query_var('ansico_wp_basic_sitemap'); + if (!$type) { + return; + } + + $name = get_query_var('ansico_wp_basic_sitemap_name'); + nocache_headers(); + header('Content-Type: application/xml; charset=' . get_bloginfo('charset'), true); + + if ($type === 'index') { + echo $this->get_sitemap_index_xml(); + exit; + } + + echo $this->get_sitemap_section_xml($type, $name); + exit; + } + + private function get_sitemap_index_xml() { + $items = []; + $settings = $this->get_settings(); + if (empty($settings['enable_sitemap_module'])) { + return ''; + } + + foreach ($settings['enabled_post_types'] as $post_type) { + $object = get_post_type_object($post_type); + if (!$object || !$this->post_type_has_published_content($post_type)) { + continue; + } + $items[] = [ + 'loc' => home_url('/ansico-sitemap-post-' . $post_type . '.xml'), + 'lastmod' => $this->get_latest_post_modified_gmt($post_type), + ]; + } + + if (!empty($settings['enable_taxonomy_fields'])) { + foreach ($this->get_public_taxonomies() as $taxonomy) { + if (!$this->taxonomy_has_terms($taxonomy->name)) { + continue; + } + $items[] = [ + 'loc' => home_url('/ansico-sitemap-taxonomy-' . $taxonomy->name . '.xml'), + 'lastmod' => current_time('mysql', true), + ]; + } + } + + if (!empty($settings['enable_author_fields']) && $this->site_has_authors_with_posts()) { + $items[] = [ + 'loc' => home_url('/ansico-sitemap-author-users.xml'), + 'lastmod' => current_time('mysql', true), + ]; + } + + $items[] = [ + 'loc' => home_url('/ansico-sitemap-page-archives.xml'), + 'lastmod' => current_time('mysql', true), + ]; + + $xml = '' . "\n"; + $xml .= ''; + foreach ($items as $item) { + $xml .= ''; + $xml .= '' . esc_url($item['loc']) . ''; + if (!empty($item['lastmod'])) { + $xml .= '' . esc_html(mysql2date('c', $item['lastmod'], false)) . ''; + } + $xml .= ''; + } + $xml .= ''; + return $xml; + } + + private function get_sitemap_section_xml($type, $name) { + $urls = []; + if ($type === 'post') { + $urls = $this->get_post_sitemap_urls($name); + } elseif ($type === 'taxonomy') { + $urls = $this->get_taxonomy_sitemap_urls($name); + } elseif ($type === 'author' && $name === 'users') { + $urls = $this->get_author_sitemap_urls(); + } elseif ($type === 'page' && $name === 'archives') { + $urls = $this->get_archive_sitemap_urls(); + } + + $xml = '' . "\n"; + $xml .= ''; + foreach ($urls as $url) { + $xml .= ''; + $xml .= '' . esc_url($url['loc']) . ''; + if (!empty($url['lastmod'])) { + $xml .= '' . esc_html(mysql2date('c', $url['lastmod'], false)) . ''; + } + if (!empty($url['changefreq'])) { + $xml .= '' . esc_html($url['changefreq']) . ''; + } + if (isset($url['priority'])) { + $xml .= '' . esc_html(number_format((float) $url['priority'], 1, '.', '')) . ''; + } + $xml .= ''; + } + $xml .= ''; + return $xml; + } + + private function post_type_has_published_content($post_type) { + global $wpdb; + $count = (int) $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_type = %s AND post_status = 'publish'", + $post_type + )); + return $count > 0; + } + + private function get_latest_post_modified_gmt($post_type) { + global $wpdb; + $lastmod = $wpdb->get_var($wpdb->prepare( + "SELECT post_modified_gmt FROM {$wpdb->posts} WHERE post_type = %s AND post_status = 'publish' ORDER BY post_modified_gmt DESC LIMIT 1", + $post_type + )); + return $lastmod ?: current_time('mysql', true); + } + + private function taxonomy_has_terms($taxonomy) { + $terms = get_terms([ + 'taxonomy' => $taxonomy, + 'hide_empty' => true, + 'number' => 1, + 'fields' => 'ids', + ]); + return !is_wp_error($terms) && !empty($terms); + } + + private function site_has_authors_with_posts() { + $users = get_users([ + 'has_published_posts' => array_keys($this->get_public_post_types()), + 'number' => 1, + 'fields' => 'ids', + ]); + return !empty($users); + } + + private function get_post_sitemap_urls($post_type) { + $settings = $this->get_settings(); + if (!in_array($post_type, $settings['enabled_post_types'], true)) { + return []; + } + + $posts = get_posts([ + 'post_type' => $post_type, + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'orderby' => 'modified', + 'order' => 'DESC', + 'fields' => 'all', + 'no_found_rows' => true, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ]); + + $urls = []; + foreach ($posts as $post) { + $loc = get_permalink($post); + if (!$loc) { + continue; + } + $urls[] = [ + 'loc' => $loc, + 'lastmod' => get_post_modified_time('Y-m-d H:i:s', true, $post), + 'changefreq' => 'weekly', + 'priority' => $post_type === 'page' ? 0.8 : 0.6, + ]; + } + return $urls; + } + + private function get_taxonomy_sitemap_urls($taxonomy) { + $settings = $this->get_settings(); + if (empty($settings['enable_taxonomy_fields'])) { + return []; + } + + $taxonomy_object = get_taxonomy($taxonomy); + if (!$taxonomy_object || empty($taxonomy_object->public)) { + return []; + } + + $terms = get_terms([ + 'taxonomy' => $taxonomy, + 'hide_empty' => true, + 'number' => 0, + ]); + if (is_wp_error($terms) || empty($terms)) { + return []; + } + + $urls = []; + foreach ($terms as $term) { + $loc = get_term_link($term); + if (is_wp_error($loc)) { + continue; + } + $urls[] = [ + 'loc' => $loc, + 'lastmod' => current_time('mysql', true), + 'changefreq' => 'weekly', + 'priority' => 0.5, + ]; + } + return $urls; + } + + private function get_author_sitemap_urls() { + $settings = $this->get_settings(); + if (empty($settings['enable_author_fields'])) { + return []; + } + + $users = get_users([ + 'has_published_posts' => array_keys($this->get_public_post_types()), + 'fields' => ['ID'], + ]); + + $urls = []; + foreach ($users as $user) { + $urls[] = [ + 'loc' => get_author_posts_url($user->ID), + 'lastmod' => current_time('mysql', true), + 'changefreq' => 'weekly', + 'priority' => 0.4, + ]; + } + return $urls; + } + + private function get_archive_sitemap_urls() { + $urls = [[ + 'loc' => home_url('/'), + 'lastmod' => current_time('mysql', true), + 'changefreq' => 'daily', + 'priority' => 1.0, + ]]; + + if (get_option('show_on_front') === 'page') { + $page_for_posts = (int) get_option('page_for_posts'); + if ($page_for_posts) { + $loc = get_permalink($page_for_posts); + if ($loc) { + $urls[] = [ + 'loc' => $loc, + 'lastmod' => get_post_modified_time('Y-m-d H:i:s', true, $page_for_posts), + 'changefreq' => 'daily', + 'priority' => 0.8, + ]; + } + } + } + + foreach ($this->get_archive_post_types() as $post_type) { + $loc = get_post_type_archive_link($post_type->name); + if ($loc) { + $urls[] = [ + 'loc' => $loc, + 'lastmod' => $this->get_latest_post_modified_gmt($post_type->name), + 'changefreq' => 'daily', + 'priority' => 0.7, + ]; + } + } + + return $urls; + } + + public function settings_notice_if_no_types() { + $screen = function_exists('get_current_screen') ? get_current_screen() : null; + if (!$screen || $screen->id !== 'toplevel_page_ansico-wp-basic') { + return; + } + + $settings = $this->get_settings(); + if (!empty($settings['enabled_post_types'])) { + return; + } + + echo '

' . esc_html__('No post types are enabled. Select at least one post type to use the SEO fields on content edit screens.', 'ansico-wp-basic') . '

'; + } +} + +new Ansico_WP_Basic(); diff --git a/ansico-wp-basic/assets/admin.css b/ansico-wp-basic/assets/admin.css new file mode 100644 index 0000000..65edd61 --- /dev/null +++ b/ansico-wp-basic/assets/admin.css @@ -0,0 +1,125 @@ +.ansico-wp-basic-metabox .description { + display: block; + margin-top: 4px; +} + +.ansico-wp-basic-counter { + display: block; + margin-top: 6px; + font-size: 12px; + font-weight: 600; +} + +.ansico-wp-basic-counter.is-good { + color: #2f6f3e; +} + +.ansico-wp-basic-counter.is-warning { + color: #b45309; +} + +.ansico-wp-basic-counter.is-danger { + color: #b32d2e; +} + +.ansico-wp-basic-snippet-wrapper { + margin-top: 18px; + border-top: 1px solid #e0e0e0; + padding-top: 14px; +} + +.ansico-wp-basic-snippet { + margin-top: 12px; + background: #fff; + padding: 6px 0; + max-width: 652px; + font-family: Arial, sans-serif; +} + +.ansico-wp-basic-snippet-site { + color: #202124; + font-size: 14px; + line-height: 1.3; + margin-bottom: 1px; +} + +.ansico-wp-basic-snippet-url { + color: #4d5156; + font-size: 14px; + line-height: 1.3; + margin-bottom: 3px; + word-break: break-word; +} + +.ansico-wp-basic-snippet-title { + color: #1a0dab; + font-size: 22px; + line-height: 1.3; + margin-bottom: 4px; + cursor: default; +} + +.ansico-wp-basic-snippet-description { + color: #4d5156; + font-size: 14px; + line-height: 1.58; +} + +.ansico-wp-basic-truncation-note { + margin-top: 8px; +} + +.ansico-wp-basic-truncation-note.is-warning { + color: #b45309; + font-weight: 600; +} + +.ansico-wp-basic-settings-page .form-table th { + width: 280px; +} + +.ansico-wp-basic-settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 16px; +} + +.ansico-wp-basic-settings-card { + background: #fff; + border: 1px solid #dcdcde; + border-radius: 8px; + padding: 14px; +} + +.ansico-wp-basic-settings-card h3 { + margin-top: 0; +} + +.ansico-wp-basic-term-fields .ansico-wp-basic-metabox, +.ansico-wp-basic-term-fields-wrap .ansico-wp-basic-metabox { + background: #fff; +} + + +.ansico-wp-basic-tools-box p { + margin: 0 0 14px; +} + +.ansico-wp-basic-tools-box label strong { + display: inline-block; + margin-bottom: 6px; +} + +#ansico_wp_basic_change_post_type, +.ansico-post-type-select { + width: auto; + min-width: 0; + max-width: 220px; + display: inline-block; + vertical-align: middle; +} + +.ansico-wp-basic-tools-box .description { + display: block; + margin-top: 6px; +} diff --git a/ansico-wp-basic/assets/admin.js b/ansico-wp-basic/assets/admin.js new file mode 100644 index 0000000..33c42d8 --- /dev/null +++ b/ansico-wp-basic/assets/admin.js @@ -0,0 +1,160 @@ +document.addEventListener('DOMContentLoaded', function () { + var titleInput = document.getElementById('ansico_wp_basic_meta_title'); + var descInput = document.getElementById('ansico_wp_basic_meta_description'); + var snippet = document.querySelector('.ansico-wp-basic-snippet'); + + if (!titleInput || !descInput || !snippet) { + return; + } + + var titleTarget = snippet.querySelector('.ansico-wp-basic-snippet-title'); + var descTextTarget = snippet.querySelector('.ansico-wp-basic-snippet-description-text'); + var urlTarget = snippet.querySelector('.ansico-wp-basic-snippet-url'); + var truncationNote = document.querySelector('.ansico-wp-basic-truncation-note'); + var titleCounter = document.querySelector('[data-counter-for="title"]'); + var descCounter = document.querySelector('[data-counter-for="description"]'); + + var titleMaxPixels = 580; + var descMaxPixels = 920; + var titleIdealMin = 30; + var titleIdealMax = 60; + var descIdealMin = 70; + var descIdealMax = 155; + + var titleCanvas = document.createElement('canvas'); + var descCanvas = document.createElement('canvas'); + var titleContext = titleCanvas.getContext('2d'); + var descContext = descCanvas.getContext('2d'); + titleContext.font = '400 22px Arial'; + descContext.font = '400 14px Arial'; + + function measure(context, text) { + return Math.round(context.measureText(text || '').width); + } + + function truncateByPixels(text, maxPixels, context) { + text = (text || '').trim(); + var fullWidth = measure(context, text); + + if (!text || fullWidth <= maxPixels) { + return { + visible: text, + isTruncated: false, + hiddenChars: 0, + hiddenText: '', + width: fullWidth + }; + } + + var ellipsis = '…'; + var low = 0; + var high = text.length; + var best = ''; + + while (low <= high) { + var mid = Math.floor((low + high) / 2); + var candidate = text.slice(0, mid).replace(/[\s.,;:-]+$/u, ''); + var candidateWithEllipsis = candidate + ellipsis; + if (measure(context, candidateWithEllipsis) <= maxPixels) { + best = candidate; + low = mid + 1; + } else { + high = mid - 1; + } + } + + if (!best) { + best = text.slice(0, 1); + } + + var hiddenText = text.slice(best.length).trim(); + return { + visible: best + ellipsis, + isTruncated: true, + hiddenChars: hiddenText.length, + hiddenText: hiddenText, + width: fullWidth + }; + } + + function setCounter(el, current, minIdeal, maxIdeal, pxWidth, pxLimit, overflowText) { + if (!el) { + return; + } + + el.classList.remove('is-good', 'is-warning', 'is-danger'); + + var statusClass = 'is-good'; + var notes = []; + + if (current === 0) { + statusClass = 'is-warning'; + notes.push('empty'); + } else if (current < minIdeal) { + statusClass = 'is-warning'; + notes.push('a bit short'); + } + + if (current > maxIdeal || pxWidth > pxLimit) { + statusClass = (current > maxIdeal + 15 || pxWidth > pxLimit + 120) ? 'is-danger' : 'is-warning'; + notes.push(overflowText); + } + + el.classList.add(statusClass); + el.textContent = current + ' chars · ' + pxWidth + ' px'; + if (notes.length) { + el.textContent += ' — ' + notes.join(' · '); + } + } + + function updateSnippet() { + var rawTitle = titleInput.value.trim() || snippet.dataset.fallbackTitle || ''; + var siteName = (snippet.dataset.siteName || '').trim(); + var title = rawTitle; + if (siteName && rawTitle.toLowerCase().indexOf(siteName.toLowerCase()) === -1) { + title += ' - ' + siteName; + } + + var desc = descInput.value.trim() || 'This is how your page may appear in Google search results when a meta description is available.'; + var permalink; + + try { + permalink = new URL(snippet.dataset.permalink); + } catch (e) { + permalink = null; + } + + var host = permalink ? permalink.hostname.replace(/^www\./, '') : ''; + var path = permalink ? permalink.pathname.replace(/^\//, '').replace(/\/$/, '') : ''; + var prettyUrl = host; + if (path) { + prettyUrl += ' › ' + path.replace(/\//g, ' › '); + } + + var titleResult = truncateByPixels(title, titleMaxPixels, titleContext); + var descResult = truncateByPixels(desc, descMaxPixels, descContext); + var rawTitleWidth = measure(titleContext, title); + var rawDescWidth = measure(descContext, desc); + + titleTarget.textContent = titleResult.visible; + descTextTarget.textContent = descResult.visible; + urlTarget.textContent = prettyUrl; + + setCounter(titleCounter, rawTitle.length, titleIdealMin, titleIdealMax, rawTitleWidth, titleMaxPixels, 'will truncate in preview'); + setCounter(descCounter, desc.length, descIdealMin, descIdealMax, rawDescWidth, descMaxPixels, 'will truncate in preview'); + + if (truncationNote) { + if (descResult.isTruncated) { + truncationNote.textContent = 'Preview is truncating the meta description. About ' + descResult.hiddenChars + ' characters are no longer visible.'; + truncationNote.classList.add('is-warning'); + } else { + truncationNote.textContent = 'Preview currently fits within the available description space.'; + truncationNote.classList.remove('is-warning'); + } + } + } + + titleInput.addEventListener('input', updateSnippet); + descInput.addEventListener('input', updateSnippet); + updateSnippet(); +}); diff --git a/ansico-wp-basic/readme.txt b/ansico-wp-basic/readme.txt new file mode 100644 index 0000000..afad00a --- /dev/null +++ b/ansico-wp-basic/readme.txt @@ -0,0 +1,43 @@ +=== Ansico WP Basic === +Contributors: ansico +Tags: seo, meta title, meta description, search preview +Requires at least: 6.0 +Tested up to: 6.5 +Requires PHP: 7.4 +Stable tag: 0.0.0.2 +License: GPLv3 or later +License URI: https://www.gnu.org/licenses/gpl-3.0.html + +Basic SEO fields for posts, pages, custom post types, author archives, taxonomy archives, and special archive pages. + +== Description == + +Ansico WP Basic adds simple SEO fields to WordPress: + +- Meta title and meta description fields for posts, pages, and public custom post types +- Live search result preview in the editor +- SEO fields for author archives via user profiles +- SEO fields for taxonomy archives such as categories, tags, and public custom taxonomies +- Settings for archive-style pages such as blog home, date archives, search results, 404, and post type archives +- Outputs the meta description tag in the frontend head when a description is available +- Uses the custom meta title as the document title when one is available + +== Installation == + +1. Upload the plugin ZIP file in WordPress. +2. Activate the plugin. +3. Go to Ansico WP Basic > Settings. +4. Choose which post types should have SEO fields. + +== Changelog == + += 1.1.0 = +- Improved search result preview styling +- Made meta description output always active +- Removed title and description output toggles from settings +- Added support for author archives +- Added support for taxonomy term archives +- Added settings for archive and special pages + += 1.0.0 = +- Initial release