diff --git a/ansico-wp-basic-1.0.0-final.zip b/ansico-wp-basic-1.0.0-final.zip new file mode 100644 index 0000000..3f70f76 Binary files /dev/null and b/ansico-wp-basic-1.0.0-final.zip differ diff --git a/ansico-wp-basic-1.0.0-final.zip:Zone.Identifier b/ansico-wp-basic-1.0.0-final.zip:Zone.Identifier new file mode 100644 index 0000000..d6c1ec6 Binary files /dev/null and b/ansico-wp-basic-1.0.0-final.zip:Zone.Identifier differ diff --git a/ansico-wp-basic-v0.0.0.6.zip b/ansico-wp-basic-v0.0.0.6.zip deleted file mode 100644 index 73fb2a7..0000000 Binary files a/ansico-wp-basic-v0.0.0.6.zip and /dev/null differ diff --git a/ansico-wp-basic/ansico-wp-basic.php b/ansico-wp-basic/ansico-wp-basic.php index df5e36d..680626e 100644 --- a/ansico-wp-basic/ansico-wp-basic.php +++ b/ansico-wp-basic/ansico-wp-basic.php @@ -3,9 +3,10 @@ * Plugin Name: Ansico WP Basic * Plugin URI: https://ansico.dk * Description: Basic SEO fields for posts, pages, custom post types, author archives, taxonomy archives, and special archive pages, including meta title, meta description, and a live search result preview. - * Version: 0.0.0.4 + * Version: 1.0.0 * Author: Andreas Andersen (Ansico) * Author URI: https://ansico.dk + * Support URI: https://ansico.dk/Ansico/Ansico-WP-Basic * License: GPL-3.0-or-later * License URI: https://www.gnu.org/licenses/gpl-3.0.html * Text Domain: ansico-wp-basic @@ -29,6 +30,11 @@ final class Ansico_WP_Basic { const NONCE_KEY = 'ansico_wp_basic_meta_box_nonce'; const TERM_NONCE_KEY = 'ansico_wp_basic_term_nonce'; const USER_NONCE_KEY = 'ansico_wp_basic_user_nonce'; + const IMPORT_ACTION = 'ansico_wp_basic_import_yoast'; + const GENERATE_ACTION = 'ansico_wp_basic_generate_missing_meta'; + const SOCIAL_IMAGE_KEY = '_ansico_wp_basic_social_image'; + const CANONICAL_KEY = '_ansico_wp_basic_canonical'; + const X_CREATOR_KEY = '_ansico_wp_basic_x_creator'; public static function activate() { $plugin = new self(); @@ -43,6 +49,8 @@ final class Ansico_WP_Basic { public function __construct() { add_action('admin_menu', [$this, 'register_admin_menu']); add_action('admin_init', [$this, 'register_settings']); + add_action('admin_init', [$this, 'maybe_handle_yoast_import']); + add_action('admin_init', [$this, 'maybe_handle_generate_missing_meta']); add_action('add_meta_boxes', [$this, 'register_meta_boxes']); add_action('save_post', [$this, 'save_meta_box']); @@ -58,10 +66,13 @@ final class Ansico_WP_Basic { add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']); add_filter('pre_get_document_title', [$this, 'filter_document_title'], 20); + add_action('wp', [$this, 'maybe_disable_core_canonical'], 1); add_action('wp_head', [$this, 'output_meta_description'], 1); + add_action('wp_head', [$this, 'output_frontend_meta_tags'], 2); 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('admin_notices', [$this, 'render_import_notice']); add_action('init', [$this, 'register_taxonomy_hooks']); add_action('init', [$this, 'register_sitemap_rewrites']); add_action('template_redirect', [$this, 'maybe_render_sitemap']); @@ -75,6 +86,11 @@ final class Ansico_WP_Basic { 'enabled_post_types' => $this->get_default_post_types(), 'enable_author_fields' => 1, 'enable_taxonomy_fields'=> 1, + 'social_defaults' => [ + 'facebook_publisher_url' => '', + 'x_site_handle' => '', + 'default_social_image' => '', + ], 'special_pages' => [ 'date' => ['title' => '', 'description' => ''], 'search'=> ['title' => '', 'description' => ''], @@ -95,6 +111,7 @@ final class Ansico_WP_Basic { $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['social_defaults'] = $this->sanitize_social_defaults($settings['social_defaults'] ?? []); $settings['special_pages'] = $this->sanitize_special_pages($settings['special_pages']); $settings['post_type_archives'] = $this->sanitize_post_type_archive_settings($settings['post_type_archives']); @@ -133,6 +150,18 @@ final class Ansico_WP_Basic { return $post_types; } + private function sanitize_social_defaults($social_defaults) { + $social_defaults = is_array($social_defaults) ? $social_defaults : []; + + return [ + 'facebook_publisher_url' => isset($social_defaults['facebook_publisher_url']) ? esc_url_raw($social_defaults['facebook_publisher_url']) : '', + 'x_site_handle' => isset($social_defaults['x_site_handle']) ? sanitize_text_field($social_defaults['x_site_handle']) : '', + 'default_social_image' => isset($social_defaults['default_social_image']) ? esc_url_raw($social_defaults['default_social_image']) : '', + 'organization_name' => isset($social_defaults['organization_name']) ? sanitize_text_field($social_defaults['organization_name']) : '', + 'organization_logo' => isset($social_defaults['organization_logo']) ? esc_url_raw($social_defaults['organization_logo']) : '', + ]; + } + private function sanitize_special_pages($special_pages) { $defaults = [ 'date' => ['title' => '', 'description' => ''], @@ -247,6 +276,23 @@ final class Ansico_WP_Basic { 'ansico_wp_basic_main_section' ); + add_settings_section( + 'ansico_wp_basic_social_section', + __('Social tags and schema', 'ansico-wp-basic'), + function () { + echo '

' . esc_html__('Configure site-wide defaults used for Open Graph, Twitter/X cards, canonical URLs, and schema markup output.', 'ansico-wp-basic') . '

'; + }, + 'ansico-wp-basic' + ); + + add_settings_field( + 'social_defaults', + __('Social defaults', 'ansico-wp-basic'), + [$this, 'render_social_defaults_fields'], + 'ansico-wp-basic', + 'ansico_wp_basic_social_section' + ); + add_settings_section( 'ansico_wp_basic_archives_section', __('Archives and special pages', 'ansico-wp-basic'), @@ -271,6 +317,31 @@ final class Ansico_WP_Basic { 'ansico-wp-basic', 'ansico_wp_basic_archives_section' ); + + add_settings_section( + 'ansico_wp_basic_tools_section', + __('Import tools', 'ansico-wp-basic'), + function () { + echo '

' . esc_html__('Import existing SEO title, meta description, social defaults, canonical URLs, and social images from another plugin into Ansico WP Basic.', 'ansico-wp-basic') . '

