Rank Math SEO schema warning: Missing field ‘hasCourseInstance’ on custom post type

The Missing field 'hasCourseInstance' warning occurs when Rank Math outputs a Course schema generator on a custom post type (CPT) without populated course instance properties, such as execution mode, location, or start dates. According to Google’s Structured Data guidelines, a Course entity requires a CourseInstance (or hasCourseInstance array) detailing when and where the course takes place to be eligible for rich results. This typically happens when Rank Math auto-assigns Course Schema globally to a CPT, but custom metadata fields mapping to hasCourseInstance remain undefined or unmapped in the post editor.

Root Cause Diagnosis

When Rank Math generates structured data for a Course entity, it constructs an @type: Course JSON-LD payload. Google requires the hasCourseInstance property to contain nested CourseInstance details such as courseMode, instructor, or location. When Rank Math’s Schema Generator is applied to a Custom Post Type without these specific sub-fields mapped or filled out in the post’s metabox, the outputted JSON-LD skips the key entirely, triggering Schema validation and Google Search Console warnings.

Quick Fix

Force Rank Math to inject a default fallback CourseInstance array into the Course schema payload using the rank_math/snippet/rich_snippet_custom_description or rank_math/json_ld filter hook.

Add this single snippet to your theme’s functions.php or a custom MU-Plugin:

add_filter('rank_math/json_ld', function ($data, $jsonld) {
    if (isset($data['richSnippet']) && $data['richSnippet']['@type'] === 'Course' && empty($data['richSnippet']['hasCourseInstance'])) {
        $data['richSnippet']['hasCourseInstance'] = [
            '@type' => 'CourseInstance',
            'courseMode' => 'Online',
            'courseWorkload' => 'P1W'
        ];
    }
    return $data;
}, 99, 2);

Step-by-Step Resolution

1. Disable Global Schema Auto-Assignment

Prevent Rank Math from automatically applying incomplete Course Schema across all entries of the CPT.

  1. Navigate to Rank Math SEO > Titles & Meta.
  2. Select your Custom Post Type under the left sidebar (e.g., Courses or Lessons).
  3. Scroll down to Schema Type.
  4. Set Schema Type to None or Article as the default fallback.
  5. Click Save Changes.
See also  WordPress wp_cron.php causing high CPU usage: Proper CLI systemd timer setup

2. Programmatically Inject Complete hasCourseInstance Schema

Create an MU-plugin file at wp-content/mu-plugins/fix-rankmath-course-schema.php to dynamically attach the required hasCourseInstance properties for post types using Course schema.

<?php
/**
 * Plugin Name: Rank Math CourseInstance Schema Fix
 * Description: Dynamically appends missing hasCourseInstance fields to Rank Math JSON-LD output.
 */

add_filter('rank_math/json_ld', 'fix_rankmath_course_has_course_instance', 10, 2);

function fix_rankmath_course_has_course_instance($data, $jsonld) {
    // Target only singular posts of your targeted custom post type
    if (!is_singular('your_custom_post_type')) {
        return $data;
    }

    // Check if Course schema is present in the rich snippet data
    if (isset($data['richSnippet']) && $data['richSnippet']['@type'] === 'Course') {
        
        // Retrieve custom post meta or ACF values if available
        $post_id = get_the_ID();
        $course_mode = get_post_meta($post_id, '_course_mode', true) ?: 'Online';
        $start_date = get_post_meta($post_id, '_course_start_date', true) ?: date('Y-m-d');

        // Construct the required CourseInstance entity
        $data['richSnippet']['hasCourseInstance'] = [
            '@type' => 'CourseInstance',
            'courseMode' => $course_mode,
            'startDate' => $start_date,
            'location' => [
                '@type' => 'VirtualLocation',
                'url' => get_permalink($post_id)
            ]
        ];
    }

    return $data;
}

Replace 'your_custom_post_type' with your actual registered CPT slug (e.g., 'sfwd-courses', 'course', or 'academy').

3. Clear Caching Layers

Flush object cache and page cache to regenerate the JSON-LD script on the frontend.

Using WP-CLI:

wp cache flush

If using a persistent object cache like Redis:

redis-cli flushdb

4. Validate JSON-LD Output

Verify the corrected schema payload by testing the URL:

  1. Open Google Rich Results Test ([https://search.google.com/test/rich-results](https://search.google.com/test/rich-results)).
  2. Input the URL of a CPT item.
  3. Confirm that the detected Course item contains the hasCourseInstance block without warnings:
"hasCourseInstance": {
  "@type": "CourseInstance",
  "courseMode": "Online",
  "startDate": "2026-01-01",
  "location": {
    "@type": "VirtualLocation",
    "url": "https://example.com/course/sample-course"
  }
}

Why This Fails for Some Users

  • Schema Template Overrides in Rank Math Pro: Users with Rank Math Pro Schema Templates may have custom display conditions that override filter hooks. The programmatic fix will not run if a custom Schema Template with manual fields is explicitly bound to the post ID.
  • LMS Plugin Conflicts (LearnDash, LifterLMS, Sensei): LMS plugins often inject their own independent Course schema onto CPTs. If both Rank Math and an LMS plugin generate schema simultaneously, duplicate nodes occur where Rank Math’s node lacks hasCourseInstance while the LMS node contains it.
  • ACF/Meta Field Key Mismatches: If custom fields feeding startDate or courseMode return an invalid date format (non-ISO 8601), Google’s parser rejects the entire CourseInstance block, reproducing the missing field warning despite the key being present in JSON-LD.
  • Headless / REST API Implementations: When using decoupled WordPress frontends (Next.js/Gatsby), standard PHP filters modifying $data['richSnippet'] do not propagate if Rank Math’s Headless REST API extension endpoint is consumed directly without hooking into rank_math/head.

Leave a Comment