Publishing
Publishing
Publish content to make it live, schedule future publications, track version history, and restore any previous version.
Content status
Every content node has a status that controls its visibility:
| Status | Visibility |
|---|---|
draft |
Visible in the admin panel and TUI. Not served by the public delivery API. |
published |
Live and served by the public delivery API. |
scheduled |
Draft with a future publication time. Automatically transitions to published when the time arrives. |
ModulaCMS creates content with draft status by default.
Good to know:
scheduledappears in API responses for clarity, but the content is stored asdraftinternally until the scheduled time arrives.
Publish content
Publishing transitions content from draft to published and makes it available through the delivery endpoint (GET /api/v1/content/{slug}).
When you publish, ModulaCMS sets the content to published, records who published it and when, creates a version snapshot of the current field values, and increments the revision counter.
curl -X POST http://localhost:8080/api/v1/content/publish \
-H "Cookie: session=YOUR_SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{
"content_data_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM"
}'
Response:
{
"status": "published",
"version_number": 1,
"content_version_id": "01JNRWCP5GNSY8Q6P0Z3B7L9EN",
"content_data_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM"
}
To publish locale-specific content, include the locale field:
curl -X POST http://localhost:8080/api/v1/content/publish \
-H "Cookie: session=YOUR_SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{
"content_data_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM",
"locale": "fr-FR"
}'
Unpublish content
Unpublishing reverts content to draft status, removing it from the public delivery API. Existing version snapshots are preserved -- you don't lose history by unpublishing.
curl -X POST http://localhost:8080/api/v1/content/unpublish \
-H "Cookie: session=YOUR_SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{
"content_data_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM"
}'
Schedule content
Set a future publication time. The content stays in draft until ModulaCMS automatically publishes it at the specified time.
curl -X POST http://localhost:8080/api/v1/content/schedule \
-H "Cookie: session=YOUR_SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{
"content_data_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM",
"publish_at": "2026-04-01T09:00:00Z"
}'
Response:
{
"status": "scheduled",
"content_data_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM",
"publish_at": "2026-04-01T09:00:00Z"
}
Good to know: The server checks for scheduled content on a periodic interval. There may be a short delay between the
publish_attime and actual publication.
Version history
Every publish and restore operation creates a version snapshot -- a frozen record of the content's field values at that point in time. Versions form a complete history that you can browse, restore from, or delete.
List versions
Retrieve all version snapshots for a content node, ordered newest first:
curl "http://localhost:8080/api/v1/content/versions?q=01JNRWBM4FNRZ7R5N9X4C6K8DM" \
-H "Cookie: session=YOUR_SESSION_COOKIE"
Each version contains:
| Field | Description |
|---|---|
content_version_id |
ID of this version snapshot |
version_number |
Sequential version number |
locale |
Locale code for this snapshot |
snapshot |
Serialized field values (JSON string) |
trigger |
What created this version: publish, manual, restore, or schedule |
label |
Optional human-readable name |
published |
Whether the content was published at snapshot time |
published_by |
ID of the user who triggered the snapshot |
date_created |
Timestamp of snapshot creation |
Get a single version
curl "http://localhost:8080/api/v1/content/versions/?q=01JNRWCP5GNSY8Q6P0Z3B7L9EN" \
-H "Cookie: session=YOUR_SESSION_COOKIE"
Create a manual version
Save a checkpoint of the current field values without changing publish status:
curl -X POST http://localhost:8080/api/v1/content/versions \
-H "Cookie: session=YOUR_SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{
"content_data_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM",
"label": "Before major rewrite"
}'
Use manual versions as save points before large edits. They work the same as publish-triggered versions -- you can restore from them at any time.
Delete a version
Remove a historical snapshot. This doesn't affect the current content.
curl -X DELETE "http://localhost:8080/api/v1/content/versions/?q=01JNRWCP5GNSY8Q6P0Z3B7L9EN" \
-H "Cookie: session=YOUR_SESSION_COOKIE"
Good to know: Version snapshots are immutable after creation. They survive unpublish, restore, and republish cycles.
Restore a version
Replace the current draft field values with those from a previous version snapshot:
curl -X POST http://localhost:8080/api/v1/content/restore \
-H "Cookie: session=YOUR_SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{
"content_data_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM",
"content_version_id": "01JNRWCP5GNSY8Q6P0Z3B7L9EN"
}'
Response:
{
"status": "restored",
"content_data_id": "01JNRWBM4FNRZ7R5N9X4C6K8DM",
"restored_version_id": "01JNRWCP5GNSY8Q6P0Z3B7L9EN",
"fields_restored": 5,
"unmapped_fields": []
}
Before overwriting the current field values, ModulaCMS automatically creates a snapshot of the current state. You can always undo a restore by restoring to this auto-created version.
After restoring, the content remains in its current publish status. Publish separately to make the restored content live.
Unmapped fields
If the datatype schema changed after a version was created (fields added, removed, or renamed), some snapshot fields may not match the current schema. These appear in the unmapped_fields array and are skipped during restore. Fields in the current schema that don't exist in the snapshot are left unchanged.
The publishing workflow
+-- Schedule ---> [scheduled] ---> auto-Publish
|
Create (draft) ---> Publish ---> [published]
^ |
| +-- Unpublish ---> [draft]
|
+-- Restore (from version) ---> [draft, fields overwritten]
Every state transition that changes content visibility creates a version snapshot. The version history is append-only.
SDK examples
Go
import modula "github.com/hegner123/modulacms/sdks/go"
client, _ := modula.NewClient(modula.ClientConfig{
BaseURL: "http://localhost:8080",
APIKey: "mcms_YOUR_API_KEY",
})
// Publish content
resp, err := client.Publishing.Publish(ctx, modula.PublishRequest{
ContentDataID: "01JNRWBM4FNRZ7R5N9X4C6K8DM",
})
// Schedule for future publication
schedResp, err := client.Publishing.Schedule(ctx, modula.ScheduleRequest{
ContentDataID: "01JNRWBM4FNRZ7R5N9X4C6K8DM",
PublishAt: "2026-04-01T09:00:00Z",
})
// List version history
versions, err := client.Publishing.ListVersions(ctx, "01JNRWBM4FNRZ7R5N9X4C6K8DM")
// Create a manual checkpoint
version, err := client.Publishing.CreateVersion(ctx, modula.CreateVersionRequest{
ContentDataID: "01JNRWBM4FNRZ7R5N9X4C6K8DM",
Label: "Before major rewrite",
})
// Restore from a previous version
restoreResp, err := client.Publishing.Restore(ctx, modula.RestoreRequest{
ContentDataID: "01JNRWBM4FNRZ7R5N9X4C6K8DM",
ContentVersionID: "01JNRWCP5GNSY8Q6P0Z3B7L9EN",
})
// Unpublish
unpubResp, err := client.Publishing.Unpublish(ctx, modula.PublishRequest{
ContentDataID: "01JNRWBM4FNRZ7R5N9X4C6K8DM",
})
TypeScript
import { ModulaCMSAdmin } from '@modulacms/admin-sdk'
const client = new ModulaCMSAdmin({
baseUrl: 'http://localhost:8080',
apiKey: 'mcms_YOUR_API_KEY',
})
// Publish content
const resp = await client.publishing.publish({
content_data_id: '01JNRWBM4FNRZ7R5N9X4C6K8DM',
})
// Schedule for future publication
const schedResp = await client.publishing.schedule({
content_data_id: '01JNRWBM4FNRZ7R5N9X4C6K8DM',
publish_at: '2026-04-01T09:00:00Z',
})
// List version history
const versions = await client.publishing.listVersions('01JNRWBM4FNRZ7R5N9X4C6K8DM')
// Restore from a previous version
const restoreResp = await client.publishing.restore({
content_data_id: '01JNRWBM4FNRZ7R5N9X4C6K8DM',
content_version_id: '01JNRWCP5GNSY8Q6P0Z3B7L9EN',
})
Admin content publishing
The admin content system has its own parallel publishing lifecycle with identical operations. Replace /api/v1/content/ with /api/v1/admin/content/ in the endpoint paths:
| Operation | Public Endpoint | Admin Endpoint |
|---|---|---|
| Publish | POST /api/v1/content/publish |
POST /api/v1/admin/content/publish |
| Unpublish | POST /api/v1/content/unpublish |
POST /api/v1/admin/content/unpublish |
| Schedule | POST /api/v1/content/schedule |
POST /api/v1/admin/content/schedule |
| List versions | GET /api/v1/content/versions |
GET /api/v1/admin/content/versions |
| Create version | POST /api/v1/content/versions |
POST /api/v1/admin/content/versions |
| Delete version | DELETE /api/v1/content/versions/ |
DELETE /api/v1/admin/content/versions/ |
| Restore | POST /api/v1/content/restore |
POST /api/v1/admin/content/restore |
Admin requests use admin_content_data_id and admin_content_version_id instead of content_data_id and content_version_id.
Next steps
Learn how to fetch and display your published content in a frontend application.