@taulell/cms
v0.13.0
Published
A content system for a site on Cloudflare Workers: schema in code, a content API, an MCP server and rendering helpers.
Readme
@taulell/cms
A content system for a site on Cloudflare Workers. A site declares its content model in code, and its own Worker serves:
- a content API under
/_cms/api, with its OpenAPI spec - an MCP server for an agent at
/_cms/mcp - uploaded images under
/_cms/media - the reader its pages render from
- a revision for every write, which can be restored
Content lives in the site's own D1, KV and R2. Edits are live: there are no drafts and no publish step. The CMS assumes no language, country, currency or business, and the site decides who may write.
Use it in a site
Declare the content model in cms.config.ts. It runs in Node as well as a Worker.
import { defineConfig, field } from '@taulell/cms';
export default defineConfig({
locales: ['en', 'es'],
defaultLocale: 'en',
singletons: {
shop: { fields: { name: field.text({ required: true }), hours: field.hours() } },
},
collections: {
products: {
path: { en: 'products', es: 'productos' },
fields: {
name: field.localized({ required: true }),
slug: field.slug(),
price: field.price({ currency: 'EUR' }),
photo: field.image(),
},
},
},
});Serve it from the site's Worker, and say who may call it. authorize has no default, so no site is
open by omission.
import { cachedPage, createCms } from '@taulell/cms/worker';
import config from './cms.config.ts';
const cms = createCms(config, {
// Turn a request's bearer token into a caller, or refuse it.
authorize: async (token, env: Env) => {
const caller = await verify(token, env); // the site's own check: { scope: 'read' | 'write', actor }
return caller ? { ok: true, caller } : { ok: false, status: 401, error: 'The token is not valid.' };
},
});
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
if (cms.handles(new URL(request.url).pathname)) return cms.fetch(request, env, ctx);
// A page reads its content through `await cms.reader(env)`.
return cachedPage(request, env.CMS_KV, ctx, () => renderPage(request, env), env.VERSION.id);
},
};The Worker needs these bindings and settings:
CMS_DB(D1),CMS_KV(KV),CMS_MEDIA(R2) andCMS_LOADER(a Worker Loader). The tables are made on first use, so there is no migration.CMS_SITE_URL, a var holding the site's public origin, which its pages name in canonical URLs and a sitemap. The CMS itself doesn't read it.- The
nodejs_compatcompatibility flag. The MCP server runs the agent's code in a Dynamic Worker, which needs Workers Paid. - A
version_metadatabinding, whoseidgoes tocachedPageso a deploy never leaves cached pages linking assets it removed.
cachedPage never caches a page asked for at localhost, so a dev server shows each change to the
code at once.
Before a deploy, validateSite(config, production?) returns a sentence for each problem with the
config, and, given production's API and a read token, for each live document the new config
would refuse. Run it in CI so a change that would break live content fails first.
To work on a site locally with its live content, importContent(target, exported, file) writes
what the live site's GET /_cms/api/export answers into local storage: target holds the D1, KV
and R2 bindings, such as wrangler's getPlatformProxy() gives in Node on the storage wrangler dev
reads, and file(key) fetches an image file the storage lacks, which the live site serves at
/_cms/media/<key>. The documents replace the local ones, the pages' snapshot is published from
them, and no revision is written.
Exports
| Import | Runs in | Holds |
| --- | --- | --- |
| @taulell/cms | Node, Workers, browsers | defineConfig and field for the config; validateSite, configProblems and contentProblems; createReader; the formatters formatPrice, formatDate, dayName and renderMarkdown; the routing functions resolvePath, alternates, collectionPath, entryPath and pagePath; importContent; and their types |
| @taulell/cms/worker | Workers | createCms and cachedPage |
| @taulell/cms/client | Node, Workers, browsers | createClient({ url, token }), a typed method for each API route, and CmsRequestError, for code outside the site; variantWidths(width) and MEDIA_WIDTHS, the widths to resize an image to before uploading it |
The field kinds are text, localized, markdown, slug, number, boolean, select, url,
email, phone, price, image, images, hours, date, timezone, geo, reference
and identifiers. A field, singleton or collection may carry a label per language.
Commands
None. validateSite is a function; @taulell/site runs it from its taulell validate.
Versions
A site pins one exact version, and @taulell/site peers on one. CHANGELOG.md lists each release,
newest first, with what changed for a site and what the site does to move to it.
