Within the IIIF image delivery and manifest generation stage, a single Manifest describes one object. But a museum does not publish one object — it publishes a hierarchy of them, and many of those objects are themselves multi-part. Two IIIF Presentation resources exist precisely for this: the Collection, which groups Manifests and sub-Collections into a browsable tree, and the Range, which imposes structure inside a Manifest — chapters, volumes, movements, a table of contents over an otherwise flat list of Canvases. Where building IIIF Presentation manifests produces the leaf documents, this guide produces the scaffolding that makes thousands of those leaves navigable.
The distinction is easy to blur and expensive to get wrong. A Collection is an inter-object container: its items are other Collections and Manifests, and it typically mirrors a department, an exhibition, or a donor group. A Range is an intra-object container: its items are Canvases (or nested Ranges, or a SpecificResource pointing at part of a Canvas), and it never introduces new content — it only reorganizes Canvases that already live in the Manifest’s items. Confusing the two produces viewers that either cannot browse the holdings or cannot page through a bound volume. This page specifies both resources, builds each from Python, sets the behavior hints that tell a viewer how to render them, and routes rights correctly as records flow from a department hierarchy into public manifests.
Workflow Context
A collections manager thinks in a tree: European Paintings sits under Fine Art, which sits under the museum’s top-level holdings. That tree is exactly a nested IIIF Collection. A Python developer wiring the delivery layer needs to turn the object schema’s department field — modeled upstream in designing museum object schemas — into that Collection hierarchy without hand-editing JSON. Meanwhile a single accessioned object, say a two-volume illuminated manuscript, is one Manifest whose sixty Canvases must be grouped into “Volume I” and “Volume II,” each with a “Table of Contents” Range pointing at the folios where chapters begin.
Both structures are pure navigation. Neither Collection nor Range carries pixels — the Image API resources referenced by each Canvas do that, configured in configuring the IIIF Image API. This separation is what keeps the delivery layer composable: you can regenerate the entire Collection tree nightly from the CMS without touching a single Manifest, and you can add a table of contents to a Manifest without moving its Canvases. The design in this guide treats the tree as a projection of the collection database and the Range set as a projection of a Manifest’s own structural metadata, so both are deterministic outputs of data you already hold.
Prerequisites
Before building either resource, confirm the following are in place:
- Python 3.9+ for PEP 604
X | Noneunions andmatchstatements used in the builders below. - IIIF Presentation API 3.0 as the target — this page uses v3 vocabulary throughout (
items,type,labelas a language map,behavior). See the IIIF Presentation API 3.0 specification for the normative definitions of Collection and Range. pydantic(v2) for typed resource models; examples use the v2 API (model_config,model_dump,field_validator).- A base URI scheme for identity. Every Collection, Manifest, Canvas, and Range needs a dereferenceable HTTP(S)
id; decide the URL template (for examplehttps://iiif.example.org/collection/{dept}) before generating anything. - A department or classification hierarchy in the source system — the tree you will project into nested Collections — plus, per Manifest, the structural metadata (volume breaks, chapter start folios) that seeds the Ranges.
- Existing v3 Manifests to reference. A Collection’s
itemspoint at Manifestids; a Range points at Canvasids that must already exist in the Manifest it belongs to. - Rights vocabulary — RightsStatements.org URIs or Creative Commons licenses — since Collections may assert rights that items inherit.
Schema & Spec Reference
Collection and Range share the Presentation API’s resource grammar but differ sharply in what their items may contain and which properties are meaningful. The table below is the working contract for both.
| Resource | items may contain |
Key structural properties | Purpose |
|---|---|---|---|
Collection |
Collection, Manifest |
label, behavior, thumbnail, requiredStatement, rights |
Browsable grouping of objects and sub-groups |
Range |
Canvas, Range, SpecificResource |
label, behavior, start, items |
Intra-Manifest structure: chapters, volumes, table of contents |
Two properties do most of the work and deserve their own reference. behavior is an array of viewer hints; start names the Canvas a viewer should open on.
| Property | Applies to | Common values | Effect |
|---|---|---|---|
behavior |
Collection, Range, Manifest | paged, continuous, individuals, multi-part, unordered |
Tells the viewer how to lay out the child Canvases or items |
start |
Range, Manifest | a Canvas id (or SpecificResource) |
The Canvas the viewer positions to when the Range is opened |
A minimal Collection and a minimal Range look like this in JSON:
{
"@context": "http://iiif.io/api/presentation/3/context.json",
"id": "https://iiif.example.org/collection/european-paintings",
"type": "Collection",
"label": { "en": ["European Paintings"] },
"items": [
{
"id": "https://iiif.example.org/manifest/1994.7.jsonld",
"type": "Manifest",
"label": { "en": ["Portrait of a Collector"] }
}
]
}{
"id": "https://iiif.example.org/manifest/ms-42/range/toc",
"type": "Range",
"label": { "en": ["Table of Contents"] },
"behavior": ["multi-part"],
"items": [
{
"id": "https://iiif.example.org/manifest/ms-42/range/vol-1",
"type": "Range",
"label": { "en": ["Volume I"] },
"start": { "id": "https://iiif.example.org/manifest/ms-42/canvas/1", "type": "Canvas" },
"items": [
{ "id": "https://iiif.example.org/manifest/ms-42/canvas/1", "type": "Canvas" }
]
}
]
}Note the asymmetry: inside a Collection an item is a reference (id, type, label — the full document lives at its own URL), while inside a Range a Canvas item is a reduced reference (id and type only) to a Canvas that is defined in full in the Manifest’s own items array. A Range never redefines a Canvas; it points at one.
Step-by-Step Implementation
1. Model the two resources with typed builders
Start with Pydantic v2 models so identity, label shape, and items typing are enforced before serialization. IIIF label is always a language map ({lang: [strings]}), never a bare string — encoding that rule once prevents the single most common validation failure.
from __future__ import annotations
from pydantic import BaseModel, ConfigDict, field_validator
LangMap = dict[str, list[str]]
class Reference(BaseModel):
"""A reduced reference used inside Collection and Range items."""
model_config = ConfigDict(extra="forbid")
id: str
type: str
label: LangMap | None = None
@field_validator("id")
@classmethod
def must_be_http(cls, value: str) -> str:
if not value.startswith(("http://", "https://")):
raise ValueError("IIIF id must be a dereferenceable HTTP(S) URI")
return value
class Collection(BaseModel):
model_config = ConfigDict(extra="forbid")
id: str
type: str = "Collection"
label: LangMap
behavior: list[str] | None = None
rights: str | None = None
items: list["Collection | Reference"] = []
def dump(self) -> dict[str, object]:
payload = {"@context": "http://iiif.io/api/presentation/3/context.json"}
payload.update(self.model_dump(exclude_none=True))
return payloadEdge cases. With extra="forbid", a stray viewingHint (the deprecated v2 spelling of behavior) is rejected rather than silently emitted — a useful guard when migrating v2 documents. Only the top-level Collection carries @context; nested Collections referenced inside items must not repeat it, which is why dump() adds the context only at the root.
2. Build a Collection tree from a department hierarchy
Project the source system’s department tree into nested Collections. The recursion mirrors the tree: each internal node becomes a Collection whose items are its child Collections and the Manifest references for objects catalogued directly under it.
from collections.abc import Iterable
BASE = "https://iiif.example.org"
def slug_to_collection_id(path: str) -> str:
return f"{BASE}/collection/{path}"
def build_collection(
node: dict[str, object], path: str
) -> Collection:
"""Recursively project a department node into a nested Collection."""
children: list[Collection | Reference] = []
# Sub-departments become nested Collections.
for child in node.get("children", []):
child_path = f"{path}/{child['slug']}"
children.append(build_collection(child, child_path))
# Objects catalogued directly under this node become Manifest references.
for obj in node.get("objects", []):
children.append(Reference(
id=f"{BASE}/manifest/{obj['object_id']}",
type="Manifest",
label={"en": [obj["title"]]},
))
return Collection(
id=slug_to_collection_id(path),
label={"en": [node["label"]]},
items=children,
)Edge cases. Guard against cycles — a mis-entered “parent” pointer in the CMS can make a department its own ancestor and send the recursion infinite; track visited slugs and raise on a repeat. For very large departments, do not inline thousands of Manifest references into one Collection: paginate with intermediate sub-Collections or the IIIF partOf/paging model, because a multi-megabyte Collection document is slow for every viewer to load. Where the source is XML (an EAD finding aid, for instance) the hierarchy is already nested and maps node-for-node; where it is a flat CSV or API export, reconstruct the tree from a parent_id column before calling build_collection.
3. Build a Range table of contents over Canvases
A Range groups Canvases that already exist in a Manifest. Given a Manifest’s ordered Canvas ids and a list of structural breaks (chapter or volume start indices), emit a nested Range tree and set each Range’s start to its first Canvas so a viewer opens at the right folio.
def canvas_ref(manifest_id: str, n: int) -> dict[str, str]:
return {"id": f"{manifest_id}/canvas/{n}", "type": "Canvas"}
def build_toc_range(
manifest_id: str,
sections: list[dict[str, object]],
) -> dict[str, object]:
"""Build a table-of-contents Range from labelled Canvas spans."""
child_ranges: list[dict[str, object]] = []
for i, section in enumerate(sections, start=1):
first = section["start"] # 1-based Canvas index
last = section["end"]
canvases = [canvas_ref(manifest_id, n) for n in range(first, last + 1)]
child_ranges.append({
"id": f"{manifest_id}/range/r{i}",
"type": "Range",
"label": {"en": [section["label"]]},
"start": canvas_ref(manifest_id, first),
"items": canvases,
})
return {
"id": f"{manifest_id}/range/toc",
"type": "Range",
"label": {"en": ["Table of Contents"]},
"behavior": ["multi-part"],
"items": child_ranges,
}The resulting Range object is placed in the Manifest’s structures array, not its items — items holds the flat, ordered Canvas list, while structures holds the Range trees that reorganize them. A viewer reads structures to render the table-of-contents sidebar.
Edge cases. A Range may cover a part of a Canvas — a single column on a broadsheet, one chapter opening mid-folio — using a SpecificResource with a fragment selector (#xywh=) in place of a bare Canvas reference; use this when structural boundaries do not align with page breaks. Deep-dive coverage of multi-volume modeling, where each volume is its own top-level Range and the Manifest spans several physical books, lives in modeling multi-volume objects as IIIF Ranges.
4. Set behavior hints for layout
behavior tells the viewer how to present the child Canvases or items. Choose the value from what the object is, not how it looks:
def behavior_for(object_kind: str) -> list[str]:
"""Map a physical object kind to its IIIF behavior hint."""
match object_kind:
case "bound_volume" | "codex":
return ["paged"] # two-up openings, page-turning UI
case "scroll" | "long_strip":
return ["continuous"] # canvases joined edge to edge
case "multi_volume_set":
return ["multi-part"] # a work split across sub-Ranges
case "photo_album" | "loose_prints":
return ["individuals"] # each canvas viewed alone, no pairing
case _:
return ["unordered"]Edge cases. paged requires an even reading order — if the first Canvas is a cover that should stand alone rather than pair with a blank verso, the spec allows tagging that Canvas non-paged. continuous suits scrolls and panoramas but is poorly supported in some viewers, so fall back to individuals when unsure. Apply behavior at the level that owns the layout: a multi-part set carries multi-part on the top Range while each volume Range carries paged.
Collection and Range Nesting
The two resources compose into one navigable structure. A Collection tree leads a browser down to a Manifest; inside that Manifest, a Range tree leads a reader down to the exact Canvas. The diagram traces both descents and shows where identity is a reference versus a full definition.
Read the solid arrows as containment and the dashed arrow as reference: the Range tree hangs off the Manifest’s structures array, but every Range ultimately points at Canvases that live, defined in full, in the Manifest’s items array. Regenerating the Range set never touches the Canvases, and regenerating the Collection tree never touches the Manifest.
Rights and Access Routing
Rights flow down the Collection tree unless an item overrides them, and this inheritance is where museums most often leak access. A Collection may carry a rights URI and a requiredStatement (the attribution a viewer must display). By convention a viewer applies a Collection’s requiredStatement to items that do not set their own — so a department Collection asserting a blanket “© The Museum” statement will stamp that credit on every Manifest beneath it that stays silent. That is convenient for attribution but dangerous for permissions: a Collection-level rights value is a default, never an enforcement boundary. The authoritative rights assertion for an object is the one on its own Manifest.
The safe routing rule is therefore explicit and item-level. Set rights on each Manifest from the object’s resolved rights status — the RightsStatements.org URI assigned upstream — and treat any Collection-level rights as advisory metadata only. A public-domain department Collection must not promote an in-copyright object to open access simply because the object’s Manifest omitted rights; the generator should refuse to emit a Manifest with no explicit rights rather than let it inherit a permissive default. Concretely, validate that every Manifest reference added by build_collection corresponds to a Manifest whose rights is set, and quarantine any object missing one. The mechanics of turning a rights status into the rights and requiredStatement blocks belong to building IIIF Presentation manifests; this stage only guarantees the tree never grants access the item itself did not assert.
Verification and Testing
Prove three properties before publishing: every id is a well-formed HTTP URI, every Range points only at Canvases that exist in the Manifest, and the Collection tree is acyclic. The assert-based test below exercises the builders in isolation.
def test_structures() -> None:
# 1. Collection projection produces nested Collections and Manifest refs.
dept = {
"label": "Fine Art", "slug": "fine-art",
"children": [{
"label": "European Paintings", "slug": "european-paintings",
"objects": [{"object_id": "1994.7", "title": "Portrait of a Collector"}],
}],
"objects": [],
}
root = build_collection(dept, "fine-art")
assert root.type == "Collection"
assert root.items[0].type == "Collection" # sub-department
assert root.items[0].items[0].type == "Manifest" # object reference
assert root.items[0].items[0].id.startswith("https://")
# 2. Every Range Canvas must exist in the Manifest's item ids.
manifest_id = "https://iiif.example.org/manifest/ms-42"
toc = build_toc_range(manifest_id, [
{"label": "Volume I", "start": 1, "end": 2},
{"label": "Volume II", "start": 3, "end": 4},
])
canvas_ids = {f"{manifest_id}/canvas/{n}" for n in range(1, 5)}
referenced = {
c["id"] for rng in toc["items"] for c in rng["items"]
}
assert referenced <= canvas_ids, "Range references a non-existent Canvas"
assert toc["behavior"] == ["multi-part"]
if __name__ == "__main__":
test_structures()
print("structures OK")For a full document, validate the serialized JSON against the official rules rather than trusting the builders alone:
python -m json.tool collection/fine-art.json > /dev/null # well-formedness
iiif-prezi3 validate collection/fine-art.json # spec conformanceA clean run reports valid JSON, zero dangling Canvas references, and a behavior value the target viewer supports. Any Range whose start or items name a Canvas absent from the Manifest is the defect to fix before publishing — it renders as a broken table-of-contents entry.
Where to Go Deeper
- Modeling Multi-Volume Objects as IIIF Ranges — extends the table-of-contents pattern to works bound across several physical volumes, where each volume is a top-level Range with its own
startCanvas, paged behavior, and volume-level labels, and the single Manifest spans every folio of the set.
FAQ
When should I use a Collection versus a Range?
Use a Collection to group different objects for browsing — a department, an exhibition, a donor gift — where each item is its own Manifest at its own URL. Use a Range to structure one object’s Canvases into a table of contents, chapters, or volumes. The rule of thumb: if the thing you are grouping has its own accession record, it is a Manifest inside a Collection; if it is a page or opening within one accessioned object, it is a Canvas inside a Range.
Do Ranges add new images to a Manifest?
No. A Range only reorganizes Canvases that already exist in the Manifest’s items array; it holds reduced references (id and type), never full Canvas definitions. This is why Range trees live in the Manifest’s separate structures array — regenerating the table of contents never touches the pixels, and a Range pointing at a Canvas that is not in items is a validation error.
What does the behavior value actually change?
behavior is a hint that tells the viewer how to lay out child Canvases: paged renders two-up openings with page-turning, continuous joins Canvases edge to edge for scrolls, individuals shows each Canvas alone, and multi-part marks a work split across sub-Ranges. It changes presentation only, never content, and viewers may ignore hints they do not support — so pick the value that matches the physical object and provide a sensible flat order as the fallback.
How do rights work across a nested Collection?
A Collection’s rights and requiredStatement act as defaults that items inherit when they stay silent, but they are advisory, not enforcing. Always set rights explicitly on each Manifest from the object’s own resolved rights status, and never let a permissive Collection-level default promote an in-copyright object to open access. Treat a Manifest missing rights as a defect to quarantine, not a candidate for inheritance.
Which Canvas does a viewer open on, and how do I control it?
Set the Range’s start property to the id of the Canvas the viewer should position to when that Range is selected. For a table of contents, each chapter or volume Range sets start to its first Canvas so selecting “Volume II” jumps straight to its opening folio. Without start, a viewer falls back to the first Canvas in the Range’s items, which is usually correct but leaves the entry point implicit rather than declared.
Related
- IIIF Image Delivery & Manifest Generation — parent delivery overview
- Building IIIF Presentation Manifests — the leaf Manifest documents
- Modeling Multi-Volume Objects as IIIF Ranges — multi-volume Range deep dive
- Designing Museum Object Schemas — the department hierarchy source
- Configuring the IIIF Image API — the pixels each Canvas points at