Ansico-SEO/ansico-wp-basic/ansico-wp-basic.php

1347 lines
53 KiB
PHP
Raw Normal View History

2026-04-16 16:52:37 +00:00
<?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();