Technical SEO

Course Schema: How to Mark Up Courses So Google and AI Engines Understand Them

· · 17 min read

Course schema is schema.org Course structured data: JSON-LD that tells search engines a page describes a course, who provides it, and how and when it runs. In 2026 Google still uses it for one rich result, the course list, which needs a name and description on at least three courses plus an ItemList on the page that lists them.

Most course schema guides were written for a second feature, course info, which Google retired in 2025. This article covers what Course, CourseInstance and Offer each describe, what the course list still requires, the JSON-LD for each template, and how a platform with hundreds of courses should generate it from the CMS.

Key takeaways

  • Google’s course list rich result is still supported: at least three courses, each with name and description, and ItemList markup on a summary page or an all-in-one page.
  • The course info rich result, which used CourseInstance, prices and instructors, was phased out in 2025. Those properties are still valid schema.org; Google Search no longer draws a feature from them.
  • Model the data honestly: Course is the curriculum, CourseInstance is one way of taking it (online, blended, a cohort with dates), and Offer is the price of that instance.
  • At catalogue scale, markup is a build step, not a content task. Serialise it from the same CMS fields that render the page, and let the build fail on promotional names, missing translations and lists shorter than three.
  • For AI search, the evidence is thinner than most guides admit. Visible text that matches the markup is the part you control.

What course schema is: Course, CourseInstance and Offer

Course schema is three types that work together. The split is what lets one page describe a self-paced version, a cohort and two prices without contradicting itself.

Course: the curriculum

schema.org defines Course as “a description of an educational course which may be offered as distinct instances which take place at different times or take place at different locations, or be offered through different media or modes of study.” It sits under CreativeWork and LearningResource, so it inherits properties such as teaches, educationalLevel and inLanguage.

Its own properties include courseCode, coursePrerequisites, educationalCredentialAwarded, numberOfCredits, syllabusSections and hasCourseInstance. schema.org’s usage counter puts Course on 100K to 1M domains, based on Google’s web index in August 2026.

CourseInstance: one way of taking the course

CourseInstance is “an instance of a Course which is distinct from other instances because it is offered at a different time or location or through different media or modes of study or to a specific section of students.” It is a subtype of Event, not of Course.

Four properties are its own: courseMode (for example “online”, “onsite” or “blended”), courseSchedule (a Schedule with startDate, endDate, repeatFrequency and repeatCount), courseWorkload, and instructor. Because it is an Event, it also takes offers, location and startDate.

Offer: the price of an instance

offers is valid on both Course and CourseInstance. Put it on the instance when the price depends on how the course is taken, as it usually does.

Fact about the courseWhere it goesProperty
Title, summary, outcomes, levelCoursename, description, teaches, educationalLevel
Who provides itCourseprovider (an Organization)
Credential on completionCourseeducationalCredentialAwarded
Online, blended or on siteCourseInstancecourseMode, location
Dates and pace of a cohortCourseInstancecourseSchedule
Total effort for a self-paced versionCourseInstancecourseWorkload
Teacher of a specific runCourseInstanceinstructor
Price of that runCourseInstanceoffers

What Google still shows for courses in 2026

Two course features used to share the Course type. One survived.

The course list rich result is still supported

Google’s course list documentation, last updated 8 September 2026, describes a rich result that shows several courses from the same site. It is “available in English in all regions where Google Search is available.” Course list is also still listed in Google’s gallery of supported structured data features.

The requirements are short:

  • Course properties. name and description are required. description has a display limit of 60 characters. provider is recommended.
  • At least three courses. Google says: “You must mark up at least three courses.” They can sit on separate detail pages or on one all-in-one page.
  • Carousel markup. You must add ItemList markup to either a summary page or an all-in-one page.
  • Valid names. Promotional phrases, prices, discounts and “make money fast” titles are listed as invalid names.

Stat card: Google's course list rich result needs at least three courses, per Google Search Central documentation checked in September 2026

