Ansico-SEO/ansico-wp-basic/ansico-wp-basic.php
2026-04-16 18:52:37 +02:00

1346 lines
53 KiB
PHP
Raw 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: 0.0.0.4
* Author: Andreas Andersen (Ansico)
* Author URI: https://ansico.dk
* 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';
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('add_meta_boxes', [$this, 'register_meta_boxes']);
add_action('save_post', [$this, 'save_meta_box']);
add_filter('wp_insert_post_data', [$this, 'maybe_switch_post_type'], 10, 2);
add_action('show_user_profile', [$this, 'render_user_fields']);
add_action('edit_user_profile', [$this, 'render_user_fields']);
add_action('personal_options_update', [$this, 'save_user_fields']);
add_action('edit_user_profile_update', [$this, 'save_user_fields']);
add_action('created_term', [$this, 'save_term_fields'], 10, 3);
add_action('edited_term', [$this, 'save_term_fields'], 10, 3);
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
add_filter('pre_get_document_title', [$this, 'filter_document_title'], 20);
add_action('wp_head', [$this, 'output_meta_description'], 1);
add_filter('the_title', [$this, 'maybe_hide_page_title'], 10, 2);
add_filter('single_post_title', [$this, 'maybe_hide_single_post_title'], 10, 2);
add_action('admin_notices', [$this, 'settings_notice_if_no_types']);
add_action('init', [$this, 'register_taxonomy_hooks']);
add_action('init', [$this, 'register_sitemap_rewrites']);
add_action('template_redirect', [$this, 'maybe_render_sitemap']);
add_filter('query_vars', [$this, 'register_query_vars']);
}
public function get_settings() {
$defaults = [
'enable_meta_module' => 1,
'enable_sitemap_module' => 1,
'enabled_post_types' => $this->get_default_post_types(),
'enable_author_fields' => 1,
'enable_taxonomy_fields'=> 1,
'special_pages' => [
'date' => ['title' => '', 'description' => ''],
'search'=> ['title' => '', 'description' => ''],
'404' => ['title' => '', 'description' => ''],
'home' => ['title' => '', 'description' => ''],
],
'post_type_archives' => [],
];
$settings = get_option(self::OPTION_KEY, []);
if (!is_array($settings)) {
$settings = [];
}
$settings = wp_parse_args($settings, $defaults);
$settings['enable_meta_module'] = empty($settings['enable_meta_module']) ? 0 : 1;
$settings['enable_sitemap_module'] = empty($settings['enable_sitemap_module']) ? 0 : 1;
$settings['enabled_post_types'] = array_values(array_filter(array_map('sanitize_key', (array) $settings['enabled_post_types'])));
$settings['enable_author_fields'] = empty($settings['enable_author_fields']) ? 0 : 1;
$settings['enable_taxonomy_fields'] = empty($settings['enable_taxonomy_fields']) ? 0 : 1;
$settings['special_pages'] = $this->sanitize_special_pages($settings['special_pages']);
$settings['post_type_archives'] = $this->sanitize_post_type_archive_settings($settings['post_type_archives']);
return $settings;
}
private function get_public_post_types() {
$post_types = get_post_types([
'public' => true,
'show_ui' => true,
], 'objects');
unset($post_types['attachment']);
return $post_types;
}
private function get_default_post_types() {
return array_keys($this->get_public_post_types());
}
private function get_public_taxonomies() {
return get_taxonomies([
'public' => true,
'show_ui' => true,
], 'objects');
}
private function get_archive_post_types() {
$post_types = $this->get_public_post_types();
foreach ($post_types as $key => $post_type) {
if (empty($post_type->has_archive)) {
unset($post_types[$key]);
}
}
return $post_types;
}
private function sanitize_special_pages($special_pages) {
$defaults = [
'date' => ['title' => '', 'description' => ''],
'search' => ['title' => '', 'description' => ''],
'404' => ['title' => '', 'description' => ''],
'home' => ['title' => '', 'description' => ''],
];
$special_pages = is_array($special_pages) ? $special_pages : [];
foreach ($defaults as $key => $value) {
$page = isset($special_pages[$key]) && is_array($special_pages[$key]) ? $special_pages[$key] : [];
$defaults[$key] = [
'title' => isset($page['title']) ? sanitize_text_field($page['title']) : '',
'description' => isset($page['description']) ? sanitize_textarea_field($page['description']) : '',
];
}
return $defaults;
}
private function sanitize_post_type_archive_settings($archives) {
$clean = [];
$allowed = array_keys($this->get_archive_post_types());
$archives = is_array($archives) ? $archives : [];
foreach ($allowed as $post_type) {
$archive = isset($archives[$post_type]) && is_array($archives[$post_type]) ? $archives[$post_type] : [];
$clean[$post_type] = [
'title' => isset($archive['title']) ? sanitize_text_field($archive['title']) : '',
'description' => isset($archive['description']) ? sanitize_textarea_field($archive['description']) : '',
];
}
return $clean;
}
public function register_admin_menu() {
add_menu_page(
__('Ansico WP Basic', 'ansico-wp-basic'),
__('Ansico WP Basic', 'ansico-wp-basic'),
'manage_options',
'ansico-wp-basic',
[$this, 'render_settings_page'],
'dashicons-search',
81
);
add_submenu_page(
'ansico-wp-basic',
__('Settings', 'ansico-wp-basic'),
__('Settings', 'ansico-wp-basic'),
'manage_options',
'ansico-wp-basic',
[$this, 'render_settings_page']
);
}
public function register_settings() {
register_setting(
'ansico_wp_basic_settings_group',
self::OPTION_KEY,
[$this, 'sanitize_settings']
);
add_settings_section(
'ansico_wp_basic_main_section',
__('General settings', 'ansico-wp-basic'),
function () {
echo '<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_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'
);
}
public function sanitize_settings($input) {
$public_post_types = array_keys($this->get_public_post_types());
$enabled_post_types = [];
if (!empty($input['enabled_post_types']) && is_array($input['enabled_post_types'])) {
foreach ($input['enabled_post_types'] as $post_type) {
$post_type = sanitize_key($post_type);
if (in_array($post_type, $public_post_types, true)) {
$enabled_post_types[] = $post_type;
}
}
}
return [
'enable_meta_module' => empty($input['enable_meta_module']) ? 0 : 1,
'enable_sitemap_module' => empty($input['enable_sitemap_module']) ? 0 : 1,
'enabled_post_types' => array_values(array_unique($enabled_post_types)),
'enable_author_fields' => empty($input['enable_author_fields']) ? 0 : 1,
'enable_taxonomy_fields' => empty($input['enable_taxonomy_fields']) ? 0 : 1,
'special_pages' => $this->sanitize_special_pages(isset($input['special_pages']) ? $input['special_pages'] : []),
'post_type_archives' => $this->sanitize_post_type_archive_settings(isset($input['post_type_archives']) ? $input['post_type_archives'] : []),
];
}
public function render_meta_module_toggle() {
$settings = $this->get_settings();
printf(
'<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();
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__('When enabled, the sitemap index is available at /ansico-sitemap.xml.', 'ansico-wp-basic')
);
}
public function render_post_types_field() {
$settings = $this->get_settings();
$public_post_types = $this->get_public_post_types();
echo '<fieldset>';
foreach ($public_post_types as $post_type) {
printf(
'<label style="display:block;margin-bottom:8px;"><input type="checkbox" name="%1$s[enabled_post_types][]" value="%2$s" %3$s> %4$s <code>(%2$s)</code></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>';
}
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_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>';
}
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_settings_page() {
if (!current_user_can('manage_options')) {
return;
}
?>
<div class="wrap ansico-wp-basic-settings-page">
<h1><?php echo esc_html__('Ansico WP Basic', 'ansico-wp-basic'); ?></h1>
<p><?php echo esc_html__('Basic SEO fields for WordPress content, archives, special pages, title visibility, post type switching, and optional XML sitemaps.', 'ansico-wp-basic'); ?></p>
<?php $settings = $this->get_settings(); ?>
<p><strong><?php echo esc_html__('XML sitemap:', 'ansico-wp-basic'); ?></strong> <?php echo !empty($settings['enable_sitemap_module']) ? '<code>' . esc_html(home_url('/ansico-sitemap.xml')) . '</code>' : esc_html__('Disabled', 'ansico-wp-basic'); ?></p>
<form method="post" action="options.php">
<?php
settings_fields('ansico_wp_basic_settings_group');
do_settings_sections('ansico-wp-basic');
submit_button(__('Save Settings', 'ansico-wp-basic'));
?>
</form>
</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>
<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);
$fallback_title = get_the_title($post);
$meta_title = $saved_meta_title !== '' ? $saved_meta_title : $fallback_title;
$meta_description = $saved_meta_description !== '' ? $saved_meta_description : $this->get_default_meta_description_for_post($post);
$permalink = get_permalink($post) ?: home_url('/');
$this->render_snippet_box([
'meta_title' => $meta_title,
'meta_description' => $meta_description,
'fallback_title' => $fallback_title,
'permalink' => $permalink,
]);
}
public function save_meta_box($post_id) {
if (!isset($_POST[self::NONCE_KEY]) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST[self::NONCE_KEY])), 'ansico_wp_basic_save_meta_box')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
$post_type = get_post_type($post_id);
$settings = $this->get_settings();
if (!in_array($post_type, $settings['enabled_post_types'], true)) {
return;
}
if ($post_type === 'page') {
$hide_title = isset($_POST['ansico_wp_basic_hide_title']) ? 1 : 0;
if ($hide_title) {
update_post_meta($post_id, self::HIDE_TITLE_KEY, 1);
} else {
delete_post_meta($post_id, self::HIDE_TITLE_KEY);
}
}
if (empty($settings['enable_meta_module'])) {
return;
}
$meta_title = isset($_POST['ansico_wp_basic_meta_title']) ? sanitize_text_field(wp_unslash($_POST['ansico_wp_basic_meta_title'])) : '';
$meta_description = isset($_POST['ansico_wp_basic_meta_description']) ? sanitize_textarea_field(wp_unslash($_POST['ansico_wp_basic_meta_description'])) : '';
if ($meta_title === '') {
delete_post_meta($post_id, self::META_TITLE_KEY);
} else {
update_post_meta($post_id, self::META_TITLE_KEY, $meta_title);
}
if ($meta_description === '') {
delete_post_meta($post_id, self::META_DESC_KEY);
} else {
update_post_meta($post_id, self::META_DESC_KEY, $meta_description);
}
}
public function maybe_switch_post_type($data, $postarr) {
if (is_admin() && isset($_POST[self::NONCE_KEY]) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST[self::NONCE_KEY])), 'ansico_wp_basic_save_meta_box')) {
if (!empty($postarr['ID']) && current_user_can('edit_post', (int) $postarr['ID']) && !empty($_POST['ansico_wp_basic_change_post_type'])) {
$new_post_type = sanitize_key(wp_unslash($_POST['ansico_wp_basic_change_post_type']));
$allowed_types = array_keys($this->get_switchable_post_types());
if (in_array($new_post_type, $allowed_types, true)) {
$data['post_type'] = $new_post_type;
}
}
}
return $data;
}
public function maybe_hide_page_title($title, $post_id = 0) {
if (is_admin() || !is_singular('page')) {
return $title;
}
$queried_id = get_queried_object_id();
if (!$queried_id || (int) $post_id !== (int) $queried_id) {
return $title;
}
if ((int) get_post_meta($queried_id, self::HIDE_TITLE_KEY, true) === 1) {
return '';
}
return $title;
}
public function maybe_hide_single_post_title($title, $post = null) {
if (is_admin() || !is_singular('page')) {
return $title;
}
$queried_id = get_queried_object_id();
if ($queried_id && (int) get_post_meta($queried_id, self::HIDE_TITLE_KEY, true) === 1) {
return '';
}
return $title;
}
public function register_taxonomy_hooks() {
foreach ($this->get_public_taxonomies() as $taxonomy) {
add_action($taxonomy->name . '_add_form_fields', [$this, 'render_term_add_fields']);
add_action($taxonomy->name . '_edit_form_fields', [$this, 'render_term_edit_fields']);
}
}
public function render_term_add_fields($taxonomy) {
$settings = $this->get_settings();
if (empty($settings['enable_meta_module']) || empty($settings['enable_taxonomy_fields'])) {
return;
}
$taxonomy_name = is_string($taxonomy) ? $taxonomy : '';
$example_url = home_url('/' . $taxonomy_name . '/example/');
wp_nonce_field('ansico_wp_basic_save_term_fields', self::TERM_NONCE_KEY);
?>
<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,
]); ?>
</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);
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('/')),
]); ?>
</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'])) : '';
if ($meta_title === '') {
delete_term_meta($term_id, self::META_TITLE_KEY);
} else {
update_term_meta($term_id, self::META_TITLE_KEY, $meta_title);
}
if ($meta_description === '') {
delete_term_meta($term_id, self::META_DESC_KEY);
} else {
update_term_meta($term_id, self::META_DESC_KEY, $meta_description);
}
}
public function render_user_fields($user) {
$settings = $this->get_settings();
if (empty($settings['enable_meta_module']) || empty($settings['enable_author_fields'])) {
return;
}
wp_nonce_field('ansico_wp_basic_save_user_fields', self::USER_NONCE_KEY);
$meta_title = get_user_meta($user->ID, self::META_TITLE_KEY, true);
$meta_description = get_user_meta($user->ID, self::META_DESC_KEY, true);
?>
<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.0.2'
);
wp_enqueue_script(
'ansico-wp-basic-admin',
plugin_dir_url(__FILE__) . 'assets/admin.js',
[],
'0.0.0.2',
true
);
}
private function get_context_meta_title() {
$settings = $this->get_settings();
if (empty($settings['enable_meta_module'])) {
return '';
}
if (is_singular()) {
$post_id = get_queried_object_id();
$post_type = get_post_type($post_id);
if ($post_id && in_array($post_type, $settings['enabled_post_types'], true)) {
return get_post_meta($post_id, self::META_TITLE_KEY, true);
}
}
if (is_author() && !empty($settings['enable_author_fields'])) {
$author = get_queried_object();
if ($author instanceof WP_User || (is_object($author) && isset($author->ID))) {
return get_user_meta((int) $author->ID, self::META_TITLE_KEY, true);
}
}
if ((is_category() || is_tag() || is_tax()) && !empty($settings['enable_taxonomy_fields'])) {
$term = get_queried_object();
if ($term instanceof WP_Term || (is_object($term) && isset($term->term_id))) {
return get_term_meta((int) $term->term_id, self::META_TITLE_KEY, true);
}
}
if (is_post_type_archive()) {
$post_type = get_query_var('post_type');
$post_type = is_array($post_type) ? reset($post_type) : $post_type;
return $settings['post_type_archives'][$post_type]['title'] ?? '';
}
if (is_home()) {
return $settings['special_pages']['home']['title'] ?? '';
}
if (is_date()) {
return $settings['special_pages']['date']['title'] ?? '';
}
if (is_search()) {
return $settings['special_pages']['search']['title'] ?? '';
}
if (is_404()) {
return $settings['special_pages']['404']['title'] ?? '';
}
return '';
}
private function get_context_meta_description() {
$settings = $this->get_settings();
if (empty($settings['enable_meta_module'])) {
return '';
}
if (is_singular()) {
$post_id = get_queried_object_id();
$post_type = get_post_type($post_id);
if ($post_id && in_array($post_type, $settings['enabled_post_types'], true)) {
return get_post_meta($post_id, self::META_DESC_KEY, true);
}
}
if (is_author() && !empty($settings['enable_author_fields'])) {
$author = get_queried_object();
if ($author instanceof WP_User || (is_object($author) && isset($author->ID))) {
return get_user_meta((int) $author->ID, self::META_DESC_KEY, true);
}
}
if ((is_category() || is_tag() || is_tax()) && !empty($settings['enable_taxonomy_fields'])) {
$term = get_queried_object();
if ($term instanceof WP_Term || (is_object($term) && isset($term->term_id))) {
return get_term_meta((int) $term->term_id, self::META_DESC_KEY, true);
}
}
if (is_post_type_archive()) {
$post_type = get_query_var('post_type');
$post_type = is_array($post_type) ? reset($post_type) : $post_type;
return $settings['post_type_archives'][$post_type]['description'] ?? '';
}
if (is_home()) {
return $settings['special_pages']['home']['description'] ?? '';
}
if (is_date()) {
return $settings['special_pages']['date']['description'] ?? '';
}
if (is_search()) {
return $settings['special_pages']['search']['description'] ?? '';
}
if (is_404()) {
return $settings['special_pages']['404']['description'] ?? '';
}
return '';
}
public function filter_document_title($title) {
if (is_admin() || is_feed()) {
return $title;
}
$meta_title = $this->get_context_meta_title();
if (!empty($meta_title)) {
return $this->build_document_title_from_meta($meta_title);
}
return $title;
}
public function output_meta_description() {
if (is_admin() || is_feed()) {
return;
}
$meta_description = $this->get_context_meta_description();
if (!empty($meta_description)) {
echo "\n" . '<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();