Technical SEO Recovery: Services Architecture Migration — A Square Solutions

748

Pages audited via REST API

718

Internal links migrated

0

Failures or broken references

6

Structural schema errors fixed

25

Posts per batch / 1.2s rate limit

30

Posts spot-checked post-migration

The Architecture Problem

The A Square Solutions WordPress site had grown organically with its primary service directory living under /services/. Over time this created a structural mismatch: the slug pattern communicated company identity rather than service taxonomy, and every internal link, nav reference, and anchor text across 700+ posts pointed to a URL that would eventually need to change.

The decision to migrate to /services/ was straightforward — the execution risk was not. Changing a high-traffic parent URL touches every post, page, and structured data block that references it. Done carelessly this creates broken internal links, orphaned canonical references, and schema @id chains that point to non-existent URLs.

The constraint that shaped everything: No 301 redirects could be added until Google Search Console confirmed the new /services/ architecture was indexed. This meant the old URL had to remain live during the migration, and all internal links had to be updated before any redirect was set. The old and new URLs would coexist temporarily, separated by canonical tags.

This case study documents the technical execution: how we audited every affected post, built an idempotent batch script, handled the edge cases that broke the first run, and fixed the six schema errors we discovered in the process.

What We Set Out to Do

  1. Audit the full post inventory — fetch every post and page via REST API with context=edit, identify all that contain /services/ references.
  2. Replace all internal link references — migrate href values, nav link text anchors, and any structured data URLs that referenced the old path.
  3. Fix existing schema errors — while auditing content, identify and resolve structural schema bugs that had been present before migration started.
  4. Verify idempotency — ensure the script could be run multiple times without double-replacing strings or corrupting already-migrated content.
  5. Document blockers — specifically, canonical URL management (blocked by Rank Math API limitation) and the legalName typo discovered during the audit.

Staged Migration Without Redirects

The migration had three phases that had to happen in sequence to avoid breaking the site at any point:

  1. Create destination first. The /services/ parent page (ID 25771) and /services/geo-seo/ child page had to exist before any internal links were changed. WordPress would silently 404 on a link to a non-existent slug.
  2. Batch-update all posts. Using WordPress REST API with context=edit to read raw content, replace all occurrences of /services/ with /services/, then POST the updated content. Idempotent: skip if no occurrences found in raw content.
  3. Set canonical on legacy URL. Add rel=canonical on /services/ pointing to /services/. This signals to Google the preferred URL before any 301 redirect. (Blocked: Rank Math canonical cannot be set via REST API — manual action required.)

Batch Script Architecture

The batch script iterated posts in pages of 25, fetching content.raw for each. Rate limiting was enforced with a 1.2-second sleep between batches. Each post was evaluated independently:

# Pseudocode: core update loop
for post in fetch_all_posts(per_page=25):
raw = post[“content”][“raw”]
if “/services/” not in raw:
continue # idempotent skip

updated = raw.replace(
D + “/services/”, D + “/services/”
).replace(
‘href=”/services/”‘, ‘href=”/services/”‘
).replace(
“href=’/services/’”, “href=’/services/’”
)

api_post(f”/posts/{post[‘id’]}”, {“content”: updated})
sleep(0) # within-batch: no sleep

sleep(1.2) # between batches

Three distinct string patterns were replaced: the absolute URL (with domain), the root-relative href with double quotes, and the root-relative href with single quotes. Missing any of these would leave orphaned references.

Six Structural Schema Errors Fixed

During the content audit, six structural schema issues were identified and fixed independently of the URL migration. These were pre-existing bugs, not introduced by the migration.

