Home / Admin / Cortex Schema API
Duplicate Snippet

Embed Snippet on Your Site

Cortex Schema API

<10
Code Preview
php
<?php
/**
 * Cortex Schema API v3.1 - Streamlined Schema Audit with Server-Side Filtering
 * 
 * Focused schema auditing for WordPress pages.
 * Returns COMPLETE live-rendered schemas (local + global + auto-generated).
 * 
 * v3.1 Changes:
 * - Added server-side filtering for pages to reduce payload size
 * - Pages filter OUT: FAQPage, VideoObject, HowTo, BreadcrumbList, standalone ImageObject
 * - Posts retain full schema data (no filtering)
 * - ~30-35% payload reduction for page audits
 * 
 * v3.0 Changes:
 * - Streamlined to essential endpoints only
 * - Removed: /schema/all, /schema/all/post, /schema/{id}, /schema/global, /schema/global/pages
 * - Removed: All SEO meta endpoints (use Cortex Pages & Posts API instead)
 * - Kept: /schema/all/page, /schema/live/{id}, all featured image endpoints
 * - All schema endpoints fetch LIVE rendered data (complete schemas)
 * 
 * Installation: Add as WPCode snippet (PHP, Run Everywhere)
 * 
 * @version 3.1
 * @requires WordPress 5.0+
 * @requires RankMath SEO
 */
// Exit if accessed directly
if (!defined('ABSPATH')) {
    exit;
}
/**
 * Register REST API Routes
 */
add_action('rest_api_init', function () {
    $namespace = 'cortex/v1';
    
    // ===========================================
    // SCHEMA AUDIT ENDPOINTS
    // ===========================================
    
    // API info and supported types
    register_rest_route($namespace, '/schema/types', [
        'methods' => 'GET',
        'callback' => 'cortex_get_schema_types',
        'permission_callback' => 'cortex_api_permissions',
    ]);
    
    // Get all schemas from pages only - LIVE RENDERED
    register_rest_route($namespace, '/schema/all/page', [
        'methods' => 'GET',
        'callback' => 'cortex_get_all_page_schemas',
        'permission_callback' => 'cortex_api_permissions',
    ]);
    
    // Get live rendered schema from single page/post
    register_rest_route($namespace, '/schema/live/(?P<id>\d+)', [
        'methods' => 'GET',
        'callback' => 'cortex_get_live_schema',
        'permission_callback' => 'cortex_api_permissions',
        'args' => [
            'id' => ['required' => true, 'type' => 'integer'],
        ],
    ]);
    
    // ===========================================
    // FEATURED IMAGE ENDPOINTS
    // ===========================================
    
    // Get featured image for single post
    register_rest_route($namespace, '/featured-image/(?P<id>\d+)', [
        'methods' => 'GET',
        'callback' => 'cortex_get_featured_image',
        'permission_callback' => 'cortex_api_permissions',
        'args' => [
            'id' => ['required' => true, 'type' => 'integer'],
        ],
    ]);
    
    // Get all featured images
    register_rest_route($namespace, '/featured-image/all', [
        'methods' => 'GET',
        'callback' => 'cortex_get_all_featured_images',
        'permission_callback' => 'cortex_api_permissions',
    ]);
    
    // Get all featured images filtered by type
    register_rest_route($namespace, '/featured-image/all/(?P<type>page|post)', [
        'methods' => 'GET',
        'callback' => 'cortex_get_all_featured_images',
        'permission_callback' => 'cortex_api_permissions',
        'args' => [
            'type' => ['required' => true, 'type' => 'string'],
        ],
    ]);
});
function cortex_api_permissions() {
    return current_user_can('edit_posts');
}
// ===========================================
// HELPER FUNCTION - Fetch live schemas from rendered page
// ===========================================
function cortex_fetch_live_schemas($post_id) {
    $url = get_permalink($post_id);
    $response = wp_remote_get($url, ['timeout' => 15, 'sslverify' => false]);
    
    if (is_wp_error($response)) {
        return ['error' => $response->get_error_message(), 'schemas' => []];
    }
    
    $body = wp_remote_retrieve_body($response);
    $schemas = [];
    
    if (preg_match_all('/<script[^>]*type=["\']application\/ld\+json["\'][^>]*>(.*?)<\/script>/si', $body, $matches)) {
        foreach ($matches[1] as $json) {
            $decoded = json_decode(trim($json), true);
            if ($decoded) {
                $schemas[] = $decoded;
            }
        }
    }
    
    return ['error' => null, 'schemas' => $schemas];
}
// ===========================================
// HELPER FUNCTION - Filter schemas for pages (v3.1)
// ===========================================
/**
 * Filter out auto-generated schema types from pages
 * 
 * For pages, removes: FAQPage, VideoObject, HowTo, BreadcrumbList, 
 * and standalone ImageObject (media library images)
 * 
 * Posts retain all schemas unfiltered.
 * 
 * @param array $schemas Array of schema objects
 * @param string $post_type The post type ('page' or 'post')
 * @return array Filtered schemas
 */
