Creating Content

Creating Content

Create content nodes, organize them into hierarchical trees, and control their order and position.

Content is hierarchical

ModulaCMS organizes content as trees. A page isn't a flat collection of fields -- it's a hierarchy of typed content nodes, each with its own datatype and field values. Nest pages under pages, sections under pages, cards under sections. Your frontend receives a ready-to-render tree.

Page (root)
+-- Hero Section
|   +-- Heading
|   +-- Background Image
+-- Cards Container
    +-- Card A
    +-- Card B

Every tree starts with a single root node. You add child nodes under it, and children under those, to any depth.

Two kinds of trees

ModulaCMS supports two kinds of content trees:

Tree Type Root Datatype Accessed Via Use Case
Route-based _root Content delivery by slug (/api/v1/content/{slug}) Page content tied to a URL
Global _global /api/v1/globals endpoint Site-wide content (menus, footers, settings) not tied to a route

Each route has exactly one content tree. Multiple independent global trees can coexist.

Create the root node

Start a content tree by creating its root node. The root node has no parent and uses a datatype with type = "_root" (for route-based content) or type = "_global" (for global content).

curl -X POST http://localhost:8080/api/v1/contentdata \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{
    "route_id": "01JNRW9P2DKTZ6Q4M8W3B5J7CL",
    "datatype_id": "01JNRW5V6QNPZ3R8W4T2YH9B0D",
    "status": "draft"
  }'

Response:

{
  "content_data_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM",
  "route_id": "01JNRW9P2DKTZ6Q4M8W3B5J7CL",
  "datatype_id": "01JNRW5V6QNPZ3R8W4T2YH9B0D",
  "status": "draft"
}

Add child nodes

Add a child by setting parent_id to the parent's content data ID. ModulaCMS positions new children at the end of the parent's child list automatically.

curl -X POST http://localhost:8080/api/v1/contentdata \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{
    "route_id": "01JNRW9P2DKTZ6Q4M8W3B5J7CL",
    "parent_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM",
    "datatype_id": "01JNRW6A7BMXY4K9P2Q5TH3JCR",
    "status": "draft"
  }'

Set field values

Once you've created a content node, populate its fields by creating content field records:

curl -X POST http://localhost:8080/api/v1/contentfields \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{
    "content_data_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM",
    "field_id": "01JNRW7K8CNQZ5P3R9W6TJ4MAS",
    "field_value": "My First Blog Post",
    "route_id": "01JNRW9P2DKTZ6Q4M8W3B5J7CL"
  }'

Good to know: Always include route_id when creating content fields. ModulaCMS uses it for query performance.

Move nodes

Move a content node to a different parent or a different position under the same parent:

curl -X POST http://localhost:8080/api/v1/contentdata/move \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{
    "node_id": "01JNRWCN5GPRZ8S6P0Y5D7L9EN",
    "new_parent_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM",
    "position": 0
  }'
Field Description
node_id The content node to move
new_parent_id The new parent's content data ID
position Zero-based position among the new parent's children (0 = first child)

ModulaCMS prevents circular moves -- you can't move a node under its own descendant.

Response:

{
  "node_id": "01JNRWCN5GPRZ8S6P0Y5D7L9EN",
  "old_parent_id": "01JNRW5V6QNPZ3R8W4T2YH9B0D",
  "new_parent_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM"
}

Reorder siblings

Reorder all children under a parent in a single operation by specifying the desired sequence:

curl -X POST http://localhost:8080/api/v1/contentdata/reorder \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{
    "parent_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM",
    "ordered_ids": [
      "01JNRWDN6HQSZ9T7Q1Z6E8M0FP",
      "01JNRWCN5GPRZ8S6P0Y5D7L9EN"
    ]
  }'

The ordered_ids array must include all children of the specified parent. ModulaCMS validates that every ID belongs to the parent and rejects duplicates.

Response:

{
  "updated": 2,
  "parent_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM"
}

Good to know: Move and reorder operations update only the affected nodes and their immediate neighbors, making them fast even for large trees.

Bulk tree operations