ErrorLocationFix Applied
audienceType as arrayService schema on GEO pageChanged ["SMEs", "Startups"] to string "SMEs and Startups" — schema.org Audience.audienceType is Text, not array
priceCurrency: “USD”Service schema on GEO pageChanged to "GBP" — site operates in UK; USD caused validator mismatch with addressCountry: “GB”
BreadcrumbList duplicated in WebPageAll service pagesRemoved inline BreadcrumbList from WebPage block; kept standalone @id reference only — prevents duplicate @type declarations in @graph
Missing @id fragmentsWebPage, Article blocksAdded #webpage and #article fragment suffixes to all @id values — required for co-reference resolution across @graph
Missing speakable schemaAll new service pagesAdded SpeakableSpecification with cssSelector targeting .cs-h1, .cs-subtitle, .cs-section-tag, .cs-callout p
legalName: “cantact”Rank Math Knowledge Graph (site-wide)Discovered — not yet fixed. Manual action required in wp-admin → Rank Math → Titles & Meta → Knowledge Graph. Affects all pages’ /#organization entity.

The legalName error is particularly significant because it propagates to every page on the site via the publisher reference in Article schema and the /#organization @id entity. AI citation systems that resolve the Organization entity will encounter “cantact” as the legal name. This is the single highest-priority manual fix remaining.

Failures and Recovery

Rank Math REST API Cannot Set Canonical URLs

We attempted to set the canonical URL on /services/ (Page 8) via REST API using the _rank_math_canonical_url meta field. The PATCH returned HTTP 200 with no error — but the value did not persist. Inspection confirmed only Astra theme meta keys were writable via the REST API at this endpoint.

Recovery: Documented as a manual blocker. The canonical must be set in wp-admin → Edit Page 8 → Rank Math → Advanced → Canonical URL → https://asquaresolution.com/services/. This remains an open action item.

legalName “cantact” Discovered in Production

During schema validation, the Organization @id block returned "legalName": "cantact" — the username used for WordPress Basic Auth credentials had been accidentally entered as the organisation legal name in Rank Math Knowledge Graph at some point during initial setup.

Recovery: Not yet recovered — manual action required. Every page on the site currently has this error in its Organization entity. Priority: fix before publishing any new pages.

BreadcrumbList Double-Declaration

Early service page drafts contained BreadcrumbList defined twice: once as a standalone @graph member with its own @id, and again inline inside the WebPage block. Schema validators accepted this but Google’s rich results test flagged the duplication. The inline copy inside WebPage should only be a {"@id": "...#breadcrumb"} reference, not a full declaration.

Recovery: Corrected in all subsequent page builds. The pattern — standalone BreadcrumbList in @graph with @id, WebPage references it by @id only — is now the standard across all service pages.

Second Run Required for 68 Remaining Posts

The first batch run processed approximately 650 posts before the terminal session timed out. Because the script was idempotent (skip if /services/ not in raw content), a second run safely processed only the remaining 68 posts without touching already-migrated content. Total runtime: approximately 66 minutes across two sessions.

Recovery: The idempotency design meant this was a non-issue operationally. The second run completed in ~8 minutes (68 posts × ~5.5s average API response time).

Confirmed Outcomes

MetricValueVerified?
Total posts/pages fetched748Yes — REST API pagination exhausted
Posts/pages updated718Yes — API returned HTTP 200 for each
Posts skipped (no match)30Yes — logged as SKIP with post ID
API failures / HTTP errors0Yes — no non-200/201 responses logged
Post-migration spot check30 posts sampledYes — all 30 confirmed /services/ in raw content
Schema errors fixed6 (5 resolved, 1 pending manual)5 of 6 confirmed — legalName pending
/services/ canonicalNot yet setManual action blocked by Rank Math API
GSC indexing of /services/Not yet confirmedPending — 4–8 week GSC recrawl expected

Evidence Gaps — What We Cannot Yet Claim

  • Google Search Console coverage report for /services/ URLs (need 4–8 weeks post-migration)
  • Impression data comparison: /services/ vs /services/ in GSC Search Analytics
  • Crawl data from Screaming Frog confirming zero /services/ references remain in rendered HTML
  • legalName fix confirmation — currently “cantact” in all Organization entities
  • Rank Math canonical confirmation on /services/ page
  • Phase 4 301 redirect implementation and GSC redirect coverage data

30 / 60 / 90-Day Measurement Plan