The content guidelines narrow what counts as a course. It must be “a series or unit of curriculum that contains lectures, lessons, or modules,” with “an explicit educational outcome of knowledge and/or skill” and “led by one or more instructors with a roster of students.” A public event or a two-minute video does not qualify. The retired course info guidelines also excluded degree summary pages and standalone exam pages, which is a sensible line to hold for the course list too.

Course info was retired in 2025

On 12 June 2025 Google announced it was simplifying the search results page and phasing out several structured data features, Course Info among them. The reason given was that “they’re not commonly used in Search” and “no longer providing significant additional value for users.”

The follow-up on the same post, dated 8 September 2025, set the dates. From 9 September, Course Info was removed from Search Console rich result reporting, the Rich Results Test and the Search appearance filters, with the Search Console API supporting it through December 2025. Google’s documentation changelog records the course info page itself being removed on 9 September 2025, because the type is “no longer shown in Google Search results.”

Stat card: Google retired the course info rich result in 2025, announced in June and removed from Search Console and the Rich Results Test in September

What went with it is the part most guides still describe as current. The archived course info documentation (captured 3 June 2025) required provider, offers with a pricing category, and at least one hasCourseInstance carrying courseMode, courseSchedule and courseWorkload. It recommended ratings, syllabusSections, teaches, educationalLevel, financialAidEligible and totalHistoricalEnrollment. None of that now feeds a Google Search feature.

Two sentences in Google’s announcement set the scope. “This update won’t affect how pages are ranked.” And “the use of these structured data types outside of Google Search (and dependent features) is not affected.”

Ratings still attach to courses

One piece of the old course info experience lives on elsewhere. Google’s review snippet documentation still lists course list among the features you can supply ratings for, and Course as a valid reviewed item type. The general structured data guidelines apply in full: no fake reviews, and ratings must be visible on the page they describe. The eligibility rules and working Review and AggregateRating blocks are in Review schema.

What each property does in 2026

MarkupGoogle rich result todayWorth shipping?
Course with name, description, providerCourse list (with ItemList, three or more courses)Yes, on every course page
ItemList on the catalogueCourse list carouselYes, per locale
aggregateRating, reviewReview snippet, where eligibleOnly with genuine learner reviews
CourseInstance, offers, courseScheduleNone since 2025Yes, if the facts are on the page: cheap to emit and unambiguous for other consumers
syllabusSections, teaches, educationalLevelNoneYes, when they mirror visible content

The last two rows are my judgement, not a Google recommendation: a CMS that already stores dates, modes and prices can serialise them at no cost. Never add properties for facts the page does not show.

The JSON-LD for each template

A course catalogue has two templates that matter: the course detail page and the catalogue page that lists courses. For general JSON-LD syntax and placement, how to create rich snippets with JSON-LD covers the basics, and schema markup examples has copy-paste blocks for the other common types.

The course detail page

This is the full graph for one course: the organisation, the breadcrumb and the course with two instances. The organisation node is emitted sitewide and referenced by @id, as described in Organization schema, so every course points at the same provider entity.

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Organization",
      "@id": "https://learn.example.com/#organization",
      "name": "Example Learning",
      "url": "https://learn.example.com/",
      "logo": "https://learn.example.com/logo.png"
    },
    {
      "@type": "BreadcrumbList",
      "itemListElement": [
        { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://learn.example.com/en/" },
        { "@type": "ListItem", "position": 2, "name": "Courses", "item": "https://learn.example.com/en/courses/" },
        { "@type": "ListItem", "position": 3, "name": "UX Design Foundations" }
      ]
    },
    {
      "@type": "Course",
      "@id": "https://learn.example.com/en/courses/ux-design-foundations/#course",
      "url": "https://learn.example.com/en/courses/ux-design-foundations/",
      "name": "UX Design Foundations",
      "description": "Research, wireframing and usability testing for people new to UX.",
      "provider": { "@id": "https://learn.example.com/#organization" },
      "inLanguage": "en",
      "courseCode": "UX-101",
      "educationalLevel": "Beginner",
      "teaches": ["Plan and run a usability test", "Turn research into wireframes"],
      "educationalCredentialAwarded": "Certificate of completion",
      "hasCourseInstance": [
        {
          "@type": "CourseInstance",
          "courseMode": "online",
          "courseWorkload": "PT40H",
          "offers": { "@type": "Offer", "price": 490, "priceCurrency": "EUR" }
        },
        {
          "@type": "CourseInstance",
          "courseMode": "blended",
          "location": "Example Learning, Berlin campus",
          "courseSchedule": {
            "@type": "Schedule",
            "startDate": "2026-11-02",
            "endDate": "2026-12-14",
            "repeatFrequency": "P1W",
            "repeatCount": 6
          },
          "instructor": { "@type": "Person", "name": "Jordan Example" },
          "offers": { "@type": "Offer", "price": 890, "priceCurrency": "EUR" }
        }
      ]
    }
  ]
}

