Ansico-SEO/ansico-wp-basic/ansico-wp-basic.php
2026-04-17 23:07:12 +02:00

2446 lines
105 KiB
PHP
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* 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: 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
* Requires at least: 6.0
* Requires PHP: 7.4
*/
if (!defined('ABSPATH')) {
exit;
}
register_activation_hook(__FILE__, ['Ansico_WP_Basic', 'activate']);
register_deactivation_hook(__FILE__, ['Ansico_WP_Basic', 'deactivate']);
final class Ansico_WP_Basic {
const OPTION_KEY = 'ansico_wp_basic_settings';
const META_TITLE_KEY = '_ansico_wp_basic_meta_title';
const META_DESC_KEY = '_ansico_wp_basic_meta_description';
const HIDE_TITLE_KEY = '_ansico_wp_basic_hide_title';
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();
$plugin->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('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']);
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', [$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']);
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,
'social_defaults' => [
'facebook_publisher_url' => '',
'x_site_handle' => '',
'default_social_image' => '',
],
'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['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']);
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_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' => ''],
'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 '<p>' . esc_html__('Choose where the SEO fields should be available.', 'ansico-wp-basic') . '</p>';
},
'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_social_section',
__('Social tags and schema', 'ansico-wp-basic'),
function () {
echo '<p>' . esc_html__('Configure site-wide defaults used for Open Graph, Twitter/X cards, canonical URLs, and schema markup output.', 'ansico-wp-basic') . '</p>';
},
'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'),
function () {
echo '<p>' . esc_html__('Set SEO title and description for archive-style pages that do not have a normal editor screen.', 'ansico-wp-basic') . '</p>';
},
'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'
);
add_settings_section(
'ansico_wp_basic_tools_section',
__('Import tools', 'ansico-wp-basic'),
function () {
echo '<p>' . 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') . '</p>';
},
'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) {
$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,
'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'] : []),
];
}
public function render_meta_module_toggle() {
$settings = $this->get_settings();
printf(
'<label><input type="checkbox" name="%1$s[enable_meta_module]" value="1" %2$s> %3$s</label><p class="description">%4$s</p>',
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();
$sitemap_url = home_url('/sitemap.xml');
printf(
'<label><input type="checkbox" name="%1$s[enable_sitemap_module]" value="1" %2$s> %3$s</label><p class="description">%4$s</p>',
esc_attr(self::OPTION_KEY),
checked($settings['enable_sitemap_module'], 1, false),
esc_html__('Enable XML sitemap generation.', 'ansico-wp-basic'),
esc_html__('Generate a sitemap index for supported content types.', 'ansico-wp-basic')
);
if (!empty($settings['enable_sitemap_module'])) {
echo '<div class="ansico-wp-basic-sitemap-url-row">';
echo '<span class="ansico-wp-basic-sitemap-url-label"><strong>' . esc_html__('Sitemap URL:', 'ansico-wp-basic') . '</strong></span>';
echo '<code class="ansico-wp-basic-sitemap-url">' . esc_html($sitemap_url) . '</code>';
echo '<button type="button" class="button button-small ansico-wp-basic-copy-button" data-copy-text="' . esc_attr($sitemap_url) . '" aria-label="' . esc_attr__('Copy sitemap URL', 'ansico-wp-basic') . '" title="' . esc_attr__('Copy sitemap URL', 'ansico-wp-basic') . '">';
echo '<span class="dashicons dashicons-admin-page" aria-hidden="true"></span>';
echo '</button>';
echo '</div>';
echo '<p class="description">' . esc_html__('Use this URL when submitting your sitemap to search engines or external tools.', 'ansico-wp-basic') . '</p>';
}
}
public function render_post_types_field() {
$settings = $this->get_settings();
$public_post_types = $this->get_public_post_types();
echo '<fieldset class="ansico-wp-basic-checkbox-grid">';
foreach ($public_post_types as $post_type) {
printf(
'<label class="ansico-wp-basic-checkbox-item"><input type="checkbox" name="%1$s[enabled_post_types][]" value="%2$s" %3$s> <span>%4$s <code>(%2$s)</code></span></label>',
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 '</fieldset>';
echo '<p class="description">' . esc_html__('Only selected post types will get Ansico SEO fields and bulk generation tools.', 'ansico-wp-basic') . '</p>';
}
public function render_author_field_toggle() {
$settings = $this->get_settings();
printf(
'<label><input type="checkbox" name="%1$s[enable_author_fields]" value="1" %2$s> %3$s</label><p class="description">%4$s</p>',
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(
'<label><input type="checkbox" name="%1$s[enable_taxonomy_fields]" value="1" %2$s> %3$s</label><p class="description">%4$s</p>',
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_social_defaults_fields() {
$settings = $this->get_settings();
$social = $settings['social_defaults'] ?? [];
echo '<div class="ansico-wp-basic-settings-card">';
echo '<p><label><strong>' . esc_html__('Facebook publisher URL', 'ansico-wp-basic') . '</strong></label><input type="url" class="widefat" name="' . esc_attr(self::OPTION_KEY) . '[social_defaults][facebook_publisher_url]" value="' . esc_attr($social['facebook_publisher_url'] ?? '') . '" placeholder="https://www.facebook.com/yourpage/"></p>';
echo '<p><label><strong>' . esc_html__('Twitter/X site handle', 'ansico-wp-basic') . '</strong></label><input type="text" class="widefat" name="' . esc_attr(self::OPTION_KEY) . '[social_defaults][x_site_handle]" value="' . esc_attr($social['x_site_handle'] ?? '') . '" placeholder="@example"></p>';
echo '<p><label><strong>' . esc_html__('Default social image URL', 'ansico-wp-basic') . '</strong></label><input type="url" class="widefat" name="' . esc_attr(self::OPTION_KEY) . '[social_defaults][default_social_image]" value="' . esc_attr($social['default_social_image'] ?? '') . '" placeholder="https://example.com/social-image.jpg"></p>';
echo '<p><label><strong>' . esc_html__('Organization / publisher name', 'ansico-wp-basic') . '</strong></label><input type="text" class="widefat" name="' . esc_attr(self::OPTION_KEY) . '[social_defaults][organization_name]" value="' . esc_attr($social['organization_name'] ?? '') . '" placeholder="' . esc_attr(get_bloginfo('name')) . '"></p>';
echo '<p><label><strong>' . esc_html__('Organization logo URL', 'ansico-wp-basic') . '</strong></label><input type="url" class="widefat" name="' . esc_attr(self::OPTION_KEY) . '[social_defaults][organization_logo]" value="' . esc_attr($social['organization_logo'] ?? '') . '" placeholder="https://example.com/logo.png"></p>';
echo '<p class="description">' . 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') . '</p>';
echo '</div>';
}
public function render_special_pages_fields() {
$settings = $this->get_settings();
if (empty($settings['enable_meta_module'])) {
echo '<p class="description">' . esc_html__('The SEO module is currently disabled.', 'ansico-wp-basic') . '</p>';
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 '<div class="ansico-wp-basic-settings-grid">';
foreach ($pages as $key => $label) {
$title = $settings['special_pages'][$key]['title'] ?? '';
$description = $settings['special_pages'][$key]['description'] ?? '';
printf(
'<div class="ansico-wp-basic-settings-card"><h3>%1$s</h3><p><label><strong>%2$s</strong></label><input type="text" class="widefat" name="%3$s[special_pages][%4$s][title]" value="%5$s" placeholder="%6$s"></p><p><label><strong>%7$s</strong></label><textarea class="widefat" rows="3" name="%3$s[special_pages][%4$s][description]" placeholder="%8$s">%9$s</textarea></p></div>',
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 '</div>';
}
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 '<p class="description">' . esc_html__('Yoast SEO was not detected on this site, and no Yoast SEO data was found in the database.', 'ansico-wp-basic') . '</p>';
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 '<div class="ansico-wp-basic-tools-box">';
echo '<p>' . 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') . '</p>';
echo '<p><a href="' . esc_url($url) . '" class="button button-secondary">' . esc_html__('Import from Yoast SEO', 'ansico-wp-basic') . '</a></p>';
echo '<p class="description">' . 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') . '</p>';
echo '</div>';
}
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 '<p class="description">' . esc_html__('The SEO module is currently disabled.', 'ansico-wp-basic') . '</p>';
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 '<div class="ansico-wp-basic-tools-box">';
echo '<p>' . 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') . '</p>';
if (!empty($labels)) {
echo '<p class="description">' . esc_html(sprintf(__('Runs on enabled post types: %s.', 'ansico-wp-basic'), implode(', ', $labels))) . '</p>';
}
echo '<p><a href="' . esc_url($url) . '" class="button button-secondary">' . esc_html__('Generate missing META fields', 'ansico-wp-basic') . '</a></p>';
echo '<p class="description">' . 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') . '</p>';
echo '</div>';
}
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'])) {
echo '<p class="description">' . esc_html__('The SEO module is currently disabled.', 'ansico-wp-basic') . '</p>';
return;
}
$archives = $this->get_archive_post_types();
if (empty($archives)) {
echo '<p>' . esc_html__('No public post type archives were found.', 'ansico-wp-basic') . '</p>';
return;
}
echo '<div class="ansico-wp-basic-settings-grid">';
foreach ($archives as $post_type) {
$title = $settings['post_type_archives'][$post_type->name]['title'] ?? '';
$description = $settings['post_type_archives'][$post_type->name]['description'] ?? '';
printf(
'<div class="ansico-wp-basic-settings-card"><h3>%1$s <code>(%2$s)</code></h3><p><label><strong>%3$s</strong></label><input type="text" class="widefat" name="%4$s[post_type_archives][%2$s][title]" value="%5$s" placeholder="%6$s"></p><p><label><strong>%7$s</strong></label><textarea class="widefat" rows="3" name="%4$s[post_type_archives][%2$s][description]" placeholder="%8$s">%9$s</textarea></p></div>',
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 '</div>';
}
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 '<div class="' . esc_attr($notice_class) . ' is-dismissible"><p>';
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 '</p></div>';
}
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'),
];
?>
<div class="wrap ansico-wp-basic-settings-page">
<div class="ansico-wp-basic-page-header">
<div>
<h1><?php echo esc_html__('Ansico WP Basic', 'ansico-wp-basic'); ?></h1>
<p class="ansico-wp-basic-page-intro"><?php echo esc_html__('Configure SEO fields, social tags, archive metadata, and maintenance tools for your WordPress content.', 'ansico-wp-basic'); ?></p>
</div>
</div>
<nav class="nav-tab-wrapper ansico-wp-basic-tab-nav" aria-label="<?php echo esc_attr__('Settings sections', 'ansico-wp-basic'); ?>">
<?php foreach ($tabs as $tab_key => $tab_label) : ?>
<a href="<?php echo esc_url(add_query_arg('tab', $tab_key, $base_url)); ?>" class="nav-tab <?php echo $current_tab === $tab_key ? 'nav-tab-active' : ''; ?>"><?php echo esc_html($tab_label); ?></a>
<?php endforeach; ?>
</nav>
<?php if ($current_tab === 'tools') : ?>
<div class="ansico-wp-basic-tab-panel">
<section class="ansico-wp-basic-panel ansico-wp-basic-panel-narrow">
<div class="ansico-wp-basic-panel-header ansico-wp-basic-panel-header-intro">
<p><?php echo esc_html__('Run one-off maintenance actions without overwriting already completed SEO values.', 'ansico-wp-basic'); ?></p>
</div>
<div class="ansico-wp-basic-tool-stack">
<div class="ansico-wp-basic-tool-card">
<h3><?php echo esc_html__('Import from Yoast SEO', 'ansico-wp-basic'); ?></h3>
<?php $this->render_yoast_import_field(); ?>
</div>
<div class="ansico-wp-basic-tool-card">
<h3><?php echo esc_html__('Generate missing META fields', 'ansico-wp-basic'); ?></h3>
<?php $this->render_generate_missing_meta_field(); ?>
</div>
</div>
</section>
</div>
<?php else : ?>
<form method="post" action="options.php" class="ansico-wp-basic-settings-form">
<?php settings_fields('ansico_wp_basic_settings_group'); ?>
<div class="ansico-wp-basic-tab-panel">
<?php if ($current_tab === 'general') : ?>
<section class="ansico-wp-basic-panel">
<div class="ansico-wp-basic-panel-header ansico-wp-basic-panel-header-intro">
<p><?php echo esc_html__('Choose where the SEO module should run and which content types should be editable.', 'ansico-wp-basic'); ?></p>
</div>
<div class="ansico-wp-basic-field-list">
<div class="ansico-wp-basic-field-row">
<div class="ansico-wp-basic-field-label"><h3><?php echo esc_html__('SEO module', 'ansico-wp-basic'); ?></h3></div>
<div class="ansico-wp-basic-field-control"><?php $this->render_meta_module_toggle(); ?></div>
</div>
<div class="ansico-wp-basic-field-row">
<div class="ansico-wp-basic-field-label"><h3><?php echo esc_html__('XML sitemap module', 'ansico-wp-basic'); ?></h3></div>
<div class="ansico-wp-basic-field-control"><?php $this->render_sitemap_module_toggle(); ?></div>
</div>
<div class="ansico-wp-basic-field-row">
<div class="ansico-wp-basic-field-label"><h3><?php echo esc_html__('Post types', 'ansico-wp-basic'); ?></h3></div>
<div class="ansico-wp-basic-field-control"><?php $this->render_post_types_field(); ?></div>
</div>
<div class="ansico-wp-basic-field-row">
<div class="ansico-wp-basic-field-label"><h3><?php echo esc_html__('Author archives', 'ansico-wp-basic'); ?></h3></div>
<div class="ansico-wp-basic-field-control"><?php $this->render_author_field_toggle(); ?></div>
</div>
<div class="ansico-wp-basic-field-row">
<div class="ansico-wp-basic-field-label"><h3><?php echo esc_html__('Taxonomy archives', 'ansico-wp-basic'); ?></h3></div>
<div class="ansico-wp-basic-field-control"><?php $this->render_taxonomy_field_toggle(); ?></div>
</div>
</div>
</section>
<section class="ansico-wp-basic-panel">
<div class="ansico-wp-basic-panel-header">
<h2><?php echo esc_html__('Social tags and schema', 'ansico-wp-basic'); ?></h2>
<p><?php echo esc_html__('Configure site-wide defaults used for Open Graph, Twitter/X cards, canonical tags, and schema output.', 'ansico-wp-basic'); ?></p>
</div>
<?php $this->render_social_defaults_fields(); ?>
</section>
<?php elseif ($current_tab === 'archives') : ?>
<section class="ansico-wp-basic-panel">
<div class="ansico-wp-basic-panel-header ansico-wp-basic-panel-header-intro">
<p><?php echo esc_html__('Set SEO values for archive-like pages that do not have a standard post editor.', 'ansico-wp-basic'); ?></p>
</div>
<div class="ansico-wp-basic-subsection">
<h3><?php echo esc_html__('Global archive pages', 'ansico-wp-basic'); ?></h3>
<?php $this->render_special_pages_fields(); ?>
</div>
<div class="ansico-wp-basic-subsection">
<h3><?php echo esc_html__('Post type archives', 'ansico-wp-basic'); ?></h3>
<?php $this->render_post_type_archive_fields(); ?>
</div>
</section>
<?php endif; ?>
</div>
<div class="ansico-wp-basic-submit-row">
<?php submit_button(__('Save Settings', 'ansico-wp-basic'), 'primary', 'submit', false, ['class' => 'button button-primary button-large']); ?>
</div>
</form>
<?php endif; ?>
</div>
<?php
}
public function register_meta_boxes() {
$settings = $this->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 '<div class="ansico-wp-basic-tools-box">';
if ($post_type === 'page') {
echo '<p><label><input type="checkbox" name="ansico_wp_basic_hide_title" value="1" ' . checked($hide_title, 1, false) . '> ' . esc_html__('Hide page title on the frontend for this page', 'ansico-wp-basic') . '</label></p>';
}
echo '<p><label for="ansico_wp_basic_change_post_type"><strong>' . esc_html__('Change post type', 'ansico-wp-basic') . '</strong></label>';
echo '<select id="ansico_wp_basic_change_post_type" name="ansico_wp_basic_change_post_type" class="ansico-post-type-select">';
foreach ($switchable_types as $type_obj) {
printf('<option value="%1$s" %2$s>%3$s</option>', esc_attr($type_obj->name), selected($type_obj->name, $post_type, false), esc_html($type_obj->labels->singular_name . ' (' . $type_obj->name . ')'));
}
echo '</select>';
echo '</div>';
}
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');
?>
<div class="ansico-wp-basic-metabox">
<p>
<label for="ansico_wp_basic_meta_title"><strong><?php echo esc_html__('Meta title', 'ansico-wp-basic'); ?></strong></label>
<input type="text" id="ansico_wp_basic_meta_title" name="ansico_wp_basic_meta_title" class="widefat" value="<?php echo esc_attr($meta_title); ?>" placeholder="<?php echo esc_attr__('Enter a custom meta title', 'ansico-wp-basic'); ?>">
<span class="description"><?php echo esc_html__('Recommended: around 5060 characters.', 'ansico-wp-basic'); ?></span>
<span class="ansico-wp-basic-counter" data-counter-for="title"></span>
</p>
<p>
<label for="ansico_wp_basic_meta_description"><strong><?php echo esc_html__('Meta description', 'ansico-wp-basic'); ?></strong></label>
<textarea id="ansico_wp_basic_meta_description" name="ansico_wp_basic_meta_description" class="widefat" rows="4" placeholder="<?php echo esc_attr__('Enter a custom meta description', 'ansico-wp-basic'); ?>"><?php echo esc_textarea($meta_description); ?></textarea>
<span class="description"><?php echo esc_html__('Recommended: around 140155 characters. Longer text will usually be cut in search results.', 'ansico-wp-basic'); ?></span>
<span class="ansico-wp-basic-counter" data-counter-for="description"></span>
</p>
<p>
<label for="ansico_wp_basic_canonical"><strong><?php echo esc_html__('Canonical URL override', 'ansico-wp-basic'); ?></strong></label>
<input type="url" id="ansico_wp_basic_canonical" name="ansico_wp_basic_canonical" class="widefat" value="<?php echo esc_attr($args['canonical'] ?? ''); ?>" placeholder="<?php echo esc_attr($permalink); ?>">
<span class="description"><?php echo esc_html__('Optional. Leave empty to use the normal WordPress canonical URL.', 'ansico-wp-basic'); ?></span>
</p>
<p>
<label for="ansico_wp_basic_social_image"><strong><?php echo esc_html__('Open Graph image URL override', 'ansico-wp-basic'); ?></strong></label>
<input type="url" id="ansico_wp_basic_social_image" name="ansico_wp_basic_social_image" class="widefat" value="<?php echo esc_attr($args['social_image'] ?? ''); ?>" placeholder="https://example.com/social-image.jpg">
<span class="description"><?php echo esc_html__('Optional. Leave empty to use the featured image, default social image, or site icon.', 'ansico-wp-basic'); ?></span>
</p>
<p>
<label for="ansico_wp_basic_x_creator"><strong><?php echo esc_html__('Twitter/X creator handle override', 'ansico-wp-basic'); ?></strong></label>
<input type="text" id="ansico_wp_basic_x_creator" name="ansico_wp_basic_x_creator" class="widefat" value="<?php echo esc_attr($args['x_creator'] ?? ''); ?>" placeholder="@example">
<span class="description"><?php echo esc_html__('Optional. Leave empty to use the site-wide X handle.', 'ansico-wp-basic'); ?></span>
</p>
<div class="ansico-wp-basic-snippet-wrapper">
<strong><?php echo esc_html__('Search result preview', 'ansico-wp-basic'); ?></strong>
<div class="ansico-wp-basic-snippet" data-fallback-title="<?php echo esc_attr($fallback_title); ?>" data-permalink="<?php echo esc_url($permalink); ?>" data-site-name="<?php echo esc_attr($site_name); ?>">
<div class="ansico-wp-basic-snippet-site"><?php echo esc_html($site_name); ?></div>
<div class="ansico-wp-basic-snippet-url"></div>
<div class="ansico-wp-basic-snippet-title"></div>
<div class="ansico-wp-basic-snippet-description"><span class="ansico-wp-basic-snippet-description-text"></span><span class="ansico-wp-basic-snippet-overflow"></span></div>
</div>
<p class="description ansico-wp-basic-truncation-note"></p>
</div>
</div>
<?php
}
private function get_trimmed_text_excerpt($text, $limit = 150) {
$text = trim((string) $text);
if ($text === '') {
return '';
}
$text = preg_replace('/\s+/u', ' ', wp_strip_all_tags($text));
if ($text === '') {
return '';
}
if (function_exists('mb_strlen') && function_exists('mb_substr')) {
if (mb_strlen($text) <= $limit) {
return $text;
}
$trimmed = mb_substr($text, 0, $limit + 1);
$last_space = mb_strrpos($trimmed, ' ');
if ($last_space !== false && $last_space > (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[^>]*>(.*?)<\/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);
$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);
$permalink = get_permalink($post) ?: home_url('/');
$this->render_snippet_box([
'meta_title' => $meta_title,
'meta_description' => $meta_description,
'fallback_title' => $fallback_title,
'permalink' => $permalink,
'canonical' => $saved_canonical,
'social_image' => $saved_social_image,
'x_creator' => $saved_x_creator,
]);
}
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'])) : '';
$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);
} 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);
}
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) {
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);
?>
<div class="form-field term-group ansico-wp-basic-term-fields">
<h2><?php echo esc_html__('Ansico WP Basic SEO', 'ansico-wp-basic'); ?></h2>
<?php $this->render_snippet_box([
'meta_title' => '',
'meta_description' => '',
'fallback_title' => ucfirst($taxonomy_name) . ' archive',
'permalink' => $example_url,
'canonical' => '',
'social_image' => '',
'x_creator' => '',
]); ?>
</div>
<?php
}
public function render_term_edit_fields($term) {
$settings = $this->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);
$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);
?>
<tr class="form-field ansico-wp-basic-term-fields-wrap">
<th scope="row"><label><?php echo esc_html__('Ansico WP Basic SEO', 'ansico-wp-basic'); ?></label></th>
<td>
<?php $this->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('/')),
'canonical' => $canonical,
'social_image' => $social_image,
'x_creator' => $x_creator,
]); ?>
</td>
</tr>
<?php
}
public function save_term_fields($term_id, $tt_id = 0, $taxonomy = '') {
unset($tt_id);
$settings = $this->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'])) : '';
$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);
} 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);
}
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) {
$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);
?>
<h2><?php echo esc_html__('Ansico WP Basic SEO', 'ansico-wp-basic'); ?></h2>
<?php $this->render_snippet_box([
'meta_title' => $meta_title,
'meta_description' => $meta_description,
'fallback_title' => $user->display_name,
'permalink' => get_author_posts_url($user->ID),
]); ?>
<?php
}
public function save_user_fields($user_id) {
$settings = $this->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.1.4'
);
wp_enqueue_script(
'ansico-wp-basic-admin',
plugin_dir_url(__FILE__) . 'assets/admin.js',
[],
'0.0.1.4',
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 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 "
" . '<link rel="canonical" href="' . esc_url($canonical) . '">' . "
";
echo '<meta property="og:locale" content="' . esc_attr($og_locale) . '">' . "
";
echo '<meta property="og:type" content="' . esc_attr($og_type) . '">' . "
";
echo '<meta property="og:title" content="' . esc_attr(wp_strip_all_tags($title)) . '">' . "
";
if ($description !== '') {
echo '<meta property="og:description" content="' . esc_attr(wp_strip_all_tags($description)) . '">' . "
";
}
echo '<meta property="og:url" content="' . esc_url($url) . '">' . "
";
echo '<meta property="og:site_name" content="' . esc_attr($site_name) . '">' . "
";
if ($facebook_publisher !== '') {
echo '<meta property="article:publisher" content="' . esc_url($facebook_publisher) . '">' . "
";
}
if ($author_url !== '') {
echo '<meta property="article:author" content="' . esc_url($author_url) . '">' . "
";
}
if (is_singular()) {
$post = get_queried_object();
if ($post instanceof WP_Post) {
if (!empty($post->post_date_gmt)) {
echo '<meta property="article:published_time" content="' . esc_attr(get_post_time('c', true, $post)) . '">' . "
";
}
if (!empty($post->post_modified_gmt)) {
echo '<meta property="article:modified_time" content="' . esc_attr(get_post_modified_time('c', true, $post)) . '">' . "
";
}
}
}
if (!empty($image['url'])) {
echo '<meta property="og:image" content="' . esc_url($image['url']) . '">' . "
";
if (!empty($image['width'])) {
echo '<meta property="og:image:width" content="' . esc_attr((string) $image['width']) . '">' . "
";
}
if (!empty($image['height'])) {
echo '<meta property="og:image:height" content="' . esc_attr((string) $image['height']) . '">' . "
";
}
if (!empty($image['type'])) {
echo '<meta property="og:image:type" content="' . esc_attr($image['type']) . '">' . "
";
}
}
if ($author_name !== '') {
echo '<meta name="author" content="' . esc_attr($author_name) . '">' . "
";
}
echo '<meta name="twitter:card" content="' . esc_attr($twitter_card) . '">' . "
";
if ($x_creator_handle !== '') {
echo '<meta name="twitter:creator" content="' . esc_attr($x_creator_handle) . '">' . "
";
}
if ($x_site_handle !== '') {
echo '<meta name="twitter:site" content="' . esc_attr($x_site_handle) . '">' . "
";
}
if ($author_name !== '') {
echo '<meta name="twitter:label1" content="' . esc_attr__('Written by', 'ansico-wp-basic') . '">' . "
";
echo '<meta name="twitter:data1" content="' . esc_attr($author_name) . '">' . "
";
}
if (is_singular()) {
$post = get_queried_object();
$reading_time = $this->get_reading_time_label($post);
if ($reading_time !== '') {
echo '<meta name="twitter:label2" content="' . esc_attr__('Estimated reading time', 'ansico-wp-basic') . '">' . "
";
echo '<meta name="twitter:data2" content="' . esc_attr($reading_time) . '">' . "
";
}
}
$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 '<script type="application/ld+json">' . wp_json_encode([
'@context' => 'https://schema.org',
'@graph' => $schema_graph,
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . '</script>' . "
";
}
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" . '<meta name="description" content="' . esc_attr(wp_strip_all_tags($meta_description)) . '">' . "\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 = '<?xml version="1.0" encoding="' . esc_attr(get_bloginfo('charset')) . '"?>' . "\n";
$xml .= '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
foreach ($items as $item) {
$xml .= '<sitemap>';
$xml .= '<loc>' . esc_url($item['loc']) . '</loc>';
if (!empty($item['lastmod'])) {
$xml .= '<lastmod>' . esc_html(mysql2date('c', $item['lastmod'], false)) . '</lastmod>';
}
$xml .= '</sitemap>';
}
$xml .= '</sitemapindex>';
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 = '<?xml version="1.0" encoding="' . esc_attr(get_bloginfo('charset')) . '"?>' . "\n";
$xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
foreach ($urls as $url) {
$xml .= '<url>';
$xml .= '<loc>' . esc_url($url['loc']) . '</loc>';
if (!empty($url['lastmod'])) {
$xml .= '<lastmod>' . esc_html(mysql2date('c', $url['lastmod'], false)) . '</lastmod>';
}
if (!empty($url['changefreq'])) {
$xml .= '<changefreq>' . esc_html($url['changefreq']) . '</changefreq>';
}
if (isset($url['priority'])) {
$xml .= '<priority>' . esc_html(number_format((float) $url['priority'], 1, '.', '')) . '</priority>';
}
$xml .= '</url>';
}
$xml .= '</urlset>';
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 '<div class="notice notice-warning"><p>' . 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') . '</p></div>';
}
}
new Ansico_WP_Basic();