Project
Velta CMS
· 4 min read
The site edits itself through four JSON operations against a git host. Any host that can answer them — including through hooks — can run this CMS.
What it is
Velta CMS is a single-page tool that edits this site straight in its repository. Every page is a complete HTML document that renders without JavaScript; shared chrome lives in _partials/; the CMS assembles pages from those parts and commits them. There is no server, no database, and no build step of its own — the git host is the backend, and git history is the undo.
The GitHub-specific code is one small module in the site repository: admin/backend-github.js. Everything else in the CMS talks to a four-operation contract, described below. Point a different module at a different host and the CMS does not notice.
The contract
Four operations, JSON over HTTPS, all content base64 in both directions. A reference implementation is admin/backend-github.js in the site source.
list(dir) → GET {base}/{dir}?ref={branch}
← [ { "path": "news/index.html", "type": "file",
"size": 1234, "sha": "e69de…" } ]
get(path) → GET {base}/{path}?ref={branch}
← { "path": "news/index.html", "sha": "e69de…",
"content": "PCFET0NUWVBF…" }
put(path) → PUT {base}/{path}
{ "message": "Update news/index.html",
"content": "PCFET0NUWVBF…",
"branch": "website", "sha": "e69de…" }
← { "content": { "sha": "4b825d…" } }
del(path) → DELETE {base}/{path}
{ "message": "Delete assets/photo.webp",
"sha": "e69de…", "branch": "website" }
Three rules give the contract its shape:
- Every
putanddelis one commit on the pinned branch, with the caller's message. There is no staging area and no draft state — the branch is the document. shais optimistic concurrency. Aputcarrying theshayou read earlier must fail (HTTP 409/422) if someone else changed the file since. The editor treats that as "reopened the saved version", not as data loss.- Reads may work without credentials on a public repository; writes need a token with contents read/write. The CMS keeps that token on the editor's device and nowhere else.
Other git hosts
Implement the four operations against the host's files API and swap the module:
| Host | Files API | Notes |
|---|---|---|
| GitHub | /repos/{owner}/{repo}/contents |
The reference implementation; everything above maps 1:1. |
| Gitea, Forgejo, Codeberg | /api/v1/repos/{owner}/{repo}/contents |
Same request and response shapes as GitHub — mostly a base-URL swap. |
| GitLab | /api/v4/projects/{id}/repository/files |
Different shape: per-file endpoints and a commits API for writes. Needs a small mapper that composes the four operations from it. |
A host with no files API at all can still qualify through hooks: if the platform can receive a webhook that carries (or triggers a fetch of) file content and can commit it — a CI job, a bot, a small receiver service — then that receiver implements put and del, and read-only hosts can serve list and get from a mirror. The contract only needs the four answers; it does not care what stands behind them.
Publishing is the host's job
The CMS only commits. Turning a commit into a live site belongs to the host's push hook: GitHub Pages rebuilds this site on every push to the website branch; on another host you point the equivalent webhook or CI job at your static builder. That separation is deliberate — the editing side never knows how publishing happens, and the publishing side never knows an editor exists.
The CMS itself is a few hundred lines of vanilla JavaScript in admin/admin.js, on the website branch of the repository alongside this page.