# Coretex SEO — extension reference

Coretex SEO is built to be extended without patching it. Everything an add-on
needs is a documented action or filter. This file is the contract.

`CORTEX_SEO_API_VERSION` is `1.0`. Check it before registering anything if you
want to be strict about compatibility.

---

## The shape of an add-on

```php
<?php
/**
 * Plugin Name: My Coretex Add-on
 * Requires Plugins: coretex-seo
 */

add_action( 'plugins_loaded', function () {
    if ( ! defined( 'CORTEX_SEO_VERSION' ) ) {
        return; // Core is not active — do nothing at all.
    }

    // Declare yourself, so the core plugin stops showing upgrade prompts.
    add_filter( 'cortex_seo_addon_active', '__return_true' );

    // Switch on the features you implement.
    add_filter( 'cortex_seo_feature_rank_tracker', '__return_true' );

    // Register a screen.
    add_filter( 'cortex_seo_tabs', function ( $tabs ) {
        $tabs['my_screen'] = array(
            'label'   => __( 'My Screen', 'my-addon' ),
            'menu'    => true,
            'order'   => 60,
            'feature' => 'rank_tracker',
            'render'  => 'my_addon_render_screen',
        );
        return $tabs;
    } );
}, 1 );
```

Hook at priority 1 on `plugins_loaded`. The core plugin builds itself at
priority 5, so anything registered later than that may be missed by the first
read of a registry.

---

## Feature flags

Every capability in the plugin resolves through `Cortex_SEO_Features::enabled( $key )`.

| Filter | Args | Purpose |
| --- | --- | --- |
| `cortex_seo_feature_enabled` | `$enabled, $feature` | Availability of any feature |
| `cortex_seo_feature_{$feature}` | `$enabled` | Availability of one feature |
| `cortex_seo_features` | `$map` | The whole key => bool map |
| `cortex_seo_feature_info` | `$info, $feature` | Label, summary and provider shown in the UI |

**Built-in keys** (always on, implemented by this plugin): `queue`,
`site_profile`, `scans`, `audit`, `spelling`,
`content_score`, `visibility_tools`, `robots`, `indexnow`, `website_agent`,
`bulk_actions`, `scheduled_scans`, `settings`.

**Extension-point keys** (off unless an add-on provides them): `rank_tracker`,
`search_console`, `strategy`, `competitor_gap`, `backlinks`, `serp`,
`visibility`, `trust`, `fullscan`, `sitemap`, `report`, `whitelabel`,
`advanced_geo`, `audit_technical`, `audit_local`, `audit_full`, `geo`,
`keywords`, `writer`.

```php
// Several at once.
add_filter( 'cortex_seo_feature_enabled', function ( $enabled, $feature ) {
    return in_array( $feature, array( 'serp', 'backlinks' ), true ) ? true : $enabled;
}, 10, 2 );
```

---

## Admin screens

| Filter / action | Args | Purpose |
| --- | --- | --- |
| `cortex_seo_tabs` | `$tabs` | Register, remove or reorder screens |
| `cortex_seo_render_tab_{$key}` | `$dashboard` | Render or append to one screen |
| `cortex_seo_before_tab` / `cortex_seo_after_tab` | `$key, $dashboard` | Wrap every screen |
| `cortex_seo_before_tab_{$key}` / `cortex_seo_after_tab_{$key}` | `$dashboard` | Wrap one screen |
| `cortex_seo_admin_menu` | `$parent_slug, $capability` | Add a page outside the registry |
| `cortex_seo_after_group_tabs` | `$group, $current_tab` | Below an in-page tab bar |

A tab definition:

```php
array(
    'label'      => 'Rank & Backlinks',  // in-page tab bar
    'menu_label' => '📈 Rank',            // WP sidebar, defaults to label
    'group'      => 'rank',              // shares an in-page tab bar
    'menu'       => true,                // gets its own sidebar entry
    'order'      => 50,                  // sort position
    'feature'    => 'rank_tracker',      // required feature key, or null
    'render'     => $callable,           // receives ( $dashboard, $key )
    'capability' => 'manage_options',
)
```

