Pages, posts and every line of copy are files in a Next.js repo.
An agent edits them. The build says no. What ships is static.
The loop · Files, not a database · A page is a file · A post is a file · The voice is a lint · The editor is an agent · So is the designer · The proof is a diff · Built on it · Start your own site
1. The agent edits a file. The landing page is content/pages/home.yaml:
its SEO block, its structured data, its sections in order with all their
copy. A post is content/blog/<slug>.md. The editorial plugin gives Claude
Code and Codex the skills that write, add, update, review, retire and report
on that content, reading the site's own rules from the repo and stopping at
every gate for a person.
2. The build says no. Every file is parsed against its schema, every string is read against the site's voice rules, every prerendered page is audited for its title, description, canonical, Open Graph, headings, image attributes, structured data and sitemap entry. Break a few things and build; this is what it says, verbatim:
$ pnpm build
FAIL content/pages/home.yaml schema: sections[0].eyebrow is required
FAIL content/pages/home.yaml schema: sections[0] unknown key(s) eyebrows
FAIL content/pages/home.yaml schema: sections[2].cards must have exactly 5 cards
FAIL content/pages/home.yaml voice-banned: sections[0].cta "seamless" is banned (hype); write "say what the reader does not have to do"
FAIL content/pages/home.yaml em-dash: sections[1].heading contains an em dash; use a period, a comma, or restructure the sentence
WARN content/blog/pages-are-files.md seo-title: seoTitle is 66 characters; Google shows about 60
pnpm content:lint gives the same answer in about a second, without the build.
3. What ships is static. Every page is prerendered and served as a file:
from a Cloudflare Worker's asset cache, as the example is, or from a Node
server's standalone package, which agentic-cms assemble completes and
checks. There is no database, no admin screen and no request-time read; a
copy change is a diff, a review and a deploy.
git clone git@github.com:HKamkar/agentic-cms.git && cd agentic-cms
pnpm install
pnpm dev # http://localhost:8000, then change a line in content/pages/home.yaml| agentic-cms | A CMS with a database | |
|---|---|---|
| Where the copy lives | content/: seven collections, one folder, one pull request per change |
rows behind an admin screen |
| Who edits it | an AI agent, with a person approving each gate; or anyone with an editor | people in the admin screen |
| What stops a mistake | the build, with a line that names the file, the field and the problem | a preview, if someone looks |
| What runs at request time | nothing: prerendered, static, cached at the edge | a query per page |
| The design | yours: what ships is a wireframe a fork replaces, with the machinery kept | the theme's |
| When it is the wrong tool | copy that must change without a deploy, a search over thousands of entries, or authors who will never open a pull request | — |
# content/pages/home.yaml
seo:
path: /
title: "Acme: content as files, checked on every build"
description: Acme is a CMS whose editor is an AI agent. Pages, posts and copy are files, validated at build time, prerendered, and served with no request-time reads.
ogImage: /images/home-og.jpg
updated: "2026-09-18"
breadcrumb: Home
jsonld:
type: WebPage
application: { name: Acme, description: …, operatingSystem: Any (static site), offer: …, featureList: [ … ] }
sections:
- type: home-hero
eyebrow: Content as files
heading: Every page, post and line of copy is a file.
text: Acme is the CMS behind this site, and its editor is an AI agent. Pages are YAML, posts are markdown, and the build validates all of it before the first route is rendered.
cta: Get in touch
- type: home-service
…The seo block becomes the head, the canonical and the sitemap entry. The
jsonld block becomes the page's structured data (WebPage, AboutPage,
ContactPage or Blog, each with its own fields). Each type names one of
24 section components (a group nests them), and its copy schema says
exactly what the section takes; a card list has an exact length, so a fourth card is a design change
rather than a typo. Presentation stays in the component. There is no
page.tsx to write: one catch-all route renders every page file.
The four pages under /sections/ are the catalogue: every section type the
landing page leaves out, rendered with copy that says what each slot is for.
---
title: Pages are files
excerpt: >-
A page on this CMS is one YAML file with three blocks. An SEO block, a
structured-data block and a list of sections in order. Here is what each block
holds and what the build does with it.
date: '2026-09-09'
category: guides
author: acme-editorial
image: /images/blog/pages-are-files/pages-are-files-hero.webp
imageAlt: A wide grey placeholder standing in for the hero illustration of this post
seoTitle: Pages are files | Acme
keywords: [page as a file, YAML, section types, structured data, SEO audit]
related: [how-a-post-is-built, the-voice-file-and-the-lint]
publishedAt: '2026-09-09T09:00:00.000Z'
---The filename is the URL. The body is markdown with four conventions the
renderer keys off: headings start at ##, a > "quote" becomes the
pull-quote card, an image alone in its paragraph becomes the figure block,
and a ## Frequently asked questions heading with ### questions becomes
the accordion and the FAQPage structured data. The lint checks the
frontmatter's SEO limits, the heading structure, the FAQ's shape, the alt
texts and that every image exists on disk and weighs under 250 KB.
content/VOICE.md says how the site writes and what it never claims. Its
fenced block is machine-readable: banned words and patterns, the brand's
spelling and mark, the words that count as claims near a regulation, model
and cloud names, the SEO ranges. agentic-cms lint reads it on every
build. Exact rules fail; heuristics warn. What the lint cannot judge (rhythm,
an unsupported number, a misattributed date) is what the two review agents
are for.
plugin/ is one plugin for Claude Code and Codex. It reads everything it
needs from the repo it stands in and carries nothing of any brand.
| Skill | Does |
|---|---|
/editorial:write-post |
a post from a brief in four gated stages: outline, body, the two reviews in isolation, SEO fields |
/editorial:new-post |
a post from a draft file, with its images and frontmatter, as draft: true |
/editorial:update-post |
an edit to a post, the two dates and the URL intact |
/editorial:new-page |
a page from the template, made of existing section types |
/editorial:retire-content |
a post hidden (its URL kept) or a page removed once a redirect exists |
/editorial:review-voice |
a file read against VOICE.md for what the lint cannot judge |
/editorial:critic |
a post read hard by an editor who did not watch it being written, every external claim checked against primary sources |
/editorial:content-status |
what is live, in draft, planned and recently changed, read-only |
The two review skills, review-voice and critic (the critic fact-checks
against primary sources), also run as Claude Code agents
editorial:voice-reviewer and editorial:critic, in isolation from the
conversation that wrote the draft. The same directory installs in Claude Code
and in Codex; the repo is its own marketplace for both, and in a checkout
Claude Code has the plugin on through .claude/settings.json. Elsewhere:
claude plugin marketplace add git@github.com:HKamkar/agentic-cms.git && claude plugin install editorial@agentic-cms --scope usercodex plugin marketplace add HKamkar/agentic-cms && codex plugin add editorial@agentic-cmsplugin/README.md is the contract: what a site offers the plugin, what the
skills write, and the optional workshop file for marketing that lives outside
the repo.
A site's look is designed the same way its copy is edited: by an agent,
one element at a time, with the owner picking at every gate. The design
skills ship in the repo (.claude/skills/, .agents/skills/, found by
Claude Code and Codex with nothing installed), and every one of them works
through the command line, so what a round leaves behind is files and a
proof, never a tool in the reader's browser.
| Skill | What it does, and the commands under it |
|---|---|
design |
a site from the wireframe to a look of its own: tokens first, then the chrome, then the sections |
design-options |
a design round: three to five ideas, the picked ones built as real components on a throwaway route in the section's own frame with the page's copy (pnpm kit demo new), the owner picks by letter; demo clean then removes everything the round created that the winner does not use, and says what it keeps and why |
design-graphics |
the site's own drawings as SVG in the lab (pnpm kit lab): scenes on the site's tokens in both schemes, at the sizes they ship and on a phone, a timeline to step through a loop, rendered to the file a page ships; a loop that should play when it is seen goes inline (InlineAnimation, readInlineSvg) |
design-icons |
icons as families: the inventory of every icon beside its copy (icons audit), Lucide and Simple Icons maps (icons add), marks from primitives (icons family), and a round for a set of the site's own (icons round new / publish / retire) |
design-measure |
a screenshot claim turned into numbers before an edit: an element's box, styles and stacking chain (probe), a crop (shot), candidates at their real size on their real grounds (sheet) |
design-proof |
the change proven on the pixels (below) |
docs/design.md is the loop, docs/lab.md the lab, docs/icons.md the icons, docs/shot-probe-sheet.md the one-shot commands.
A refactor must not move a pixel, and a design change must move only the
pixels it meant to. agentic-cms visual-parity photographs every page of
the production build at eight widths, in either colour scheme, and diffs two
captures pixel by pixel:
pnpm kit visual-parity capture before --ref develop # the baseline: that commit built in a sibling worktree and captured
pnpm kit visual-parity capture after --build # this tree, built and copied first, so it is free while the capture runs
pnpm kit visual-parity compare before after # exit 1 on any difference, one line per file
pnpm kit visual-parity proof --states # or all of it in one: baseline, after, compare, per passThe rendering is made deterministic before a shot is taken: reduced
motion, frozen transitions, every reveal at its end state, every image
loaded, every inline SVG animation held at its rest frame, other origins'
scripts and beacons (analytics, widgets) held back. --motion plays
the animations and photographs each scroll step mid-flight and settled,
with an inventory of every animation; --states photographs hover, focus,
checked and open. A compare line says what changed and where: SIZE … shift when one section grew and pushed the page down, and a cause: line
naming the section and its height change, down to a fraction of a pixel.
What a site shows until a visitor's choice is stored — a consent banner
over the page — is closed by a seed in src/config/harness.ts, written
into every page's storage before its scripts run, on both sides of a
proof.
A proof costs what the change touched. Every page-width whose build files
are the same bytes as when it was last photographed is copied from the
cache, so the after capture takes the changed pages alone and a baseline
taken again is seconds; several browsers work at once (--jobs), and a
site with many posts can photograph a few of each template (--sample).
On the example, the whole static set is 44 seconds from nothing on four
cores and one edited post is 18. --fresh retakes everything.
docs/visual-parity.md is the contract.
Four colours, each a light-dark() pair (paper, ink, fill, muted);
a system font stack; no radius, shadow or motion; a theme switch that follows
the system until a reader picks. Every section renders in a box that prints
its own YAML type, so the page and the page file read side by side.
Illustrations are crossed placeholder boxes; content images come from
pnpm kit placeholder <out> <width> <height> until real ones exist.
STANDARD.md is the design system; src/components/README.md the catalogue.
The agentic-cms/ix library stays in the package for a fork that wants
motion: scroll-into-view reveals and sequences, and InlineAnimation, a
site's own SVG loop that plays in view and rests for a reader who prefers
reduced motion.
deeplit®, private AI infrastructure from Delft, is
the first site on the package. Its design — the sections, the chrome, the
CSS modules, the images, the reveals and sequences on agentic-cms/ix, the
icons drawn in design rounds, the loop on its Platform page — its content
and its config live in its own repo; the engines, the libraries and the
command line come from here by tag (github:HKamkar/agentic-cms#v0.7.5),
composed once in its src/kit.ts, and it runs as a Node server from the
standalone package agentic-cms assemble completes. Same page files, same
post pipeline, same lint and audit as the wireframe above; the design is the
part a site brings.
Two ways, one result. Fork this repo when you want the example around you — the wireframe, the section galleries, the posts that describe it — and replace it piece by piece. Install the package when your site is its own repo, as deeplit's above is, and let it lay the site out:
mkdir my-site && cd my-site && pnpm init
pnpm add agentic-cms@github:HKamkar/agentic-cms#v0.7.5 # allowBuilds below, first
pnpm exec agentic-cms init . # the site: the example, the agent files, the config
pnpm install && pnpm devA git dependency builds its dist/ on install (prepare), which pnpm runs
only when pnpm-workspace.yaml allows it:
allowBuilds:
"agentic-cms@git+https://github.com/HKamkar/agentic-cms.git": true
sharp: trueEither way the site carries, from the first session and with nothing
installed, what an agent needs to design it: the rules (AGENTS.md, and
.claude/rules/, which Claude Code loads by path and Codex finds through
AGENTS.md's scoped-rules table), the design skills (.claude/skills/, .agents/skills/
— design, design-options, design-measure, design-icons,
design-graphics, design-proof) and the commands they call. docs/init.md is what init writes and
what comes next; docs/design.md is the loop.
Then: the six steps
-
Forking: rename the package and the plugin marketplace to your own name, and drop the Cloudflare files if you host elsewhere. Installing:
initwrotesrc/kit.ts, where your site composes the engine — the one file the app and the command line read (server code only):import { createKit } from "agentic-cms"; import { sectionSchema } from "@/components/sections/schemas"; import { site } from "@/config/site"; export const kit = createKit({ site, sections: sectionSchema }); // kit.collections · kit.content (getPages, getPage, getFaq, …) · kit.blog (getAllPosts, getRelatedPosts, …) // kit.seo (pageMetadata, pageJsonLd, postMetadata, sitemap, feed, robots, …) · kit.urls (postUrl, absoluteUrl) · kit.site
and
package.jsonnames the commands:"content:lint": "agentic-cms lint","content:check","content:status","content:docs","kit": "agentic-cms","build": "agentic-cms lint && agentic-cms docs --check && next build && agentic-cms seo", and for the site's own tests"test": "node --import agentic-cms/loader --test \"src/**/*.test.ts\"". -
src/config/site.ts: the brand, the URL, the e-mail and address, the nav, the footer, the calls to action (data only,satisfies SiteConfig). -
src/app/globals.css: the tokens — or start thedesignskill, which begins there. Both themes follow from thelight-dark()pairs. -
Copy
content/_templates/VOICE.mdtocontent/VOICE.mdand write your rules; the prose and the fenced block say the same thing. -
Replace the content: the registries, the reviews, the FAQ sets, the use cases, the page files, the posts.
content/README.mdis the door;content/_templates/has an annotated template per collection. Keep or delete the/sections/*demo pages. -
pnpm content:lintuntil clean,pnpm build, then the host of your choice: the example's Cloudflare Worker, or a Node server (docs/deploy.md:output: "standalone"andagentic-cms assembleas the build's last step).
A new kind of section is a copy schema, a component and a registry entry
(src/components/sections/). A collection of your own is a schema and a
definition returned from createKit's collections option
(src/lib/content/README.md). A new form is an entry in src/config/forms.ts
(every ordinary field type, notes and groups, a prefill from the URL); it
delivers by mailto, or through the site's one dynamic route to a
webhook or a provider of the site's choosing (docs/forms.md).
Analytics waits for the visitor's consent: agentic-cms/consent keeps the
choice in their browser and agentic-cms/consent/google loads the Google
tag only once they allow it, with consent mode v2 denying everything until
then; the banner is the site's own design, and the example's is off until
GOOGLE_TAG_ID is set (docs/consent.md).
A site pins the package by tag. An upgrade is the new tag, pnpm install,
pnpm exec agentic-cms init . --agent-files (the new rules and skills in,
the site's own edits kept), the steps of that release in
docs/upgrading.md, then pnpm build and a new
baseline for the screenshot harness. CHANGELOG.md says what changed and
marks every change to what a capture writes.
pnpm dev # next dev on :8000
pnpm build # content lint → docs check → next build → SEO audit; every page prerendered
pnpm preview # build for Cloudflare and serve the Worker locally on :8000
pnpm run deploy # build for Cloudflare and deploy (pnpm wrangler login first)
pnpm test # the engine's, the post pipeline's and the lint's node:test suites, about a second
pnpm test:browser # the Chromium-backed suite (the harness's library; skips, naming the fix, without a browser)
pnpm test:pack # packs the package and builds a scratch site from the tarball, the way a site that installs it does
pnpm hygiene # the public repo's hygiene: no one site's name, no machine paths, no captures committed, docs current
pnpm content:lint # the content rules, about a second
pnpm content:status # what is live, in draft and planned, from the files
pnpm content:docs # the field tables into content/README.md
pnpm kit <command> # the command line: placeholder, optimize-webp, optimize-svg-rasters, parity, visual-parity, seo, …
pnpm kit shot|probe|sheet # a section's picture with its box, the numbers behind a screenshot claim, a candidate sheet
pnpm kit demo <new|clean> # the design round's throwaway route: candidates for a section in its frame with the page's copy, on the site's theme, the current version last
pnpm kit init <dir> # a site from the package: the example, the agent files, the config
pnpm kit guard-email # fails a build whose served files carry the site's e-mail address as text
pnpm kit critical-css # each prerendered page paints from its own inlined rules, its stylesheets no longer block
pnpm kit assemble # a standalone build packaged for a Node host: public/ and .next/static copied in, every file checked
pnpm kit icons <add|remove|family|audit|round> # a site's icon map from Lucide, Simple Icons and its own drawings, a family of marks from primitives, the inventory, a design round for its own
pnpm kit lab <new|serve|route|render|clean> # the design canvas: SVG scenes on the site's tokens, on a phone, and as a throwaway route on the site's theme; rendered to the files a page ships; removed after
pnpm kit --help # every command; <command> --help prints its flags and exit codesEvery command's flags, defaults and exit codes are in docs/commands.md (generated from the specs the parser reads, so it is the contract); the screenshot harness has its own guide, docs/visual-parity.md, and the one-shot page commands theirs, docs/shot-probe-sheet.md; docs/ is the index.
- Node 22.18+ (
.nvmrcsays 26) and pnpm. The build fetches nothing: no webfont, no external data. light-dark()wants a current browser (Chromium 123+, Safari 17.5+, Firefox 120+).- The Worker is static-only but for the form route (
/api/forms/[id]): OpenNext's static-assets cache serves the prerendered pages, and onlypreview,deployanduploadpopulate it, sowrangler devstraight afternext buildreturns 500s. The ~40 "Failed to copy node_modules/…" lines during the OpenNext build are an OpenNext bug and harmless. agentic-cms visual-parityscreenshots every page at eight widths, in either colour scheme, and diffs two captures pixel by pixel: the proof for a refactor that must not move anything. Unchanged pages come from its cache and several browsers work at once, so a proof takes minutes.
- Decide where the contact form delivers. It ships on the
mailtobackend; switching it toendpointand settingFORM_WEBHOOK_URL(or a sink of the site's for a provider's API) delivers it (docs/forms.md), and the privacy notice then names the provider. - If the site replaces an existing one, recreate its redirects before the
DNS change (on the Worker, or in
next.config'sredirects()on a Node server). A public URL never changes without one. pnpm build,pnpm preview, click through every page in both themes.- On the example's Cloudflare Worker:
pnpm run deploy. On a Node server: the build runsagentic-cms critical-cssafternext buildand ends withagentic-cms assemble, and.next/standaloneis the package (node server.js; docs/deploy.md). - Attach the domain, verify
/, a post and/sitemap.xml; submit the sitemap.
content/pages/<slug>.yaml every page: seo, jsonld, sections
content/blog/<slug>.md posts (slug = filename); _template.md is the annotated template
content/authors.json content/categories.json content/reviews.yaml content/faqs/<key>.yaml content/use-cases.yaml
content/VOICE.md the voice and claim rules; _templates/VOICE.md is the blank
content/editorial/ calendar.md, backlog.md, and workshop.yaml when marketing lives elsewhere
plugin/ the editorial plugin: skills/, agents/, README.md (its contract)
.claude/skills/, .agents/skills/ the design skills, for Claude Code and Codex, found from a checkout with nothing installed
templates/site/ what init writes into a site: AGENTS.md, the rules, the config; templates/lab-demo/ the lab's route
src/lib/ the package (agentic-cms): content/, blog/, seo/, forms/ (and forms/server), consent/ (and consent/google), ix/, components/, lab/, email, cx, createKit
src/kit.ts the example composing the package for itself; the file every site has
src/components/sections/ the section registry and the copy schemas
src/components/ui/ Section, Placeholder, Button, Navbar, Footer, Faq, ThemeToggle, ConsentBanner, the form primitives
src/config/site.ts the brand, URLs, nav, footer, calls to action
src/config/forms.ts every form, as data; form-delivery.ts, where endpoint forms deliver (server only)
src/config/analytics.ts the Google tag id (GOOGLE_TAG_ID) and the consent policy; harness.ts, the screenshot harness's seed
src/app/api/forms/[id]/ the one dynamic route: endpoint forms post here
bin/, scripts/ the command line: lint, check, status, docs, seo, critical-css, guard-email, assemble, placeholder, the optimisers, parity, visual-parity, shot, probe, sheet, icons, lab, demo, init
STANDARD.md the design system AGENTS.md the rules for any agent working on the code (CLAUDE.md includes it)
PLAN.md how the engine was built, condensed CHANGELOG.md every release
docs/ the guides: commands.md, visual-parity.md, shot-probe-sheet.md, design.md, skills.md, init.md, upgrading.md, deploy.md, forms.md, consent.md, icons.md, lab.md, email.md, roadmap.md
MIT licensed.




