# SEO Audit → Fix → Report Workflow

> Royal MCP Pro agency workflow for wp_audit_seo_bulk and bulk_update_meta writers: scan a portfolio for SEO issues, fix in bulk with 168h undo, and report.

- Canonical: <https://royalplugins.com/support/royal-mcp-pro/seo-audit-fix-report-workflow/>
- HTML version: <https://royalplugins.com/support/royal-mcp-pro/seo-audit-fix-report-workflow/>

---

End-to-end agency workflow combining `wp_audit_seo_bulk` (the audit) with the four `*_bulk_update_meta` writers (the fix), then turning the audit's structured response into a client-facing report. Designed for the recurring rhythm most agencies run monthly against every portfolio site: scan for problems, fix in bulk, verify, hand the client a summary.

When to run this workflow

- Monthly SEO health check across a portfolio of client sites
- Onboarding a new site — establish a baseline of what needs fixing before the first content push
- After a site migration or theme swap — surface anything that got dropped or broken
- Post-content-audit follow-up — you know some pages have thin content; this quantifies which ones
- Pre-launch cleanup — pair with [Site Launch Readiness](https://royalplugins.com/support/royal-mcp-pro/site-launch-readiness/) to catch the on-page issues that the composer's home-page-only check doesn't cover

## Prerequisites

- Royal MCP Pro installed and licensed (see [Getting Started](https://royalplugins.com/support/royal-mcp-pro/getting-started/))
- MCP client connected to your site
- Cap: `manage_options` for the audit; `manage_options` + per-post `edit_post` for the bulk fix
- An SEO plugin (Yoast, Rank Math, SEOPress, or All in One SEO) — the audit auto-detects which one is active. If none is active the audit falls back to reading WP native fields (`post_title` and `post_excerpt`), but the bulk-fix writers require the matching plugin to be active
- Optional: SiteVault Pro (for the `snapshot: true` flag on the audit call to fire a pre-audit backup)

## The workflow at a glance

1. **Optional pre-audit snapshot** — SiteVault backup fires before any read so you have a known-good restore point.
2. **Run `wp_audit_seo_bulk`** — scan the target post type, paginating through if needed. Response contains per-page findings + a rollup with issues\_by\_type and worst\_offenders.
3. **Bucket the findings by issue key** — group posts by issue so each bulk-fix call touches a homogeneous batch.
4. **Dry-run the bulk fix** — call the plugin-specific writer with `dry_run: true` first. Review the diff. Commit with the returned `commit_token` within 5 minutes.
5. **Re-audit** — run `wp_audit_seo_bulk` again on the same slice. Issue counts should drop to reflect what you just fixed.
6. **Format the report** — the audit's `structuredContent` is your report data. Ask the AI to shape it into whatever the client wants.

Steps 2–6 are all MCP calls. Nothing in this workflow requires opening wp-admin unless you want to spot-check a fix visually.

## Step 1: Optional pre-audit snapshot

The audit itself is read-only, but the bulk fixes that follow are not. If you want a fresh restore point from just before this workflow started:

```
Run wp_audit_seo_bulk on post_type "page" with snapshot=true.
```

The audit response will include a `sitevault_snapshot` object describing what happened:

- `status: ok` with `via` and `id` — SiteVault fired and returned a backup ID
- `status: skipped` with `message: 'SiteVault not installed — snapshot skipped, audit proceeds against live state.'` — no SiteVault detected; audit still runs

Snapshot failure never aborts the audit — the audit is safe to run without one. You get informed either way.

## Step 2: Run `wp_audit_seo_bulk`

### Basic call

```
Run wp_audit_seo_bulk on post_type "post".
```

Default: 100 posts per page, page 1, rollup on. If the post type has more than 100 posts you'll need to paginate (next section).

### Response shape

Abbreviated to one representative page for readability:

```
{
  "isError": false,
  "content": [
    { "type": "text", "text": "SEO audit — 47 post audited. Top issues: 22× description_missing, 18× title_too_short, 9× no_featured_image. SEO plugin: yoast." }
  ],
  "structuredContent": {
    "post_type": "post",
    "pagination": { "page": 1, "per_page": 100, "total_pages": 1, "total_posts": 47 },
    "seo_plugin_detected": "yoast",
    "pages": [
      {
        "post_id": 128,
        "url": "https://example.com/why-half-of-your-plugins-are-eating-your-tttf/",
        "title":       { "value": "Why half of your plugins are eating your TTFB", "length": 47, "issue": null },
        "description": { "value": "",                                                "length": 0,  "issue": "missing" },
        "noindex": false,
        "h1_count": 1,
        "word_count": 942,
        "image_count": 4,
        "images_missing_alt": 2,
        "featured_image": true,
        "issues": [ "description_missing", "2_images_missing_alt" ]
      }
      // … 46 more page objects
    ],
    "sitevault_snapshot_id": null,
    "sitevault_snapshot": { "status": "skipped", "message": "…" },
    "rollup": {
      "total_pages_audited": 47,
      "issues_by_type": {
        "description_missing":  22,
        "title_too_short":      18,
        "no_featured_image":     9,
        "images_missing_alt":    7,
        "h1_missing":            3,
        "title_missing":         2
      },
      "worst_offenders": [
        { "post_id": 128, "issue_count": 5 },
        { "post_id":  91, "issue_count": 4 }
        // … up to 10 worst offenders
      ]
    }
  },
  "undo": { "token": "…", "expires_at": "…", "ttl_hours": 168, "summary": "SEO audit — read-only, undo is a no-op" }
}
```

About that undo token

The audit is read-only, but the response ships an undo token anyway for API consistency (so MCP clients that always look for one don't blow up). Redeeming it resolves to a no-op. The subsequent bulk-fix calls ship real undo tokens — those are the ones that matter.

### Pagination

For post types with more than 100 posts (or if you want smaller page sizes for slower servers):

```
Run wp_audit_seo_bulk on post_type "post" with paginate={page:1, per_page:200}.
```

Pagination caps: `per_page` minimum 1, maximum 500 (enforced by the tool regardless of what the schema minimum suggests). Loop through `pagination.total_pages` to cover the full portfolio.

### Statuses included

The audit queries posts in these statuses: `publish`, `draft`, `pending`, `private`, `future`. Trashed posts are intentionally excluded — they don't appear in SEO surfaces so scoring them would inflate issue counts against posts nobody sees.

### Disable the rollup

If you only want the per-page findings (no aggregate):

```
Run wp_audit_seo_bulk on post_type "post" with rollup=false.
```

Response omits the `rollup` key. Useful when piping into your own aggregation script.

## Step 3: Bucket the findings by issue key

Each per-page object has an `issues` array. Group posts by which issue is on their list so the fix batches are homogeneous — all `title_too_short` in one call, all `description_missing` in the next, etc.

### The full issue-key catalog

| Issue key | Trigger | Fixable via bulk\_update\_meta? |
| --- | --- | --- |
| `title_missing` | Both SEO plugin title and `post_title` are empty | Yes — pass a `title` value |
| `title_too_short` | Title length < 30 chars | Yes — pass a longer `title` |
| `title_too_long` | Title length > 60 chars | Yes — pass a shorter `title` |
| `description_missing` | Both SEO plugin description and `post_excerpt` are empty | Yes — pass a `description` value |
| `description_too_short` | Description length < 120 chars | Yes — pass a longer `description` |
| `description_too_long` | Description length > 160 chars | Yes — pass a shorter `description` |
| `noindex_true` | Post is set to noindex | Yes — pass `noindex: false` (see per-plugin notes below) |
| `h1_missing` | No `<h1>` in post\_content | No — edit the post content in the editor |
| `h1_multiple` | More than one `<h1>` in post\_content | No — edit the post content |
| `no_featured_image` | Post has no featured image set | No — set via `wp_set_featured_image` (Free tier) after uploading media |
| `N_images_missing_alt` | N images in post\_content lack an `alt` attribute | No — edit each image's alt text in the Media Library or editor |

**Rollup bucketing:** the rollup normalizes compound keys like `3_images_missing_alt` into a single `images_missing_alt` bucket for counting purposes. The per-page objects still show the compound version so you know how many images per post need alt text.

### Length thresholds (constants)

- **Title:** 30–60 chars (min `TITLE_MIN`, max `TITLE_MAX`)
- **Description:** 120–160 chars (min `DESC_MIN`, max `DESC_MAX`)

These match Yoast / Rank Math / SEOPress / AIOSEO / SEObolt's own default snippet-preview ranges. Not configurable via filter as of 1.0.2.

## Step 4: Bulk-fix with the SEO-plugin-specific writer

Four writers, one per supported plugin. Pick the one that matches the value in the audit's `seo_plugin_detected` field:

- `yoast_bulk_update_meta`
- `rankmath_bulk_update_meta`
- `seopress_bulk_update_meta`
- `aioseo_bulk_update_meta`

All four share the same argument shape, two-step commit pattern, drift detection, and 168h undo TTL. See the [SEO Agency Suite reference](https://royalplugins.com/support/royal-mcp-pro/seo-agency-suite/) for the full argument grammar including the **3-state field model** (OMIT / null / value) that keeps you from accidentally overwriting untouched fields.

### Two-step commit walkthrough

#### Step 4a: Dry-run to preview

```
Call yoast_bulk_update_meta with dry_run=true and updates=[
  { post_id: 128, description: "Half the plugins on the average WordPress site are quietly eating your TTFB. Here's how to spot them, and what to swap them for." },
  { post_id: 91,  description: "A 12-minute walkthrough on turning a slow WooCommerce cart into a sub-second checkout." }
  // … up to 100 rows per call
].
```

Response includes a per-row preview showing what the value would change from and to, plus a `commit_token` to redeem in step 4b. Nothing is written yet.

#### Step 4b: Commit with the returned token

```
Call yoast_bulk_update_meta with commit_token="{token from step 4a}".
```

The tool re-reads the current values of every row and compares against what they were at dry-run time. Any post whose value changed between dry-run and commit gets skipped with a `drift` warning (someone else edited it in that 5-minute window). The rest apply.

Commit tokens have a 5-minute TTL and are one-shot

If you take more than 5 minutes between dry-run and commit, the token expires — you'll need to re-dry-run. Tokens also can't be redeemed twice; a successful commit invalidates the token immediately.

### Row cap

Each call handles up to **100 rows**. For portfolio-wide fixes, chunk your buckets into calls of 100 or fewer.

### The noindex quirks

SEOPress noindex has no explicit-index option

On Yoast, Rank Math, and AIOSEO, passing `noindex: false` explicitly sets the post to index. On **SEOPress**, both `noindex: false` and `noindex: null` delete the meta row (reverting to the site default). SEOPress just doesn't have a "force index" concept — if the site default is index-all, that's what you get; if it's noindex-all, you'd need to change the site default instead.

AIOSEO's dual-table quirk

AIOSEO writes to legacy `post_meta` AND to a custom `wp_aioseo_posts` table on modern versions. The writer dual-writes to keep both surfaces coherent. Posts that AIOSEO has never touched (no row in `wp_aioseo_posts`) surface in warnings — open them once in the AIOSEO editor first to seed a row, then re-run the bulk fix.

## Step 5: Re-audit to verify

Run `wp_audit_seo_bulk` a second time on the same post type. Compare the two `issues_by_type` rollups:

```
Before:
  description_missing:  22
  title_too_short:      18

After (same 100-row batch fixed):
  description_missing:   0   ← fixed the 22, no new ones
  title_too_short:      18   ← untouched batch, still there
```

If a fixed issue count didn't drop as expected, likely causes:

- Drift-detection skipped some rows — check the commit response's `skipped` array for post IDs that were touched during your 5-minute window
- Wrong SEO plugin writer was called (audit detected Yoast, you called `rankmath_bulk_update_meta`) — check `seo_plugin_detected` in the audit response
- A subset of your rows had a length that still hit the threshold (e.g. you set a description that's 118 chars — still under the 120-char `DESC_MIN`)

## Step 6: Format the report

Royal MCP Pro doesn't ship a "generate a client PDF" button — the report data is the audit's `structuredContent`. The AI you're using is what formats it. Ask for whatever shape works for the client.

### Executive summary shape

```
Take the two wp_audit_seo_bulk responses (before + after) and write me an
executive summary in 5 bullets for a non-technical client. Mention what
was audited, the top 3 issue types found, how many were fixed, and what's
left as a follow-up.
```

### Punch-list shape (for the client's dev/content team)

```
From the after-fix audit response, list every page with remaining issues
grouped by URL. For each URL show which issues remain and what needs to
happen to close each one (edit content vs. edit meta vs. add featured image).
```

### Trend-over-time shape (agency retention)

Log each month's `issues_by_type` rollup to a spreadsheet. Ask the AI to visualize the trend across 3–6 months so the client sees the graph go down over time.

The audit's plain-text `content[0].text` field

The audit's top-level `content` field always contains a single-sentence summary in the format: `"SEO audit — {N} {post_type} audited. Top issues: {N× issue1, N× issue2, N× issue3}. SEO plugin: {detected}."` Great as a one-line agency status update in Slack / Discord / email without any AI formatting.

## Common scenarios

### Monthly agency check across 20 portfolio sites

Same audit call on each site. Log `issues_by_type` rollups to a shared spreadsheet. The client sites where an issue count creeps up month over month are your action list.

### New-site onboarding baseline

Run the audit on `post` and `page` post types (and any custom post types the site uses). The rollup is your first-month project scope. Fix the top-3 buckets in bulk, hand the client a before/after report at the end of month 1.

### Post-migration cleanup

Migrations frequently lose SEO meta (theme change wipes theme-stored meta, migration tools skip custom fields, plugin swap doesn't map old-plugin keys to new-plugin keys). Run the audit right after migration; the resulting rollup shows exactly what got lost. Bulk-restore in one batch.

### Thin-content follow-up

Content audit surfaces "these 40 pages have < 300 words." Cross-reference with `wp_audit_seo_bulk`'s `word_count` field per page. Anything under 300 words AND with a `no_featured_image` or `description_missing` issue is a genuinely thin, un-loved page. Prioritize accordingly.

## Related documentation

- [SEO Agency Suite reference](https://royalplugins.com/support/royal-mcp-pro/seo-agency-suite/) — full argument grammar for the 4 bulk writers + the 3-state field model + drift-detection semantics
- [Site Launch Readiness](https://royalplugins.com/support/royal-mcp-pro/site-launch-readiness/) — `wp_prepare_for_launch` includes a home-page-only SEO check; use this workflow for the deep site-wide scan the launch check defers to
- [Cross-plugin Composers](https://royalplugins.com/support/royal-mcp-pro/cross-plugin-composers/) — where to plug SEO audits into a larger monthly-maintenance composer if you're chaining scans

## Still Stuck? Two-Step Support Path

If the audit or bulk-fix calls aren't producing what you expect, work through these two steps in order.

### Step 1: Start with the Royal MCP Troubleshooting Guide

[**Royal MCP Troubleshooting — Start Here**](https://royalplugins.com/support/royal-mcp/troubleshooting-start-here/) covers MCP-layer issues (connection, discovery, auth).

For Pro-specific patterns:

- [Pro tool returned upgrade prompt with valid license](https://royalplugins.com/support/royal-mcp-pro/pro-tool-returned-upgrade-prompt/)
- [Undo token errors](https://royalplugins.com/support/royal-mcp-pro/undo-token-errors/)

### Step 2: Email priority support

If you've worked through Start Here and the relevant Pro-specific doc and the workflow still isn't working, email priority support from your purchase email address at **[support@royalplugins.com](mailto:support@royalplugins.com)**. Priority email support is included with your license — typical response within 24 hours. **Never include your license key in email**; we look it up from your purchase address.

What to include in your email

- **Your hosting provider**
- **Royal MCP Pro version**
- **Which SEO plugin** is active + its version (the audit's `seo_plugin_detected` field answers this)
- **Which MCP client** and how the call was phrased
- **Post type** being audited + approximate total post count
- **The full audit response** if you're reporting an audit issue, or the full dry-run + commit responses if you're reporting a bulk-fix issue
- **Any drift-warnings from the commit response** if you're troubleshooting missed rows
- **Screenshot of the Pro tool row in Audit Log** with View Details expanded

[← Site Launch Readiness](https://royalplugins.com/support/royal-mcp-pro/site-launch-readiness/)
[Troubleshooting Start Here →](https://royalplugins.com/support/royal-mcp-pro/troubleshooting-start-here/)