`render` may be a method name on the dashboard, or any callable. Core groups
are `tools`, `rank` and `settings`.

If a tab's feature is off and nothing renders, the core shows a placeholder
explaining what would go there.

---

## Dashboard layout

| Action | Args | Position |
| --- | --- | --- |
| `cortex_seo_dashboard_header` | `$tab` | Inside the header bar |
| `cortex_seo_before_dashboard` | `$tab` | Below the header, above the stats |
| `cortex_seo_dashboard_stats` | `$dashboard` | Inside the ring row |
| `cortex_seo_dashboard_sidebar` | `$tab` | The right-hand column |
| `cortex_seo_after_dashboard` | `$tab` | Above the footer |

Screen-specific:

| Action | Args | Position |
| --- | --- | --- |
| `cortex_seo_after_queue` | `$dashboard` | Bottom of the approval queue |
| `cortex_seo_before_site_profile` | `$dashboard` | Top of Site Profile |
| `cortex_seo_after_audit` | `$dashboard` | Bottom of the SEO audit |
| `cortex_seo_after_ai_visibility` | `$dashboard` | Below the AI engine tools |
| `cortex_seo_settings_fields` | `$dashboard` | Inside the settings form |
| `cortex_seo_after_settings` | `$dashboard` | Below the settings form |
| `cortex_seo_scan_cards_after` | `$dashboard` | In the Site Scans grid |
| `cortex_seo_geo_cards` | `$dashboard` | In the GEO generation grid |
| `cortex_seo_after_plans` | — | Bottom of the Plans screen |

---

## Form actions

The core verifies the nonce and the user's capability, then dispatches
anything it does not recognise.

| Action | Args |
| --- | --- |
| `cortex_seo_before_handle_action` | `$action, $dashboard` |
| `cortex_seo_action_{$action}` | `$dashboard` |
| `cortex_seo_action` | `$action, $dashboard` |
| `cortex_seo_save_settings` | `$dashboard` |

```php
add_action( 'cortex_seo_action_my_thing', function ( $dashboard ) {
    // Nonce and capability are already checked.
    $value = isset( $_POST['my_field'] ) ? sanitize_text_field( wp_unslash( $_POST['my_field'] ) ) : '';
    update_option( 'my_addon_thing', $value );
    $dashboard->flash( 'success', __( 'Saved.', 'my-addon' ) );
} );
```

Post to it from a form with `cortex_action` set to your key, a `tab` field for
the redirect target, and `wp_nonce_field( 'cortex_seo_form' )`.

---

## The scan engine

| Filter / action | Args | Purpose |
| --- | --- | --- |
| `cortex_seo_scan_types` | `$types` | Which scan types exist |
| `cortex_seo_scan_type_labels` | `$labels` | key => human label |
| `cortex_seo_scan_cards` | `$cards` | key => `array( label, description )` |
| `cortex_seo_run_scan_{$type}` | `$result, $type` | Run one type; return non-null to take over |
| `cortex_seo_run_scan` | `$result, $type` | Catch-all runner |
| `cortex_seo_scan_prompt` | `$prompt, $type, $post, $site` | The AI prompt for a type |
| `cortex_seo_apply_{$action_type}` | `$post_id, $val, $row` | Apply an approved suggestion |
| `cortex_seo_apply_suggestion` | `$action_type, $post_id, $val, $row` | Every approval |
| `cortex_seo_batch_cap` | `$cap` | Items per batch (default 10) |

`Cortex_SEO_Logic::run_post_scan( $type, $args )` is public, so your scan type
gets the same cursor, batching and live progress bar as the built-in ones:

```php
add_filter( 'cortex_seo_scan_types', function ( $types ) {
    $types[] = 'my_scan';
    return $types;
} );

add_filter( 'cortex_seo_scan_prompt', function ( $prompt, $type, $post, $site ) {
    return 'my_scan' === $type
        ? $site . "Do something with: {$post->post_title}"
        : $prompt;
}, 10, 4 );

add_filter( 'cortex_seo_run_scan_my_scan', function ( $result, $type ) {
    return Cortex_SEO_Logic::run_post_scan( $type, array(
        'cursor_option' => 'my_addon_cursor',
        'post_types'    => array( 'post' ),
    ) );
}, 10, 2 );

add_action( 'cortex_seo_apply_my_scan_update', function ( $post_id, $val ) {
    update_post_meta( $post_id, '_my_meta', sanitize_text_field( $val ) );
}, 10, 2 );
```

