feat(batch14): Yoast SEO deep — cornerstone, primary category, scores, redirects, sitemap (guarded)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q8t9GEyyLZbUQu26na1JQg
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
/**
|
||||
* Batch 14 — Yoast SEO deep (Pro-equivalent, net-new).
|
||||
*
|
||||
* Yoast-specific surface beyond the generic seo_* family (b2): cornerstone,
|
||||
* primary category, SEO/readability scores, redirects (Premium), sitemap state.
|
||||
* Guarded by WPSEO_VERSION — inert unless Yoast is active.
|
||||
* GPL-2.0-or-later. See NOTICE.md.
|
||||
*/
|
||||
|
||||
namespace AskTamMCP\Extensions;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class Batch14 {
|
||||
|
||||
private const MAP = [
|
||||
'title' => '_yoast_wpseo_title',
|
||||
'metadesc' => '_yoast_wpseo_metadesc',
|
||||
'focuskw' => '_yoast_wpseo_focuskw',
|
||||
'canonical' => '_yoast_wpseo_canonical',
|
||||
'og_title' => '_yoast_wpseo_opengraph-title',
|
||||
'og_description' => '_yoast_wpseo_opengraph-description',
|
||||
'og_image' => '_yoast_wpseo_opengraph-image',
|
||||
'twitter_title' => '_yoast_wpseo_twitter-title',
|
||||
'twitter_description' => '_yoast_wpseo_twitter-description',
|
||||
'cornerstone' => '_yoast_wpseo_is_cornerstone',
|
||||
'primary_category' => '_yoast_wpseo_primary_category',
|
||||
'noindex' => '_yoast_wpseo_meta-robots-noindex', // '1' noindex, '2' index, '' default
|
||||
'nofollow' => '_yoast_wpseo_meta-robots-nofollow', // '1' nofollow, '' default
|
||||
'seo_score' => '_yoast_wpseo_linkdex',
|
||||
'readability' => '_yoast_wpseo_content_score',
|
||||
];
|
||||
|
||||
public static function register($registry): void {
|
||||
if (!defined('WPSEO_VERSION')) return;
|
||||
|
||||
$def = function (string $name, string $desc, array $props, array $required, string $scope, string $method) {
|
||||
$schema = ['type' => 'object', 'properties' => $props];
|
||||
if ($required) $schema['required'] = $required;
|
||||
return ['name' => $name, 'description' => $desc, 'scope' => $scope,
|
||||
'handler' => 'AskTamMCP\\Extensions\\Batch14::' . $method, 'inputSchema' => $schema];
|
||||
};
|
||||
|
||||
$registry->register($def('yoast_detect', 'Report Yoast SEO version, whether Premium is active, and whether the XML sitemap is enabled.',
|
||||
[], [], 'read', 'detect'));
|
||||
|
||||
$registry->register($def('yoast_get_meta', 'Get all Yoast meta for a post: title, metadesc, focus keyword, canonical, OG/Twitter, cornerstone flag, primary category, robots, and SEO/readability scores.',
|
||||
['post_id' => ['type' => 'integer']], ['post_id'], 'read', 'getMeta'));
|
||||
|
||||
$registry->register($def('yoast_set_meta', 'Set Yoast meta on a post. Only supplied fields change. cornerstone is boolean; noindex/nofollow accept true/false.',
|
||||
['post_id' => ['type' => 'integer'], 'title' => ['type' => 'string'], 'metadesc' => ['type' => 'string'],
|
||||
'focuskw' => ['type' => 'string'], 'canonical' => ['type' => 'string'],
|
||||
'og_title' => ['type' => 'string'], 'og_description' => ['type' => 'string'], 'og_image' => ['type' => 'string'],
|
||||
'twitter_title' => ['type' => 'string'], 'twitter_description' => ['type' => 'string'],
|
||||
'cornerstone' => ['type' => 'boolean'], 'primary_category' => ['type' => 'integer'],
|
||||
'noindex' => ['type' => 'boolean'], 'nofollow' => ['type' => 'boolean']],
|
||||
['post_id'], 'write', 'setMeta'));
|
||||
|
||||
$registry->register($def('yoast_bulk_audit', 'Scan posts/pages for Yoast problems: missing meta description, missing focus keyword, or low SEO score (<40). Returns the offenders.',
|
||||
['post_types' => ['type' => 'array'], 'per_page' => ['type' => 'integer', 'default' => 100]], [], 'read', 'bulkAudit'));
|
||||
|
||||
$registry->register($def('yoast_list_redirects', 'List Yoast Premium redirects (origin -> target, type). Requires Yoast Premium; reports supported=false otherwise.',
|
||||
[], [], 'read', 'listRedirects'));
|
||||
}
|
||||
|
||||
public static function detect(array $args): array {
|
||||
$opts = get_option('wpseo', []);
|
||||
return [
|
||||
'version' => WPSEO_VERSION,
|
||||
'premium' => defined('WPSEO_PREMIUM_VERSION') ? WPSEO_PREMIUM_VERSION : false,
|
||||
'sitemap_enabled' => !empty($opts['enable_xml_sitemap']),
|
||||
'sitemap_url' => home_url('/sitemap_index.xml'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getMeta(array $args): array {
|
||||
$id = (int) ($args['post_id'] ?? 0);
|
||||
if (!get_post($id)) throw new \RuntimeException('Post not found: ' . $id);
|
||||
$out = ['post_id' => $id];
|
||||
foreach (self::MAP as $k => $mk) {
|
||||
$v = get_post_meta($id, $mk, true);
|
||||
if ($k === 'cornerstone') $v = ($v === '1');
|
||||
$out[$k] = $v === '' ? null : $v;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public static function setMeta(array $args): array {
|
||||
$id = (int) ($args['post_id'] ?? 0);
|
||||
if (!get_post($id)) throw new \RuntimeException('Post not found: ' . $id);
|
||||
$changed = [];
|
||||
foreach (self::MAP as $k => $mk) {
|
||||
if (!array_key_exists($k, $args)) continue;
|
||||
if (in_array($k, ['seo_score', 'readability'], true)) continue; // computed, not user-set
|
||||
$val = $args[$k];
|
||||
if ($k === 'cornerstone') $val = !empty($val) ? '1' : '';
|
||||
elseif ($k === 'noindex') $val = !empty($val) ? '1' : '2';
|
||||
elseif ($k === 'nofollow') $val = !empty($val) ? '1' : '';
|
||||
else $val = (string) $val;
|
||||
update_post_meta($id, $mk, $val);
|
||||
$changed[] = $k;
|
||||
}
|
||||
return ['post_id' => $id, 'changed' => $changed];
|
||||
}
|
||||
|
||||
public static function bulkAudit(array $args): array {
|
||||
$types = !empty($args['post_types']) && is_array($args['post_types']) ? $args['post_types'] : ['post', 'page'];
|
||||
$posts = get_posts(['post_type' => $types, 'post_status' => 'publish',
|
||||
'numberposts' => (int) ($args['per_page'] ?? 100), 'fields' => 'ids']);
|
||||
$issues = [];
|
||||
foreach ($posts as $pid) {
|
||||
$md = get_post_meta($pid, '_yoast_wpseo_metadesc', true);
|
||||
$fk = get_post_meta($pid, '_yoast_wpseo_focuskw', true);
|
||||
$sc = (int) get_post_meta($pid, '_yoast_wpseo_linkdex', true);
|
||||
$probs = [];
|
||||
if ($md === '') $probs[] = 'no_metadesc';
|
||||
if ($fk === '') $probs[] = 'no_focus_keyword';
|
||||
if ($sc > 0 && $sc < 40) $probs[] = 'low_seo_score(' . $sc . ')';
|
||||
if ($probs) $issues[] = ['id' => $pid, 'title' => get_the_title($pid), 'issues' => $probs];
|
||||
}
|
||||
return ['scanned' => count($posts), 'with_issues' => count($issues), 'offenders' => $issues];
|
||||
}
|
||||
|
||||
public static function listRedirects(array $args): array {
|
||||
if (!defined('WPSEO_PREMIUM_VERSION')) {
|
||||
return ['supported' => false, 'note' => 'Yoast Premium not active; redirect manager unavailable.', 'redirects' => []];
|
||||
}
|
||||
$opt = get_option('wpseo-premium-redirects-base', []);
|
||||
$out = [];
|
||||
if (is_array($opt)) {
|
||||
foreach ($opt as $origin => $r) {
|
||||
$out[] = ['origin' => $origin, 'target' => $r['url'] ?? '', 'type' => $r['type'] ?? ''];
|
||||
}
|
||||
}
|
||||
return ['supported' => true, 'count' => count($out), 'redirects' => $out];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user