function cortex_filter_schemas_by_post_type($schemas, $post_type) {
    // Only filter pages - posts retain full schema data
    if ($post_type !== 'page') {
        return $schemas;
    }
    
    // Schema types to filter out for pages
    $filtered_types = ['FAQPage', 'VideoObject', 'HowTo', 'BreadcrumbList'];
    
    $filtered_schemas = [];
    
    foreach ($schemas as $schema) {
        // Handle @graph arrays
        if (isset($schema['@graph']) && is_array($schema['@graph'])) {
            $filtered_graph = [];
            
            foreach ($schema['@graph'] as $item) {
                if (!cortex_should_filter_schema_item($item, $filtered_types)) {
                    $filtered_graph[] = $item;
                }
            }
            
            // Only include schema if @graph still has items
            if (!empty($filtered_graph)) {
                $schema['@graph'] = $filtered_graph;
                $filtered_schemas[] = $schema;
            }
        }
        // Handle single schema objects
        else {
            if (!cortex_should_filter_schema_item($schema, $filtered_types)) {
                $filtered_schemas[] = $schema;
            }
        }
    }
    
    return $filtered_schemas;
}
/**
 * Check if a schema item should be filtered out
 * 
 * @param array $item Single schema item
 * @param array $filtered_types Array of @type values to filter
 * @return bool True if item should be filtered out
 */
