Headless CMS: build a blog backend
Use Anythink as a headless CMS. Model articles, let editors write in the dashboard, publish a public read API and fetch it from a Next.js site.
Last updated
A headless CMS stores and manages content, and leaves the website to you. Editors write in an admin interface. Your frontend, whether that's Next.js, a mobile app or a static site generator, reads the content as JSON over an API.
Anythink fits that shape without a separate product. Your content types are entities, the Anythink dashboard is the editing interface, and every entity already has a REST API. This page builds a small blog end to end: the content model, an editor workspace, a public read endpoint, a Next.js page that fetches it, and an optional rebuild when content changes. Each step shows the dashboard, the CLI and an AI assistant (MCP).
If you haven't used Anythink before, read Core concepts first.
What you'll build
- An
articlesentity: title, slug, body (rich text), cover image, status (draft or published) and publish date. - An
authorsentity, linked from each article. - An editor role with a short menu in the dashboard.
- A public API that returns published articles with no sign-in, plus public search.
- A Next.js page that lists and renders them.
Step 1: Model your content
Create authors first, so articles can point at it.
| Entity | Field | Type | Notes |
|---|---|---|---|
authors |
name |
Small text | Required. The name field. |
articles |
title |
Small text | Required. The name field. |
articles |
slug |
Small text | Required and unique. Used in page URLs. |
articles |
body |
Large text, rich-text display |
The article, edited with the rich-text editor. |
articles |
cover_image |
File Upload | One image. |
articles |
status |
Small text, select display |
Options draft and published. Default draft. |
articles |
published_at |
Timestamp | When it went, or goes, live. |
articles |
author |
Many-to-One | Points at authors. |
Mark the fields your public site needs as Searchable and Publicly Searchable (--searchable --public on the CLI). That's what step 3 uses, so it's easiest to set them now.
In the Anythink dashboard
- Go to Settings › Data Model and select Add Entity. Name it
authorsand select Create Entity. - Open
authors, select Add Field, choose Small text, name itname, tick Required and Name field, and in the Performance & API card tick Searchable and Publicly Searchable. Select Create Field. - Select Add Entity again and create
articles. Leave Public off for now. Step 3 turns it on. - Add the
articlesfields from the table. Forbody, choose Large text and the Rich Text display. Forstatus, choose Small text, the Select display, add the optionsdraftandpublished, and set the default todraft. Forcover_image, choose File Upload. Forauthor, choose Many-to-One and pickauthorsas the target.
With the CLI
anythink entities create authors
anythink fields add authors name --type varchar --required --name-field --searchable --public
anythink entities create articles
anythink fields add articles title --type varchar --required --name-field --searchable --public
anythink fields add articles slug --type varchar --required --unique --indexed --searchable --public
anythink fields add articles body --type text --display rich-text --searchable --public
anythink fields add articles status --type varchar --display select --options "draft,published" --default draft --indexed --searchable --public
anythink fields add articles published_at --type timestamp --searchable --public
anythink fields add articles cover_image --type file --searchable --public
anythink fields add articles author --type many-to-one --target authors --on-delete "SET NULL" --searchable --public
--public only works together with --searchable. The CLI refuses a field that is public but not searchable. --on-delete "SET NULL" means deleting an author leaves their articles in place with no author.
With an AI assistant (MCP)
Create two entities, authors and articles. authors has a required name field that is the name field. articles has a required title (name field), a required unique slug, a rich-text body, a file field called cover_image, a select field called status with draft and published and a default of draft, a timestamp called published_at, and a many-to-one author field pointing at authors. Make every field searchable and publicly searchable.
The assistant uses the cli tool to run entities create and one fields add per field. Connect an AI assistant sets up the MCP server. For every field type and option, see Model your data.
Rich text is stored as HTML in a text field, so your frontend receives markup in body.
Step 2: Let editors write in the dashboard
The dashboard already gives you a grid and a form for every entity. What editors need is access to the right entities, and a sidebar that doesn't show them everything else. That takes two things: a role with permissions, and a menu assigned to that role.
In the Anythink dashboard
- Open Settings, then Roles and Permissions, and create a role called
editor. - Give it read, create and update on
articlesandauthors. Leave delete off if you want editors to unpublish rather than remove. - Open Settings, then Menu Configuration, and select Add Menu. Name it
Editorsand choose theeditorrole. - Add an Entity item for
articlesand another forauthors. - Add the editors as users with the
editorrole.
When an editor signs in, the sidebar shows Articles and Authors between the built-in items. Opening Articles gives them the record grid, where they can create and edit articles, upload the cover image and use the rich-text editor.
With the CLI
anythink roles create editor --description "Writes and edits content"
anythink menus list
anythink menus add-item 12 articles --icon FileText
anythink menus add-item 12 authors --icon Users
Replace 12 with the id of the menu anythink menus list shows for the role. Create the menu itself, and grant the role its permissions on articles and authors, in the dashboard.
With an AI assistant (MCP)
Create a role called editor, then add articles and authors to the Editors menu with the FileText and Users icons.
The assistant runs roles create and menus add-item through the cli tool. Creating the menu and setting role permissions are dashboard tasks.
A menu item is a shortcut, not a permission. Hiding a link doesn't block access, and showing one doesn't grant it, so set up both. Menu configuration covers menus in full, and Roles and permissions covers what each permission allows.
Step 3: Publish content publicly
By default an entity's API needs a signed-in user or an API key. For a public website you want anonymous visitors to read published articles. Three pieces make that work.
- The entity's Public flag. A public entity can be read without credentials through the public endpoints.
- Public fields. A public endpoint returns only the fields marked Publicly Searchable. Every other field,
bodyincluded if you left it off, is left out of the response. Mark only what a web page should show. - A status filter. You choose which records to ask for. The endpoint returns every record of a public entity, so your frontend asks for
status=published.
In the Anythink dashboard
- Open Settings › Data Model and choose Edit on
articles. In the Edit Entity drawer, switch on Public and select Update Entity. - Do the same for
authorsif your site shows author names. - On each field you want exposed, check that Publicly Searchable is ticked. Leave it off for anything internal, such as an editor's notes field.
With the CLI
anythink entities update articles --public true
anythink entities update authors --public true
New entities can also be made public at creation with anythink entities create <name> --public. Check which fields a visitor can read:
anythink fields list articles
anythink search query "*" --public --entities articles --limit 5
The search result is what an anonymous visitor gets from public search. Public results always include id, created_at, updated_at, __entity and __url, plus your publicly searchable fields.
With an AI assistant (MCP)
Make the articles entity public, then check what an anonymous visitor can read on it and list the public fields.
Read published articles
The public list endpoint takes no token. page and pageSize are required. Any other query parameter is a filter on that field.
GET /org/{project_id}/entities/articles/public/items?status=published&sortBy=published_at&sortDirection=desc&page=1&pageSize=20&fields=title,slug,published_at
{
"items": [
{
"locked": false,
"id": 1,
"title": "Espresso guide",
"slug": "espresso-guide",
"published_at": "2026-10-01T09:00:00Z",
"__name": "Espresso guide"
}
],
"page": 1,
"page_size": 20,
"total_items": 1,
"total_pages": 1,
"has_next_page": false,
"has_previous_page": false,
"retrieval_time": 143
}
Fetch one article by slug with the same endpoint, ?slug=espresso-guide&status=published&page=1&pageSize=1. GET /entities/{entity}/public/items/{id} fetches by id. For an entity that isn't public, both return 404. Filter operators such as GTE: and C: and the fields parameter work the same as on the signed-in list: see REST API reference.
Note: The public endpoint returns every record of a public entity unless the request filters. status=published is a part of your request, so don't put anything in a public entity that you wouldn't show a visitor. For content that must stay private until it's ready, keep it out of the public entity or serve it from your own server with an API key.
Search published content
Public search finds articles by words in their publicly searchable fields. It needs no token.
GET /org/{project_id}/search/public?q=espresso&e=articles&fl=status%20%3D%20published
q is the search text, e limits it to the articles entity, and fl is a filter, here status = published, URL-encoded. The response has the same items and paging shape as above, and each item carries __entity and __url. Public search only matches and returns publicly searchable fields, so a field that is only Searchable stays hidden. Search covers filters, sorting, facets and ranking.
Step 4: Fetch it from a frontend
This is a minimal Next.js App Router example. The page is a Server Component, so the request runs on your server and the page can be cached.
// app/blog/page.tsx
const API = "https://api.my.anythink.cloud";
const PROJECT = process.env.ANYTHINK_PROJECT_ID;
type Article = { id: number; title: string; slug: string; published_at: string };
export default async function Blog() {
const params = new URLSearchParams({
status: "published",
sortBy: "published_at",
sortDirection: "desc",
page: "1",
pageSize: "20",
fields: "title,slug,published_at",
});
const res = await fetch(
`${API}/org/${PROJECT}/entities/articles/public/items?${params}`,
{ next: { revalidate: 60 } },
);
if (!res.ok) throw new Error(`Could not load articles (${res.status})`);
const { items }: { items: Article[] } = await res.json();
return (
<ul>
{items.map((a) => (
<li key={a.id}>
<a href={`/blog/${a.slug}`}>{a.title}</a>
</li>
))}
</ul>
);
}
revalidate: 60 makes Next.js refresh the cached page at most once a minute, so new articles appear without a deploy. The page for one article is the same request with a slug filter:
// app/blog/[slug]/page.tsx
export default async function Article({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const query = new URLSearchParams({ slug, status: "published", page: "1", pageSize: "1" });
const res = await fetch(
`${API}/org/${PROJECT}/entities/articles/public/items?${query}`,
{ next: { revalidate: 60 } },
);
const { items } = await res.json();
if (!items.length) return <p>Not found.</p>;
return (
<article>
<h1>{items[0].title}</h1>
<div dangerouslySetInnerHTML={{ __html: items[0].body }} />
</article>
);
}
body is HTML written by your own editors. If anyone else can write to that field, sanitise it before rendering. The project id is in every API URL and isn't a secret. Keep it in an environment variable so staging and production sites can point at different projects. The same endpoint serves a mobile app or any other client that can make an HTTP request.
Step 5: Images and files
A File Upload field holds the files you attach to a record, such as cover_image. Files live in your project's storage, and editors upload them from the record form in the dashboard. To make the files on a field readable by visitors, tick Public in the field's file options. Public files can be fetched without signing in at GET /org/{project_id}/files/{id}/public, which redirects to the file's CDN address. Files that aren't public need a signed-in request.
Upload and manage files covers upload limits, accepted types, public and private files, and how files appear in API responses.
Step 6: Optional: rebuild the site when content changes
If your site is statically generated, a content change doesn't reach it until something rebuilds it. Most hosts give you a deploy hook: a URL that starts a build when you send a request to it. A workflow can call that URL when an article is published.
The trigger is an Event trigger on articles, set to run when a record is updated, with a filter so it only fires when status changes to published. The step is Call An API, which sends one HTTP request. Keep the hook URL in your project's secrets, because anyone who has it can trigger builds.
In the Anythink dashboard
- Open Settings, select Secrets under Platform Settings, and add a secret named
DEPLOY_HOOK_URLwith your host's hook URL as the value. - Create a workflow. Add an Event trigger, set the Event Type to Entity Updated and the Entity to
articles. - Add a filter to the trigger: field
status, operator changed to, valuepublished. - Add a Call An API step. Set the Method to
POSTand the URL to{{ $anythink.secrets.DEPLOY_HOOK_URL }}. Tick Enable Step, save it and make it the start step. - Enable the workflow.
With the CLI
anythink secrets create DEPLOY_HOOK_URL --value "<your host's deploy hook URL>"
anythink workflows create rebuild-site --trigger Event --entity articles --event EntityUpdated \
--filter '{"field":"status","op":"changed_to","value":"published"}' --enabled
anythink workflows step-add 42 call_deploy_hook --name "Call deploy hook" --action CallAnApi \
--start --enabled \
--params '{"url":"{{ $anythink.secrets.DEPLOY_HOOK_URL }}","method":"POST"}'
Replace 42 with the id that workflows create prints.
With an AI assistant (MCP)
Create a workflow called rebuild-site that runs when an articles record is updated and its status changes to published. Add a Call An API step that POSTs to the URL in the DEPLOY_HOOK_URL secret, and enable the workflow.
The assistant runs workflows create and workflows step-add through the cli tool.
An update trigger doesn't fire for a record that is created already published. If editors do that, add a second workflow with the Entity Created event and the same step. Anythink doesn't retry a failed call, so check Monitor workflows if a rebuild doesn't happen. The Call An API step and the other ways to send and receive HTTP calls are covered in Webhooks and HTTP calls.
If you use Next.js, you don't need a rebuild for every change. revalidate in step 4 already refreshes cached pages on a timer.
What you get, and the limits
What you get
- Content types you model yourself, with typed fields, a rich-text editor and file uploads.
- A REST API for every entity, as soon as you create it, plus public read endpoints and public search.
- An editing interface in the Anythink dashboard, scoped by role and menu.
- Workflows that can call a deploy hook or any other URL when content changes.
- The same content for any frontend or app that can make an HTTP request.
Limits to plan around
- No draft model.
statusis a field you define and filter on. The public endpoint returns every record of a public entity unless your request filters, so anything you make public can be read by anyone. Keep content that must stay private, such as unpublished drafts with sensitive detail, in a separate entity that isn't public. - No preview environments. There's no built-in preview URL for unpublished content. Build a preview route in your site that reads the signed-in API with a user token or a server-side API key.
- No locales. There's no built-in translation of content. Model languages yourself, for example with a
localefield or a separate field per language. - No version history or scheduled publishing. An edit replaces the previous value. You can build scheduling with a timed workflow.
- Rich text is HTML. It's stored and returned as HTML, so sanitise it if untrusted users can write to it.
- Public means readable by anyone. Only publicly searchable fields are returned, and search results lag a moment behind a save because indexing runs in the background.
- Menus are for dashboard pages. An editor's menu can show entity grids and other dashboard pages, not external links.