Back to Blog
Guides14 min read3 September 2026

hasPart and isPartOf: Schema for Multi-Page Content

One work split across many URLs, and nothing says so. How hasPart and isPartOf work, when to skip them, and the Google feature that uses them.

By AI Schema Gen Team

Plenty of sites publish something that is genuinely one work and then scatter it across a dozen URLs. An eight-part buying guide. A documentation set with forty pages. A course with twelve modules. A long report split into chapters so it loads properly.

A reader understands the relationship immediately, because the navigation shows it. A machine reading any single page usually sees an article that happens to link to some other articles. Nothing in the markup says these pages are pieces of one thing.

hasPart and isPartOf are the pair that says it. They are also, confusingly, the properties behind two narrow Google features that have nothing to do with multi-page content at all, which is a large part of why people either skip them or use them wrongly.

One pair, pointing both ways

Start with what they are, because the relationship between the two is the only rule you have to remember.

hasPart is defined as:

Indicates an item or CreativeWork that is part of this item, or CreativeWork (in some sense).

isPartOf is defined as:

Indicates an item or CreativeWork that this item, or CreativeWork (in some sense), is part of.

They are the same statement read from opposite ends, and schema.org lists each as the inverse of the other. The parent says hasPart pointing down at its pieces. A piece says isPartOf pointing up at its parent.

Both live on CreativeWork, which is the first practical constraint: these properties describe relationships between works, things like articles, pages, books, courses and reports. They are not for saying your company has departments or your product has components. There are other properties for those.

One small asymmetry worth knowing: hasPart expects a CreativeWork, while isPartOf accepts a CreativeWork or a plain URL. That makes isPartOf slightly more forgiving when you only have a link to point at.

You do not need both directions. Since they are inverses, stating the relationship once is enough. Declaring hasPart on the parent and isPartOf on every child is not twice as strong, it is the same fact written twice, and now you have two places to keep in sync when the structure changes.

The practical default: let each child declare isPartOf. Child pages are usually generated from a template that already knows which parent it belongs to, so the fact stays accurate on its own. A parent listing every child in hasPart has to be regenerated every time you add, remove or reorder a piece, and that list goes stale quietly.

The baseline almost every site already has

Before the interesting uses, the ordinary one. A page is part of a site:

{
  "@context": "https://schema.org",
  "@type": "WebPage",
  "@id": "https://example.com/guides/pricing-strategy/part-3",
  "isPartOf": { "@id": "https://example.com/#website" }
}

Most SEO plugins and framework integrations emit something like this already, and it is worth knowing it is there so you do not think you are starting from nothing. It is also the least informative version of the relationship, because "this page belongs to this website" is not a surprise to anyone.

The version that carries real information is one level down: this page is part of this specific work, not merely part of the site.

Connecting a multi-part work

Here is a guide published across several pages, marked up so the pieces are legible as pieces.

Each part declares its parent:

{
  "@context": "https://schema.org",
  "@type": "Article",
  "@id": "https://example.com/guides/pricing-strategy/part-3#article",
  "headline": "Part 3: Choosing a pricing model",
  "position": 3,
  "isPartOf": {
    "@id": "https://example.com/guides/pricing-strategy#guide"
  },
  "author": { "@id": "https://example.com/about#priya" }
}

And the parent exists as a work in its own right, on the guide's landing page:

{
  "@context": "https://schema.org",
  "@type": "Article",
  "@id": "https://example.com/guides/pricing-strategy#guide",
  "headline": "The Complete Guide to Pricing Strategy",
  "author": { "@id": "https://example.com/about#priya" },
  "hasPart": [
    { "@id": "https://example.com/guides/pricing-strategy/part-1#article" },
    { "@id": "https://example.com/guides/pricing-strategy/part-2#article" },
    { "@id": "https://example.com/guides/pricing-strategy/part-3#article" }
  ]
}

Two details do the work here.

The parent has its own @id and exists as a real node. Without that, isPartOf on each child points at nothing, and a reference to a thing that does not exist is worse than no reference. The parent should live on a page that genuinely describes the whole guide.

The children are referenced by @id, not duplicated. Each entry in hasPart is a pointer, not a second copy of the article. That is the same reference pattern that connects the rest of your markup, which we cover in the Organization schema guide.

Adding position to each part is worth doing when the order is meaningful. hasPart itself carries no ordering, so a sequence of parts with no positions is a set, not a sequence.