function cortex_should_filter_schema_item($item, $filtered_types) {
    if (!isset($item['@type'])) {
        return false;
    }
    
    $type = $item['@type'];
    
    // Handle array types (e.g., ["Electrician", "LocalBusiness"])
    if (is_array($type)) {
        foreach ($type as $t) {
            if (in_array($t, $filtered_types)) {
                return true;
            }
        }
    } else {
        // Single type
        if (in_array($type, $filtered_types)) {
            return true;
        }
        
        // Special case: Filter standalone ImageObject from media library
        if ($type === 'ImageObject') {
            if (isset($item['@id']) && strpos($item['@id'], '/wp-content/uploads/') !== false) {
                return true;
            }
            if (isset($item['contentUrl']) && strpos($item['contentUrl'], '/wp-content/uploads/') !== false) {
                // Check if this is a standalone media image (not a logo or primary image)
                // Logos typically have @id ending in #logo or #primaryimage
                if (isset($item['@id'])) {
                    $id = $item['@id'];
                    if (strpos($id, '#logo') === false && 
                        strpos($id, '#primaryimage') === false &&
                        strpos($id, '/#') === false) {
                        return true;
                    }
                }
            }
        }
    }
    
    return false;
}
// ===========================================
// HELPER FUNCTION - Extract schema types from live schemas
// ===========================================
function cortex_extract_types_from_live_schemas($live_schemas) {
    $types = [];
    
    foreach ($live_schemas as $schema) {
        // Handle @graph arrays
        if (isset($schema['@graph']) && is_array($schema['@graph'])) {
            foreach ($schema['@graph'] as $item) {
                if (isset($item['@type'])) {
                    $type = is_array($item['@type']) ? implode('/', $item['@type']) : $item['@type'];
                    if (!in_array($type, $types)) {
                        $types[] = $type;
                    }
                }
            }
        }
        // Handle single schema
        elseif (isset($schema['@type'])) {
            $type = is_array($schema['@type']) ? implode('/', $schema['@type']) : $schema['@type'];
            if (!in_array($type, $types)) {
                $types[] = $type;
            }
        }
    }
    
    return $types;
}
// ===========================================
// SCHEMA AUDIT FUNCTIONS
// ===========================================
function cortex_get_schema_types($request) {
    return rest_ensure_response([
        'success' => true,
        'api_version' => '3.1',
        'api_mode' => 'audit_only',
        'note' => 'Streamlined schema audit API with server-side filtering. Pages filter out auto-generated schemas (FAQPage, VideoObject, HowTo, BreadcrumbList, standalone ImageObject). Posts retain full data.',
        'supported_schema_types' => [
            'LocalBusiness' => 'Homepage - main business entity',
            'Organization' => 'Homepage - brand entity (separate from LocalBusiness)',
            'WebSite' => 'Homepage - site identity with SearchAction',
            'WebPage' => 'All pages - page-level context with speakable',
            'Service' => 'Service pages',
            'JobPosting' => 'Careers/Jobs pages',
            'BlogPosting' => 'Blog posts',
            'FAQPage' => 'FAQ pages (filtered on pages, kept on posts)',
            'HowTo' => 'Tutorial/Guide pages (filtered on pages, kept on posts)',
            'Person' => 'Founder/Team pages',
        ],
        'local_business_types' => [
            'Electrician', 'Plumber', 'HVACBusiness', 'RoofingContractor',
            'GeneralContractor', 'HomeAndConstructionBusiness', 'LocksmithService',
            'MovingCompany', 'PestControlService', 'ProfessionalService',
        ],
        'page_filtering' => [
            'enabled' => true,
            'filtered_types' => ['FAQPage', 'VideoObject', 'HowTo', 'BreadcrumbList', 'ImageObject (standalone)'],
            'note' => 'Pages filter out auto-generated schemas to focus on manually configured schemas. Posts retain all schemas.',
        ],
        'endpoints' => [
            'GET /cortex/v1/schema/types' => 'API info + supported types',
            'GET /cortex/v1/schema/all/page' => 'All live schemas from pages (filtered)',
            'GET /cortex/v1/schema/live/{id}' => 'Live schema for single page/post (pages filtered, posts unfiltered)',
            'GET /cortex/v1/featured-image/{id}' => 'Featured image for post',
            'GET /cortex/v1/featured-image/all' => 'All featured images',
            'GET /cortex/v1/featured-image/all/page' => 'Featured images for pages',
            'GET /cortex/v1/featured-image/all/post' => 'Featured images for posts',
        ],
    ]);
}
/**
 * Get all schemas from pages - LIVE RENDERED with filtering
 * Returns filtered schema data for pages (removes auto-generated schemas)
 */
