对齐 Zibll 的 AI 生成 SEO 流程:统一 WP AI Client/主题 Provider 双后端、文章/分类 SEO 框与生成回填、投稿摘要/标签建议,并收敛扩展页为连接配置。
246 lines
10 KiB
PHP
246 lines
10 KiB
PHP
<?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')
|
||
);
|
||
}
|
||
}
|