# Custom CMS with AI
> A custom headless CMS and content dashboard for managing websites, pages, translations and media from a single interface — with AI connected to the content workflow over MCP.
- URL: https://spoko.space/custom-cms-with-ai/
- Published: 2026-09-01
- Tags: laravel, filament, astro, typescript, mariadb, rest-api, mcp
---## At a glance

Most of what a company calls "the website" is really two things: the site visitors see, and the place where someone sits down to change it. This project is the second one — a content dashboard, built from scratch, that runs a whole roster of client websites from a single interface.

It is commercial work, so the client and the product name stay out of this write-up. Everything below is about what the system does and how it is put together.

## The problem it replaces

The default answer to "we need a site the client can edit" is one WordPress per client. It works right up until you have a dozen of them. Then every site is its own installation with its own core, theme and plugin tree to patch; every site has a login page sitting on the client's own public domain; and every site has a database and a PHP runtime exposed to anyone who goes looking. Nobody has time to keep twelve of those current, so they quietly drift until something breaks or gets exploited.

I know what that looks like from the other side — the [WordPress rescue work](/wordpress-rescue/) that starts after an outdated plugin lets someone in.

## What the dashboard actually lets a person do

Before any of the technology matters: this is a place where a non-technical person opens a page, changes it, and publishes it.

## The page builder

A page is not a blob of HTML. It is an ordered list of typed blocks, stored per language, and each block knows its own fields. There are 32 block types today — hero, rich content, features, testimonials, pricing table, FAQ, gallery, team, opening hours, map, contact, video, tabs, carousel and the rest.

Two things about that list matter more than its length:

- **Adding a block type is a config entry, not a new codebase.** The palette an editor sees, the description beside each type, and the fields inside the block all come from one config file. The frontend then registers a component for that block name and it is done.
- **A capability shared by many blocks is declared once, not copied into each.** The background band, the alignment, the reveal animation and the anchor id live in one definition, and a block simply states which of them it supports — 26 of the 32 do.
- **Every block gets a stable id the first time it is saved.** That is what makes it possible to patch one block from the outside — by an API call, or by an AI tool — without touching the rest of the page.

The editor keeps an outline of the page beside it, one row per block with a line of its actual content, so a long page stays navigable instead of turning into a wall of collapsed panels.

## New content types without new code

The other half of the content engine is collections. A site that needs "case studies" or "opening hours per branch" or "pricing plans, but three separate sets of them" does not get a new module written for it. A collection is defined in the panel — pick the item type, switch the fields on, choose an icon — and it appears in the sidebar with its own editing screen, its own categories and its own API endpoint.

Each client also gets to have only the collections they actually use. Everything else stays switched off and out of their sidebar.

## Translations you can see the state of

Multilingual content usually fails quietly: someone adds an English page, nobody adds the Polish one, and the gap is only discovered months later by a visitor. The panel keeps a running count instead — how many pages are translated per language, which ones are missing, and whether the SEO title and description were filled in as well as the body.

## The part I find most interesting: AI is a first-class client of the CMS

Everything the dashboard can read, the CMS also exposes twice — once as a REST API and once as an **MCP** server, behind the same authentication. MCP is the protocol AI assistants use to talk to outside systems, so the second surface means an AI tool can work with the content directly instead of being handed copy-pasted text.

There are 82 tools on that surface today: list and read pages, search content, read the block schema, create a page, edit a single block by its id, set metadata, manage categories and menus and redirects, upload media, trigger a rebuild. A client can connect Claude to their own dashboard with their own panel login — OAuth with PKCE, not a token pasted into a config file — and then ask it to draft a page, fix the pages missing a description, or translate a section, with the changes landing in the CMS rather than in a chat window.

### Two doors into the same content

The dashboard is one way in. The terminal is the other. Because MCP is a protocol rather than a feature of one app, the same tools that answer a chat window also answer Claude Code — so a developer can stay where they already are and write to the CMS from the command line: create the page, fill its blocks, set the metadata, trigger the rebuild.

That is not a demo path. The copy on the sites running on this system went in from a terminal, not by clicking through the panel. Writing twelve pages is a different job when you can hand over a brief and a structure instead of opening twelve forms.

Both doors lead to the same content, the same validation and the same permissions. The person who edits the About page once a quarter gets a panel that asks nothing of them; the developer who is setting up a whole site gets a command line. Neither has to pretend to be the other.

The guardrails were the interesting engineering problem, not the tools:

## From an edit to a live page

A rebuild takes 10 to 20 seconds. That number is what makes "just publish it" a reasonable thing to say to a client rather than a scheduling conversation — and the panel keeps the history, so any earlier release is one button away.

What visitors get at the end of that is a static site: HTML, CSS and images on a CDN. No database query, no PHP process, nothing to log into. The content lives in the dashboard; the public site is a snapshot of it.

Because the contract between the two is plain JSON over HTTP, the frontend is not locked in either. Astro renders it today; a Next.js or Nuxt or SvelteKit site would consume exactly the same endpoints without the backend changing at all.

## Onboarding a new site

Adding a client is one form. Everything after it runs in the background as a chain of steps that can each be retried on their own: create the site's repository, create the web app on the server, wire up the git deploy, issue the certificate. The panel shows which step it is on rather than a single pass/fail, and a failed run can be restarted without producing a duplicate of anything it already created.

## My role

I am the primary author of the system: the multi-tenant content model, the block and collection engines, the Filament panel, the REST and MCP surfaces, the provisioning chain, the media pipeline, and the Astro core package the client sites are built from. Around 1,900 commits went in over the first ten weeks, most of them mine, alongside a small team and code review on every pull request.

## Result

## Technologies