Nesting, for genuinely large sets

The relationship nests, which is what makes it workable on a documentation set rather than an eight-part guide. A page can be a part of a section that is itself a part of the whole:

{
  "@context": "https://schema.org",
  "@type": "TechArticle",
  "@id": "https://example.com/docs/api/authentication#article",
  "headline": "Authentication",
  "isPartOf": { "@id": "https://example.com/docs/api#section" }
}
{
  "@context": "https://schema.org",
  "@type": "CreativeWork",
  "@id": "https://example.com/docs/api#section",
  "name": "API Reference",
  "isPartOf": { "@id": "https://example.com/docs#docs" }
}

Each level states only its immediate parent. Nothing has to know the full tree, and no page carries a list that goes stale when you add a page three levels down. A forty-page documentation set needs exactly one isPartOf per page plus one node per section, all of it generated from the structure the navigation already uses.

Resist the urge to have the top level enumerate every descendant in hasPart. That list is the thing that breaks.

This is where most of the confusion sits, and the distinction is cleaner than it looks.

A link says "there is another page over there." It says nothing about the relationship.

hasPart says composition: this thing is made of those things. The parts are pieces of the whole, and the whole is incomplete without them.

ItemList says enumeration: here is a list of things, optionally in an order. schema.org defines it as:

A list of items of any sort, for example, Top 10 Movies About Weathermen, or Top 100 Party Songs.

The items in a "top 10 movies" list are not parts of that list in any real sense. They exist independently, and the list is a view over them.

There is a hard technical line underneath the conceptual one. ItemList sits under Intangible, not under CreativeWork. hasPart expects a CreativeWork. So an ItemList is not a valid hasPart value, and reaching for one to express "the parts of my guide" is using the wrong tool at the vocabulary level, not just stylistically.

The rule that resolves nearly every case: if removing an item would leave the parent incomplete, it is a part. If removing it would just make the list shorter, it is a list item. Chapter 4 of your guide is a part. The fourth item in your roundup of project management tools is a list item.

BreadcrumbList is a third thing again, describing where a page sits in the navigation path rather than what it is made of, and our breadcrumb schema guide covers that separately. A page can legitimately have all three: a breadcrumb trail, an isPartOf parent, and an ItemList of products it mentions.

And one more pair it gets confused with

mainEntity and mainEntityOfPage are another inverse pair, and they answer a different question again: not what a work is made of, but which thing a page is primarily about.

schema.org defines mainEntityOfPage as indicating "a page (or other CreativeWork) for which this thing is the main entity being described." So a recipe might declare mainEntityOfPage pointing at the page it appears on, and that page might declare mainEntity pointing back at the recipe.

The difference from isPartOf in one line: isPartOf says "I am a piece of that." mainEntityOfPage says "that page is where I am described." A part is a component; a main entity is a subject. Part 3 of your guide is part of the guide. The pricing model it describes is the main entity of that page.

The two Google features that use hasPart for something else entirely

Here is the part that catches people out. hasPart appears in two of Google's documented features, and in neither of them does it mean "chapters of a work."

Paywalled content

This is the one almost nobody knows about, and it is a currently supported feature rather than a curiosity.

If you put content behind a paywall, you have a problem: showing Google the full article while showing visitors a truncated version looks exactly like cloaking, which is a spam policy violation. Google's answer is to have you declare the paywall explicitly, and the mechanism is hasPart.

Google's paywalled content documentation describes it as a way to "differentiate paywalled content from the practice of cloaking, which violates spam policies." You set isAccessibleForFree to false and use hasPart to point at the gated region of the page with a CSS selector:

{
  "@context": "https://schema.org",
  "@type": "NewsArticle",
  "headline": "The full report",
  "isAccessibleForFree": false,
  "hasPart": {
    "@type": "WebPageElement",
    "isAccessibleForFree": false,
    "cssSelector": ".paywall"
  }
}

Look at what hasPart is pointing at: a WebPageElement, identified by a CSS class, on the same page. Not another page. Not a chapter. A region of markup. Multiple gated sections become an array with different selectors.

It applies to CreativeWork and a specific set of subtypes: Article, NewsArticle, Blog, Comment, Course, HowTo, Message, Review and WebPage. And the selector has to match the class you actually used in your HTML, which is the part that breaks when someone renames a CSS class months later and nobody connects the two.

