# Wp Audit SEO Standards

> Rank Math SEO configuration reference, schema JSON-LD templates, meta patterns, and SEO seeding commands

- Skill: `yojahny55/wp-audit-seo-standards` (Agent Skill)
- Install (CLI): `npx skillmds add yojahny55/wp-audit-seo-standards`
- Raw SKILL.md: https://api.skillmd.com/api/skills/yojahny55/wp-audit-seo-standards/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Marketing & Growth
- Author: yojahny55 (https://skillmd.com/u/yojahny55)
- Updated: 2026-08-19
- Page: https://skillmd.com/skills/yojahny55/wp-audit-seo-standards

---


# SEO Standards — Rank Math Reference

This skill defines the SEO configuration standards, schema templates, and seeding commands for WordPress sites using **Rank Math SEO** as the primary SEO plugin.

---

## 1. Rank Math Plugin Detection

```php
// Plugin slug: seo-by-rank-math
// Detection: defined('RANK_MATH_VERSION') or class_exists('RankMath')
// WP-CLI install: $WP plugin install seo-by-rank-math --activate
```

Before running any Rank Math commands, verify the plugin is active:

```bash
$WP eval "
if (defined('RANK_MATH_VERSION')) {
    echo 'Rank Math v' . RANK_MATH_VERSION . ' is active.';
} else {
    echo 'Rank Math is NOT active.';
}
"
```

---

## 2. Rank Math Modules Reference

| Module | Slug | Recommended | Notes |
|--------|------|-------------|-------|
| SEO Analysis | `seo-analysis` | Yes | On-page content scoring |
| Sitemap | `sitemap` | Yes | XML sitemap generation |
| Rich Snippets | `rich-snippet` | Yes | JSON-LD structured data |
| Breadcrumbs | `breadcrumbs` | Yes | Breadcrumb trail + schema |
| 404 Monitor | `404-monitor` | Yes | Track broken links |
| Redirections | `redirections` | Yes | 301/302 redirect manager |
| Local SEO | `local-seo` | Yes (business sites) | LocalBusiness schema |
| Image SEO | `image-seo` | Yes | Auto alt/title attributes |
| Instant Indexing | `instant-indexing` | Yes | IndexNow / Bing |
| Link Counter | `link-counter` | Yes | Internal/external link audit |

### Enable All Recommended Modules

```bash
$WP eval "
\$modules = get_option('rank_math_modules', []);
\$enable = ['seo-analysis','sitemap','rich-snippet','breadcrumbs','404-monitor','redirections','local-seo','image-seo','instant-indexing','link-counter'];
foreach (\$enable as \$mod) { if (!in_array(\$mod, \$modules)) { \$modules[] = \$mod; } }
update_option('rank_math_modules', \$modules);
echo 'Enabled modules: ' . implode(', ', \$modules);
"
```

---

## 3. Rank Math Option Keys

| Option | Purpose |
|--------|---------|
| `rank_math_modules` | Array of active module slugs |
| `rank-math-options-general` | Breadcrumbs, links, image SEO, strip category base |
| `rank-math-options-titles` | Title templates, schema type, social profiles, noindex rules |
| `rank-math-options-sitemap` | Sitemap inclusions, ping settings |
| `rank-math-options-instant-indexing` | IndexNow configuration |

### Reading Options

```bash
# Get all general options
$WP eval "print_r(get_option('rank-math-options-general'));"

# Get all title options
$WP eval "print_r(get_option('rank-math-options-titles'));"

# Get active modules
$WP eval "print_r(get_option('rank_math_modules'));"
```

### Writing Options

Rank Math stores options as serialized arrays. Always merge rather than overwrite:

```bash
$WP eval "
\$opts = (array) get_option('rank-math-options-general', []);
\$opts['breadcrumbs']          = 'on';
\$opts['strip_category_base']  = 'on';
\$opts['nofollow_external_links'] = 'on';
\$opts['new_window_external_links'] = 'on';
\$opts['add_img_alt']          = 'on';
\$opts['add_img_title']        = 'on';
update_option('rank-math-options-general', \$opts);
echo 'General options updated.';
"
```

---

## 4. Rank Math Post Meta Keys

| Meta Key | Purpose | Example Value |
|----------|---------|---------------|
| `rank_math_title` | SEO title template | `%title% %sep% %sitename%` |
| `rank_math_description` | Meta description | Free text, 150-160 chars |
| `rank_math_focus_keyword` | Focus keyword(s) | `keyword1,keyword2` |
| `rank_math_robots` | Robots directives | `["index","follow"]` |
| `rank_math_canonical_url` | Canonical URL override | `https://...` |
| `rank_math_facebook_title` | OG title | Free text |
| `rank_math_facebook_description` | OG description | Free text |
| `rank_math_facebook_image` | OG image URL | `https://...` |
| `rank_math_facebook_image_id` | OG image attachment ID | `42` |
| `rank_math_twitter_card_type` | Twitter card type | `summary_large_image` |
| `rank_math_twitter_title` | Twitter title | Free text |
| `rank_math_twitter_description` | Twitter description | Free text |
| `rank_math_schema_Article` | Article schema | Serialized array |
| `rank_math_schema_FAQPage` | FAQ schema | Serialized array |
| `rank_math_breadcrumb_title` | Breadcrumb label override | Free text |
| `rank_math_pillar_content` | Pillar content flag | `on` |

### Seeding Post Meta via WP-CLI

```bash
# Set SEO meta for a single post
$WP eval "
\$post_id = 42;
update_post_meta(\$post_id, 'rank_math_title', '%title% %sep% %sitename%');
update_post_meta(\$post_id, 'rank_math_description', 'Your meta description here.');
update_post_meta(\$post_id, 'rank_math_focus_keyword', 'primary keyword');
update_post_meta(\$post_id, 'rank_math_robots', ['index','follow']);
update_post_meta(\$post_id, 'rank_math_twitter_card_type', 'summary_large_image');
echo 'SEO meta set for post ' . \$post_id;
"
```

### Bulk-Seed Meta Descriptions from Content

```bash
$WP eval "
\$posts = get_posts(['post_type' => ['post','page'], 'posts_per_page' => -1, 'post_status' => 'publish']);
foreach (\$posts as \$p) {
    \$existing = get_post_meta(\$p->ID, 'rank_math_description', true);
    if (!empty(\$existing)) continue;
    \$text = wp_strip_all_tags(\$p->post_excerpt ?: \$p->post_content);
    \$desc = mb_substr(trim(preg_replace('/\s+/', ' ', \$text)), 0, 155);
    if (mb_strlen(\$desc) > 10) {
        update_post_meta(\$p->ID, 'rank_math_description', \$desc);
        echo 'Set description for: ' . \$p->post_title . PHP_EOL;
    }
}
"
```

---

## 5. Schema JSON-LD Templates

### Organization

```json
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "",
  "url": "",
  "logo": {
    "@type": "ImageObject",
    "url": ""
  },
  "sameAs": [
    "https://www.facebook.com/PROFILE",
    "https://www.instagram.com/PROFILE",
    "https://x.com/PROFILE",
    "https://www.linkedin.com/company/PROFILE"
  ]
}
```

### LocalBusiness

```json
{
  "@context": "https://schema.org",
  "@type": "LocalBusiness",
  "name": "",
  "url": "",
  "image": "",
  "telephone": "",
  "email": "",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "",
    "addressLocality": "",
    "addressRegion": "",
    "postalCode": "",
    "addressCountry": ""
  },
  "geo": {
    "@type": "GeoCoordinates",
    "latitude": "",
    "longitude": ""
  },
  "openingHoursSpecification": [
    {
      "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
      "opens": "09:00",
      "closes": "17:00"
    }
  ],
  "sameAs": []
}
```

### FAQPage

```json
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "What is the question?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "The answer to the question."
      }
    },
    {
      "@type": "Question",
      "name": "Another question?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Another answer."
      }
    }
  ]
}
```

### BreadcrumbList

```json
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://example.com/"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Category",
      "item": "https://example.com/category/"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "Current Page"
    }
  ]
}
```

### Article

```json
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "",
  "author": {
    "@type": "Person",
    "name": "",
    "url": ""
  },
  "datePublished": "2025-01-01T00:00:00+00:00",
  "dateModified": "2025-01-01T00:00:00+00:00",
  "image": {
    "@type": "ImageObject",
    "url": "",
    "width": 1200,
    "height": 630
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://example.com/post-slug/"
  },
  "publisher": {
    "@type": "Organization",
    "name": "",
    "logo": {
      "@type": "ImageObject",
      "url": ""
    }
  }
}
```

---

## 6. Meta Title Formulas by Page Type

| Page Type | Formula |
|-----------|---------|
| Homepage | `%sitename% %sep% %sitedesc%` |
| Page | `%title% %sep% %sitename%` |
| Blog Post | `%title% %sep% %sitename%` |
| Category | `%term% %sep% %sitename% %page%` |
| Search | `Search: %searchphrase% %sep% %sitename%` |
| 404 | `Page Not Found %sep% %sitename%` |
| Author | `%name% %sep% %sitename%` |

### Available Variables

- `%title%` — Post/page title
- `%sitename%` — Site name from Settings > General
- `%sitedesc%` — Site tagline
- `%sep%` — Separator character (default: `-`)
- `%term%` — Category/tag name
- `%searchphrase%` — Current search query
- `%page%` — Page number (blank on page 1)
- `%name%` — Author display name
- `%date%` — Post publication date
- `%excerpt%` — Post excerpt

### Seed Title Templates via WP-CLI

```bash
$WP eval "
\$opts = (array) get_option('rank-math-options-titles', []);
\$opts['homepage_title']    = '%sitename% %sep% %sitedesc%';
\$opts['pt_post_title']     = '%title% %sep% %sitename%';
\$opts['pt_page_title']     = '%title% %sep% %sitename%';
\$opts['tax_category_title'] = '%term% %sep% %sitename% %page%';
\$opts['search_title']      = 'Search: %searchphrase% %sep% %sitename%';
\$opts['404_title']         = 'Page Not Found %sep% %sitename%';
\$opts['author_archive_title'] = '%name% %sep% %sitename%';
update_option('rank-math-options-titles', \$opts);
echo 'Title templates updated.';
"
```

---

## 7. Meta Description Generation

### Priority Order

1. **Manual** — `rank_math_description` post meta (highest priority)
2. **Excerpt** — Post excerpt if set
3. **First paragraph** — First `<p>` block in post content
4. **Trimmed content** — First 155 characters of stripped content

**Target length:** 150–160 characters.

### Auto-Generate Description Function

```php
function prefix_auto_meta_description( $post_id ) {
    $existing = get_post_meta( $post_id, 'rank_math_description', true );
    if ( ! empty( $existing ) ) {
        return $existing;
    }

    $post = get_post( $post_id );
    if ( ! $post ) {
        return '';
    }

    // Try excerpt first
    if ( ! empty( $post->post_excerpt ) ) {
        $text = $post->post_excerpt;
    } else {
        // Try first paragraph
        $content = $post->post_content;
        if ( preg_match( '/<p[^>]*>(.*?)<\/p>/is', $content, $matches ) ) {
            $text = $matches[1];
        } else {
            $text = $content;
        }
    }

    $text = wp_strip_all_tags( $text );
    $text = trim( preg_replace( '/\s+/', ' ', $text ) );
    $desc = mb_substr( $text, 0, 155 );

    // Avoid cutting mid-word
    if ( mb_strlen( $text ) > 155 ) {
        $desc = mb_substr( $desc, 0, mb_strrpos( $desc, ' ' ) );
        $desc .= '...';
    }

    return $desc;
}
```

### Bulk Seed via WP-CLI

```bash
$WP eval "
\$posts = get_posts(['post_type' => ['post','page'], 'posts_per_page' => -1, 'post_status' => 'publish']);
\$count = 0;
foreach (\$posts as \$p) {
    \$existing = get_post_meta(\$p->ID, 'rank_math_description', true);
    if (!empty(\$existing)) continue;
    \$text = wp_strip_all_tags(\$p->post_excerpt ?: \$p->post_content);
    \$text = trim(preg_replace('/\s+/', ' ', \$text));
    \$desc = mb_substr(\$text, 0, 155);
    if (mb_strlen(\$text) > 155) {
        \$desc = mb_substr(\$desc, 0, mb_strrpos(\$desc, ' '));
    }
    if (mb_strlen(\$desc) > 10) {
        update_post_meta(\$p->ID, 'rank_math_description', \$desc);
        \$count++;
    }
}
echo \"Seeded \$count meta descriptions.\";
"
```

---

## 8. llms.txt Template

```
# {Site Name}

> {Site Description}

## Pages
- [Page Title](url): excerpt or first 20 words

## Blog Posts
- [Post Title](url): excerpt

## Contact
- Website: {home_url}
```

### Generate llms.txt via WP-CLI

```bash
$WP eval "
\$name = get_bloginfo('name');
\$desc = get_bloginfo('description');
\$home = home_url('/');
\$out  = \"# \$name\n\n> \$desc\n\n\";

// Pages
\$out .= \"## Pages\n\";
\$pages = get_posts(['post_type' => 'page', 'posts_per_page' => -1, 'post_status' => 'publish', 'orderby' => 'menu_order', 'order' => 'ASC']);
foreach (\$pages as \$p) {
    \$url     = get_permalink(\$p->ID);
    \$excerpt = \$p->post_excerpt ?: wp_trim_words(wp_strip_all_tags(\$p->post_content), 20, '...');
    \$out    .= \"- [\$p->post_title](\$url): \$excerpt\n\";
}

// Blog posts
\$out  .= \"\n## Blog Posts\n\";
\$posts = get_posts(['post_type' => 'post', 'posts_per_page' => -1, 'post_status' => 'publish', 'orderby' => 'date', 'order' => 'DESC']);
foreach (\$posts as \$p) {
    \$url     = get_permalink(\$p->ID);
    \$excerpt = \$p->post_excerpt ?: wp_trim_words(wp_strip_all_tags(\$p->post_content), 20, '...');
    \$out    .= \"- [\$p->post_title](\$url): \$excerpt\n\";
}

// Contact
\$out .= \"\n## Contact\n\";
\$out .= \"- Website: \$home\n\";

file_put_contents(ABSPATH . 'llms.txt', \$out);
echo 'Generated llms.txt at ' . ABSPATH . 'llms.txt';
echo PHP_EOL . '---' . PHP_EOL . \$out;
"
```

---

## 9. robots.txt Template

```
# robots.txt for WordPress
User-agent: *
Allow: /
Disallow: /wp-admin/
Allow: /wp-admin/admin-ajax.php
Disallow: /wp-includes/
Disallow: /wp-content/plugins/
Disallow: /readme.html
Disallow: /license.txt
Disallow: /?s=
Disallow: /search/
Disallow: /cgi-bin/
Disallow: /trackback/

# AI Crawlers — Allow by default (confirm with user before changing)
User-agent: GPTBot
Allow: /

User-agent: ClaudeBot
Allow: /

User-agent: Google-Extended
Allow: /

User-agent: PerplexityBot
Allow: /

User-agent: CCBot
Allow: /

User-agent: Bytespider
Disallow: /

# Sitemap
Sitemap: {home_url}/sitemap_index.xml
```

> **Note:** AI crawler policy (Allow vs Disallow) should be confirmed with the site owner before writing. The defaults above allow all major AI crawlers except Bytespider.

### Generate robots.txt via WP-CLI

```bash
$WP eval "
\$home = home_url('/');
\$robots = 'User-agent: *
Allow: /
Disallow: /wp-admin/
Allow: /wp-admin/admin-ajax.php
Disallow: /wp-includes/
Disallow: /wp-content/plugins/
Disallow: /readme.html
Disallow: /license.txt
Disallow: /?s=
Disallow: /search/
Disallow: /cgi-bin/
Disallow: /trackback/

# AI Crawlers
User-agent: GPTBot
Allow: /

User-agent: ClaudeBot
Allow: /

User-agent: Google-Extended
Allow: /

User-agent: PerplexityBot
Allow: /

User-agent: CCBot
Allow: /

User-agent: Bytespider
Disallow: /

Sitemap: ' . \$home . 'sitemap_index.xml
';
file_put_contents(ABSPATH . 'robots.txt', \$robots);
echo 'Generated robots.txt at ' . ABSPATH . 'robots.txt';
"
```

---

## 10. Breadcrumb Integration Code

### PHP Function

```php
function prefix_breadcrumbs() {
    if (is_front_page()) return;
    echo '<nav class="breadcrumbs" aria-label="Breadcrumb">';
    if (function_exists('rank_math_the_breadcrumbs')) {
        rank_math_the_breadcrumbs();
    } else {
        echo '<a href="' . esc_url(home_url('/')) . '">Home</a>';
        echo ' &raquo; ';
        if (is_singular()) { the_title(); }
        elseif (is_archive()) { the_archive_title(); }
        elseif (is_search()) { echo 'Search results'; }
        elseif (is_404()) { echo 'Page Not Found'; }
    }
    echo '</nav>';
}
```

### Breadcrumb CSS

```css
.breadcrumbs {
    padding: 12px 0;
    font-size: 0.875rem;
    color: #6b7280;
}
.breadcrumbs a {
    color: #3b82f6;
    text-decoration: none;
}
.breadcrumbs a:hover {
    text-decoration: underline;
}
.breadcrumbs .separator {
    margin: 0 0.5rem;
    color: #9ca3af;
}
```

### Enable Breadcrumbs in Rank Math

```bash
$WP eval "
\$opts = (array) get_option('rank-math-options-general', []);
\$opts['breadcrumbs'] = 'on';
update_option('rank-math-options-general', \$opts);
echo 'Breadcrumbs enabled.';
"
```

---

## 11. FAQ Detection Patterns

FAQ content can appear in several forms. Detect and convert to JSON-LD schema automatically.

### Detection Sources

1. **ACF repeater fields** — field names: `faq_items`, `faqs`, `faq_cards`, `faq`
2. **`<details>/<summary>` HTML** — accordion-style FAQ in content
3. **H2/H3 headings ending with `?`** — followed by `<p>` answer content

### FAQ Auto-Detection and JSON-LD Generator

```php
function prefix_detect_faq_jsonld( $post_id ) {
    $faqs = [];

    // 1. Check ACF repeater fields
    $acf_names = ['faq_items', 'faqs', 'faq_cards', 'faq'];
    foreach ( $acf_names as $field_name ) {
        if ( function_exists('have_rows') && have_rows( $field_name, $post_id ) ) {
            while ( have_rows( $field_name, $post_id ) ) {
                the_row();
                $q = get_sub_field('question') ?: get_sub_field('title');
                $a = get_sub_field('answer')   ?: get_sub_field('content') ?: get_sub_field('text');
                if ( $q && $a ) {
                    $faqs[] = [ 'q' => wp_strip_all_tags($q), 'a' => wp_strip_all_tags($a) ];
                }
            }
        }
    }

    // 2. Check <details>/<summary> in content
    $content = get_post_field('post_content', $post_id);
    if ( preg_match_all('/<summary[^>]*>(.*?)<\/summary>\s*(.*?)<\/details>/is', $content, $matches, PREG_SET_ORDER) ) {
        foreach ( $matches as $m ) {
            $q = wp_strip_all_tags( $m[1] );
            $a = wp_strip_all_tags( $m[2] );
            if ( $q && $a ) {
                $faqs[] = [ 'q' => $q, 'a' => $a ];
            }
        }
    }

    // 3. Check H2/H3 headings ending with ?
    if ( preg_match_all('/<h[23][^>]*>(.*?\?)<\/h[23]>\s*<p>(.*?)<\/p>/is', $content, $matches, PREG_SET_ORDER) ) {
        foreach ( $matches as $m ) {
            $q = wp_strip_all_tags( $m[1] );
            $a = wp_strip_all_tags( $m[2] );
            if ( $q && $a ) {
                $faqs[] = [ 'q' => $q, 'a' => $a ];
            }
        }
    }

    if ( empty( $faqs ) ) {
        return '';
    }

    // Build JSON-LD
    $schema = [
        '@context'   => 'https://schema.org',
        '@type'      => 'FAQPage',
        'mainEntity' => [],
    ];
    foreach ( $faqs as $faq ) {
        $schema['mainEntity'][] = [
            '@type'          => 'Question',
            'name'           => $faq['q'],
            'acceptedAnswer' => [
                '@type' => 'Answer',
                'text'  => $faq['a'],
            ],
        ];
    }

    return '<script type="application/ld+json">' . wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT ) . '</script>';
}
```

### Inject FAQ Schema into Head

```php
add_action('wp_head', function() {
    if ( is_singular() ) {
        $jsonld = prefix_detect_faq_jsonld( get_the_ID() );
        if ( $jsonld ) {
            echo $jsonld;
        }
    }
});
```

---

## 12. SEO Seeding Sequence

Recommended order for bulk SEO setup on a new or existing site:

1. **Install + activate Rank Math** — `$WP plugin install seo-by-rank-math --activate`
2. **Enable modules** — activate all recommended modules (Section 2)
3. **Configure general settings** — breadcrumbs, link behavior, image SEO (Section 3)
4. **Configure title templates** — set formulas per page type (Section 6)
5. **Configure sitemap** — set post types and taxonomies to include
6. **Set Organization / LocalBusiness schema** — configure site-wide schema (Section 5)
7. **Configure OG / Social defaults** — set default OG image, social profiles
8. **Enable IndexNow** — activate instant indexing for Bing / Yandex
9. **Seed per-page SEO meta** — `rank_math_title`, `rank_math_focus_keyword`, robots (Section 4)
10. **Seed meta descriptions** — auto-generate from excerpt/content (Section 7)
11. **Set image alt texts** — bulk update missing alt attributes
12. **Generate robots.txt** — write optimized robots.txt (Section 9)
13. **Generate llms.txt** — write AI-readable site summary (Section 8)
14. **Flush sitemap cache + rewrite rules** — finalize:

```bash
$WP eval "
if (class_exists('RankMath')) {
    // Trigger sitemap regeneration
    delete_transient('rank_math_sitemap_cache');
    \RankMath\Sitemap\Cache::invalidate_storage();
}
"
$WP rewrite flush
```

### Verification Checklist

After seeding, verify:

```bash
# Check title templates are set
$WP eval "print_r(get_option('rank-math-options-titles'));"

# Count posts with meta descriptions
$WP eval "
global \$wpdb;
\$count = \$wpdb->get_var(\"SELECT COUNT(*) FROM \$wpdb->postmeta WHERE meta_key = 'rank_math_description' AND meta_value != ''\");
echo \"Posts with meta descriptions: \$count\";
"

# Verify sitemap exists
$WP eval "echo home_url('/sitemap_index.xml');"

# Verify robots.txt exists
$WP eval "echo file_exists(ABSPATH . 'robots.txt') ? 'robots.txt exists' : 'robots.txt missing';"

# Verify llms.txt exists
$WP eval "echo file_exists(ABSPATH . 'llms.txt') ? 'llms.txt exists' : 'llms.txt missing';"
```

---

## 13. Lighthouse SEO Audit Gotchas (hard-won)

Non-obvious traps that make a fix *look* applied while the audit keeps failing. Verify against these before reporting an SEO finding fixed.

### `link-text` matches VISIBLE innerText — not `aria-label`

The Lighthouse `link-text` audit (`core/audits/seo/link-text.js`) lowercases/trims the anchor's **`link.text`** (rendered innerText) and checks it against a blocklist: `click here`, `here`, `learn more`, `more`, `read more`, `this`, `start`, … (per-language). **`aria-label`, `title`, and `nodeLabel` are ignored** — adding an `aria-label` fixes the a11y accessible-name but does **not** satisfy this SEO audit.

To keep a design's non-descriptive label (e.g. a "LEARN MORE" button) *and* pass, append a visually-hidden descriptive suffix **inside** the anchor so it becomes part of innerText:

```php
<a class="btn" href="<?php echo esc_url( $url ); ?>">
    <?php echo esc_html( $label ); // e.g. "LEARN MORE" — stays visible ?>
    <?php if ( $context ) : ?><span class="screen-reader-text"><?php
        echo esc_html( 'about ' . $context ); // e.g. "about Home Search"
    ?></span><?php endif; ?>
</a>
```

Now `link.text` = "LEARN MORE about Home Search" → not a blocklist match → audit passes; the button still visually reads "LEARN MORE".

- **Critical:** the hidden span must use the `.screen-reader-text` **clip** pattern (`position:absolute; clip: rect(...); width:1px`) — NOT `display:none` or `visibility:hidden`, which are excluded from innerText and would fail again. (A `.screen-reader-text` rule is required in the theme CSS; see a11y standards.)
- The same non-descriptive label often repeats across a reusable button component — fix the component/template part once, then re-check *every* page that uses it (Lighthouse audits per-URL; a homepage pass doesn't clear inner pages).
- `identical-links-same-purpose` (a11y, weight 0) can also flag two same-text links (e.g. two "VIEW ALL") pointing to different destinations — the same hidden-suffix technique resolves it.

### Meta description: let the SEO plugin own it — don't emit a theme fallback

If Rank Math (or Yoast) is active and configured, do **not** also `echo` a hardcoded `<meta name="description">` from the theme `wp_head`. Two tags = a duplicate-description warning, and the plugin's dynamic/templated value is the one you want. A common failure mode: a theme adds a front-page meta-description fallback "to be safe" while the SEO plugin's setup wizard was incomplete → once the wizard is finished, the tag is duplicated. Remove the theme fallback; verify only one `<meta name="description">` renders (`curl -s <url> | grep -c 'name="description"'` → `1`).

