BlogFindings
Course schema markup: 1 of 242 course pages declared it, and 124 declared nothing at all
Lantad asked all 1,419 hostnames in this repository's two committed corpus seed files for robots.txt on 11 October 2026, read every permitted home page with no JavaScript executed, then followed each page's own link to a courses, programmes, training or classes page and read that too. 242 of those pages answered HTTP 200 with HTML on both passes. One declared a schema.org Course node. 124 carried no JSON-LD block at all, and not one of the 242 used Course in microdata or RDFa.
So this run went looking for the markup on exactly those pages. On 11 October 2026 Lantad asked all 1,419 hostnames in this repository's two committed corpus seed files for /robots.txt as LantadBot, read every home page the files left open with no JavaScript executed, and then followed each home page's own link to a page whose URL path named courses, programmes, training, classes, a curriculum, a degree, a certification, an academy, a bootcamp, a workshop or a diploma. 251 sites offered one. 242 of those pages answered HTTP 200 with an HTML content type on two separate passes the same day. One of the 242 declared a Course node.
In short
- Course schema markup is almost absent from the pages built to sell courses: of the 242 course, programme and training pages Lantad read across its committed 1,419 host corpus on 11 October 2026, exactly one declared a schema.org Course node.
- That one page was uxcel.com/courses, carrying 59 Course nodes inside a single ItemList, every one of them with the name, description and provider that Google's course list documentation names.
- Two of the 1,094 readable corpus home pages carried anything from the course family on 11 October 2026: unacademy.com with 11 Course nodes whose description repeated the course name verbatim, and acponline.org with one EducationEvent.
- Coursera's degrees page and edX's online finance bachelors page each declared an ItemList of bare ListItem nodes holding a position and a URL and no Course node at all, so the markup states an order and never says what is taught.
- 124 of the 242 course pages carried no JSON-LD block whatsoever, among them the University of Glasgow open courses page, which linked 67 individual course URLs across 2,222 visible words of prose.
Flow: 1,419 corpus hostnames to 1,086 robots.txt parsed; 1,086 robots.txt parsed (1,407 allowed) to 1,094 home pages read; 1,094 home pages read (own link) to 251 named a course page; 251 named a course page to 242 answered twice; 242 answered twice to 1 declared Course; 242 answered twice to 124 declared nothing.
One course page in 242 declared a Course node
The funnel is worth stating in full, because the interesting number is only interesting if the denominator is honest. 1,122 of the 1,419 hostnames answered the robots.txt request with HTTP 200 and 1,086 of those returned a body that parsed as plain text rather than an HTML error document. 163 answered 403 and 56 answered 503, which this scanner reads as closing the site. 12 hostnames ran a file that refused LantadBot at the root. That left 1,407 hostnames with no prohibition on the root, and 1,094 of them answered the home page request with HTTP 200 and an HTML content type. 627 of the 1,094 carried at least one application/ld+json block and 621 held at least one that parsed.
Two of those 1,094 home pages carried anything from the course family at all. unacademy.com declared 11 Course nodes and acponline.org declared one EducationEvent. Following each site's own navigation reached 242 readable course pages, and uxcel.com was the only one with a Course node on it. Nothing in the other 241 used the type in microdata or RDFa either, so JSON-LD is the whole of the story rather than half of it.
The selection is the part of this that needs stating plainly rather than defending. Choosing pages by the words a site puts in its own URL is cheap, repeatable and blind: it cannot tell a course catalogue from a bond programme. hsbc.com contributed its fixed income issuance programmes page, london.gov.uk its programmes and strategies index, seattle.gov a road maintenance programme, and cuure.com a French loyalty programme. Those are not courses and nobody should mark them up as courses. So the headline figure is deliberately paired with a tighter one further down, counting only the pages that link three or more individual course URLs, which is the shape Google's documentation actually asks for markup on. The looser number says how often the word appears. The tighter one says how often the thing does. The method itself is the one earlier runs used for careers pages and pricing pages: follow the link the site put in its own navigation, and read what it points at.
The strata say something the total hides. 49 of the 242 came from the education stratum, which is 49 of the 103 readable university and school home pages in the corpus, and not one of those 49 declared a Course node. 44 came from healthcare, 30 from the SaaS stratum, 23 from government and 21 from finance. The one page that did declare it sits in the Webflow platform stratum.
What does course schema markup require?
Less than most people assume, which is part of why the absence is odd. Google's course list documentation, carrying Last updated 2026-09-08 UTC, lists exactly two required properties on the Course itself: name, which it defines as the title of the course, and description, which it defines as a description of the course and annotates with the words "Display limit of 60 characters". provider sits under recommended properties in that table, and then the technical guidelines on the same page say "Each course must have valid name and provider properties", which is a firmer statement than the table it follows.
The wrapper is where the real requirement lives. The guidelines say "You must mark up at least three courses" and "You must add Carousel markup to either a summary page or an all-in-one page", and the ItemList required properties are itemListElement, ListItem.position and ListItem.url, the last defined as the canonical URL of the item page with the note that every item must have a unique URL. So a single course page is not eligible on its own. It has to be paired with a list, which is exactly the page shape this run went looking for.
schema.org's own Course definition is broader than Google's slice of it and is where the useful vocabulary sits. The type defines ten properties of its own, among them hasCourseInstance for the scheduled runs of a course, courseCode, coursePrerequisites, numberOfCredits, educationalCredentialAwarded and financialAidEligible. Every one of those answers a question a person types into an assistant before they type it into a search box: when does it run, what do I need first, what do I come out with, does funding apply. None of them is required by Google, because Google is specifying a rich result rather than a knowledge representation, and the gap between those two jobs is the gap this blog keeps finding. The same split showed up in the run on HowTo schema, where Google had withdrawn the rich result entirely and the markup still described the steps for anything else reading the page.
Google also draws a line around what counts, and it is worth quoting because it is the kind of constraint that stops a site marking up its newsletter: the page says a general public event such as "Astronomy Day" is not a course, and a single two minute "How to make a Sandwich Video" is not a course. A course is a body of teaching with instances. That rules out a great deal of what the 242 pages contain, and it still leaves the catalogues.
| Requirement | Where it is stated | uxcel.com, 59 nodes | unacademy.com, 11 nodes |
|---|---|---|---|
| name | Required properties | 59 of 59 | 11 of 11 |
| description | Required properties | 59 of 59 | 11 of 11 |
| description within 60 characters | Display limit, same table | 0 of 59 | 11 of 11 |
| provider | Recommended, then required in the guidelines | 59 of 59 | 11 of 11 |
| at least three courses marked up | Technical guidelines | Yes | Yes |
| itemListElement of ListItem | ItemList required properties | 59 ListItems | 0, elements are Course |
| ListItem.position | ItemList required properties | 59 of 59 | On the Course node |
| ListItem.url | ItemList required properties | 0, url is inside item | On the Course node |
Five sites reached for the construct and no two built it the same way
Across the whole corpus, five pages declared an ItemList in a course context, and the five disagree about what goes inside one. That disagreement is more informative than the declaration rate, because it says the construct is not merely unused. It is unsettled among the people who did use it.
uxcel.com built it the way the documentation's all-in-one example is written. One ItemList carries numberOfItems and 59 ListItem entries, positions running 1 to 59 with no gaps, each element holding an item object typed Course with a unique url, a name, a description, a provider naming Uxcel with a LinkedIn sameAs, an about value naming the subject, an offers node and a hasCourseInstance giving courseMode Online and a courseWorkload. 58 of the 59 also carry educationalLevel. It is the most complete course markup in the corpus by a wide margin, and the only property from the ten that schema.org defines on Course itself is hasCourseInstance, which is the one that matters most. The page is 288,385 bytes carrying 2,794 visible words and 67 second and third level headings, so the markup describes a document that genuinely holds the catalogue.
unacademy.com built something that validates as neither shape. Its home page ItemList holds 11 elements, and the elements are typed Course rather than ListItem, each carrying a position property that belongs on a ListItem. Google's required ItemList properties name ListItem as the annotation for a single item page, so there is no wrapper here for the requirement to attach to. Every one of the 11 carries name, url, description, inLanguage and provider, and none carries any of the ten properties schema.org defines on Course. The descriptions are the detail worth keeping: all 11 are identical to the name of the course they describe, running from 7 to 13 characters, so the Course named UPSC CSE - GS is described as UPSC CSE - GS. The field is present, the requirement is met, and nothing has been said. That is the same failure as markup whose values are not on the page it describes, arriving from the opposite direction.
uxcel's descriptions fail the limit in the other direction. All 59 run past the 60 characters Google's table gives as the display limit, from 90 to 175 characters with a median of 150. They are real sentences and they are better prose than a truncated fragment would be, so this is a case where the documentation and the usefulness of the field pull apart, and the honest thing to note is that a reader of the rich result sees roughly the first third of each one. Neither site hit the target the documentation describes, in opposite ways.
| Page | Lists | Elements | Element type | Carries a Course |
|---|---|---|---|---|
| uxcel.com/courses | 1 | 59 | ListItem with an item object | Yes, 59 |
| unacademy.com home page | 1 | 11 | Course with a position | Yes, 11 |
| coursera.org/degrees | 3 | 11, 45 and 3 | ListItem, position and url only | No |
| edx.org online finance bachelors | 1 | 2 | ListItem, url and a provider id | No |
| mdanderson.org education and training | 1 | 15 | SiteNavigationElement | No |
Why an ordered list of URLs is not a list of courses
The two largest course platforms in the corpus are the clearest case in it. coursera.org/degrees declared three ItemList nodes holding 11, 45 and 3 elements. All 59 elements are ListItems carrying a position and a url, three of them also carrying a name, and not one carries an item object or a Course node. edx.org did the same thing more briefly on its online bachelors degree in finance page: a single ItemList of two ListItems, each with a url and a provider reference pointing at the site's own organisation node, and no Course anywhere in the document. Both pages are substantial. The Coursera page is 996,017 bytes with 2,367 visible words and 59 second and third level headings. The edX page is 801,339 bytes with 1,058 visible words.
Read against the ItemList required properties table alone, that markup satisfies all three rows. itemListElement is present, ListItem.position is present, ListItem.url is present, and ListItem.url is the property that table names. Read against the examples on the same page, it is missing the thing the feature exists to carry. The single course page example is a bare Course with a name, a description and a provider and no url at all; the all-in-one example nests the url inside an item object alongside the Course. Neither example gives a ListItem a top level url. So the one page in the corpus that followed the examples has no property matching the required table, and the two pages that match the required table contain no courses. Reasonable engineers reading the same page landed on incompatible readings, which is a documentation problem before it is an implementation problem.
For a blue link result, the difference is invisible: either shape hands Google an ordered set of URLs to crawl, and the crawl collects the names. For generative engine optimization it is the whole difference. An engine assembling an answer about online finance degrees, whether that is Google AI Overviews or Perplexity, is not going to fetch 45 URLs to find out what they are called. It reads what the page states. A bare ListItem states an ordering of addresses. A Course states a title, a teaching body, a provider and, where hasCourseInstance is present, when it runs. The first is a sitemap fragment with a position on it. The second is an answer. This is the same distinction measured in the run on ItemList schema, where 525 of 766 list items across the corpus carried no name, and it recurs because the construct makes the empty version so easy to ship.
mdanderson.org shows the third way to spend the construct. Its education and training page declares one ItemList of 15 elements and every element is a SiteNavigationElement: Patients and Family, and the rest of the site's top level menu. The page is 153,304 bytes with 525 visible words. What the markup publishes is the navigation bar, on the page a prospective fellow would land on, which is a variation on the finding that half the text blocks on a page are navigation.
coursera.org/degrees, 45 elements
- ListItem, position 1
- url /degrees/imba
- no item object
- no name, no provider
- states an order and an address
uxcel.com/courses, 59 elements
- ListItem, position 1
- item typed Course
- name 3D Design Foundations
- provider Uxcel, courseMode Online
- states what is taught
What the 242 course pages declared instead
124 of the 242 carried no JSON-LD at all, which is the single largest group and the least complicated. The other 118 all carried at least one block that parsed, and what those blocks hold is a consistent picture of structured data as site furniture rather than subject matter. 64 of the 118 declared an Organization, 53 a WebPage, 50 a WebSite, 48 a BreadcrumbList, 32 a SearchAction, 18 a Person, 16 a PostalAddress, 14 a NewsArticle, 12 an FAQPage and 5 a Product.
Read that list against the page it sits on. A breadcrumb trail states the path a reader took to reach the catalogue. A SearchAction states that the site has a search box. An Organization states who runs the site. All three are true, all three are correctly marked up, and none of them is about the courses. 48 pages described the route to the catalogue in machine readable form and one described what the catalogue contains. The same pattern turned up when this corpus was read for node identity: the markup that ships is the markup a template generates, and a course catalogue is not something a template knows about.
That is the mechanical explanation and it holds for most of the 118. A WordPress or Webflow theme emits Organization, WebSite and BreadcrumbList without being asked. Course markup has to be written by somebody who knows the catalogue exists, which means it only appears where a person decided to put it there. Both sites that did decide are course businesses whose product is the catalogue itself. Nobody in the education stratum made that decision, which is the finding this post would least have predicted: 49 universities, colleges and schools published a course page, 17 of them published JSON-LD on it, and zero published a Course.
The cost is not a lost rich result, because a rich result for a list of courses is a modest prize. The cost is that a page which the site itself treats as its catalogue arrives at an answer engine as prose and nothing else, and prose is the thing structured data exists to make unnecessary to guess at. Every other signal on these pages is in good order. They are readable and almost all of them are open: 85 of the 1,086 parsed robots.txt files in the corpus refuse GPTBot at the root, and 21 of the 242 course pages sit on one of those sites, 14 of the 21 being news publishers. For the other 221 the door is open and the label on the shelf is missing.
The pages with the most courses and the least markup
Here is the tighter count promised above. 18 of the 242 pages linked three or more individual same-host URLs whose own path says course, courses, class or classes, which is as close as a crawl gets to confirming that a page lists separate courses rather than merely using the word. 17 of those 18 declared no Course node, and 11 of the 18 carried no JSON-LD block at all. Three or more is not an arbitrary threshold: it is the minimum Google's own technical guidelines set before the feature applies.
The named cases are ordinary sites doing everything else right. The University of Glasgow's online open courses page links 67 individual course URLs across 106,963 bytes and 2,222 visible words under 72 second and third level headings, and declares no structured data of any kind. Imperial College's course index links 24 and carries one block holding no Course. UCL's short courses page links 17 across 78 visible words. The Bundesbank's international central banking courses page links 16 across 829 words with no JSON-LD. Interactive Brokers' Campus links 141 course URLs across 2,687 words and declares a CollectionPage, an FAQPage and an Organization. The Indian Institute of Science links 8 across 37 visible words, and Kyoto University's open course page delivers 62,639 bytes containing 2 visible words, which is a JavaScript rendering problem rather than a markup one and a worse problem to have.
Two of those deserve separating out, because they fail in a way markup cannot fix. Georgia Tech's all degree programs page is 228,462 bytes and delivers 39 visible words with no JSON-LD, while containing 258 same-host links whose paths name a programme or a degree. The links are in the delivered HTML and the text that would explain them is not. Adding Course nodes to a page in that state would be marking up content a reader cannot see, and the first fix is the one the run on semantic HTML describes: get the words into the document. Across all 242 pages the corpus delivered 200,759 visible words, a median of 539 per page, which is enough prose to describe a catalogue on most of them and not on all.
The practical reading is narrow and worth keeping narrow. Course markup is not a ranking tactic, and nothing measured here says anything about AI visibility outcomes, because 1 of 242 is not a sample from which an outcome can be inferred. What it does establish is that the field is empty. If a page is the catalogue, the two properties Google requires are a title and a description it already prints, the ten schema.org defines on the type answer the questions buyers ask first, and the only site in a 1,419 host corpus that filled them in sells online UX design courses. Anybody auditing their own pages can start by checking what the crawler receives with the GPTBot view, then read the methodology for how this corpus is sampled, and the study rollups on research for what the corpus has shown elsewhere.
| Page | Course URLs linked | Bytes | Visible words | JSON-LD blocks | Course node |
|---|---|---|---|---|---|
| interactivebrokers.com/campus/academy | 141 | 613,682 | 2,687 | 2 | No |
| gla.ac.uk/study/online-open-courses | 67 | 106,963 | 2,222 | 0 | No |
| imperial.ac.uk/study/courses | 24 | 189,803 | 376 | 1 | No |
| ucl.ac.uk/short-courses | 17 | 27,214 | 78 | 0 | No |
| bundesbank.de international courses | 16 | 296,851 | 829 | 0 | No |
| bettersheets.co/courses | 14 | 120,106 | 340 | 0 | No |
| flux-academy.com/our-courses | 13 | 221,540 | 371 | 0 | No |
| evolvehealing.net/courses | 10 | 1,286,618 | 1,927 | 5 | No |
| acponline.org/meetings-courses | 10 | 99,477 | 379 | 0 | No |
| iisc.ac.in/courses | 8 | 104,453 | 37 | 0 | No |
| seota.com msp marketing 101 | 7 | 157,256 | 2,160 | 1 | No |
| mcgill.ca/teaching-academic-programs | 7 | 110,644 | 328 | 1 | No |
| kyoto-u.ac.jp open course | 7 | 62,639 | 2 | 0 | No |
| uxcel.com/courses | 6 | 288,385 | 2,794 | 1 | Yes, 59 |
| weizmann.ac.il about courses | 4 | 91,412 | 2,248 | 1 | No |
| outseta.com/online-course-examples | 3 | 116,273 | 951 | 0 | No |
| auckland.ac.nz course information | 3 | 193,219 | 96 | 0 | No |
| unsw.edu.au find a degree or course | 3 | 129,996 | 224 | 0 | No |
Lantad
Published .
Course schema markup is the oldest kind of answer to a question an AI assistant now gets asked constantly: what can I study here, and what does it cover. The construct is not new and not contested. Schema.org has defined the Course type for years, Google documents a course list rich result built on it, and the pages that would use it are easy to find because sites name them themselves. A university puts its catalogue at /courses. A training provider puts it at /academy. The URL is a declaration of intent.
Common questions
What is course schema markup?
It is the schema.org Course type, usually published as JSON-LD, which states that a page describes a body of teaching and names its title, its description and the organisation providing it. Google builds a course list rich result on top of it, which needs the Course nodes wrapped in an ItemList and at least three courses marked up. Google's documentation, last updated 2026-09-08 UTC, requires only name and description on each Course, and its technical guidelines also require a valid provider.
Do AI answer engines need Course markup to list my courses?
No, and this post measured nothing that would support a claim either way. A crawler that reads the page can extract course names from the prose, which is what it has to do on 241 of the 242 corpus course pages Lantad read on 11 October 2026. The argument for the markup is cheaper than a ranking claim: a Course node states the title, the provider and, through hasCourseInstance, the scheduled runs, in a form that cannot be misread, and the two properties Google requires are already printed on the page.
Why did only one page in 242 declare it?
The pattern across the 118 pages that did carry JSON-LD suggests the markup that ships is the markup a template generates. 64 declared an Organization and 48 a BreadcrumbList, both of which a theme emits without being asked, while a Course node has to be written by somebody who knows the catalogue exists. The one page that declared it, uxcel.com/courses, belongs to a business whose product is the catalogue itself.
How were the 242 course pages chosen?
By the words each site put in its own URLs. Lantad read every corpus home page it was allowed to read on 11 October 2026 and took the best same-host link whose path named courses, programmes, training, classes, a curriculum, a degree, a certification, an academy, a bootcamp, a workshop or a diploma. That is blind to meaning, so it collected a bank's bond issuance programmes and a city's road maintenance programme alongside real catalogues, and links to other hosts were not followed. The tighter count of 18 pages linking three or more individual course URLs exists to bound that error.
See what AI can read on your site
Run a free scan and get a graded report of exactly what AI crawlers can and cannot read, with ranked fixes.