Day 30GSC Coverage CheckConfirm /services/ URLs appear in GSC Coverage report. Count indexed vs discovered-not-indexed.
Day 45Rank Math Canonical FixSet canonical on /services/ manually. Verify Screaming Frog sees correct canonical in rendered HTML.
Day 60GSC Search AnalyticsCompare impressions on /services/ page-group vs historical /services/ data. Expect no traffic drop if migration was clean.
Day 75Phase 4 DecisionIf /services/ URLs confirmed indexed and impressions stable → implement 301 redirect from /services/ → /services/.
Day 90Redirect ConfirmationGSC Coverage should show /services/ URLs as “Redirect” status. Clicks should transfer to /services/ URLs in analytics.

Tools Used

Python 3.x
WordPress REST API v2
requests library
HTTPBasicAuth
Rank Math SEO
Google Search Console
Schema Markup Validator
Google Rich Results Test

No third-party migration plugins were used. The entire migration was executed via direct REST API calls, giving full control over rate limiting, idempotency logic, and error logging.

Implementation Timeline

Day 1Architecture Decision/services/ → /services/ migration planned. Staged approach chosen to avoid redirect dependency. /services/ parent page created (ID 25771).
Day 1GEO Page Migration/services/geo-seo/ created with full schema, slug correction (WordPress changed to services-geo-seo until parent existed), corrected with parent=25771.
Day 2Batch Script — Run 1748 posts fetched in 30 batches of 25. ~650 posts migrated before timeout. 1.2s sleep between batches. 0 API errors.
Day 2Batch Script — Run 2Re-ran idempotent script. 650 posts SKIPPED (already migrated). 68 remaining posts updated. Migration complete.
Day 2Schema AuditDiscovered 6 schema errors. Fixed 5 programmatically. Documented legalName typo as manual blocker.
Day 3Service Pages (Draft)4 child service pages created as drafts under /services/: entity-seo, technical-seo, ai-automation, ai-consulting.
PendingManual ActionsSet /services/ canonical in Rank Math. Fix legalName in Knowledge Graph. Publish 4 draft service pages after meta set.
Week 6–8Phase 4 — RedirectOnce GSC confirms /services/ indexed and impressions stable, add 301 redirect from /services/ → /services/.

What This Migration Taught Us

Lesson 1

Create the destination before updating any links. The WordPress slug auto-modification issue (slug changed to services-geo-seo when parent didn’t exist) confirmed this. Any link update script that runs before the destination page exists will create valid-looking but broken hrefs.

Lesson 2

Idempotency is not optional for bulk operations. The two-run completion scenario would have been catastrophic with a non-idempotent script — double-replacements, malformed URLs, or corrupted anchor text. The if "/services/" not in raw: continue pattern made the second run trivially safe.

Lesson 3

Rank Math SEO blocks programmatic canonical management. The REST API returns 200 for _rank_math_canonical_url updates but silently discards them. This is a significant gap for automated SEO workflows. Any WordPress-based SEO operation that requires canonical management must budget for manual wp-admin time or a Rank Math-specific PHP approach.

Lesson 4

Schema audits surface pre-existing errors you didn’t know existed. The legalName “cantact” typo, audienceType array format, and USD/GBP currency mismatch were all pre-migration bugs. A structured data migration is a useful forcing function for a full schema audit — the audit work has to happen anyway, so pair it with the migration to fix both simultaneously.

Lesson 5

Rate limiting is about server health, not just API politeness. The 1.2-second sleep between batches was not imposed by the API (no rate limit error was received). It was chosen based on observed response times (~5.5s per POST on a live WordPress server). Pushing faster would risk overwhelming the server during peak hours. Conservative rate limiting is cheap insurance.

Methodology & Execution Context

This case study documents work performed on asquaresolution.com (WordPress, Astra, Elementor, Rank Math, LiteSpeed). Changes were applied programmatically through the WordPress REST API in staged, idempotent batches and verified live after each step. Reported figures and timelines reflect this specific site architecture and the crawl and indexing conditions at the time of implementation.

🤖 Ask Our AI — A Square Solutions