Skip to content

Plain files. Plain PHP.

A CMS you can read to the bottom.

See it on GitHub

CLI

Terminal
$ php cms run entries/create collection=pages title=About
$ php cms run entries/publish id=01m7c2q4
$ php cms build --strict
$ php cms links:check
$ php cms plugin enable letterncms/forms
$ php cms token:create you@studio.com --scope='entries/*'

Your site is a folder.

Commit it, branch it, review it like code.

Project structure

pages/home.md

content/collections/pages/home.md
---
title: Home
status: published
---

::: box {"id":"hero","props":{"tag":"section"}}
:::: heading {"id":"word","props":{"text":"Ondine","level":"1"}}
::::

:::: paragraph {"id":"line","props":{"text":"Fish from the morning boat."}}
::::

:::: button {"id":"book","props":{"label":"Book a table","url":"/book"}}
::::
:::

Write an action once. It’s everywhere.

Validated, permission-checked and logged.

Abilities
ShopPlugin.php
$this->cms()->abilities()->register('shop/price-update', [
    'label' => 'Update a price',
    'description' => 'Set the price of a product, in cents.',
    'permission' => 'shop.prices',
    'input' => [
        'type' => 'object',
        'properties' => [
            'id' => ['type' => 'string'],
            'cents' => ['type' => 'integer', 'minimum' => 0],
        ],
        'required' => ['id', 'cents'],
    ],
    'annotations' => ['idempotent' => true],
    'handler' => fn (array $input) =>
        Product::setPrice($input['id'], $input['cents']),
]);
In the admin@letterncms/sdk

run('shop/price-update', { id, cents })

Over HTTPREST

POST /api/v1/abilities/shop/price-update

In the terminalCLI

php cms run shop/price-update id=p1 cents=1200

In your AI toolMCP

shop.price-update

Nothing exotic underneath.

If you know these, you know it.

PHP 8.4

Any host

Laravel

The kernel

React

The admin

MCP

First-party plugin

Start reading.

It’s all documented, down to the hooks.

MIT licensed. Read all of it.