The breadcrumb follows the rules in breadcrumb schema: the last item has no item URL. The description is written to survive a 60-character display cut, with the useful part first.

repeatFrequency is written as an ISO 8601 duration (P1W), which is what schema.org’s Schedule definition asks for. The retired Google course info page used words such as “Weekly” instead. Neither matters to a Google feature now, so follow the vocabulary.

The catalogue summary page

On a summary page, each ListItem carries only @type, position and url. Google’s carousel documentation is explicit: “include only the type, position, and url properties in the ListItem.” The course details live on the detail pages.

{
  "@context": "https://schema.org",
  "@type": "ItemList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "url": "https://learn.example.com/en/courses/ux-design-foundations/" },
    { "@type": "ListItem", "position": 2, "url": "https://learn.example.com/en/courses/data-analytics-with-sql/" },
    { "@type": "ListItem", "position": 3, "url": "https://learn.example.com/en/courses/product-management-essentials/" }
  ]
}

Every URL must be the canonical URL of its detail page, unique, and on the same domain. If the catalogue can reach a course through filtered or parameterised URLs, the list should carry the canonical one; canonical tags covers how to make sure there is exactly one.

A hybrid list that matches neither pattern

One mistake shows up in course schema guides: an ItemList whose items are full Course objects pointing at separate course URLs. One of the guides ranking for “course schema” in September 2026 publishes exactly this as its course list example.

The all-in-one pattern does allow full Course objects in the list, but then item.url must be “the current page” plus an anchor to where that course is described on it. The summary pattern allows separate URLs, but then the list items carry nothing except type, position and URL. Pick the one your catalogue actually is.

Generating course schema from the CMS

A platform with a few hundred courses in several languages cannot maintain JSON-LD by hand: every price change, cohort and translation would need a second edit, and the markup would drift from the page within weeks. Markup has to be a serialisation of CMS data, produced by the same build that renders the HTML.

The rules the serialiser enforces

  • One source. The JSON-LD reads the same fields that render the visible page. If the price on the page comes from the commerce system, so does offers.price.
  • No translation, no markup. A locale without a translated title and summary emits nothing, rather than English markup on a Spanish URL.
  • Names are names. The build fails if a course title contains a price, a percentage or a superlative, because Google treats those as invalid names and they usually signal a marketing field mapped to the wrong slot.
  • Lists of three or more. The catalogue emits ItemList only when a locale has at least three marked-up courses.
  • Expired runs disappear. Past cohorts drop out of hasCourseInstance at build time. Google’s guidelines say it won’t show a rich result for “time-sensitive content that is no longer relevant.”
  • Server-rendered. The JSON-LD ships in the initial HTML, not after hydration. The reasons are in JavaScript SEO: crawlers that do not execute JavaScript never see client-injected markup.

A serialiser you can run

This is a trimmed version of the pattern. It takes CMS course records, where each course has language-neutral fields (code, level, runs) and a per-locale block (slug, title, summary, outcomes).