function cortex_get_all_page_schemas($request) {
    $pages = get_posts([
        'post_type' => 'page',
        'post_status' => 'publish',
        'posts_per_page' => -1,
        'orderby' => 'title',
        'order' => 'ASC',
    ]);
    
    $results = [];
    $with_schema = 0;
    $without_schema = 0;
    $schema_type_counts = [];
    $missing_schema = [];
    $fetch_errors = [];
    $total_filtered = 0;
    
    foreach ($pages as $page) {
        // Fetch LIVE schemas from rendered page
        $live_result = cortex_fetch_live_schemas($page->ID);
        $raw_schemas = $live_result['schemas'];
        $fetch_error = $live_result['error'];
        
        // Count raw schemas before filtering
        $raw_count = cortex_count_schema_items($raw_schemas);
        
        // Apply filtering for pages
        $filtered_schemas = cortex_filter_schemas_by_post_type($raw_schemas, 'page');
        
        // Count filtered schemas
        $filtered_count = cortex_count_schema_items($filtered_schemas);
        $items_filtered = $raw_count - $filtered_count;
        $total_filtered += $items_filtered;
        
        // Extract schema types from filtered schemas
        $schema_types = cortex_extract_types_from_live_schemas($filtered_schemas);
        $has_schema = !empty($filtered_schemas);
        
        if ($fetch_error) {
            $fetch_errors[] = [
                'id' => $page->ID,
                'title' => $page->post_title,
                'error' => $fetch_error,
            ];
        }
        
        if ($has_schema) {
            $with_schema++;
            foreach ($schema_types as $type) {
                $schema_type_counts[$type] = isset($schema_type_counts[$type]) ? $schema_type_counts[$type] + 1 : 1;
            }
        } else {
            $without_schema++;
            $missing_schema[] = [
                'id' => $page->ID,
                'title' => $page->post_title,
                'url' => get_permalink($page->ID),
            ];
        }
        
        $results[] = [
            'id' => $page->ID,
            'title' => $page->post_title,
            'url' => get_permalink($page->ID),
            'has_schema' => $has_schema,
            'schema_types' => $schema_types,
            'schema_count' => $filtered_count,
            'raw_schema_count' => $raw_count,
            'filtered_out' => $items_filtered,
            'live_schemas' => $filtered_schemas,
            'test_url' => 'https://search.google.com/test/rich-results?url=' . urlencode(get_permalink($page->ID)),
        ];
    }
    
    return rest_ensure_response([
        'success' => true,
        'api_version' => '3.1',
        'data_source' => 'live_rendered',
        'filtering' => [
            'enabled' => true,
            'post_type' => 'page',
            'filtered_types' => ['FAQPage', 'VideoObject', 'HowTo', 'BreadcrumbList', 'ImageObject (standalone)'],
            'total_items_filtered' => $total_filtered,
        ],
        'summary' => [
            'total' => count($results),
            'with_schema' => $with_schema,
            'without_schema' => $without_schema,
            'schema_type_counts' => $schema_type_counts,
        ],
        'missing_schema' => $missing_schema,
        'fetch_errors' => $fetch_errors,
        'results' => $results,
    ]);
}
/**
 * Count total schema items (including @graph items)
 */
function cortex_count_schema_items($schemas) {
    $count = 0;
    foreach ($schemas as $schema) {
        if (isset($schema['@graph']) && is_array($schema['@graph'])) {
            $count += count($schema['@graph']);
        } else {
            $count++;
        }
    }
    return $count;
}
/**
 * Get live rendered schema for single page/post
 * Pages are filtered, posts retain full data
 */