'; + }, + 'ansico-wp-basic' + ); + + add_settings_field( + 'yoast_import', + __('Import from Yoast SEO', 'ansico-wp-basic'), + [$this, 'render_yoast_import_field'], + 'ansico-wp-basic', + 'ansico_wp_basic_tools_section' + ); + + add_settings_field( + 'generate_missing_meta', + __('Generate missing META fields', 'ansico-wp-basic'), + [$this, 'render_generate_missing_meta_field'], + 'ansico-wp-basic', + 'ansico_wp_basic_tools_section' + ); } public function sanitize_settings($input) { @@ -292,6 +363,7 @@ final class Ansico_WP_Basic { '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, + 'social_defaults' => $this->sanitize_social_defaults(isset($input['social_defaults']) ? $input['social_defaults'] : []), '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'] : []), ]; @@ -311,23 +383,35 @@ final class Ansico_WP_Basic { public function render_sitemap_module_toggle() { $settings = $this->get_settings(); + $sitemap_url = home_url('/sitemap.xml'); 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') + esc_html__('Generate a sitemap index for supported content types.', 'ansico-wp-basic') ); + + if (!empty($settings['enable_sitemap_module'])) { + echo '
'; + echo '' . esc_html__('Sitemap URL:', 'ansico-wp-basic') . ''; + echo '' . esc_html($sitemap_url) . ''; + echo ''; + echo '
'; + echo '

' . esc_html__('Use this URL when submitting your sitemap to search engines or external tools.', 'ansico-wp-basic') . '

'; + } } public function render_post_types_field() { $settings = $this->get_settings(); $public_post_types = $this->get_public_post_types(); - echo '
'; + 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), @@ -335,6 +419,7 @@ final class Ansico_WP_Basic { ); } echo '
'; + echo '

' . esc_html__('Only selected post types will get Ansico SEO fields and bulk generation tools.', 'ansico-wp-basic') . '

'; } public function render_author_field_toggle() { @@ -359,6 +444,20 @@ final class Ansico_WP_Basic { ); } + public function render_social_defaults_fields() { + $settings = $this->get_settings(); + $social = $settings['social_defaults'] ?? []; + + echo '
'; + echo '

'; + echo '

'; + echo '

'; + echo '

'; + echo '

'; + echo '

' . esc_html__('Used for Open Graph, Twitter/X, canonical tags, and lightweight schema output. These values can also be imported from Yoast SEO when available.', 'ansico-wp-basic') . '

'; + echo '
'; + } + public function render_special_pages_fields() { $settings = $this->get_settings(); if (empty($settings['enable_meta_module'])) { @@ -392,6 +491,394 @@ final class Ansico_WP_Basic { echo ''; } + private function is_yoast_available_for_import() { + if (defined('WPSEO_VERSION') || class_exists('WPSEO_Options')) { + return true; + } + + if (get_option('wpseo') || get_option('wpseo_taxonomy_meta') || get_option('wpseo_titles')) { + return true; + } + + $active_plugins = (array) get_option('active_plugins', []); + if (in_array('wordpress-seo/wp-seo.php', $active_plugins, true)) { + return true; + } + + if (is_multisite()) { + $network_active = array_keys((array) get_site_option('active_sitewide_plugins', [])); + if (in_array('wordpress-seo/wp-seo.php', $network_active, true)) { + return true; + } + } + + return false; + } + + public function render_yoast_import_field() { + if (!current_user_can('manage_options')) { + return; + } + + if (!$this->is_yoast_available_for_import()) { + echo '

' . esc_html__('Yoast SEO was not detected on this site, and no Yoast SEO data was found in the database.', 'ansico-wp-basic') . '

'; + return; + } + + $url = wp_nonce_url( + add_query_arg([ + 'page' => 'ansico-wp-basic', + 'tab' => 'tools', + 'action' => self::IMPORT_ACTION, + ], admin_url('admin.php')), + self::IMPORT_ACTION, + 'ansico_wp_basic_import_nonce' + ); + + echo '
'; + echo '

' . esc_html__('Copies filled Yoast SEO meta title, meta description, canonical URL, social image, and selected site-wide social settings into Ansico WP Basic. Existing Ansico values are kept unless you explicitly overwrite them.', 'ansico-wp-basic') . '

'; + echo '

' . esc_html__('Import from Yoast SEO', 'ansico-wp-basic') . '

'; + echo '

' . esc_html__('Safe to run multiple times. This importer currently brings over title, description, canonical URL, Open Graph image, and selected Yoast social defaults.', 'ansico-wp-basic') . '

'; + echo '
'; + } + + public function render_generate_missing_meta_field() { + if (!current_user_can('manage_options')) { + return; + } + + $settings = $this->get_settings(); + if (empty($settings['enable_meta_module'])) { + echo '

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

'; + return; + } + + $enabled_types = !empty($settings['enabled_post_types']) ? $settings['enabled_post_types'] : []; + $labels = []; + foreach ($enabled_types as $post_type) { + $obj = get_post_type_object($post_type); + if ($obj && !empty($obj->labels->singular_name)) { + $labels[] = $obj->labels->singular_name; + } else { + $labels[] = $post_type; + } + } + + $url = wp_nonce_url( + add_query_arg([ + 'page' => 'ansico-wp-basic', + 'tab' => 'tools', + 'action' => self::GENERATE_ACTION, + ], admin_url('admin.php')), + self::GENERATE_ACTION, + 'ansico_wp_basic_generate_nonce' + ); + + echo '
'; + echo '

' . esc_html__('Generate missing META title and META description values for existing posts where one or both fields are empty. Existing Ansico values are kept.', 'ansico-wp-basic') . '

'; + if (!empty($labels)) { + echo '

' . esc_html(sprintf(__('Runs on enabled post types: %s.', 'ansico-wp-basic'), implode(', ', $labels))) . '

'; + } + echo '

' . esc_html__('Generate missing META fields', 'ansico-wp-basic') . '

'; + echo '

' . esc_html__('META title is generated from the post title. META description uses the first paragraph of the content, trimmed to up to 150 characters.', 'ansico-wp-basic') . '

'; + echo '
'; + } + + public function maybe_handle_yoast_import() { + if (!is_admin() || !current_user_can('manage_options')) { + return; + } + + if (!isset($_GET['page'], $_GET['action']) || $_GET['page'] !== 'ansico-wp-basic' || $_GET['action'] !== self::IMPORT_ACTION) { + return; + } + + check_admin_referer(self::IMPORT_ACTION, 'ansico_wp_basic_import_nonce'); + + $result = $this->import_yoast_meta_data(); + + $redirect_url = add_query_arg([ + 'page' => 'ansico-wp-basic', + 'tab' => 'tools', + 'ansico_wp_basic_notice' => rawurlencode(wp_json_encode($result)), + ], admin_url('admin.php')); + + wp_safe_redirect($redirect_url); + exit; + } + + private function import_yoast_meta_data() { + $overwrite_existing = false; + $result = [ + 'type' => 'success', + 'message' => __('Yoast SEO data import completed.', 'ansico-wp-basic'), + 'posts_imported' => 0, + 'terms_imported' => 0, + 'posts_skipped' => 0, + 'terms_skipped' => 0, + 'settings_imported' => 0, + ]; + + $settings = $this->get_settings(); + $yoast_social = get_option('wpseo_social', []); + if (is_array($yoast_social)) { + $site_settings_imported = 0; + if (empty($settings['social_defaults']['facebook_publisher_url']) && !empty($yoast_social['facebook_site'])) { + $settings['social_defaults']['facebook_publisher_url'] = esc_url_raw($yoast_social['facebook_site']); + $site_settings_imported++; + } + if (empty($settings['social_defaults']['x_site_handle']) && !empty($yoast_social['twitter_site'])) { + $settings['social_defaults']['x_site_handle'] = $this->normalize_x_handle($yoast_social['twitter_site']); + $site_settings_imported++; + } + if (empty($settings['social_defaults']['default_social_image']) && !empty($yoast_social['og_default_image'])) { + $settings['social_defaults']['default_social_image'] = esc_url_raw($yoast_social['og_default_image']); + $site_settings_imported++; + } + if ($site_settings_imported > 0) { + update_option(self::OPTION_KEY, $settings); + $result['settings_imported'] = $site_settings_imported; + } + } + + $public_post_types = array_keys($this->get_public_post_types()); + if (!empty($public_post_types)) { + $posts = get_posts([ + 'post_type' => $public_post_types, + 'post_status' => 'any', + 'posts_per_page' => -1, + 'fields' => 'ids', + 'orderby' => 'ID', + 'order' => 'ASC', + 'suppress_filters' => true, + ]); + + foreach ((array) $posts as $post_id) { + $yoast_title = get_post_meta($post_id, '_yoast_wpseo_title', true); + $yoast_description = get_post_meta($post_id, '_yoast_wpseo_metadesc', true); + $yoast_canonical = get_post_meta($post_id, '_yoast_wpseo_canonical', true); + $yoast_social_image = get_post_meta($post_id, '_yoast_wpseo_opengraph-image', true); + if ($yoast_social_image === '') { + $yoast_social_image = get_post_meta($post_id, '_yoast_wpseo_opengraph-image-url', true); + } + + if ($yoast_title === '' && $yoast_description === '' && $yoast_canonical === '' && $yoast_social_image === '') { + continue; + } + + $existing_title = get_post_meta($post_id, self::META_TITLE_KEY, true); + $existing_description = get_post_meta($post_id, self::META_DESC_KEY, true); + $existing_canonical = get_post_meta($post_id, self::CANONICAL_KEY, true); + $existing_social_image = get_post_meta($post_id, self::SOCIAL_IMAGE_KEY, true); + + if ($yoast_title !== '' && ($overwrite_existing || $existing_title === '')) { + update_post_meta($post_id, self::META_TITLE_KEY, sanitize_text_field($yoast_title)); + $result['posts_imported']++; + } + elseif ($yoast_title !== '') { + $result['posts_skipped']++; + } + + if ($yoast_description !== '' && ($overwrite_existing || $existing_description === '')) { + update_post_meta($post_id, self::META_DESC_KEY, sanitize_textarea_field($yoast_description)); + $result['posts_imported']++; + } + elseif ($yoast_description !== '') { + $result['posts_skipped']++; + } + + if ($yoast_canonical !== '' && ($overwrite_existing || $existing_canonical === '')) { + update_post_meta($post_id, self::CANONICAL_KEY, esc_url_raw($yoast_canonical)); + $result['posts_imported']++; + } + elseif ($yoast_canonical !== '') { + $result['posts_skipped']++; + } + + if ($yoast_social_image !== '' && ($overwrite_existing || $existing_social_image === '')) { + update_post_meta($post_id, self::SOCIAL_IMAGE_KEY, esc_url_raw($yoast_social_image)); + $result['posts_imported']++; + } + elseif ($yoast_social_image !== '') { + $result['posts_skipped']++; + } + } + } + + $yoast_taxonomy_meta = get_option('wpseo_taxonomy_meta', []); + if (is_array($yoast_taxonomy_meta) && !empty($yoast_taxonomy_meta)) { + $public_taxonomies = $this->get_public_taxonomies(); + + foreach ($public_taxonomies as $taxonomy => $taxonomy_obj) { + if (empty($yoast_taxonomy_meta[$taxonomy]) || !is_array($yoast_taxonomy_meta[$taxonomy])) { + continue; + } + + foreach ($yoast_taxonomy_meta[$taxonomy] as $term_id => $term_meta) { + $term_id = (int) $term_id; + if ($term_id <= 0 || !is_array($term_meta)) { + continue; + } + + $term = get_term($term_id, $taxonomy); + if (!$term || is_wp_error($term)) { + continue; + } + + $yoast_title = isset($term_meta['wpseo_title']) ? (string) $term_meta['wpseo_title'] : ''; + $yoast_description = isset($term_meta['wpseo_desc']) ? (string) $term_meta['wpseo_desc'] : ''; + $yoast_canonical = isset($term_meta['wpseo_canonical']) ? (string) $term_meta['wpseo_canonical'] : ''; + $yoast_social_image = isset($term_meta['wpseo_opengraph-image']) ? (string) $term_meta['wpseo_opengraph-image'] : ''; + if ($yoast_social_image === '' && isset($term_meta['wpseo_opengraph-image-url'])) { + $yoast_social_image = (string) $term_meta['wpseo_opengraph-image-url']; + } + + if ($yoast_title === '' && $yoast_description === '' && $yoast_canonical === '' && $yoast_social_image === '') { + continue; + } + + $existing_title = get_term_meta($term_id, self::META_TITLE_KEY, true); + $existing_description = get_term_meta($term_id, self::META_DESC_KEY, true); + $existing_canonical = get_term_meta($term_id, self::CANONICAL_KEY, true); + $existing_social_image = get_term_meta($term_id, self::SOCIAL_IMAGE_KEY, true); + + if ($yoast_title !== '' && ($overwrite_existing || $existing_title === '')) { + update_term_meta($term_id, self::META_TITLE_KEY, sanitize_text_field($yoast_title)); + $result['terms_imported']++; + } + elseif ($yoast_title !== '') { + $result['terms_skipped']++; + } + + if ($yoast_description !== '' && ($overwrite_existing || $existing_description === '')) { + update_term_meta($term_id, self::META_DESC_KEY, sanitize_textarea_field($yoast_description)); + $result['terms_imported']++; + } + elseif ($yoast_description !== '') { + $result['terms_skipped']++; + } + + if ($yoast_canonical !== '' && ($overwrite_existing || $existing_canonical === '')) { + update_term_meta($term_id, self::CANONICAL_KEY, esc_url_raw($yoast_canonical)); + $result['terms_imported']++; + } + elseif ($yoast_canonical !== '') { + $result['terms_skipped']++; + } + + if ($yoast_social_image !== '' && ($overwrite_existing || $existing_social_image === '')) { + update_term_meta($term_id, self::SOCIAL_IMAGE_KEY, esc_url_raw($yoast_social_image)); + $result['terms_imported']++; + } + elseif ($yoast_social_image !== '') { + $result['terms_skipped']++; + } + } + } + } + + if ($result['posts_imported'] === 0 && $result['terms_imported'] === 0 && $result['settings_imported'] === 0) { + $result['type'] = 'warning'; + $result['message'] = __('No Yoast SEO values were imported.', 'ansico-wp-basic'); + } + + return $result; + } + + public function maybe_handle_generate_missing_meta() { + if (!is_admin() || !current_user_can('manage_options')) { + return; + } + + if (!isset($_GET['page'], $_GET['action']) || $_GET['page'] !== 'ansico-wp-basic' || $_GET['action'] !== self::GENERATE_ACTION) { + return; + } + + check_admin_referer(self::GENERATE_ACTION, 'ansico_wp_basic_generate_nonce'); + + $result = $this->generate_missing_meta_data(); + + $redirect_url = add_query_arg([ + 'page' => 'ansico-wp-basic', + 'tab' => 'tools', + 'ansico_wp_basic_notice' => rawurlencode(wp_json_encode($result)), + ], admin_url('admin.php')); + + wp_safe_redirect($redirect_url); + exit; + } + + private function generate_missing_meta_data() { + $settings = $this->get_settings(); + $post_types = !empty($settings['enabled_post_types']) ? array_values(array_filter(array_map('sanitize_key', (array) $settings['enabled_post_types']))) : []; + + $result = [ + 'type' => 'success', + 'message' => __('Missing META values were generated.', 'ansico-wp-basic'), + 'posts_imported' => 0, + 'terms_imported' => 0, + 'posts_skipped' => 0, + 'terms_skipped' => 0, + 'settings_imported' => 0, + ]; + + if (empty($settings['enable_meta_module']) || empty($post_types)) { + $result['type'] = 'warning'; + $result['message'] = __('No enabled post types were available for META generation.', 'ansico-wp-basic'); + return $result; + } + + $posts = get_posts([ + 'post_type' => $post_types, + 'post_status' => 'any', + 'posts_per_page' => -1, + 'fields' => 'ids', + 'orderby' => 'ID', + 'order' => 'ASC', + 'suppress_filters' => true, + ]); + + foreach ((array) $posts as $post_id) { + $post = get_post($post_id); + if (!$post || empty($post->post_type)) { + continue; + } + + $existing_title = get_post_meta($post_id, self::META_TITLE_KEY, true); + $existing_description = get_post_meta($post_id, self::META_DESC_KEY, true); + $updated = false; + + if ($existing_title === '') { + $generated_title = sanitize_text_field(get_the_title($post)); + if ($generated_title !== '') { + update_post_meta($post_id, self::META_TITLE_KEY, $generated_title); + $result['posts_imported']++; + $updated = true; + } + } + + if ($existing_description === '') { + $generated_description = $this->get_default_meta_description_for_post($post); + if ($generated_description !== '') { + update_post_meta($post_id, self::META_DESC_KEY, sanitize_textarea_field($generated_description)); + $result['posts_imported']++; + $updated = true; + } + } + + if (!$updated) { + $result['posts_skipped']++; + } + } + + if ($result['posts_imported'] === 0) { + $result['type'] = 'warning'; + $result['message'] = __('No missing META values needed to be generated.', 'ansico-wp-basic'); + } + + return $result; + } + public function render_post_type_archive_fields() { $settings = $this->get_settings(); if (empty($settings['enable_meta_module'])) { @@ -425,28 +912,153 @@ final class Ansico_WP_Basic { echo ''; } + public function render_import_notice() { + if (!is_admin() || !current_user_can('manage_options') || empty($_GET['ansico_wp_basic_notice'])) { + return; + } + + $payload = json_decode(wp_unslash($_GET['ansico_wp_basic_notice']), true); + if (!is_array($payload)) { + return; + } + + $type = (!empty($payload['type']) && in_array($payload['type'], ['success', 'warning', 'error'], true)) ? $payload['type'] : 'success'; + $notice_class = $type === 'error' ? 'notice notice-error' : ($type === 'warning' ? 'notice notice-warning' : 'notice notice-success'); + $message = isset($payload['message']) ? (string) $payload['message'] : __('Done.', 'ansico-wp-basic'); + $posts_imported = isset($payload['posts_imported']) ? (int) $payload['posts_imported'] : 0; + $terms_imported = isset($payload['terms_imported']) ? (int) $payload['terms_imported'] : 0; + $posts_skipped = isset($payload['posts_skipped']) ? (int) $payload['posts_skipped'] : 0; + $terms_skipped = isset($payload['terms_skipped']) ? (int) $payload['terms_skipped'] : 0; + $settings_imported = isset($payload['settings_imported']) ? (int) $payload['settings_imported'] : 0; + + echo '

'; + echo esc_html($message); + echo ' '; + echo esc_html(sprintf(__('Imported post fields: %1$d. Imported term fields: %2$d. Skipped existing post fields: %3$d. Skipped existing term fields: %4$d. Imported site settings: %5$d.', 'ansico-wp-basic'), $posts_imported, $terms_imported, $posts_skipped, $terms_skipped, $settings_imported)); + echo '

'; + } + + private function get_current_settings_tab() { + $allowed_tabs = ['general', 'archives', 'tools']; + $tab = isset($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : 'general'; + + return in_array($tab, $allowed_tabs, true) ? $tab : 'general'; + } + public function render_settings_page() { if (!current_user_can('manage_options')) { return; } + + $settings = $this->get_settings(); + $current_tab = $this->get_current_settings_tab(); + $base_url = admin_url('admin.php?page=ansico-wp-basic'); + $tabs = [ + 'general' => __('General settings', 'ansico-wp-basic'), + 'archives' => __('Archives and special pages', 'ansico-wp-basic'), + 'tools' => __('Tools', 'ansico-wp-basic'), + ]; ?>
-

-

- get_settings(); ?> -

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

-
- -
+
+
+

+

+
+
+ + + + +
+
+
+

+
+
+
+

+ render_yoast_import_field(); ?> +
+
+

+ render_generate_missing_meta_field(); ?> +
+
+
+
+ +
+ + +
+ +
+
+

+
+
+
+

+
render_meta_module_toggle(); ?>
+
+
+

+
render_sitemap_module_toggle(); ?>
+
+
+

+
render_post_types_field(); ?>
+
+
+

+
render_author_field_toggle(); ?>
+
+
+

+
render_taxonomy_field_toggle(); ?>
+
+
+
+ +
+
+

+

+
+ render_social_defaults_fields(); ?> +
+ +
+
+

+
+
+

+ render_special_pages_fields(); ?> +
+
+

+ render_post_type_archive_fields(); ?> +
+
+ +
+ +
+ 'button button-primary button-large']); ?> +
+
+
get_settings(); foreach ($settings['enabled_post_types'] as $post_type) { if (!empty($settings['enable_meta_module'])) { @@ -518,6 +1130,22 @@ final class Ansico_WP_Basic {

+

+ + + +

+

+ + + +

+

+ + + +

+
@@ -629,6 +1257,9 @@ final class Ansico_WP_Basic { $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); + $saved_canonical = get_post_meta($post->ID, self::CANONICAL_KEY, true); + $saved_social_image = get_post_meta($post->ID, self::SOCIAL_IMAGE_KEY, true); + $saved_x_creator = get_post_meta($post->ID, self::X_CREATOR_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); @@ -639,6 +1270,9 @@ final class Ansico_WP_Basic { 'meta_description' => $meta_description, 'fallback_title' => $fallback_title, 'permalink' => $permalink, + 'canonical' => $saved_canonical, + 'social_image' => $saved_social_image, + 'x_creator' => $saved_x_creator, ]); } @@ -676,6 +1310,9 @@ final class Ansico_WP_Basic { $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'])) : ''; + $canonical = isset($_POST['ansico_wp_basic_canonical']) ? esc_url_raw(wp_unslash($_POST['ansico_wp_basic_canonical'])) : ''; + $social_image = isset($_POST['ansico_wp_basic_social_image']) ? esc_url_raw(wp_unslash($_POST['ansico_wp_basic_social_image'])) : ''; + $x_creator = isset($_POST['ansico_wp_basic_x_creator']) ? sanitize_text_field(wp_unslash($_POST['ansico_wp_basic_x_creator'])) : ''; if ($meta_title === '') { delete_post_meta($post_id, self::META_TITLE_KEY); @@ -688,6 +1325,24 @@ final class Ansico_WP_Basic { } else { update_post_meta($post_id, self::META_DESC_KEY, $meta_description); } + + if ($canonical === '') { + delete_post_meta($post_id, self::CANONICAL_KEY); + } else { + update_post_meta($post_id, self::CANONICAL_KEY, $canonical); + } + + if ($social_image === '') { + delete_post_meta($post_id, self::SOCIAL_IMAGE_KEY); + } else { + update_post_meta($post_id, self::SOCIAL_IMAGE_KEY, $social_image); + } + + if ($x_creator === '') { + delete_post_meta($post_id, self::X_CREATOR_KEY); + } else { + update_post_meta($post_id, self::X_CREATOR_KEY, $this->normalize_x_handle($x_creator)); + } } public function maybe_switch_post_type($data, $postarr) { @@ -757,6 +1412,9 @@ final class Ansico_WP_Basic { 'meta_description' => '', 'fallback_title' => ucfirst($taxonomy_name) . ' archive', 'permalink' => $example_url, + 'canonical' => '', + 'social_image' => '', + 'x_creator' => '', ]); ?>
term_id, self::META_TITLE_KEY, true); $meta_description = get_term_meta($term->term_id, self::META_DESC_KEY, true); + $canonical = get_term_meta($term->term_id, self::CANONICAL_KEY, true); + $social_image = get_term_meta($term->term_id, self::SOCIAL_IMAGE_KEY, true); + $x_creator = get_term_meta($term->term_id, self::X_CREATOR_KEY, true); wp_nonce_field('ansico_wp_basic_save_term_fields', self::TERM_NONCE_KEY); ?> @@ -780,6 +1441,9 @@ final class Ansico_WP_Basic { 'meta_description' => $meta_description, 'fallback_title' => $term->name, 'permalink' => (!is_wp_error(get_term_link($term)) ? get_term_link($term) : home_url('/')), + 'canonical' => $canonical, + 'social_image' => $social_image, + 'x_creator' => $x_creator, ]); ?> @@ -808,6 +1472,9 @@ final class Ansico_WP_Basic { $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'])) : ''; + $canonical = isset($_POST['ansico_wp_basic_canonical']) ? esc_url_raw(wp_unslash($_POST['ansico_wp_basic_canonical'])) : ''; + $social_image = isset($_POST['ansico_wp_basic_social_image']) ? esc_url_raw(wp_unslash($_POST['ansico_wp_basic_social_image'])) : ''; + $x_creator = isset($_POST['ansico_wp_basic_x_creator']) ? sanitize_text_field(wp_unslash($_POST['ansico_wp_basic_x_creator'])) : ''; if ($meta_title === '') { delete_term_meta($term_id, self::META_TITLE_KEY); @@ -820,6 +1487,24 @@ final class Ansico_WP_Basic { } else { update_term_meta($term_id, self::META_DESC_KEY, $meta_description); } + + if ($canonical === '') { + delete_term_meta($term_id, self::CANONICAL_KEY); + } else { + update_term_meta($term_id, self::CANONICAL_KEY, $canonical); + } + + if ($social_image === '') { + delete_term_meta($term_id, self::SOCIAL_IMAGE_KEY); + } else { + update_term_meta($term_id, self::SOCIAL_IMAGE_KEY, $social_image); + } + + if ($x_creator === '') { + delete_term_meta($term_id, self::X_CREATOR_KEY); + } else { + update_term_meta($term_id, self::X_CREATOR_KEY, $this->normalize_x_handle($x_creator)); + } } public function render_user_fields($user) { @@ -891,14 +1576,14 @@ final class Ansico_WP_Basic { 'ansico-wp-basic-admin', plugin_dir_url(__FILE__) . 'assets/admin.css', [], - '0.0.0.2' + '0.0.1.4' ); wp_enqueue_script( 'ansico-wp-basic-admin', plugin_dir_url(__FILE__) . 'assets/admin.js', [], - '0.0.0.2', + '0.0.1.4', true ); } @@ -1009,6 +1694,421 @@ final class Ansico_WP_Basic { return ''; } + + public function maybe_disable_core_canonical() { + $settings = $this->get_settings(); + if (!empty($settings['enable_meta_module'])) { + remove_action('wp_head', 'rel_canonical'); + } + } + + private function get_context_url() { + if (is_singular()) { + $post_id = get_queried_object_id(); + if ($post_id) { + return get_permalink($post_id) ?: home_url('/'); + } + } + + if (is_author()) { + $author = get_queried_object(); + if ($author instanceof WP_User || (is_object($author) && isset($author->ID))) { + return get_author_posts_url((int) $author->ID); + } + } + + if ((is_category() || is_tag() || is_tax()) && ($term = get_queried_object()) && (isset($term->term_id))) { + $link = get_term_link($term); + return !is_wp_error($link) ? $link : home_url('/'); + } + + if (is_post_type_archive()) { + $post_type = get_query_var('post_type'); + $post_type = is_array($post_type) ? reset($post_type) : $post_type; + return $post_type ? get_post_type_archive_link($post_type) : home_url('/'); + } + + if (is_home()) { + $page_for_posts = (int) get_option('page_for_posts'); + if ($page_for_posts) { + return get_permalink($page_for_posts) ?: home_url('/'); + } + } + + global $wp; + if (isset($wp->request)) { + return home_url(add_query_arg([], $wp->request)); + } + + return home_url('/'); + } + + private function get_canonical_url() { + if (is_singular()) { + $post_id = get_queried_object_id(); + if ($post_id) { + $override = get_post_meta($post_id, self::CANONICAL_KEY, true); + if (!empty($override)) { + return esc_url_raw($override); + } + } + $canonical = wp_get_canonical_url(); + if (!empty($canonical)) { + return $canonical; + } + } + return $this->get_context_url(); + } + + private function get_og_locale() { + $locale = get_locale(); + return str_replace('-', '_', (string) $locale); + } + + private function get_og_type() { + if (is_singular() && !is_page()) { + return 'article'; + } + return 'website'; + } + + private function normalize_x_handle($handle) { + $handle = trim((string) $handle); + if ($handle === '') { + return ''; + } + return strpos($handle, '@') === 0 ? $handle : '@' . ltrim($handle, '@'); + } + + private function get_author_name_for_context() { + if (is_singular()) { + $post = get_queried_object(); + if ($post instanceof WP_Post) { + return get_the_author_meta('display_name', (int) $post->post_author); + } + } + return ''; + } + + private function get_author_url_for_context() { + if (is_singular()) { + $post = get_queried_object(); + if ($post instanceof WP_Post) { + return get_author_posts_url((int) $post->post_author); + } + } + return ''; + } + + private function get_social_image_data() { + $settings = $this->get_settings(); + + if (is_singular()) { + $post_id = get_queried_object_id(); + if ($post_id) { + $override_url = get_post_meta($post_id, self::SOCIAL_IMAGE_KEY, true); + if (!empty($override_url)) { + return [ + 'url' => esc_url_raw($override_url), + 'width' => 0, + 'height' => 0, + 'type' => '', + ]; + } + } + $thumb_id = $post_id ? get_post_thumbnail_id($post_id) : 0; + if ($thumb_id) { + $src = wp_get_attachment_image_src($thumb_id, 'full'); + if ($src) { + $file = get_attached_file($thumb_id); + $mime = $file && function_exists('wp_check_filetype') ? wp_check_filetype($file) : ['type' => '']; + return [ + 'url' => $src[0], + 'width' => (int) $src[1], + 'height' => (int) $src[2], + 'type' => !empty($mime['type']) ? $mime['type'] : '', + ]; + } + } + } + + if (is_tax() || is_category() || is_tag()) { + $term = get_queried_object(); + if ($term instanceof WP_Term) { + $override_url = get_term_meta($term->term_id, self::SOCIAL_IMAGE_KEY, true); + if (!empty($override_url)) { + return [ + 'url' => esc_url_raw($override_url), + 'width' => 0, + 'height' => 0, + 'type' => '', + ]; + } + } + } + + if (!empty($settings['social_defaults']['default_social_image'])) { + return [ + 'url' => esc_url_raw($settings['social_defaults']['default_social_image']), + 'width' => 0, + 'height' => 0, + 'type' => '', + ]; + } + + $icon_id = (int) get_option('site_icon'); + if ($icon_id) { + $src = wp_get_attachment_image_src($icon_id, 'full'); + if ($src) { + $file = get_attached_file($icon_id); + $mime = $file && function_exists('wp_check_filetype') ? wp_check_filetype($file) : ['type' => '']; + return [ + 'url' => $src[0], + 'width' => (int) $src[1], + 'height' => (int) $src[2], + 'type' => !empty($mime['type']) ? $mime['type'] : '', + ]; + } + } + + return []; + } + + private function get_reading_time_label($post) { + if (!($post instanceof WP_Post)) { + return ''; + } + + $word_count = str_word_count(wp_strip_all_tags((string) $post->post_content)); + if ($word_count <= 0) { + return ''; + } + + $minutes = max(1, (int) ceil($word_count / 200)); + return sprintf(_n('%d minute', '%d minutes', $minutes, 'ansico-wp-basic'), $minutes); + } + + + private function get_x_creator_for_context($fallback = '') { + if (is_singular()) { + $post_id = get_queried_object_id(); + if ($post_id) { + $override = get_post_meta($post_id, self::X_CREATOR_KEY, true); + if (!empty($override)) { + return $this->normalize_x_handle($override); + } + } + } + + if (is_tax() || is_category() || is_tag()) { + $term = get_queried_object(); + if ($term instanceof WP_Term) { + $override = get_term_meta($term->term_id, self::X_CREATOR_KEY, true); + if (!empty($override)) { + return $this->normalize_x_handle($override); + } + } + } + + return $this->normalize_x_handle($fallback); + } + + public function output_frontend_meta_tags() { + if (is_admin() || is_feed()) { + return; + } + + $settings = $this->get_settings(); + if (empty($settings['enable_meta_module'])) { + return; + } + + $canonical = $this->get_canonical_url(); + $title = $this->get_context_meta_title(); + if ($title === '') { + $title = wp_get_document_title(); + } else { + $title = $this->build_document_title_from_meta($title); + } + $description = $this->get_context_meta_description(); + $url = $this->get_context_url(); + $site_name = get_bloginfo('name'); + $og_locale = $this->get_og_locale(); + $og_type = $this->get_og_type(); + $social = $settings['social_defaults'] ?? []; + $facebook_publisher = !empty($social['facebook_publisher_url']) ? esc_url($social['facebook_publisher_url']) : ''; + $x_site_handle = $this->normalize_x_handle($social['x_site_handle'] ?? ''); + $x_creator_handle = $this->get_x_creator_for_context($x_site_handle); + $author_name = $this->get_author_name_for_context(); + $author_url = $this->get_author_url_for_context(); + $image = $this->get_social_image_data(); + $twitter_card = !empty($image['url']) ? 'summary_large_image' : 'summary'; + + echo " +" . '' . " +"; + echo '' . " +"; + echo '' . " +"; + echo '' . " +"; + if ($description !== '') { + echo '' . " +"; + } + echo '' . " +"; + echo '' . " +"; + + if ($facebook_publisher !== '') { + echo '' . " +"; + } + if ($author_url !== '') { + echo '' . " +"; + } + + if (is_singular()) { + $post = get_queried_object(); + if ($post instanceof WP_Post) { + if (!empty($post->post_date_gmt)) { + echo '' . " +"; + } + if (!empty($post->post_modified_gmt)) { + echo '' . " +"; + } + } + } + + if (!empty($image['url'])) { + echo '' . " +"; + if (!empty($image['width'])) { + echo '' . " +"; + } + if (!empty($image['height'])) { + echo '' . " +"; + } + if (!empty($image['type'])) { + echo '' . " +"; + } + } + + if ($author_name !== '') { + echo '' . " +"; + } + + echo '' . " +"; + if ($x_creator_handle !== '') { + echo '' . " +"; + } + if ($x_site_handle !== '') { + echo '' . " +"; + } + if ($author_name !== '') { + echo '' . " +"; + echo '' . " +"; + } + if (is_singular()) { + $post = get_queried_object(); + $reading_time = $this->get_reading_time_label($post); + if ($reading_time !== '') { + echo '' . " +"; + echo '' . " +"; + } + } + + $organization_name = !empty($social['organization_name']) ? $social['organization_name'] : $site_name; + $organization_logo = !empty($social['organization_logo']) ? esc_url_raw($social['organization_logo']) : ''; + + $schema_graph = [ + [ + '@type' => 'WebSite', + '@id' => trailingslashit(home_url('/')) . '#website', + 'url' => home_url('/'), + 'name' => $site_name, + 'publisher' => ['@id' => trailingslashit(home_url('/')) . '#organization'], + 'inLanguage' => str_replace('_', '-', $og_locale), + ], + [ + '@type' => 'Organization', + '@id' => trailingslashit(home_url('/')) . '#organization', + 'name' => $organization_name, + 'url' => home_url('/'), + 'sameAs' => array_values(array_filter([$facebook_publisher])), + 'logo' => $organization_logo !== '' ? [ + '@type' => 'ImageObject', + 'url' => $organization_logo, + ] : null, + ], + [ + '@type' => is_singular() && !is_page() ? 'Article' : 'WebPage', + '@id' => trailingslashit($url) . '#webpage', + 'url' => $url, + 'name' => wp_strip_all_tags($title), + 'isPartOf' => ['@id' => trailingslashit(home_url('/')) . '#website'], + 'description' => $description, + 'inLanguage' => str_replace('_', '-', $og_locale), + ], + ]; + + if ($author_name !== '') { + $schema_graph[2]['author'] = [ + '@type' => 'Person', + 'name' => $author_name, + 'url' => $author_url, + ]; + } + + if (!empty($image['url'])) { + $schema_graph[] = [ + '@type' => 'ImageObject', + '@id' => trailingslashit($url) . '#primaryimage', + 'url' => $image['url'], + 'contentUrl' => $image['url'], + 'width' => !empty($image['width']) ? (int) $image['width'] : null, + 'height' => !empty($image['height']) ? (int) $image['height'] : null, + ]; + $schema_graph[2]['image'] = ['@id' => trailingslashit($url) . '#primaryimage']; + } + + if (is_singular()) { + $post = get_queried_object(); + if ($post instanceof WP_Post) { + $schema_graph[1]['datePublished'] = get_post_time('c', true, $post); + $schema_graph[1]['dateModified'] = get_post_modified_time('c', true, $post); + } + } + + $schema_graph = array_map(function($item) { + return array_filter($item, function($value) { + return !($value === '' || $value === null || $value === []); + }); + }, $schema_graph); + + echo '' . " +"; + } + public function filter_document_title($title) { if (is_admin() || is_feed()) { return $title; diff --git a/ansico-wp-basic/assets/admin.css b/ansico-wp-basic/assets/admin.css index 65edd61..9bcc926 100644 --- a/ansico-wp-basic/assets/admin.css +++ b/ansico-wp-basic/assets/admin.css @@ -74,8 +74,155 @@ font-weight: 600; } -.ansico-wp-basic-settings-page .form-table th { - width: 280px; +.ansico-wp-basic-settings-page { + max-width: 1320px; +} + +.ansico-wp-basic-page-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 20px; + margin: 22px 0 20px; +} + +.ansico-wp-basic-page-header h1 { + margin: 0 0 8px; +} + +.ansico-wp-basic-page-intro { + margin: 0; + color: #50575e; + max-width: 760px; +} + +.ansico-wp-basic-status-card, +.ansico-wp-basic-panel, +.ansico-wp-basic-settings-card, +.ansico-wp-basic-tool-card { + background: #fff; + border: 1px solid #dcdcde; + border-radius: 12px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); +} + +.ansico-wp-basic-status-card { + min-width: 260px; + padding: 16px 18px; +} + +.ansico-wp-basic-status-label { + display: block; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.03em; + text-transform: uppercase; + color: #50575e; + margin-bottom: 8px; +} + +.ansico-wp-basic-status-muted { + color: #50575e; +} + +.ansico-wp-basic-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 340px; + gap: 20px; + align-items: start; +} + +.ansico-wp-basic-main-column, +.ansico-wp-basic-side-column, +.ansico-wp-basic-tool-stack { + display: grid; + gap: 20px; +} + +.ansico-wp-basic-panel { + padding: 22px; +} + +.ansico-wp-basic-panel-sticky { + position: sticky; + top: 42px; +} + +.ansico-wp-basic-panel-header { + margin-bottom: 18px; + padding-bottom: 16px; + border-bottom: 1px solid #f0f0f1; +} + +.ansico-wp-basic-panel-header h2 { + margin: 0 0 6px; + font-size: 20px; + line-height: 1.3; +} + +.ansico-wp-basic-panel-header p, +.ansico-wp-basic-subsection > h3 + p { + margin: 0; + color: #50575e; +} + +.ansico-wp-basic-field-list { + display: grid; + gap: 0; +} + +.ansico-wp-basic-field-row { + display: grid; + grid-template-columns: minmax(180px, 240px) minmax(0, 1fr); + gap: 24px; + padding: 18px 0; + border-top: 1px solid #f0f0f1; +} + +.ansico-wp-basic-field-row:first-child { + border-top: 0; + padding-top: 0; +} + +.ansico-wp-basic-field-row:last-child { + padding-bottom: 0; +} + +.ansico-wp-basic-field-label h3, +.ansico-wp-basic-subsection > h3, +.ansico-wp-basic-tool-card h3, +.ansico-wp-basic-settings-card h3 { + margin: 0; + font-size: 15px; + line-height: 1.4; +} + +.ansico-wp-basic-field-control .description, +.ansico-wp-basic-settings-card .description, +.ansico-wp-basic-tool-card .description, +.ansico-wp-basic-tools-box .description { + color: #646970; +} + +.ansico-wp-basic-checkbox-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 10px; + margin: 0; +} + +.ansico-wp-basic-checkbox-item { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 14px; + border: 1px solid #e0e0e0; + border-radius: 10px; + background: #fcfcfc; +} + +.ansico-wp-basic-checkbox-item input { + margin-top: 2px; } .ansico-wp-basic-settings-grid { @@ -85,24 +232,42 @@ } .ansico-wp-basic-settings-card { - background: #fff; - border: 1px solid #dcdcde; - border-radius: 8px; - padding: 14px; + padding: 16px; } .ansico-wp-basic-settings-card h3 { + margin-bottom: 14px; +} + +.ansico-wp-basic-subsection { + margin-top: 22px; +} + +.ansico-wp-basic-subsection:first-of-type { margin-top: 0; } +.ansico-wp-basic-subsection > h3 { + margin-bottom: 14px; +} + .ansico-wp-basic-term-fields .ansico-wp-basic-metabox, .ansico-wp-basic-term-fields-wrap .ansico-wp-basic-metabox { background: #fff; } +.ansico-wp-basic-tool-card { + padding: 18px; +} +.ansico-wp-basic-tools-box, .ansico-wp-basic-tools-box p { - margin: 0 0 14px; + margin: 0; +} + +.ansico-wp-basic-tools-box { + display: grid; + gap: 12px; } .ansico-wp-basic-tools-box label strong { @@ -119,7 +284,103 @@ vertical-align: middle; } -.ansico-wp-basic-tools-box .description { - display: block; - margin-top: 6px; +.ansico-wp-basic-submit-row { + margin-top: 20px; +} + +@media (max-width: 1100px) { + .ansico-wp-basic-layout { + grid-template-columns: 1fr; + } + + .ansico-wp-basic-panel-sticky { + position: static; + } +} + +@media (max-width: 782px) { + .ansico-wp-basic-page-header, + .ansico-wp-basic-field-row { + grid-template-columns: 1fr; + display: grid; + } + + .ansico-wp-basic-page-header { + gap: 14px; + } + + .ansico-wp-basic-panel, + .ansico-wp-basic-tool-card, + .ansico-wp-basic-settings-card, + .ansico-wp-basic-status-card { + padding: 16px; + } +} + + +.ansico-wp-basic-tab-nav { + margin: 0 0 20px; +} + +.ansico-wp-basic-tab-panel { + display: grid; + gap: 20px; +} + +.ansico-wp-basic-panel-narrow { + max-width: 900px; +} + + +.ansico-wp-basic-panel-header-intro { + margin-bottom: 8px; +} + +.ansico-wp-basic-panel-header-intro p { + font-size: 14px; +} + +.ansico-wp-basic-inline-code { + margin: 12px 0 0; +} + +.ansico-wp-basic-inline-code code { + word-break: break-all; +} + + +.ansico-wp-basic-sitemap-url-row { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-top: 12px; +} + +.ansico-wp-basic-sitemap-url-label { + margin-right: 2px; +} + +.ansico-wp-basic-sitemap-url { + display: inline-block; + padding: 6px 8px; +} + +.ansico-wp-basic-copy-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 34px; + padding: 0 6px; +} + +.ansico-wp-basic-copy-button .dashicons { + font-size: 16px; + width: 16px; + height: 16px; +} + +.ansico-wp-basic-copy-button.is-copied { + color: #008a20; + border-color: #8bc34a; } diff --git a/ansico-wp-basic/assets/admin.js b/ansico-wp-basic/assets/admin.js index 33c42d8..1aca09d 100644 --- a/ansico-wp-basic/assets/admin.js +++ b/ansico-wp-basic/assets/admin.js @@ -3,9 +3,7 @@ document.addEventListener('DOMContentLoaded', function () { var descInput = document.getElementById('ansico_wp_basic_meta_description'); var snippet = document.querySelector('.ansico-wp-basic-snippet'); - if (!titleInput || !descInput || !snippet) { - return; - } + if (titleInput && descInput && snippet) { var titleTarget = snippet.querySelector('.ansico-wp-basic-snippet-title'); var descTextTarget = snippet.querySelector('.ansico-wp-basic-snippet-description-text'); @@ -157,4 +155,58 @@ document.addEventListener('DOMContentLoaded', function () { titleInput.addEventListener('input', updateSnippet); descInput.addEventListener('input', updateSnippet); updateSnippet(); + } + + + document.querySelectorAll('.ansico-wp-basic-copy-button').forEach(function (button) { + button.addEventListener('click', function () { + var text = button.getAttribute('data-copy-text') || ''; + if (!text) { + return; + } + + var resetButtonState = function () { + button.classList.remove('is-copied'); + button.setAttribute('title', button.getAttribute('data-original-title') || ''); + button.setAttribute('aria-label', button.getAttribute('data-original-label') || ''); + }; + + if (!button.hasAttribute('data-original-title')) { + button.setAttribute('data-original-title', button.getAttribute('title') || ''); + } + + if (!button.hasAttribute('data-original-label')) { + button.setAttribute('data-original-label', button.getAttribute('aria-label') || ''); + } + + var markCopied = function () { + button.classList.add('is-copied'); + button.setAttribute('title', 'Copied'); + button.setAttribute('aria-label', 'Copied'); + window.setTimeout(resetButtonState, 1500); + }; + + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(markCopied).catch(function () { + var tempInput = document.createElement('input'); + tempInput.value = text; + document.body.appendChild(tempInput); + tempInput.select(); + document.execCommand('copy'); + document.body.removeChild(tempInput); + markCopied(); + }); + return; + } + + var tempInput = document.createElement('input'); + tempInput.value = text; + document.body.appendChild(tempInput); + tempInput.select(); + document.execCommand('copy'); + document.body.removeChild(tempInput); + markCopied(); + }); + }); + }); diff --git a/ansico-wp-basic/readme.txt b/ansico-wp-basic/readme.txt index afad00a..4c138b0 100644 --- a/ansico-wp-basic/readme.txt +++ b/ansico-wp-basic/readme.txt @@ -1,43 +1,70 @@ === Ansico WP Basic === -Contributors: ansico -Tags: seo, meta title, meta description, search preview +Contributors: aphandersen +Tags: seo, metadata, meta tags, open graph, xml sitemap Requires at least: 6.0 -Tested up to: 6.5 +Tested up to: 6.9.4 Requires PHP: 7.4 -Stable tag: 0.0.0.2 +Stable tag: 1.0.0 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. +Lightweight SEO tools for WordPress with meta titles, meta descriptions, social tags, canonical URLs, and XML sitemaps. == Description == -Ansico WP Basic adds simple SEO fields to WordPress: +Ansico WP Basic adds essential SEO controls to WordPress without the overhead of a large SEO suite. -- 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 +Features include: + +* 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 +* SEO fields for taxonomy archives such as categories, tags, and public custom taxonomies +* SEO settings for archive-style pages such as the posts page, date archives, search results, 404 pages, and post type archives +* Open Graph and Twitter/X meta tags +* Canonical URL output +* Lightweight JSON-LD schema output +* XML sitemap generation +* Import tools for migrating SEO data from Yoast SEO +* Bulk generation of missing meta titles and meta descriptions for existing content + +The plugin is designed for site owners who want straightforward SEO fields and metadata management directly inside WordPress. == 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. +1. Upload the plugin ZIP file through the WordPress admin plugin installer, or upload the plugin folder to `/wp-content/plugins/`. +2. Activate the plugin through the **Plugins** screen in WordPress. +3. Go to **Ansico WP Basic > Settings**. +4. Choose the post types and archive areas where SEO fields should be available. +5. Configure optional social defaults, sitemap settings, and maintenance tools as needed. + +== Frequently Asked Questions == + += Can I import my SEO data from Yoast SEO? = + +Yes. The **Tools** tab includes an importer for supported Yoast SEO metadata and selected social defaults. + += Will existing values be overwritten? = + +No. The importer and bulk generator are designed to preserve existing Ansico WP Basic values unless a field is empty. + += Does the plugin support custom post types? = + +Yes. Public custom post types can be enabled from the settings screen. == 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 = +* First public release prepared for WordPress.org +* Added meta title and meta description support for posts, pages, public custom post types, author archives, taxonomy archives, and archive-style pages +* Added live search result preview in the editor +* Added Open Graph, Twitter/X, canonical URL, and lightweight schema output +* Added XML sitemap generation +* Added Yoast SEO import tools for supported metadata and social defaults +* Added bulk generation of missing meta titles and meta descriptions +* Improved the settings screen with tabs and cleaner organization + +== Upgrade Notice == = 1.0.0 = -- Initial release +Initial public release.