--- name: seo-post description: Optimizar posts WordPress com RankMath Pro via WP-CLI. Auditar, corrigir e aplicar SEO completo incluindo schema, OG, robots e links internos. Usar quando "seo post", "rankmath", "schema", "optimizar artigo", "seo wordpress". context: fork --- # /seo-post — Optimizar Posts WordPress com RankMath Pro Modos: auditar (com ID), aplicar (com ID + dados), bulk (com lista IDs), novo (pipeline completo). ## Constantes ``` SERVIDOR: server (via mcp__ssh-unified__ssh_execute) WP_PATH: cd /home/ealmeida/public_html WP_CLI: wp --allow-root WP_USER: --user=2 (para wp eval) MANUAL: Hub/06-Operacoes/Documentacao/Manuais/WP-CLI/Rank-Math-WP-CLI-Manual-Definitivo.md ``` ## Modo auditar (/seo-post ID) Verifica estado SEO de um post e lista problemas. ```bash cd /home/ealmeida/public_html # 1. Dados do post wp --allow-root post get POST_ID --fields=ID,post_title,post_name,post_status --format=table # 2. Todos os meta RankMath wp --allow-root post meta list POST_ID --format=table | grep rank_math # 3. Thumbnail e alt text THUMB_ID=$(wp --allow-root post meta get POST_ID _thumbnail_id) wp --allow-root post meta get $THUMB_ID _wp_attachment_image_alt # 4. Schema no HTML curl -s "URL_DO_POST" | grep -o '"@type":"[^"]*"' | sort -u ``` **Checklist de auditoria:** | Campo | Verificacao | |-------|-----------| | rank_math_title | Existe? 50-70 chars? Tem keyword? | | rank_math_description | Existe? 150-160 chars? Tem keyword? | | rank_math_focus_keyword | Existe? 1-3 keywords CSV? | | rank_math_primary_category | Existe? ID valido? | | rank_math_robots | Array? Tem ["index"]? | | rank_math_advanced_robots | Array JSON? max-image-preview large? | | rank_math_rich_snippet | "article"? | | rank_math_snippet_article_type | "NewsArticle" ou "BlogPosting" ou "Article"? | | rank_math_schema_* | Existe meta com schema? Tem isPrimary 1? | | rank_math_facebook_title | Existe? | | rank_math_facebook_image | Existe? URL valido? | | rank_math_twitter_card_type | "summary_large_image"? | | _thumbnail_id | Existe? Imagem real? | | Alt text imagem | Tem keywords? | | post_name (slug) | Curto? <40 chars? Tem keyword? | | rank_math_seo_score | Existe? >0? Se N/A -> executar Passo 6 | **Output:** tabela com campo, estado (OK/FALHA), valor actual, accao sugerida. ## Modo aplicar (/seo-post ID com dados) Aplica SEO completo a um post. Pedir ao utilizador: - Titulo SEO (ou gerar a partir do post_title) - Meta description - Focus keywords (CSV) - Tipo schema: NewsArticle (noticias) ou Article (guias) ou BlogPosting (blog) ### Passo 1: Strings simples ```bash wp --allow-root post meta update POST_ID rank_math_title 'TITULO SEO' wp --allow-root post meta update POST_ID rank_math_description 'META DESC' wp --allow-root post meta update POST_ID rank_math_focus_keyword 'kw1,kw2,kw3' wp --allow-root post meta update POST_ID rank_math_primary_category 'CAT_ID' ``` ### Passo 2: Arrays (OBRIGATORIO --format=json) ```bash wp --allow-root post meta update POST_ID rank_math_robots '["index"]' --format=json wp --allow-root post meta update POST_ID rank_math_advanced_robots '{"max-snippet":"-1","max-video-preview":"-1","max-image-preview":"large"}' --format=json ``` ### Passo 3: Schema — DOIS formatos (AMBOS obrigatorios) ```bash # Legacy (dashboard RankMath) wp --allow-root post meta update POST_ID rank_math_rich_snippet 'article' wp --allow-root post meta update POST_ID rank_math_snippet_article_type 'TIPO' # TIPO: NewsArticle (noticias), Article (guias), BlogPosting (blog) # Novo (schema real no HTML — via wp eval) wp --allow-root eval ' $schema = [ "metadata" => ["title" => "TIPO", "type" => "custom", "shortcode" => "s-" . uniqid(), "isPrimary" => "1"], "@type" => "TIPO", "headline" => "%seo_title%", "description" => "%seo_description%", "keywords" => "%keywords%", "image" => ["@type" => "ImageObject", "url" => "%post_thumbnail%"], "author" => ["@type" => "Organization", "name" => "Descomplicar", "url" => "https://descomplicar.pt"], "publisher" => ["@type" => "Organization", "name" => "Descomplicar", "url" => "https://descomplicar.pt"], "datePublished" => "%date(Y-m-dTH:i:sP)%", "dateModified" => "%modified(Y-m-dTH:i:sP)%", "articleSection" => "%categories%" ]; update_post_meta(POST_ID, "rank_math_schema_TIPO", $schema); echo "Schema OK\n"; ' --user=2 ``` ### Passo 4: Social / OG ```bash wp --allow-root post meta update POST_ID rank_math_facebook_title 'TITULO OG' wp --allow-root post meta update POST_ID rank_math_facebook_description 'DESC OG' wp --allow-root post meta update POST_ID rank_math_facebook_image 'IMAGE_URL' wp --allow-root post meta update POST_ID rank_math_og_content_image 'IMAGE_URL' wp --allow-root post meta update POST_ID rank_math_twitter_card_type 'summary_large_image' ``` ### Passo 5: Slug (se necessario) ```bash wp --allow-root post update POST_ID --post_name='slug-curto-keyword' ``` ### Passo 6: SEO Score (OBRIGATORIO — posts via CLI nao calculam score automaticamente) O RankMath calcula o SEO Score apenas no editor Gutenberg (JavaScript). Posts criados via WP-CLI ficam com score "N/A" no dashboard. Este passo calcula e grava o score server-side. ```bash wp --allow-root eval ' $post_id = POST_ID; $post = get_post($post_id); $title = get_post_meta($post_id, "rank_math_title", true); $desc = get_post_meta($post_id, "rank_math_description", true); $kw_raw = get_post_meta($post_id, "rank_math_focus_keyword", true); $keywords = array_map("trim", explode(",", $kw_raw)); $kw = strtolower($keywords[0] ?? ""); $content = $post->post_content; $slug = $post->post_name; $word_count = str_word_count(strip_tags($content)); $score = 0; // Testes SEO (20 criterios, max 86 pontos brutos -> normalizado 0-100) if ($kw) $score += 5; if ($title) $score += 5; if ($desc) $score += 5; if ($title && stripos($title, $kw) !== false) $score += 5; if ($desc && stripos($desc, $kw) !== false) $score += 5; if (stripos($slug, str_replace(" ", "-", $kw)) !== false) $score += 5; if ($title && stripos($title, $kw) === 0) $score += 3; $tlen = strlen($title); if ($tlen >= 30 && $tlen <= 70) $score += 5; $dlen = strlen($desc); if ($dlen >= 100 && $dlen <= 160) $score += 5; if (has_post_thumbnail($post_id)) $score += 5; if ($word_count >= 600) $score += 5; if ($word_count >= 300) $score += 3; if (stripos($content, $kw) !== false) $score += 5; if (preg_match_all("/href=[\"\\x27]https?:\\/\\/descomplicar\\.pt/i", $content) >= 1) $score += 3; if (preg_match_all("/href=[\"\\x27]https?:\\/\\/(?!descomplicar\\.pt)/i", $content) >= 1) $score += 3; if (preg_match("/]*>.*" . preg_quote($kw, "/") . "/si", $content)) $score += 3; if (preg_match("/alt=[\"\\x27][^\"\\x27]+[\"\\x27]/i", $content)) $score += 2; if (get_post_meta($post_id, "rank_math_rich_snippet", true)) $score += 3; if (get_post_meta($post_id, "rank_math_primary_category", true)) $score += 2; $final = min(100, max(0, round(($score / 86) * 100))); update_post_meta($post_id, "rank_math_seo_score", $final); echo "SEO Score: $final/100\n"; ' --user=2 ``` ### Passo 7: Verificar ```bash curl -s "URL" | grep -o '"@type":"[^"]*"' | sort -u wp --allow-root post meta list POST_ID --format=table | grep rank_math ``` ## Modo bulk (/seo-post bulk) Aplicar schema a multiplos posts. Util para corrigir posts antigos. ```bash # Aplicar schema Article a todos os guias sem schema wp --allow-root eval ' $schema = [ "metadata" => ["title" => "Article", "type" => "custom", "shortcode" => "s-" . uniqid(), "isPrimary" => "1"], "@type" => "Article", "headline" => "%seo_title%", "description" => "%seo_description%", "keywords" => "%keywords%", "image" => ["@type" => "ImageObject", "url" => "%post_thumbnail%"], "author" => ["@type" => "Organization", "name" => "Descomplicar", "url" => "https://descomplicar.pt"], "publisher" => ["@type" => "Organization", "name" => "Descomplicar", "url" => "https://descomplicar.pt"], "datePublished" => "%date(Y-m-dTH:i:sP)%", "dateModified" => "%modified(Y-m-dTH:i:sP)%", "articleSection" => "%categories%" ]; $posts = get_posts(["numberposts" => -1, "post_status" => "publish", "meta_query" => [["key" => "rank_math_rich_snippet", "compare" => "NOT EXISTS"]]]); $count = 0; foreach ($posts as $p) { update_post_meta($p->ID, "rank_math_rich_snippet", "article"); update_post_meta($p->ID, "rank_math_snippet_article_type", "BlogPosting"); update_post_meta($p->ID, "rank_math_schema_Article", $schema); $count++; } echo "$count posts actualizados com schema Article\n"; ' --user=2 ``` ### Bulk: calcular SEO Score para todos os posts sem score ```bash wp --allow-root eval ' $posts = get_posts(["numberposts" => -1, "post_status" => "publish", "meta_query" => [ ["key" => "rank_math_focus_keyword", "compare" => "EXISTS"], ["relation" => "OR", ["key" => "rank_math_seo_score", "compare" => "NOT EXISTS"], ["key" => "rank_math_seo_score", "value" => "", "compare" => "="] ] ]]); $count = 0; foreach ($posts as $p) { $title = get_post_meta($p->ID, "rank_math_title", true); $desc = get_post_meta($p->ID, "rank_math_description", true); $kw = strtolower(trim(explode(",", get_post_meta($p->ID, "rank_math_focus_keyword", true))[0] ?? "")); $slug = $p->post_name; $content = $p->post_content; $wc = str_word_count(strip_tags($content)); $s = 0; if ($kw) $s += 5; if ($title) $s += 5; if ($desc) $s += 5; if ($title && stripos($title, $kw) !== false) $s += 5; if ($desc && stripos($desc, $kw) !== false) $s += 5; if (stripos($slug, str_replace(" ", "-", $kw)) !== false) $s += 5; if ($title && stripos($title, $kw) === 0) $s += 3; $tl = strlen($title); if ($tl >= 30 && $tl <= 70) $s += 5; $dl = strlen($desc); if ($dl >= 100 && $dl <= 160) $s += 5; if (has_post_thumbnail($p->ID)) $s += 5; if ($wc >= 600) $s += 5; if ($wc >= 300) $s += 3; if (stripos($content, $kw) !== false) $s += 5; if (preg_match_all("/href=[\"\\x27]https?:\\/\\/descomplicar\\.pt/i", $content) >= 1) $s += 3; if (preg_match_all("/href=[\"\\x27]https?:\\/\\/(?!descomplicar\\.pt)/i", $content) >= 1) $s += 3; if (preg_match("/]*>.*" . preg_quote($kw, "/") . "/si", $content)) $s += 3; if (preg_match("/alt=[\"\\x27][^\"\\x27]+[\"\\x27]/i", $content)) $s += 2; if (get_post_meta($p->ID, "rank_math_rich_snippet", true)) $s += 3; if (get_post_meta($p->ID, "rank_math_primary_category", true)) $s += 2; $final = min(100, max(0, round(($s / 86) * 100))); update_post_meta($p->ID, "rank_math_seo_score", $final); $count++; } echo "$count posts actualizados com SEO score\n"; ' --user=2 ``` ## Categorias de noticias (referencia) | Categoria | ID | Slug | |-----------|-----|------| | Noticias (pai) | 1188 | noticias | | IA e Agentes | 1189 | ia-agentes | | Automacao | 1190 | automacao | | Marketing Digital | 1191 | marketing-digital | | Tecnologia | 1192 | tecnologia | | Regulacao e Europa | 1193 | regulacao-europa | | Negocios e PMEs | 1194 | negocios-pmes | ## Erros comuns | Erro | Causa | Solucao | |------|-------|---------| | Schema: Off no dashboard | Faltam campos legacy | Definir rank_math_rich_snippet + rank_math_snippet_article_type | | Schema nao aparece no HTML | Falta rank_math_schema_* | Usar wp eval com array PHP | | Robots corrompidos | Sem --format=json | SEMPRE --format=json para arrays | | User ID invalido | --user=1 nao existe | Usar --user=2 (ealmeida) | | Heredoc no conteudo | cat <", date:""})`. **Fallback REST** (se MCP indisponível): ```bash SK=$(grep SERVICE_KEY /media/ealmeida/Dados/Hub/05-Projectos/Banco-Imagens-Videos/scripts/.env | cut -d= -f2) curl -s "https://mem.descomplicar.pt/rest/v1/media_bank_assets?type=eq.image&orientation=eq.landscape&quality_score=gte.7&deleted_at=is.null&order=quality_score.desc&limit=5" \ -H "apikey: $SK" -H "Authorization: Bearer $SK" ``` **Regra inviolável:** matéria-prima local. Usar como input para composição — NUNCA inserir o ficheiro raw directamente em publicação pública. Ver `Hub/06-Operacoes/Procedimentos/D7-Tecnologia/MCP/PROC-Media-Bank.md` (D7-MB-001) para detalhes. --- ## Healing Log Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar. ```jsonl {"date":"","issue":"","fix":"","source":"user|auto"} ``` *Adicionar nova linha após cada erro corrigido.*