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 | None unions and match statements used in the builders below.
  • IIIF Presentation API 3.0 as the target — this page uses v3 vocabulary throughout (items, type, label as 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 example https://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 items point at Manifest ids; a Range points at Canvas ids that must already exist in the Manifest it belongs to.
  • Rights vocabularyRightsStatements.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:

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"] }
    }
  ]
}
json
{
  "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.

python
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 payload

Edge 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.

python
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.

python
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 itemsitems 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:

python
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.

Collection-to-Manifest-to-Range-to-Canvas nesting A top-level IIIF Collection contains a department sub-Collection. The sub-Collection references a Manifest by id. The Manifest carries two arrays: an items array holding a flat ordered list of Canvases that own the pixels, and a structures array holding a table-of-contents Range. That Range contains two volume Ranges, each of which points back at a span of the same Canvases in the items array rather than redefining them. Collection items are references to documents at their own URLs, while Range items are reduced references to Canvases defined in the Manifest. Two descents — Collection browses to a Manifest, Range navigates within it Collection Fine Art (root) Sub-Collection European Paintings Manifest MS-42 · one object items: Collection items: Manifest ref items[]: Canvases canvas/1 … canvas/n Range: TOC structures[] · multi-part structures Range: Vol I start + span Range: Vol II start + span points at

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.

python
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:

bash
python -m json.tool collection/fine-art.json > /dev/null   # well-formedness
iiif-prezi3 validate collection/fine-art.json              # spec conformance

A 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 start Canvas, 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.