For complex changes that involve creating, updating, and deleting nodes in a single request, use the bulk tree endpoint:

curl -X POST http://localhost:8080/api/v1/content/tree \
  -H "Cookie: session=YOUR_SESSION_COOKIE" \
  -H "Content-Type: application/json" \
  -d '{
    "content_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM",
    "creates": [
      {
        "client_id": "temp-1",
        "datatype_id": "01JNRW6A7BMXY4K9P2Q5TH3JCR",
        "parent_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM"
      }
    ],
    "updates": [
      {
        "content_data_id": "01JNRWCN5GPRZ8S6P0Y5D7L9EN",
        "next_sibling_id": "temp-1"
      }
    ],
    "deletes": ["01JNRWDN6HQSZ9T7Q1Z6E8M0FP"]
  }'

Use client_id as a temporary identifier in the creates array. The server generates real IDs and returns the mapping so you can reference newly created nodes in updates.

Response:

{
  "created": 1,
  "updated": 1,
  "deleted": 1,
  "id_map": {
    "temp-1": "01JNRWFQ8KRUZ0V8R2A7F9N1GQ"
  }
}

Delete nodes

Delete a content node with a DELETE request:

curl -X DELETE "http://localhost:8080/api/v1/contentdata/?q=01JNRWCN5GPRZ8S6P0Y5D7L9EN" \
  -H "Cookie: session=YOUR_SESSION_COOKIE"

Good to know: Deleting a node does not automatically delete its children. Reassign or delete child nodes separately before removing a parent.

Content delivery

When you publish content, ModulaCMS assembles the tree into a snapshot. Frontend clients requesting content by slug receive this snapshot as nested JSON:

{
  "root": {
    "datatype": {"info": {"label": "Page", "type": "_root"}},
    "fields": [
      {"info": {"label": "title", "type": "text"}, "content": {"field_value": "About Us"}}
    ],
    "nodes": [
      {
        "datatype": {"info": {"label": "Hero Section", "type": "section"}},
        "fields": [
          {"info": {"label": "heading", "type": "text"}, "content": {"field_value": "Welcome"}}
        ],
        "nodes": []
      }
    ]
  }
}

Children appear as an ordered nodes array. Your frontend consumes this structure directly -- no assembly required.

Output formats

The content delivery endpoint supports multiple output formats that restructure the JSON to match other CMS conventions. Set the default in modula.config.json with output_format, or override per request:

curl "http://localhost:8080/api/v1/content/about?format=clean"

Available formats: contentful, sanity, strapi, wordpress, clean, raw. The default is raw (the native tree structure).

Preview mode

Preview unpublished changes through the delivery endpoint by adding ?preview=true. This builds the tree from live data instead of the published snapshot, so editors can see draft content through the client delivery path.

Heal malformed trees

If tree structure becomes inconsistent (from bugs, interrupted operations, or data issues), repair it with the heal endpoint:

curl -X POST http://localhost:8080/api/v1/admin/content/heal \
  -H "Cookie: session=YOUR_SESSION_COOKIE"

API reference

Method Path Permission Description
GET /api/v1/content/{slug} Public Deliver content tree for a slug
GET /api/v1/globals Public Deliver all published global trees
POST /api/v1/contentdata content:create Create a content node
GET /api/v1/contentdata/ content:read Get a content node (?q=ID)
PUT /api/v1/contentdata/ content:update Update a content node
DELETE /api/v1/contentdata/ content:delete Delete a content node (?q=ID)
POST /api/v1/contentdata/move content:update Move a node to a new parent/position
POST /api/v1/contentdata/reorder content:update Reorder sibling nodes
POST /api/v1/content/tree content:update Bulk tree operations (create, update, delete)
POST /api/v1/contentfields content:create Create a content field value
GET /api/v1/contentfields/ content:read Get a content field (?q=ID)
PUT /api/v1/contentfields/ content:update Update a content field value
DELETE /api/v1/contentfields/ content:delete Delete a content field value (?q=ID)
POST /api/v1/admin/content/heal content:update Repair malformed tree structure

Next steps

Your content is organized and populated. Publish it to make it live.