function cortex_get_live_schema($request) {
    $post_id = $request->get_param('id');
    $post = get_post($post_id);
    
    if (!$post) {
        return new WP_Error('not_found', 'Post not found', ['status' => 404]);
    }
    
    $url = get_permalink($post_id);
    $response = wp_remote_get($url, ['timeout' => 15, 'sslverify' => false]);
    
    if (is_wp_error($response)) {
        return new WP_Error('fetch_failed', 'Could not fetch page: ' . $response->get_error_message(), ['status' => 500]);
    }
    
    $body = wp_remote_retrieve_body($response);
    $raw_schemas = [];
    
    if (preg_match_all('/<script[^>]*type=["\']application\/ld\+json["\'][^>]*>(.*?)<\/script>/si', $body, $matches)) {
        foreach ($matches[1] as $json) {
            $decoded = json_decode(trim($json), true);
            if ($decoded) {
                $raw_schemas[] = $decoded;
            }
        }
    }
    
    // Count raw schemas before filtering
    $raw_count = cortex_count_schema_items($raw_schemas);
    
    // Apply filtering based on post type (pages filtered, posts unfiltered)
    $filtered_schemas = cortex_filter_schemas_by_post_type($raw_schemas, $post->post_type);
    
    // Count filtered schemas
    $filtered_count = cortex_count_schema_items($filtered_schemas);
    $items_filtered = $raw_count - $filtered_count;
    
    // Extract types for summary (from filtered schemas)
    $schema_types = cortex_extract_types_from_live_schemas($filtered_schemas);
    
    // Determine if filtering was applied
    $filtering_applied = ($post->post_type === 'page');
    
    return rest_ensure_response([
        'success' => true,
        'post_id' => $post_id,
        'post_type' => $post->post_type,
        'post_title' => $post->post_title,
        'url' => $url,
        'has_schema' => !empty($filtered_schemas),
        'schema_types' => $schema_types,
        'schema_count' => $filtered_count,
        'filtering' => [
            'applied' => $filtering_applied,
            'raw_count' => $raw_count,
            'filtered_out' => $items_filtered,
            'note' => $filtering_applied 
                ? 'Auto-generated schemas filtered (FAQPage, VideoObject, HowTo, BreadcrumbList, standalone ImageObject)' 
                : 'No filtering applied (posts retain full schema data)',
        ],
        'live_schemas' => $filtered_schemas,
        'test_url' => 'https://search.google.com/test/rich-results?url=' . urlencode($url),
    ]);
}
// ===========================================
// FEATURED IMAGE FUNCTIONS
// ===========================================
function cortex_get_featured_image($request) {
    $post_id = $request->get_param('id');
    $post = get_post($post_id);
    
    if (!$post) {
        return new WP_Error('not_found', 'Post not found', ['status' => 404]);
    }
    
    $thumbnail_id = get_post_thumbnail_id($post_id);
    
    if (!$thumbnail_id) {
        return rest_ensure_response([
            'success' => true,
            'post_id' => $post_id,
            'post_title' => $post->post_title,
            'has_featured_image' => false,
            'featured_image' => null,
        ]);
    }
    
    $image_url = wp_get_attachment_image_url($thumbnail_id, 'full');
    $image_meta = wp_get_attachment_metadata($thumbnail_id);
    $image_alt = get_post_meta($thumbnail_id, '_wp_attachment_image_alt', true);
    
    return rest_ensure_response([
        'success' => true,
        'post_id' => $post_id,
        'post_title' => $post->post_title,
        'has_featured_image' => true,
        'featured_image' => [
            'id' => $thumbnail_id,
            'url' => $image_url,
            'width' => isset($image_meta['width']) ? $image_meta['width'] : null,
            'height' => isset($image_meta['height']) ? $image_meta['height'] : null,
            'alt' => $image_alt,
        ],
    ]);
}
function cortex_get_all_featured_images($request) {
    $post_type = $request->get_param('type');
    $post_types = empty($post_type) ? ['page', 'post'] : [$post_type];
    
    $posts = get_posts([
        'post_type' => $post_types,
        'post_status' => 'publish',
        'posts_per_page' => -1,
        'orderby' => 'title',
        'order' => 'ASC',
    ]);
    
    $results = [];
    $with_image = 0;
    $without_image = 0;
    $missing_images = [];
    
    foreach ($posts as $post) {
        $thumbnail_id = get_post_thumbnail_id($post->ID);
        $has_image = !empty($thumbnail_id);
        
        if ($has_image) {
            $with_image++;
            $image_url = wp_get_attachment_image_url($thumbnail_id, 'full');
            $image_meta = wp_get_attachment_metadata($thumbnail_id);
            $image_alt = get_post_meta($thumbnail_id, '_wp_attachment_image_alt', true);
            
            $results[] = [
                'id' => $post->ID,
                'post_type' => $post->post_type,
                'title' => $post->post_title,
                'url' => get_permalink($post->ID),
                'has_featured_image' => true,
                'featured_image' => [
                    'id' => $thumbnail_id,
                    'url' => $image_url,
                    'width' => isset($image_meta['width']) ? $image_meta['width'] : null,
                    'height' => isset($image_meta['height']) ? $image_meta['height'] : null,
                    'alt' => $image_alt,
                ],
            ];
        } else {
            $without_image++;
            $missing_images[] = [
                'id' => $post->ID,
                'post_type' => $post->post_type,
                'title' => $post->post_title,
                'url' => get_permalink($post->ID),
            ];
            
            $results[] = [
                'id' => $post->ID,
                'post_type' => $post->post_type,
                'title' => $post->post_title,
                'url' => get_permalink($post->ID),
                'has_featured_image' => false,
                'featured_image' => null,
            ];
        }
    }
    
    return rest_ensure_response([
        'success' => true,
        'filter' => empty($post_type) ? 'all' : $post_type,
        'summary' => [
            'total' => count($results),
            'with_image' => $with_image,
            'without_image' => $without_image,
        ],
        'missing_images' => $missing_images,
        'results' => $results,
    ]);
}

Comments

Add a Comment