One editorial desk for every site you run.
Dheys CMS manages content for many websites from one deployment, on any host. It handles articles written by people and by AI agents in a single editorial pipeline, with configurable approval gates and automated scheduling — and the control plane runs on GitHub Pages with no server, no database, and nothing to pay for.
MIT licensed. No telemetry. No required backend. No paid service in the default path.
A static site cannot run a scheduler, hold an API key, or orchestrate agents. So it does not try. There are three layers:
| Layer | Runs on | Owns |
|---|---|---|
| Control plane | Static Astro build on GitHub Pages | Admin, editor, review queue, calendar. Talks only to api.github.com. Holds no secret beyond a session-scoped token. |
| Automation | GitHub Actions | Scheduler, agent runner, deploy dispatch. Every AI key lives here, readable only inside a runner. |
| Content | Your own site repositories, any host | Markdown + frontmatter, committed by the automation layer or by the admin. Each site keeps its own build and deploy. |
The admin never calls an AI provider. It dispatches a workflow and polls the run through the GitHub API. That is what keeps every key on the far side of a boundary a browser cannot cross.
git clone https://github.com/nazilafeef/dheys-cms.git
cd dheys-cms
pnpm install
pnpm devThat is the whole setup. The demo content — three locales, two of them right-to-left —
renders immediately at http://localhost:4321/dheys-cms/.
To manage real sites you need two more things:
- A registry. It never lives in this repository. Copy
src/sites.example.jsoninto a private gist, a private companion repository, or a repository secret, replace every value, and point the deployment at it. See docs/site-registry.md. - A token. A GitHub fine-grained PAT scoped to the repositories you want to manage.
Paste it into
/admin. It is held insessionStoragefor that browser tab only.
Deploying is one push: .github/workflows/deploy.yml builds and publishes to GitHub Pages,
reading its base path from actions/configure-pages rather than assuming one.
This repository is the published product. It holds no keys, no site registry, and no Actions secrets, and it never will. That is deliberate, not an oversight: a public repository is the wrong place for either, and a CMS that invites you to put them there has already made your first mistake for you.
To actually run Dheys against your own sites, run your own private instance:
# fork on GitHub and keep the fork private, or:
git clone https://github.com/nazilafeef/dheys-cms.git my-dheys
cd my-dheys
git remote set-url origin <your own private repository>
git push -u origin mainThen, in that repository and nowhere else:
- AI provider keys as Actions secrets —
ANTHROPIC_API_KEY,OPENAI_API_KEY,GEMINI_API_KEY, or none at all. Every provider is opt-in and the CMS runs fine without any of them. DHEYS_SITE_TOKENas an Actions secret, so the runner can commit to your site repositories.- Your site registry — a private gist, a private companion repository, or a repository secret. Never a file in the repository itself.
- Repository variables for the things that are merely configuration: the
DHEYS_PUBLISHING_HALTEDkill switch, a gist id, a model name.
docs/configuration.md lists every variable and which of the two it belongs in.
Why not just use this repository? Because secrets set on a public repository are one misconfigured workflow away from being printed into a log that anyone can read, and because a registry naming your sites, their repositories and their deploy hooks is a map of your infrastructure. The split costs one fork and removes both problems. Pull updates from here whenever you want them; nothing flows back.
The gate enforces this from the other direction too: pnpm check:clean-room fails the build
if a credential-shaped string, a foreign domain, or a reference to any repository other than
this one appears anywhere in the tree or in the commit history. You cannot leak a key
into this copy by accident, because the build stops.
Multi-site. One dashboard and one review queue across every connected site. Per-site permissions, per-site guardrails, per-site cost caps. Sites can be on GitHub Pages, Cloudflare Pages, Netlify, Vercel, or anything reachable by webhook.
Editorial pipeline. idea → commissioned → researching → drafting → draft → in-review → approved → scheduled → published, with changes-requested and rejected branches. Every
transition records who did it and when, in frontmatter. The git history is the audit
trail — recoverable from a clone, with no database to query.
Human and AI writers in one queue. Agent output arrives through a strict job contract and lands in review, never live. Every AI-authored item carries provenance — model, prompt version, sources, tokens, cost, run id — and renders an attribution line. AI content is never published under a bare human byline, and that is enforced in the schema rather than the interface.
Scheduling that survives reality. Cron runners are late and occasionally skipped, so every tick is idempotent, a missed window publishes late rather than silently vanishing, and randomised publish times are seeded per item so the calendar, the tick and the test all agree. One repository variable halts publishing across every site without a redeploy.
Guardrails that block, not warn. Required fields, banned phrases, minimum word count, required disclosure, human review, locale completeness, Thaana punctuation. One ships on by default: an item carrying an affiliate offer cannot publish without a disclosure — enforced in the schema, so it binds agent output and direct commits too.
Right-to-left as a first-class citizen. Dhivehi (Thaana) and Arabic ship alongside
English. Logical CSS properties throughout, correct dir and lang, Thaana-safe slug
transliteration, Thaana punctuation enforced in content, an RTL-correct admin, and every UI
string in locale JSON.
A front end that stays out of the way. Under 30 KB of JavaScript on an article page — currently zero bytes of framework code. Share links are plain anchors with no third-party script. An unfilled ad slot makes no network request at all. Dark and light with no flash.
The one-prompt connector. Point it at an existing site and it migrates the whole thing: analyses the framework, migrates content including content no CMS is managing, infers a schema, removes the previous CMS, and refuses the migration outright if a single URL would stop resolving. See docs/connecting-a-site.md.
Every one of these runs in CI on Node 22 and 24, and each must pass on a clean clone.
pnpm install --frozen-lockfile # no warnings, no peer conflicts
pnpm typecheck # astro check + tsc --noEmit
pnpm lint # eslint + prettier
pnpm test # vitest
pnpm test:e2e # playwright; GitHub and every AI provider mocked
pnpm check:clean-room # no leaked domain, repo reference or credential
pnpm build # zero errors, zero warnings
pnpm check:links # every internal link resolves, in both hosting modes
pnpm lighthouse # all four categories >= 95No test may reach a real AI provider or the real GitHub API, and that is a property of the design rather than a promise: every provider and every client is handed its transport.
Not included. The demo site is the screenshot — run pnpm dev and it is in front of you in
about ten seconds, in three languages, which is more useful than a PNG that goes stale.
Getting started — installation · configuration · site registry · connecting a site
Using it — content authoring · admin · automation and scheduling · guardrails
Extending it — theming · custom content types · agent job contract · writing a provider
Reference — i18n and RTL · fonts and licensing · security · WordPress import · troubleshooting
About the build — plan · decisions · state · theme provenance
Stated here rather than discovered later:
- No font binary ships, and none should be added without resolving the licensing position in docs/FONTS.md. Dhivehi renders in whatever Thaana face the reader's system provides, which on many systems is none.
- The token lives in
sessionStorage. Any script running on the origin can read it. There is no way around that in a browser without a server; the trade-offs are stated honestly in docs/SECURITY.md, along with the GitHub App path for operators who want the credential out of the browser entirely. - Only Anthropic model rates ship. Rates for other providers are deliberately absent rather than guessed; an unpriced model is treated as costing its job ceiling, so it is expensive rather than invisible. Set your own in the registry.
- Pagefind does not stem Dhivehi. Search works and matches whole words; it will not match across root forms.
- The Dhivehi and Arabic UI strings have not been reviewed by a native speaker. They are complete and structurally correct — right script, right direction, right punctuation — but the wording deserves a pass.
See CONTRIBUTING.md. The short version: run pnpm gate before opening a
pull request, and do not add a dependency without saying what it replaces.
MIT — see LICENSE.