30. SF-117 phase 2 — Smart Insights + URL Inspection | SEO Forge - Rank Higher with AI-Powered SEO
Download Log in

30. SF-117 phase 2 — Smart Insights + URL Inspection

Developer Guide

SEOFORGE_GSC::detect_cannibalization($days = 28, $limit = 20): array

Queries where 2+ of the site’s pages rank in parallel. Returns the per-query page breakdown plus aggregate totals. Filters out queries with fewer than 20 impressions to kill noise.

php
foreach (SEOFORGE_GSC::instance()->detect_cannibalization(28, 10) as $row) {
    echo $row['query'] . ' — ' . count($row['pages']) . ' competing pages';
    foreach ($row['pages'] as $p) {
        printf("  %s (pos %.1f, %d clicks)n", $p['page'], $p['position'], $p['clicks']);
    }
}

SEOFORGE_GSC::detect_ctr_anomalies($days = 28, $limit = 20): array

Top-3 pages with CTR at least 40% below the position’s rough benchmark (pos 1 ≈ 28%, pos 2 ≈ 15%, pos 3 ≈ 10%). Each row: { query, page, position, ctr, benchmark, gap_percent, impressions, clicks }. Minimum 50 impressions per row.

SEOFORGE_GSC::get_winners_losers($days = 7, $limit = 5): array

Week-over-week click-change per page. Returns { winners: array, losers: array }. Each row carries post_id (via url_to_postid) so the UI can link back to the editor.

SEOFORGE_GSC::classify_queries_by_intent($days = 28, $limit = 30): array

Takes the top N queries, batches them into a single AI prompt, parses the response into one of four intents: informational, commercial, transactional, navigational. Caches in a 6-hour transient keyed by (property, days, limit). Returns each original row augmented with an intent field.

SEOFORGE_GSC::inspect_url($url): array|WP_Error

Wraps the Worker’s URL Inspection proxy. Flattened return shape:

php
[
    'coverage'       => 'Submitted and indexed',
    'indexing_state' => 'INDEXING_ALLOWED',
    'verdict'        => 'PASS',
    'last_crawl'     => '2026-04-21T09:12:00Z',
    'page_fetch'     => 'SUCCESSFUL',
    'robots_allowed' => 'ALLOWED',
    'canonical_user' => 'https://example.com/about/',
    'canonical_goog' => 'https://example.com/about/',
    'sitemap'        => [],
    'referring_urls' => [ ... ],
    'mobile_verdict' => 'PASS',
    'rich_verdict'   => 'PASS',
    'inspection_url' => 'https://search.google.com/search-console/inspect?...',
]

Cached in a 6-hour per-URL transient so the post editor doesn’t burn Google’s 2000/day quota.

POST /integrations/google/gsc/url-inspection (Worker)

Thin proxy over searchconsole.googleapis.com/v1/urlInspection/index:inspect. Request:

json
{
  "license_key": "...",
  "product": "seoforge",
  "site_url": "https://example.com/",
  "page_url": "https://example.com/about/"
}

Response:

json
{
  "success": true,
  "inspectionResult": { /* Google's payload verbatim */ }
}

Primary sync on Connect

SEOFORGE_GSC::handle_post_connect() now dispatches Forge_Queue::dispatch('gsc_sync', ['days'=>90, 'limit'=>200, 'primary'=>true], 'seoforge') immediately after the property auto-detect step. Avoids the 24-hour wait for the daily cron on first-time connects.

wp_ajax_seoforge_gsc_inspect (admin-side)

Takes post_id + nonce, resolves get_permalink, calls inspect_url(), returns the flattened JSON the metabox renders. Permission: edit_posts.

wp_ajax_seoforge_get_issues (admin-side, R3)

Live Issues list refresh used by the SEO metabox while the user types in the

focus keyword / SEO title / meta description. Re-runs the rule-based scorer

with the un-saved field values plus content-derived signals collected from

resolve_content_html() — does NOT cache to wp_seoforge_analysis (the cache

row is updated only on save_post, so a typing burst can’t thrash it).

php
// Body params (sanitized server-side)
post_id      // (int)    target post; capability check `edit_post`
nonce        // (string) action `seoforge_nonce`
meta_title   // (string) optional — un-saved SEO title
meta_desc    // (string) optional — un-saved meta description
keyword      // (string) optional — un-saved focus keyword

// Successful response (matches the renderer shape so JS can rebuild <li> items)
{
  "score": 78,
  "issues": [
    {"severity": "error",   "message": "Content too short (49 words, aim for 300+)", "fixable": true,  "cost": 2},
    {"severity": "warning", "message": "No internal links found",                    "fixable": true,  "cost": 2},
    {"severity": "info",    "message": "No images in content",                       "fixable": false, "cost": 2}
  ],
  "count": 3,
  "fixable_count": 2,
  "ai_configured": true
}
fixable mirrors the renderer’s gating: image/alt issues are never fixable

(no text-AI target), and content-rewrite issues require an editable

post_content (page-builder / ACF-only posts get false). cost is 1 for the

“No focus keyword set” path (routes through /meta/generate?type=keyword,

1 credit) and 2 otherwise (fix_issue).

Frontend wiring lives in assets/js/admin.jsscheduleLiveIssues() is

called from runLiveScore(), debounced 600 ms (longer than the score’s

180 ms because it hits the network). In-flight requests are aborted on the

next type so the UI never displays a stale older-than-current snapshot.

On AJAX failure the existing list stays put — no flicker, no empty state.

HowTo step preview in prepare_live_scoring_context() (R3)

SEOFORGE_Analyzer::prepare_live_scoring_context($post) now also returns:
php
'howto_step_count' => 3,
'howto_step_names' => ['Step 1: Mix flour', 'Step 2: Knead 10 min', 'Step 3: Bake at 220C'],

These mirror what SEOFORGE_Schema::preview_howto_steps() extracts from

$post->post_content — same logic the frontend renderer runs, including

the round 2 question-form filter. The Schema Builder JS reads them when

the dropdown is set to HowTo to render a warn / notice / success

panel. Frozen at page load — the SEO metabox doesn’t expose the post

body, so the count is stable across user typing in title/desc/keyword.

SEOFORGE_Schema::preview_howto_steps() is the only public wrapper around

the private extract_howto_steps() — third-party code that needs to know

“would this post emit a HowTo on the frontend?” should call this and

check count($steps) >= 1.

Drawer bulk-review mode (SF-121 story 5)

window.sfDrawerOpenBulk(groups) accepts [{ issue, ids }] — the selection payload the Site Audit sticky bar already builds. It filters to text-issue groups via sfTypeFromIssue, fans a parallel /meta/preview per selected page, and renders one accordion row per item. A single Apply All button walks dirty rows and commits each through /meta/apply. Non-text groups are passed to the legacy chunked handler.
Forge AI Assistant Online

Hi! I'm the SEO Forge AI assistant. Ask me anything about the plugin — setup, features, troubleshooting, or development.

Just now
Powered by Forge AI · Browse docs