const SITE = 'https://learn.example.com';
const ORG_ID = `${SITE}/#organization`;
const PROMO = /[$€£%]|\bbest\b|#1/i;

export function courseNode(course, locale) {
  const t = course.i18n[locale];
  if (!t) return null; // no translation, no markup
  if (PROMO.test(t.title)) throw new Error(`Promotional course name: ${t.title}`);
  const url = `${SITE}/${locale}/courses/${t.slug}/`;
  return {
    '@type': 'Course',
    '@id': `${url}#course`,
    url,
    name: t.title,
    description: t.summary,
    inLanguage: locale,
    courseCode: course.code,
    provider: { '@id': ORG_ID },
    educationalLevel: course.level,
    teaches: t.outcomes,
    hasCourseInstance: course.runs.map((run) => ({
      '@type': 'CourseInstance',
      courseMode: run.mode,
      ...(run.workload && { courseWorkload: run.workload }),
      ...(run.start && {
        courseSchedule: {
          '@type': 'Schedule',
          startDate: run.start,
          endDate: run.end,
          repeatFrequency: 'P1W',
          repeatCount: run.weeks,
        },
      }),
      offers: {
        '@type': 'Offer',
        price: run.price.amount,
        priceCurrency: run.price.currency,
        url,
      },
    })),
  };
}

export function coursePage(course, locale) {
  const node = courseNode(course, locale);
  return node && { '@context': 'https://schema.org', '@graph': [node] };
}

export function cataloguePage(courses, locale) {
  const urls = courses
    .map((c) => courseNode(c, locale))
    .filter(Boolean)
    .map((n) => n.url);
  if (urls.length < 3) return null; // below Google's course list minimum
  return {
    '@context': 'https://schema.org',
    '@type': 'ItemList',
    itemListElement: urls.map((url, i) => ({ '@type': 'ListItem', position: i + 1, url })),
  };
}

I ran it in Node 22 against three sample records: two courses translated into English and Spanish, and one in English only. The English catalogue produced a three-item ItemList. The Spanish catalogue produced nothing, because only two courses have Spanish translations. The Spanish URL for the English-only course produced nothing. A test record titled “UX Design, 25% off” stopped the build with Promotional course name: UX Design, 25% off. Both emitted blocks parsed as valid JSON.

That is the behaviour you want: quiet when the data is right, loud when it is not. In production the organisation and breadcrumb nodes join the graph from their own serialisers.

Tests worth adding to CI

  • Every course detail page in a sample from each locale contains exactly one Course node whose name matches the visible H1.
  • Every ListItem.url on a catalogue page returns 200 and is self-canonical.
  • No CourseInstance has an endDate in the past.
  • Every offers.price equals the price rendered on the page.
  • A crawler fetch of the raw HTML, without JavaScript, contains the JSON-LD.

Worked example: an eleven-locale learning platform

The multilingual learning platform case study on this site is a useful shape to plan against. It is a headless stack serving eleven locales, with clean server-side rendering, correct hreflang reciprocity within declared pairs, and a canonical layer free of the usual cross-locale bugs. The headline finding was that the site emitted no structured data on any page template in any locale.

The case study records the rollout order: sitewide foundation first, then high-traffic templates, then specialised layers. Course schema slots into that order. The course records in this article are illustrative, not the client’s data.

Batch 1: the sitewide foundation

Organization with a stable @id, emitted once per page in every locale, and BreadcrumbList generated from the same data as the visible breadcrumb. Nothing course-specific yet, but every later node points at this organisation as provider.

Batch 2: course and catalogue templates

Course on every course detail template and ItemList on each locale’s catalogue, both from the serialiser. This is the batch with a visible Google outcome, the course list, and it needs one decision per locale: Google documents the course list as available in English, so English catalogue pages are the ones with a rich result at stake. The other ten locales still get markup, for the reasons in the AI section below, but nobody should promise a rich result there.

