Part of the jedee wiki — pages written by an AI (Claude Code), supervised by Johan.
Sveltia CMS
A git-based CMS gives a static site an editing screen without giving it a database. It runs in the browser, reads the repo’s markdown files through the GitHub API, shows their front matter as a form, and saves an edit as a commit, which then triggers the normal deploy. Netlify CMS started this kind of tool. It was renamed Decap CMS in 2023, and Sveltia CMS is a rewrite that its author offers as the successor: the same config.yml format, a much better phone interface, and direct uploads to Cloudflare R2. Sveltia is still in beta before 1.0 and mostly one developer’s work, so its behavior can change between versions. Pin the version in the script address.
Setting it up takes two files in an admin/ folder:
index.html, which holds a single<script>tag and nothing else. The docs point out that AI assistants tend to add a stylesheet<link>ortype="module", and neither belongs there.config.yml, which lists collections (each maps to a content folder) and each collection’s fields (each maps to a front matter key).
The question that decides whether you can use one
What does the CMS do with front matter keys that aren’t in its config? A site with a real data model has many keys nobody edits on a phone: media byte counts, syndication links, keys only one post type uses. A CMS that rebuilds the file from its field list deletes them all on the first save. A CMS that keeps them lets the config stay partial, so you declare only what you want to edit.
The docs rarely answer this, so check the source. Sveltia keeps them. Opening a post starts from a full copy of the parsed file, and saving writes the configured fields first, then “the remainder”, every other key sorted alphabetically (draft/create/index.js → buildDraft, draft/save/serialize.js → finalizeContent, checked 2026-09-10). Pages CMS still deleted undeclared keys in 2.1.8. For this site that difference decided between the two.
What a first save changes
Even when nothing is deleted, the first save of a file rewrites its shape. None of this loses data, and all of it shows in the git diff:
- Key order. Declared fields go first, in config order, and the rest follow alphabetically. ⚠ Sveltia sorts undeclared keys at every depth: it flattens them to paths like
photo.downloads.0.bytesand sorts those, so the inside of a nested object gets reordered too. - Quote marks around values are dropped or changed from double to single, which makes no difference to YAML.
- Empty keys get added for optional fields a post doesn’t have, unless the config sets
output: { omit_empty_optional_fields: true }. - Whitespace: a blank line after the closing
---, and a final newline if the file had none.
Declaring fields in the order most files already use keeps the first diff small.
In jedee
Live at /admin/ since 2026-09-10 (admin/index.html and admin/config.yml, copied through unchanged, noindex), pinned to @sveltia/cms@0.209.1, saving straight to main. It is the third way content gets into the site: Micropub creates posts from a phone, the Web Clipper captures them from a source page, and Sveltia edits existing ones. It is also the only phone route for the post types Micropub doesn’t handle (audio, video, event, recipe). Neither Eleventy Excellent nor indiee ships a CMS, so all of this is jedee’s own.
All 16 post types are configured. Each shows a shared set of fields (title, description, date, tags, draft, body), response types add their link field, and photos and jams have a few more. New posts can only be created in notes and articles; everything else starts from Micropub or the clipper, which already know each type’s shape (see The authoring tool decides the data model). category is never declared, because each folder’s data file sets it and posts don’t carry it.
backend:
name: github
repo: pjedlund/jedee
branch: main
auth_scope: public_repo
output:
omit_empty_optional_fields: true
slug: # keep Title-Case filenames for Obsidian
lowercase: false
sanitize_replacement: ' 'The post body uses the plain markdown mode (widget: markdown, modes: [raw]), so wikilinks, footnotes and {:attrs} are never rewritten. The rich-text mode once escaped **bold** on saves that only touched the title (issue #556, fixed 2025-12). Plain mode avoids that whole kind of bug.
Five traps, each found by a real save
- ⚠ The datetime field drops seconds. A stored
2026-06-15T21:02:11+02:00came back as21:02:00on save, even withstep: 1. The picker builds its value from hour and minute only (date-time/helpers.js→formatDateTimeValue), so every stored time with seconds reads back as:00, and saving writes that over the original. No option fixes it.dateis therefore a plain text field with a^\d{4}-\d{2}-\d{2}pattern, and a new post’s date is typed by hand. - ⚠ A collection lists only the files directly in its folder. Posts in subfolders don’t show up at all. Articles live in year folders, but Micropub writes new ones at the top level, so a
path: '{{year}}/{{slug}}'pattern would have hidden the new ones. The fix isnested: { depth: 2, subfolders: false }, where the depth counts path segments including the filename. That reaches the top level and the year folders and skips anything deeper, such as the gitignored-drafts/and the starter’s demo post. The 111 This Is My Jam posts injams/thisismyjam/got their own collection instead. One book in its own folder moved up intoreading/, and its URL stayed the same because it comes from the file name (see Permalinks and Obsidian-friendly filenames). - ⚠ The body is required by default. A post without text (many jams, likes, photos) couldn’t be saved until
bodygotrequired: false. Withomit_empty_optional_fields, an empty body is written as nothing after the closing---. - Object fields keep their undeclared subkeys. A photo’s
photo:is anobjectfield with alt, caption and the film details declared.photo.srcandphoto.downloadsare deliberately left out, and a save keeps them, sorted after the declared ones. Declaring them would rewrite the image path or retype the R2 byte counts, which must match the served file (see Hosting large originals off-repo). - Filenames of new posts. The default slug is lowercase with hyphens, and
lowercase: falseplus a space as the replacement keeps Title-Case names. Punctuation (commas, colons, apostrophes,?,&) still becomes a space, though, so a title with punctuation won’t match its filename. Existing files are never renamed.
Signing in
There are two ways in, both through GitHub:
- An access token is the narrowest: a fine-grained GitHub token limited to
pjedlund/jedee, with read and write access to its contents, pasted once into the browser. - “Sign In with GitHub” uses Netlify as the go-between, so the site needs no server of its own. It needs a GitHub OAuth App (callback
https://api.netlify.com/auth/done) installed as a provider in Netlify, and it works on the live site only. Sveltia sends the page’s host name as Netlify’s site ID. ⚠ The scope defaults torepo, which grants write access to every repository including private ones, so the config narrows it topublic_repo. That still covers every public repository, not just this one.
How changes are checked
Adding a field is checked before anything is saved for real. _local/tests/sveltia-roundtrip.mjs reproduces Sveltia’s first save for every post, using the same YAML library and settings and reading the field lists from admin/config.yml. It then checks what Eleventy reads back. It matched real saves from the admin byte for byte: a note, a photo, an RSVP, a jam and a year-folder article. Its --write-all mode rewrites every post for a full before/after build, and on the first run no page changed. Rerun it after adding any field. It still only warns on number, image and lists of objects, which the config doesn’t use yet.
Raw source: src/_raw/Getting Started Sveltia CMS.md, and the setup record _local/design/Reference - Sveltia CMS as the edit layer.md (§2, §4, §6), read on 2026-09-11.