Modules/Page

Page

A page is an object carrying a document type: a type with one part whose dataset names the editor module. Two kinds qualify. The built-in page type is the plain document — hidden from the type picker, present in every space, no properties of its own; attach it (or pass it in types at create) and the object holds a body. A user document type is one you declare yourself, with the same kind of part, and normally register as a bundle so every client and device converges on one — that is the type to use when a page needs columns, a weight or a layout. Both share the one editor_blocks collection, so an object carrying both has one body.

The built-in page

curl -X POST http://127.0.0.1:7001/v1/spaces/$SP/objects \
  -H 'Content-Type: application/json' \
  -d '{"types": ["page"], "initialProperties": {"any": {"name": "Reading list"}}}'

That is the whole declaration: page is registered, not created, so there is nothing to ensure and nothing that can fork. It is hidden (GET …/types lists it only with ?includeHidden=true; GET …/types/page resolves it always) and carries no weight or layout — an object that is only a page renders by the client's default.

Declaring your own type

PAGE=$(curl -s -X POST http://127.0.0.1:7001/v1/spaces/$SP/types \
  -H 'Content-Type: application/json' \
  -d '{"name": "Page", "xKey": "page", "weight": 10, "layout": {"type": "page"}}' | jq -r .typeId)

curl -X POST http://127.0.0.1:7001/v1/spaces/$SP/types/$PAGE/parts \
  -H 'Content-Type: application/json' \
  -d '{"key": "body", "name": "Body", "ui": {"type": "document"},
       "datasets": [{"module": "editor", "shared": true}]}'

"shared": true puts the body in the module's canonical collection, editor_blocks — the one every document type shares, so an object that is both a page and, say, a meeting has one body. weight makes the type the object's primary type (the highest weight wins) and layout is the descriptor a client renders for it; both are opaque client vocabulary. The type carries properties like any other — a status, a priority, a relation — which is what the registered built-in cannot.

What a page is made of

Concern Where it lives
Display name any.name
Labels the built-in any.tags (free-form string array)
Body the editor_blocks collection the type's part declares — editor
Columns a user document type's own properties, at <typeId>.<propId> (the built-in page has none)
Position in the tree the wiki type's parentId / pos / folder columns, when the page carries it (see objects)
Recency the derived row-root modifiedAt instant, with modifiedBy naming who signed that change (see system fields)

Creating and listing pages

File a document by creating an object with the type:

curl -X POST http://127.0.0.1:7001/v1/spaces/$SP/objects \
  -H 'Content-Type: application/json' \
  -d '{"types": ["'$PAGE'"], "initialProperties": {"any": {"name": "Reading list", "tags": ["books"]}}}'

Then write its body through the editor — the object already holds the collection because it carries the declaring type:

curl -X POST http://127.0.0.1:7001/v1/spaces/$SP/objects/$OBJ/editor/editor_blocks/markdown/append \
  -H 'Content-Type: application/json' \
  -d '{"content": "# Reading list\n\n- [ ] Children of Time"}'

List a space's documents with a filter on the type, most recently edited first:

curl -X POST http://127.0.0.1:7001/v1/spaces/$SP/objects/query \
  -H 'Content-Type: application/json' \
  -d '{"filter": {"any.types": "'$PAGE'"}, "sort": ["-modifiedAt"], "limit": 50}'

The same body against …/objects/query/subscribe gives a live document list. Filter by label with {"any.tags": "books"} — array fields match on any element. "Every object with a body, whatever its type" is a filter on every type that shares the editor: the owners of editor_blocks in GET /v1/spaces/:spaceId/datasets (the built-in page is always among them), matched with {"any.types": {"$in": [...]}}.

Built-in or bundle

In a local-first system there is no central moment where "the pages type" gets created. Two members working offline would each create one, and the CRDT would faithfully keep both — real spaces carried several parallel "Pages" types. The built-in page avoids that by being registered: it exists everywhere, but its definition is frozen — no properties, no weight, no layout. Registering your own type as a bundle keeps it a plain user type — properties, weight, layout, parts — while the registry converges every device on one id: POST …/bundles with the type declared on the root is adopt-or-install, so whoever runs it second adopts the first one's type. Pick page for a plain body, a bundle-registered type for a document that is also a record.

Note. A page's body is not part of the type's row. It is the editor collection on the same object, which is why an object can be a page with no blocks yet (an empty records array on the editor_blocks query) and why the body is searchable through the editor chunker under the basic scope (see search).