CMS
CMS lets you hand a website’s content over to the person who owns it. Your client edits here; the site you built keeps rendering the pages.
It is a headless CMS: it has no themes, no templates and no front end of its own. When content is published, Minion writes plain JSON files to public storage. Your site reads those files — usually at build time — and renders them however you like.
That split is the whole design:
- Visitors never reach us. Your site reads static files, so a spike in traffic does not change your bill, and an outage on our side does not take your client’s site down.
- Traffic is not metered. There is no per-request pricing, because the requests do not come to us.
- You can leave. What is published is already a set of plain JSON files. Export gives you those files, the schema and the images in one ZIP.
CMS is experimental and off by default. Ask us to enable it for your workspace if you do not see it in the sidebar.
Connect a site in five minutes
Section titled “Connect a site in five minutes”1. Create a site. One site per project. It is the unit you can later duplicate, hand over, or delete.
2. Create a content type. A content type is one kind of content — blog posts, news, staff members. Pick its shape:
| Kind | Use it for | Published as |
|---|---|---|
| List | Many entries of the same shape: posts, news, products | An array of entries |
| Object | Exactly one entry: site settings, the home page | A single object |
You also give it an API ID (posts, news). It becomes the filename of the published JSON, so it cannot be changed later.
3. Define its fields. Each field has a label and an API ID. The label is what the editor sees and you can rename it whenever you like. The API ID is the key in the published JSON, so it is fixed once the schema is published. The schema builder shows a live preview of the JSON your fields produce.
4. Write an entry and publish it. Saving a draft never changes what is live. Publishing pins that version and writes the files.
5. Turn on publishing for the site. Publishing writes to public storage, so we ask for a payment method on the workspace first. We do not charge you for it — it exists so that free anonymous hosting cannot be used for phishing and malware.
6. Read it from your site. The Delivery tab lists the exact URLs for your content types, with a copy button and a snippet for fetch, Next.js and Astro:
const res = await fetch('https://…/{siteId}/api/posts.json')const { contents } = await res.json()
The JSON you get
Section titled “The JSON you get”Publishing writes one file per content type, plus one file per entry, plus a manifest:
| File | Contains |
|---|---|
/api/index.json | Every content type on the site, so the front end can discover them |
/api/{apiId}.json | A list type’s entries, or an object type’s single entry |
/api/{apiId}/{entryId}.json | One entry, by id |
/api/{apiId}/{slug}.json | The same entry, by slug |
A list file is a page-shaped response, so the same code works against the read API:
{ "contents": [ /* entries */ ], "totalCount": 12, "offset": 0, "limit": 12}Every entry carries the same system keys, followed by your fields keyed on their API IDs:
{ "id": "…", "slug": "hello-world", "createdAt": "2026-01-01T00:00:00.000Z", "updatedAt": "2026-01-01T00:00:00.000Z", "publishedAt": "2026-01-01T00:00:00.000Z", "sortOrder": 0, "title": "Hello world", "body": "# Hello\n…"}Two things are worth knowing before you write your templates:
- Ordering is already applied. Entries come out in the order shown on the entry list — the manual ordering (the ↑ / ↓ buttons) first, then newest published.
- References are expanded one level deep. A referenced entry appears as an object. If it is unpublished or deeper than one level, you get
{ "id": "…" }instead ofnull, so a template that readsref.idnever crashes.
Field types
Section titled “Field types”| Type | JSON | Notes |
|---|---|---|
| Text | "…" | Up to 1,000 characters |
| Text area | "…" | Up to 20,000 characters |
| Rich text | "…" | Markdown, up to 200,000 characters |
| Number | 0 | |
| Boolean | true | |
| Date | "2026-01-01" | |
| Select | "news" | The option’s label, not its internal value |
| Image / File | { … } | A media object, see below |
| Content reference | { "id": …, "slug": … } | Another entry, expanded one level |
| Repeat | [ { … } ] | An array of objects. Nests one level, up to 200 rows |
| Embed URL | "https://…" | A YouTube or Vimeo URL. We do not host video |
Any field marked multiple becomes an array of the same shape.
Media fields expand to the full file:
{ "id": "…", "url": "https://…", "object_key": "…", "filename": "cover.jpg", "mime_type": "image/jpeg", "byte_size": 148213, "width": 1200, "height": 630, "alt": "…", "variants": [{ "width": 800, "format": "webp", "url": "https://…" }]}variants are the resized copies generated on upload. Use them directly, or use srcset.
Renaming things safely
Section titled “Renaming things safely”Fields are stored against an immutable internal id, and select options against an immutable value. That means:
- Renaming a label is always safe. No published site changes.
- Renaming an API ID is not possible once the schema is published, because it is a key your front end reads.
- Renaming a select option changes the published JSON, because the label is what gets published. Treat it as an edit to your content.
- Changing a schema never breaks existing entries. Each entry is interpreted with the schema version it was written against. A field you delete simply stops appearing.
Previewing drafts
Section titled “Previewing drafts”Drafts are never written to the static files — that is what makes “editing cannot break the live site” true. To show unpublished content, read the API instead:
curl -H "X-CMS-API-KEY: YOUR_READ_KEY" \ "https://minion-agent.com/api/public/cms/{siteId}/posts?draftKey=YOUR_DRAFT_KEY"Create the read key under Delivery → API keys. The draft key is on the same tab and can be regenerated if it leaks. (X-MICROCMS-API-KEY is accepted as well, so a front end written against microCMS needs no change here.)
Set Preview URL under Settings to your site’s preview route, and the editor’s Preview button opens your page instead of raw JSON.
The read API takes a microCMS-compatible subset of query parameters:
| Parameter | Example | |
|---|---|---|
limit / offset | limit=10&offset=20 | Default 10, max 100 |
orders | orders=-publishedAt,title | - for descending |
fields | fields=id,title | Trim the response |
filters | filters=category[equals]news[and]title[contains]sale | equals, not_equals, contains, begins_with, exists, not_exists, joined with [and] / [or], evaluated left to right |
depth | depth=2 | How far to expand references, max 3 |
q | q=keyword | Free text over the entry |
Reads are limited to 300 requests per minute per site, writes to 60.
Rebuilding on publish
Section titled “Rebuilding on publish”For a statically built site, publishing content is only half of the job — the site has to rebuild. That is what webhooks are for.
Create a build hook on your host (Vercel: Settings → Git → Deploy Hooks; Netlify: Site configuration → Build & deploy → Build hooks), then paste its URL under Delivery → Webhooks. Every publish then triggers a build. Use Test right after adding it: a webhook that was never delivered is easiest to notice now, not next week.
You can subscribe to specific events, or leave the list empty to receive all of them:
entry.published · entry.unpublished · entry.deleted · content_type.updated · site.published
The payload is JSON, delivered with x-cms-event:
{ "event": "entry.published", "site_id": "…", "content_type": "posts", "entry_id": "…", "entry_slug": "hello-world", "occurred_at": "2026-01-01T00:00:00.000Z"}Enable Sign payload and we add an x-cms-signature header — the HMAC-SHA256 of the raw body with the secret shown once at creation. Verify it if your endpoint does anything more interesting than triggering a build.
A failed webhook never rolls back a publish. If your build breaks, the content is still correctly published; fix the build and trigger it again.
Scheduling
Section titled “Scheduling”An entry can carry Publish at and Unpublish at times. A scheduler runs every five minutes, so treat the times as “within five minutes of”. A scheduled publish goes through the same checks as a manual one: if a required field is empty, the schedule stays and reports the failure rather than quietly dropping it, so fixing the entry is enough to make the next tick publish it.
Limits, and what happens when you hit them
Section titled “Limits, and what happens when you hit them”Sites and media storage are counted per workspace:
| Plan | Sites | Media storage |
|---|---|---|
| Free | 2 | 1 GB |
| Starter | 5 | 10 GB |
| Team | 20 | 50 GB |
| Business | 100 | 200 GB |
| Enterprise | Unlimited | 1 TB |
Going over the quota stops new uploads only. Files that are already published keep being served, and publishing keeps working. We never take a live client site down over a quota.
Who can do what
Section titled “Who can do what”Everyone in the workspace can see a site. Nobody can change it until you say so.
| Role | Can |
|---|---|
| Admin | Everything: publishing, API keys, webhooks, access, deleting the site |
| Can edit | Content types and their schema, plus everything a writer can do |
| Can write | Entries and media — writing, publishing, uploading |
| View only | Read. The default for every workspace member |
Admin is automatic: workspace owners and admins, plus whoever created the site. The other two are granted per site under Settings → Access. Removing a grant puts that person back to view-only.
Grant Can write to the people who write the content, and Can edit only to the people who should be able to change its structure. Deleting a field takes it out of the published JSON the next time that entry is published, which is not something a writer should be able to do by accident.
Folding away the integration screens
Section titled “Folding away the integration screens”Once a site is wired up, the Delivery and Schema tabs stop being useful and start being noise for whoever writes the content. Settings → Integration mode folds them away, leaving only the screens needed for writing. Turn it off before you hand a site over, and turn it back on whenever you need to change the wiring.
It changes what is shown, not what anyone is allowed to do — the roles above are what actually protect the site. Someone with write or view-only access never sees those tabs either way.
Duplicating and handing over a site
Section titled “Duplicating and handing over a site”Duplicate creates a new site with the same content types, the same schema and the same API IDs — optionally with the entries and images too. The copy always starts unpublished with its entries as drafts, and API keys and webhooks are deliberately not carried over. Use it to try a risky schema change, or to reuse the last project’s structure on the next one.
Transfer moves a site to another workspace: enter their workspace slug, and the site moves once one of their admins accepts. Nothing about the site’s public identity changes — the media URLs, the API paths, the API keys and the webhooks are all bound to the site, not to the workspace. A live site keeps running through the handover, and your client’s CI needs no new environment variables.
Two consequences worth planning around. Per-site access is cleared, because it pointed at members of the old workspace — and since view-only is the default, only the receiving workspace’s admins can edit until they grant access to their own people. And if the site is bigger than the receiving plan’s quota, the transfer still succeeds: existing files keep being served, and only new uploads are blocked until they upgrade.
Exporting everything
Section titled “Exporting everything”Export produces a single ZIP with everything: all content including drafts, the schema definitions in a neutral format, and the media.
The published/ folder holds exactly the JSON that was being served. That matters during a migration: you can drop those files onto any static host and the site keeps working while you move.
Media is bundled up to 200 MB. Anything beyond that is listed in the manifest as a URL instead — the cut-off is stated in the README, the manifest and the response headers, never silently applied.