`run_post_scan()` accepts `cursor_option`, `post_types`, `label` (a format
string taking the post title), and `ids` when you want to select the batch
yourself.

---

## The audit engine

| Filter / action | Args | Purpose |
| --- | --- | --- |
| `cortex_seo_audit_sections` | `$sections` | Register an audit section |
| `cortex_seo_audit_cards` | `$dashboard` | Add a card to the audit grid |
| `cortex_seo_audit_section_ran` | `$key, $result` | After one section runs |
| `cortex_seo_audit_full_run` | — | After a full audit pass |

A section definition carries its own card, runner and results block, so
registering one is all that is needed:

```php
add_filter( 'cortex_seo_audit_sections', function ( $sections ) {
    $sections['my_check'] = array(
        'title'    => __( 'My Check', 'my-addon' ),
        'desc'     => __( 'What this check looks at.', 'my-addon' ),
        'icon'     => '🔎',
        'callback' => 'my_addon_run_check',
        'render'   => 'checklist',   // or 'links', 'headings', a callable, or false
        'order'    => 25,
        'feature'  => 'my_feature',  // optional gate
    );
    return $sections;
} );

function my_addon_run_check() {
    $items = array(
        array( 'label' => 'Something', 'status' => 'pass', 'detail' => 'Looks fine.' ),
    );
    // Writes into the shared store, so it shows on the audit screen and
    // counts toward the score like any built-in section.
    Cortex_SEO_Audit::save_section( 'my_check', array( 'items' => $items ) );
}
```

`Cortex_SEO_Audit::save_section()`, `::queue_ai_recommendation()` and
`::get_all_content_posts()` are public for exactly this purpose. Set
`'aggregate' => true` on a section that runs the others, so `run_full()`
skips it rather than recursing.

---

## Upgrade messaging

| Filter | Args | Purpose |
| --- | --- | --- |
| `cortex_seo_addon_active` | `$active` | Return true to silence every upgrade prompt |
| `cortex_seo_show_upgrade_ui` | `$show` | Same, for site owners rather than add-ons |
| `cortex_seo_upgrade_url` | `$url, $medium, $content` | Where upgrade links point |
| `cortex_seo_upgrade_url_args` | `$args, $medium, $content` | Campaign parameters; return `array()` for clean links |
| `cortex_seo_plan` / `cortex_seo_plan_labels` | `$plan` / `$labels` | The plan name shown in the admin |

Defining `CORTEX_SEO_HIDE_UPSELL` as true in `wp-config.php` does the same as
the second filter.

---

## Other

| Filter / action | Args | Purpose |
| --- | --- | --- |
| `cortex_seo_register_features` | — | Earliest safe point to register feature filters |
| `cortex_seo_loaded` | `$version` | Core is fully built |
| `cortex_seo_activated` / `cortex_seo_deactivated` | — | Lifecycle |
| `cortex_seo_register_rest_routes` | — | After core REST routes |
| `cortex_seo_ai_providers` | `$providers` | The AI provider list |
| `cortex_seo_sitemap_url` | `$url` | Which sitemap the plugin references and submits |

---

## Rules for a well-behaved add-on

1. Never `include` or edit a core file. Everything you need is a hook.
2. Guard every core class reference with `class_exists()` if it could load in
   a different order than you expect.
3. Declare `Requires Plugins: coretex-seo` in your plugin header, and still
   check `defined( 'CORTEX_SEO_VERSION' )` at runtime.
4. Clean up after yourself on deactivation — especially cron events, which
   otherwise outlive your plugin inside the core plugin's schedule.
5. Use your own text domain, your own option prefix, and your own nonces for
   anything you add outside the core form.
