# Rank Math → ThinkRank Migration Gap Report

**Date:** 2026-06-18
**Site:** ealite (WordPress 6.9.4)
**Source:** Rank Math 1.0.272 + Rank Math Pro 3.0.73
**Target:** ThinkRank 1.13.0 + ThinkRank Pro 1.1.0
**Procedure:** Full clear of all ThinkRank data → fresh export → fresh migrate (mirrors the REST `/import` workflow), with the post-migration SEO scoring now wired into `Snapshot_Migrator`.

> **Update 2026-06-18:** Gaps #1, #2, #4 and #5 have been **fixed** in `Rankmath_Exporter` / `Snapshot_Migrator` and re-validated by a second clean migration. See the Resolution column and the revised counts below. Gap #3 (`review`) is now handled (dropped to empty rather than stored invalid). The remaining open items are unsupported-by-design integrations.

---

## 1. Run Summary (post-fix re-run)

| Phase | Result |
|-------|--------|
| Cleared | 9,132 `_thinkrank_*` postmeta, 7 termmeta, seo_scores truncated, migrated option blobs + settings rows + snapshot rows removed |
| Exported | postmeta **2,384**, termmeta **6**, usermeta 0, redirections **1**, settings 1 |
| Migrated | processed **689**, skipped **1,709**, **analyzed 2,384**, keywords seeded 1,707 |
| SEO Overview | `content_optimized = 2384`, `average_seo_score = 42.1` — **populated immediately, no manual reanalysis** |

