Files
wordpress-theme-aksrt/inc/classes/class-aksrt-seo-meta-box.php
T
kate522 b36186da65 feat(ai): 打通 SEO 闭环与投稿写作辅助
对齐 Zibll 的 AI 生成 SEO 流程:统一 WP AI Client/主题 Provider 双后端、文章/分类 SEO 框与生成回填、投稿摘要/标签建议,并收敛扩展页为连接配置。
2026-07-12 15:36:04 +08:00

246 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
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
/**
* AKSRT SEO Meta Box — 文章/页面独立 SEO + AI 生成按钮
*
* Meta keys 与 AKSRT_SEO_Head 读取一致:
* _aksrt_seo_title / _aksrt_seo_keywords / _aksrt_seo_description
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_SEO_Meta_Box {
private static $instance = null;
public const META_TITLE = '_aksrt_seo_title';
public const META_KEYWORDS = '_aksrt_seo_keywords';
public const META_DESCRIPTION = '_aksrt_seo_description';
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_action('add_meta_boxes', [$this, 'register_meta_box']);
add_action('save_post', [$this, 'save_meta_box'], 10, 2);
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
// Term SEO (category / post_tag)
add_action('category_edit_form_fields', [$this, 'render_term_fields'], 20);
add_action('post_tag_edit_form_fields', [$this, 'render_term_fields'], 20);
add_action('edited_category', [$this, 'save_term_fields']);
add_action('edited_post_tag', [$this, 'save_term_fields']);
}
public function register_meta_box(): void {
foreach (['post', 'page'] as $screen) {
add_meta_box(
'aksrt_seo_meta',
__('独立 SEO', 'aksrt'),
[$this, 'render_meta_box'],
$screen,
'normal',
'high'
);
}
}
public function render_meta_box(WP_Post $post): void {
wp_nonce_field('aksrt_seo_meta_save', 'aksrt_seo_meta_nonce');
$title = (string) get_post_meta($post->ID, self::META_TITLE, true);
$keywords = (string) get_post_meta($post->ID, self::META_KEYWORDS, true);
$description = (string) get_post_meta($post->ID, self::META_DESCRIPTION, true);
$ai_btn = $this->render_ai_button('post', (int) $post->ID);
?>
<div class="aksrt-seo-meta-box">
<p>
<label for="aksrt_seo_title"><strong><?php esc_html_e('SEO 标题', 'aksrt'); ?></strong></label>
<input type="text" class="widefat" id="aksrt_seo_title" name="aksrt_seo_title" value="<?php echo esc_attr($title); ?>" />
</p>
<p>
<label for="aksrt_seo_keywords"><strong><?php esc_html_e('SEO 关键词', 'aksrt'); ?></strong></label>
<input type="text" class="widefat" id="aksrt_seo_keywords" name="aksrt_seo_keywords" value="<?php echo esc_attr($keywords); ?>" placeholder="<?php esc_attr_e('多个关键词用英文逗号分隔', 'aksrt'); ?>" />
</p>
<p>
<label for="aksrt_seo_description"><strong><?php esc_html_e('SEO 描述', 'aksrt'); ?></strong></label>
<textarea class="widefat" rows="3" id="aksrt_seo_description" name="aksrt_seo_description"><?php echo esc_textarea($description); ?></textarea>
<span class="description"><?php esc_html_e('建议 50–150 字。', 'aksrt'); ?><?php echo $ai_btn; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
</p>
</div>
<?php
}
public function save_meta_box(int $post_id, $post = null): void {
if (!isset($_POST['aksrt_seo_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['aksrt_seo_meta_nonce'])), 'aksrt_seo_meta_save')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
if (wp_is_post_revision($post_id)) {
return;
}
$map = [
self::META_TITLE => 'aksrt_seo_title',
self::META_KEYWORDS => 'aksrt_seo_keywords',
self::META_DESCRIPTION => 'aksrt_seo_description',
];
foreach ($map as $meta_key => $field) {
if (!isset($_POST[$field])) {
continue;
}
$value = sanitize_text_field(wp_unslash((string) $_POST[$field]));
if ($value === '') {
delete_post_meta($post_id, $meta_key);
} else {
update_post_meta($post_id, $meta_key, $value);
}
}
}
/**
* @param WP_Term $term
*/
public function render_term_fields($term): void {
if (!$term instanceof WP_Term) {
return;
}
$title = (string) get_term_meta($term->term_id, self::META_TITLE, true);
$keywords = (string) get_term_meta($term->term_id, self::META_KEYWORDS, true);
$description = (string) get_term_meta($term->term_id, self::META_DESCRIPTION, true);
$ai_btn = $this->render_ai_button('term', (int) $term->term_id);
?>
<tr class="form-field">
<th scope="row"><label for="aksrt_seo_title"><?php esc_html_e('SEO 标题', 'aksrt'); ?></label></th>
<td>
<input type="text" name="aksrt_seo_title" id="aksrt_seo_title" class="aksrt-term-seo-title" value="<?php echo esc_attr($title); ?>" />
</td>
</tr>
<tr class="form-field">
<th scope="row"><label for="aksrt_seo_keywords"><?php esc_html_e('SEO 关键词', 'aksrt'); ?></label></th>
<td>
<input type="text" name="aksrt_seo_keywords" id="aksrt_seo_keywords" class="aksrt-term-seo-keywords" value="<?php echo esc_attr($keywords); ?>" />
</td>
</tr>
<tr class="form-field">
<th scope="row"><label for="aksrt_seo_description"><?php esc_html_e('SEO 描述', 'aksrt'); ?></label></th>
<td>
<textarea name="aksrt_seo_description" id="aksrt_seo_description" class="aksrt-term-seo-description" rows="4"><?php echo esc_textarea($description); ?></textarea>
<p class="description"><?php esc_html_e('主题默认会自动设置 SEO 内容,如需单独设置请在此输入。', 'aksrt'); ?><?php echo $ai_btn; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p>
</td>
</tr>
<?php
}
public function save_term_fields(int $term_id): void {
if (!current_user_can('edit_term', $term_id)) {
return;
}
$map = [
self::META_TITLE => 'aksrt_seo_title',
self::META_KEYWORDS => 'aksrt_seo_keywords',
self::META_DESCRIPTION => 'aksrt_seo_description',
];
foreach ($map as $meta_key => $field) {
if (!isset($_POST[$field])) {
continue;
}
$value = sanitize_text_field(wp_unslash((string) $_POST[$field]));
if ($value === '') {
delete_term_meta($term_id, $meta_key);
} else {
update_term_meta($term_id, $meta_key, $value);
}
}
}
public function enqueue_assets(string $hook): void {
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
if (!$screen) {
return;
}
$ok = in_array($screen->base, ['post', 'term', 'edit-tags'], true);
if (!$ok) {
return;
}
$path = AKSRT_DIR . '/assets/js/admin-ai-seo.js';
if (!is_file($path)) {
return;
}
wp_enqueue_script(
'aksrt-admin-ai-seo',
AKSRT_URI . '/assets/js/admin-ai-seo.js',
[],
(string) filemtime($path),
true
);
$status = class_exists('AKSRT_AI') ? AKSRT_AI::get_instance()->get_status() : [];
$seo = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_seo_settings() : [];
wp_localize_script('aksrt-admin-ai-seo', 'aksrtAiSeo', [
'restUrl' => esc_url_raw(rest_url('aksrt/v1')),
'nonce' => wp_create_nonce('wp_rest'),
'ai' => [
'available' => !empty($status['available']),
'seo_ai_enabled' => !empty($seo['seo_ai_enabled']) && !empty($seo['seo_enabled']),
],
'i18n' => [
'generating' => __('生成中…', 'aksrt'),
'generate' => __('AI 生成 SEO', 'aksrt'),
'success' => __('已填入 SEO 字段,请检查后更新保存', 'aksrt'),
'failed' => __('生成失败', 'aksrt'),
'needSave' => __('请先保存后再生成 SEO', 'aksrt'),
'notAvailable' => __('AI 未配置或未启用', 'aksrt'),
],
]);
wp_add_inline_style('wp-admin', '
.aksrt-ai-seo-button{display:inline-block;margin-left:8px;vertical-align:middle}
.aksrt-ai-seo-generate{cursor:pointer}
.aksrt-ai-seo-generate.is-loading{opacity:.6;pointer-events:none}
.aksrt-ai-seo-msg{display:block;margin-top:6px;font-size:12px}
.aksrt-ai-seo-msg.is-error{color:#b32d2e}
.aksrt-ai-seo-msg.is-ok{color:#007017}
');
}
private function render_ai_button(string $type, int $id): string {
$seo = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_seo_settings() : [];
if (empty($seo['seo_enabled']) || empty($seo['seo_ai_enabled'])) {
return '';
}
$available = class_exists('AKSRT_AI') && AKSRT_AI::get_instance()->is_available();
$disabled = (!$available || $id <= 0) ? ' disabled' : '';
$title = !$available
? esc_attr__('请先配置 AI 连接(扩展 → AI 辅助 或 WP Connectors)', 'aksrt')
: ($id <= 0 ? esc_attr__('请先保存后再生成', 'aksrt') : '');
return sprintf(
'<span class="aksrt-ai-seo-button"><button type="button" class="button aksrt-ai-seo-generate"%s data-type="%s" data-id="%d" title="%s">%s</button></span>',
$disabled,
esc_attr($type),
$id,
$title,
esc_html__('AI 生成 SEO', 'aksrt')
);
}
}