Video key moments

The second is hasPart carrying Clip objects with time offsets, to mark segments within a video. Again, the parts are not pages, they are moments. Our video schema guide covers the implementation, including the offsets and the deep-linking URLs.

Why this matters

Two documented Google features, one property, and neither of them is the structural use this article started with. That is worth holding onto, because it explains a common and reasonable mistake: someone reads that hasPart is a supported Google property, adds it to connect their multi-page guide, sees nothing happen in search, and concludes the property is broken. It is not broken. They used it correctly for a purpose Google does not render.

So what does the structural use actually do?

Honest answer: no rich result, and no visible change in search.

There is no Google feature that renders "this article is part 3 of an 8-part guide." Marking up composition will not produce a carousel, a sitelink set, or anything else you can screenshot.

What it does is describe your content accurately to anything reading the markup as a graph. A page that declares itself part of a larger work, pointing at a parent that exists and is described, is legible as a piece of something rather than as an orphan that happens to link sideways. That is the same category of value as the rest of your entity markup: it removes a guess.

Be sceptical of anyone promising more. Nobody documents how AI systems weight composition relationships, and any specific claim about it is invented. The reasonable position is that describing your content honestly and completely is worth doing, and that this is one of the cheaper ways to do it when your content genuinely has this shape.

Which leads to the real filter: use it where the relationship is real, and skip it where it is not.

Where it earns its place

Multi-page guides and long reports. The clearest case. One work, many URLs, an obvious parent page.

Documentation sets. A docs section with a landing page and many child pages is exactly this shape, and the parent usually already exists as a real page.

Course modules. A course made of lessons, where the lessons are not independent articles. Note that Course has its own dedicated properties and its own Google feature, so check the course schema guide before modelling it purely with hasPart.

Books and chapters. The original case the vocabulary was designed around.

Where to skip it: a blog with categories (that is classification, not composition), a roundup post (that is an ItemList), a set of related articles that each stand alone (those are just links), and any relationship you would have to argue for. If you find yourself justifying why these pages are "really" one work, they probably are not.

Common mistakes

Declaring both directions. They are inverses. State it once, preferably as isPartOf on each child, so it stays accurate as the structure changes.

Pointing isPartOf at a parent that does not exist. A reference to an @id nothing defines is a dangling reference, and it is worse than leaving the relationship out. The parent needs a real node on a real page.

Using hasPart where you mean ItemList. Composition versus enumeration. ItemList is not a CreativeWork, so it is not a valid hasPart value either.

Using it for non-works. These properties are for CreativeWork. A company's departments and a product's components are different relationships with different properties.

Expecting a rich result from the structural use. There is not one. The two documented Google features that use hasPart are for paywalls and video segments.

A cssSelector that no longer matches the page. The paywall markup points at a class name in your HTML. Rename the class, and the markup now describes a region that does not exist.

Regenerating a parent's hasPart list by hand. Any list maintained manually drifts. If the parent must carry the list, generate it from the same source that builds the navigation.

Adding parts with no position where order matters. hasPart is unordered. If part 3 has to come after part 2, say so.

How to add this without making things worse

1. Decide whether the relationship is real. Apply the removal test: would taking this page away leave the parent incomplete? If not, stop here. This is the step that saves the most trouble.

2. Make sure the parent exists as a node. It needs a page that describes the whole work, and a stable @id on that page. Do this before adding a single isPartOf.

3. Add isPartOf to the children from a template. One property, generated from the parent the page already belongs to, so it cannot drift.

4. Add position if the order is meaningful. Skip it if the parts are genuinely unordered.

5. Check your paywall markup separately, if you have any. Confirm the cssSelector still matches a class that exists in the rendered HTML. This is the one piece here that silently breaks during a redesign.

6. Validate, and read the result correctly. Run the page through validator.schema.org, which checks the vocabulary. The Rich Results Test will report no eligible rich result for the structural use, and that is the expected outcome rather than a failure. If you use the paywall markup, that one does have a Google feature to test against.

Frequently Asked Questions


AI Schema Gen builds your schema as a connected graph, so a page that belongs to a larger work says so and points at a parent that actually exists, rather than shipping a dangling reference. Start free and see how your pages currently relate to each other.

Is your site ready for AI?

Get a free readiness score in under a minute. No signup, no card.

Run the free check