`processed` dropped from 2,395 (pre-fix) to 689 because the robots over-migration fix (Gap #4) no longer writes a redundant `index,follow` override for posts that had no real SEO data — those are now correctly counted as skipped. Every migrated post is still scored (`analyzed = 2,384`). All scores are written under the administrator (user_id = 1), the same id the SEO Overview query filters on — so the dashboard reflects data on first load.

---

## 2. Field-Level Verification (counts: Rank Math source vs ThinkRank result)

| Field | RM source | TR result | Verdict |
|-------|-----------|-----------|---------|
| SEO title (`rank_math_title` → `_thinkrank_seo_title`) | 163 | 163 | ✅ Match (template vars resolved, e.g. `%title%` → real title) |
| Meta description | 497 | 497 | ✅ Match |
| Focus keyword | 546 | 546 | ✅ Match |
| Canonical URL | 19 | 19 | ✅ Match |
| Primary category | 251 (199 are `0`/empty, 52 real) | 52 | ✅ Correct — zero/empty intentionally skipped |
| Schema type (`rank_math_rich_snippet`) | 392 (incl. 132 `off`, 31 `review`) | 229 valid (`Article` 135, `SoftwareApplication` 80, `Product` 14) | ✅ **Fixed** — mapped to ThinkRank vocabulary; `off`/unsupported dropped (Gaps #1–#3) |
| OG (Facebook) title | 1 | 1 | ✅ Match |
| OG image | — | 6 | ✅ Migrated |
| Pillar content | 3 | 3 | ✅ Match |
| Robots payload | 1,919 had `rank_math_robots` | **315** `_thinkrank_robots_meta_enabled` (only active directives) | ✅ **Fixed** — all-false arrays no longer written (Gap #4) |
| SEO score (analysis) | n/a (computed locally) | 2,384 | ✅ Every migrated post analyzed |
| Term SEO title | 3 | 3 | ✅ **Fixed** — `%term%` now resolved (e.g. `Elementor Blog - …`; Gap #5) |
| Term focus keyword | 3 (2 empty templates) | 1 | ✅ Correct — empty templates skipped |

### Settings verified ✅
- `thinkrank_global_seo_settings`: separator `|`, per-type title/description templates (post, page, attachment, **elementor_library** CPT), attachment noindex — all migrated.
- `thinkrank_social_media_settings`: facebook URL (the only RM-configured profile).
- `thinkrank_global_robot_meta_settings`: `noindex_date_archives`, `noindex_author_archives`.
- `seo_settings` table → `site_identity`: homepage_title, site_name, breadcrumbs; `image_seo`: add_missing_alt/title + formats.
- **Sitemap**: correctly wrote nothing — every RM value (posts/pages/categories on, tags off, 1000/page) already equals ThinkRank's defaults, so there was nothing to change.

---

## 3. Gap Table

Gaps #1–#5 are **resolved** (see Resolution column). #6–#10 remain open as unsupported-by-design.

| # | Feature / Data Type | Source in Rank Math | Expected in ThinkRank | Actual (pre-fix) | Status | Resolution | Priority |
|---|--------------------|---------------------|-----------------------|------------------------|--------|---------------|----------|
| 1 | **Schema type vocabulary** | `rank_math_rich_snippet` = `article`, `software`, `product` (lowercase slugs) | `_thinkrank_selected_schema_type` = PascalCase from `get_supported_schema_types()` | Values stored **verbatim lowercase**; none matched ThinkRank's vocabulary, so the metabox Schema tab showed no valid selection. | ✅ **Fixed** (was Incorrectly Migrated) | Added `SCHEMA_TYPE_MAP` + `map_schema_type()` in `Rankmath_Exporter` (`article→Article`, `product/woocommerce→Product`, `software→SoftwareApplication`, `event→Event`, `howto→HowTo`, `faq→FAQPage`, `person→Person`, `restaurant→LocalBusiness`). Verified: `Article` 135, `SoftwareApplication` 80, `Product` 14. | **High** |
| 2 | **Schema type `off`** | `rank_math_rich_snippet` = `off` (132 posts = "no schema") | No schema-type written | `_thinkrank_selected_schema_type` = `off` for 132 posts (invalid) | ✅ **Fixed** (was Incorrectly Migrated) | `map_schema_type()` returns `''` for `off`/`none`; migrator's empty-skip leaves no value. Verified: zero `off` values after re-run. | **High** |
| 3 | **Schema `review` (and book/course/recipe/…) types** | `rank_math_rich_snippet` = `review` (31 posts), etc. | Equivalent ThinkRank schema type | Stored verbatim `review` (invalid) | ✅ **Handled** (Unsupported) | Slugs with no ThinkRank equivalent map to `''` (dropped cleanly) rather than storing an invalid value. Adding a native `Review` type to `get_supported_schema_types()` remains optional future work. | Medium |
| 4 | **Robots meta over-migration** | `rank_math_robots` present on 1,919 posts | `robots_meta_enabled` only where a directive existed | `_thinkrank_robots_meta_enabled = 1` on **all 2,384** posts | ✅ **Fixed** (was Incorrectly Migrated) | `migrate_robots_payload()` now skips all-false robots arrays (writes only when a directive is active or an advanced directive exists). Verified: 315 posts after re-run. | Low |
| 5 | **Term title template token `%term%`** | `rank_math_title` = `%term% Archives %page% %sep% %sitename%` | `%term%` resolved to the term name | `%term%` stripped to empty → `"Archives  - …"` | ✅ **Fixed** (was Partially Migrated) | Added `convert_term_template_variables()` resolving `%term%`/`%term_description%` with term context + whitespace collapse. Verified: term 195 → `"Elementor Blog - …"`. (Terms in unregistered taxonomies, e.g. product_cat when WooCommerce is inactive, are left unresolved by design.) | Medium |
| 6 | **Redirections** | `wp_rank_math_redirections` table (1 record); exported to snapshot as `redirections` type | Imported into a ThinkRank redirect store | **Not migrated** — `Snapshot_Migrator::MIGRATABLE_TYPES` is `postmeta/termmeta/settings`; `redirections` is exported but never consumed | **Unsupported** | If ThinkRank/Pro has a redirect manager, add a `migrate_redirections()` branch; otherwise document redirections as out of scope. | Medium |
| 7 | **Instant Indexing settings** | Rank Math Instant Indexing (Bing/IndexNow + Google API) | ThinkRank Instant Indexing config | **Not migrated** — `thinkrank_instant_indexing_settings` keeps its own ThinkRank key/post types; nothing read from RM | **Unsupported** | Map RM's IndexNow key + enabled post types into `thinkrank_instant_indexing_settings` if parity is desired. | Low |
| 8 | **Analytics settings** | RM Analytics (`console_email`, GSC/GA OAuth) | ThinkRank Analytics connection | **Not migrated** — exporter captures only `analytics_connected` (bool) in `extended`; `migrate_settings` never applies it. OAuth tokens are non-transferable by design. | **Unsupported** (expected) | Cannot migrate OAuth; optionally surface a "reconnect Google" notice when `analytics_connected` was true. | Low |
| 9 | **Taxonomy archive title/description templates** | `rank-math-options-titles` per-taxonomy keys (e.g. `tax_category_title`) | Per-taxonomy templates in ThinkRank Global SEO | **Not migrated** — `extract_rm_post_type_settings()` covers post types only, not taxonomies | **Partially Migrated** | Extend extraction to taxonomy templates and add a taxonomy branch to `migrate_post_type_settings()`. | Low |
| 10 | **Per-post extended meta** (breadcrumb title, FB/Twitter image IDs, twitter card type per post, secondary keywords beyond rank tracker) | RM post meta preserved in snapshot `extended` | Where ThinkRank has an equivalent field | **Not migrated** — `extended` is intentionally ignored by the migrator (secondary keywords are seeded to the Pro Rank Tracker only) | **Unsupported** (by design) | Document; add mappings only where a ThinkRank field exists. | Low |

---

## 4. Conclusion

The core post/term/settings migration is **accurate and complete** for every field ThinkRank models: titles, descriptions, focus keywords, canonical URLs, primary categories, social meta, pillar flags, robots directives, and global/site/image/sitemap settings all match the Rank Math source (the only count differences — primary_category and term focus keyword — are correct skips of empty/zero source values).

**The SEO scoring fix works end to end:** all 2,384 migrated posts were analyzed during migration and the SEO Overview is populated on first load (2,384 posts, avg 42.1) with **no manual reanalysis** required.

**All correctness defects are now fixed and re-validated** (Gaps #1–#5): schema types map to ThinkRank's PascalCase vocabulary with `off`/unsupported slugs dropped, robots overrides are only written for active directives, and term archive titles resolve `%term%`. A second clean migration confirmed the results above.

The remaining open items (#6–#10) are unsupported-by-design integrations — redirections, instant indexing, analytics OAuth, and per-taxonomy/extended meta — none of which block a clean Rank Math → ThinkRank migration of the data ThinkRank models.