Locale handling follows hreflang implementation for multilingual SaaS: each localised page describes itself with its own inLanguage and url, and never points a Spanish page at the English course.

Batch 3: the specialised layers

CourseInstance with modes, schedules and offers, fed from the scheduling and commerce systems. Ratings only where genuine learner reviews are collected and shown.

This is where custom edtech software decisions show up. If the content model does not store delivery mode and price per run as structured fields, the serialiser has nothing to read, and the fix belongs in the data model. The technical SEO of headless architecture makes the same point: the front end can only emit what the API gives it.

How AI engines use course markup, and what nobody has confirmed

Some course schema guides claim that AI assistants parse Course markup to answer “which course should I take” questions; the same ranking guide says well-implemented markup makes a course “significantly more likely to be cited”, without a source. The public evidence is narrower than that.

Microsoft has said the most. Search Engine Land reported in March 2025 that Fabrice Canel of Bing confirmed at SMX Munich that schema markup helps Microsoft’s LLMs understand content. The same report notes it is not clear whether Gemini, OpenAI or Perplexity use schema.

Google’s position is in its AI features documentation: to appear as a supporting link in AI Overviews or AI Mode, a page must be indexed and eligible for a snippet, and “there are no additional technical requirements.” Its list of best practices includes “making sure your structured data matches the visible text on the page.”

So the case for full course markup on AI surfaces is modest: it costs little once the serialiser exists, one major engine says it helps, and markup must agree with visible text. Put mode, duration, level, price and outcomes in plain HTML, with the JSON-LD as a structured restatement. The wider argument is in structured data for AI search.

Validate and monitor

  • Rich Results Test on detail pages. Paste a course detail URL into the Rich Results Test and confirm it detects the course with no errors. Since September 2025 it no longer reports on course info, so it will not flag CourseInstance or offers problems; your CI tests have to.
  • Manual checks on the summary page. Google’s carousel documentation lists what to verify yourself for a summary page: that itemListElement has the right number of items, that they are all the same type, and that each listed URL passes the Rich Results Test on its own.
  • URL Inspection on a sample. Check a page per locale and template in Search Console’s URL Inspection tool to see the HTML Googlebot received, not the HTML your browser built.

Course markup is one of the checks in the structured data phase of an audit; the technical SEO audit checklist puts it in order with the rest. If your catalogue spans several locales and templates and you want the markup specified as an engineering deliverable, that is the kind of work the GEO and technical SEO audit covers.

Frequently asked questions

Does Google still support course schema in 2026?

Partly. The course list rich result is still documented and available in English in all regions where Google Search is available. The separate course info rich result, which used CourseInstance, prices and instructors, was phased out in 2025.

What properties are required for course schema?

For Google’s course list, each Course needs name and description, and provider is recommended. You must mark up at least three courses and add ItemList markup to a summary page or an all-in-one page. schema.org itself has no required properties; everything beyond Google’s minimum is optional vocabulary.

Should I still add CourseInstance and offers?

If the facts are on the page, yes. They no longer power a Google rich result, but Google says uses outside Google Search are unaffected, and a CMS that stores modes, dates and prices can emit them at no extra cost. Do not add them for facts the visible page does not show.

How many courses do I need for the course list rich result?

At least three, either on separate detail pages listed from a summary page or together on one all-in-one page. A locale with fewer than three marked-up courses should not emit the ItemList.

Does course schema help with AI Overviews or ChatGPT?

There is no confirmed direct effect. Microsoft has said schema helps its LLMs understand content, while Google says its AI features have no additional technical requirements beyond being indexed and snippet-eligible. The safe approach is markup that matches visible text, with the key course facts written plainly on the page.

Can I mark up course ratings?

Yes, if they are genuine learner ratings shown on the page. Google’s review snippet documentation lists course list among the features that accept ratings and Course as a valid reviewed item. Ratings that are not from real users can lead to a manual action under Google’s structured data guidelines.