Table of Contents
- When Sveltia CMS Fits
- Overall Architecture
- 1. Place the Admin App Under public/admin
- 2. Configure the GitHub Backend
- 3. Add an OAuth Worker
- 4. Decide the Media Folder Early
- 5. Split Editable Content Into Collections
- 6. Use Relation and Select Widgets
- 7. Make Japanese Source JSON Editable
- 8. Keep main as the Publication Branch in Preview
- 9. Create Short-Lived Branches with a Save Proxy
- 10. Trigger Translation Only for CMS Commits
- 11. Give /admin Its Own CSP
- Keep Turnstile Separate
- Lessons From Pull Requests and Commits
- 1. Update Articles When the CMS Changes
- 2. Do Not Postpone OAuth
- 3. Fix Media Paths Before Editors Upload Images
- 4. Expand CMS Fields Gradually
- 5. Treat Commit Subjects as an API
- 6. Do Not Switch the Publication Branch by Environment
- Minimal Starting Point
- References
- Summary

Sveltia CMS is a good fit when you want to add an editing screen to a static site without moving content into an external database. This guide explains how we introduced it to the Acecore Astro site and what we fixed later after real pull requests and commits exposed operational gaps.
The title is intentionally simple: Sveltia CMS Setup Guide. This is not a CMS comparison article. It is a practical checklist for people who want to add Sveltia CMS to their own site.
When Sveltia CMS Fits
Sveltia CMS is not a CMS that owns your database and serves content through a separate API. It is a single-page admin app that edits files in your Git repository through a backend such as GitHub.
It is a good match when:
- your site stores content as Markdown or JSON in the repository
- you want article, author, tag, and page-text changes to remain reviewable as Git diffs
- you do not want to add a database or a separate admin service
- uploaded images can live under a folder such as
public/uploads - CMS edits should still go through pull requests before production
If you need complex editorial permissions, large asset management, scheduled publishing workflows, or real-time data editing, a full headless CMS or a custom admin app may be a better fit.
Overall Architecture
Acecore runs Sveltia CMS with this structure:
public/admin/index.html
-> loads @sveltia/cms from a CDN
public/admin/config.yml
-> defines the GitHub backend, editable collections, and media folders
workers/sveltia-cms-auth
-> Cloudflare Worker for GitHub OAuth
main branch
-> the single source of truth for production
CMS save proxy
-> validates allowed paths and creates a short-lived branch and pull request for each edit
.github/workflows/create-translation-prs.yml
-> creates translation PR tasks only for cms: commits
The first lesson is that installing the admin app is only the beginning. Authentication, media paths, preview branches, translations, and merge strategy all become part of the CMS design.
1. Place the Admin App Under public/admin
In Astro, files under public are served as static assets. The Sveltia CMS docs also list public as the static folder for Astro, Next.js, Nuxt, Remix, and VitePress projects.
A minimal page looks like this:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="robots" content="noindex,nofollow" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CMS</title>
</head>
<body>
<script src="https://unpkg.com/@sveltia/cms@0.172.4/dist/sveltia-cms.js"></script>
</body>
</html>
Do not add an extra stylesheet or type="module" unless you have a specific reason. Sveltia CMS bundles its UI styles in the JavaScript file, and the current CDN build is loaded as a normal script.
Acecore uses manual initialization so the backend can be overridden explicitly. The publication branch remains main in every environment.
<script src="/admin/runtime-config.js"></script>
<script src="https://unpkg.com/@sveltia/cms@0.172.4/dist/sveltia-cms.js"></script>
<script src="/admin/init.js"></script>
CMS.init({
config: {
backend: {
branch: 'main',
},
},
})
2. Configure the GitHub Backend
The minimal GitHub backend needs backend.name and backend.repo. In production, you should also decide the branch, OAuth endpoint, and commit messages up front.
backend:
name: github
repo: owner/repository
branch: main
base_url: https://your-sveltia-cms-auth-worker.example.workers.dev
api_root: /admin/api/github
graphql_api_root: /admin/api/graphql
auth_methods: [oauth]
commit_messages:
create: 'cms: create {{collection}} "{{slug}}"'
update: 'cms: update {{collection}} "{{slug}}"'
delete: 'cms: delete {{collection}} "{{slug}}"'
uploadMedia: 'cms: upload "{{path}}"'
deleteMedia: 'cms: delete media "{{path}}"'
Keep main as the publication branch and route reads and saves through a same-origin API proxy. Before opening a short-lived branch and pull request, the proxy validates the repository, the GitHub user’s write permission, changed paths, and the latest main HEAD.
As of July 20, 2026, Editorial Workflow is not implemented in Sveltia CMS. Adding Decap CMS’s publish_mode: editorial_workflow setting does not make Sveltia CMS create short-lived branches or pull requests.
A permanent branch such as cms-content creates ongoing synchronization, conflict, and deployment-source risks. Closing a short-lived branch with each pull request keeps main as the only source of truth.
3. Add an OAuth Worker
A Personal Access Token is enough for a quick local test. It is not ideal when non-engineers or multiple editors need access.
Acecore uses Sveltia CMS Authenticator on Cloudflare Workers and points backend.base_url to that Worker.
backend:
name: github
repo: acecore-systems/acecore-net
branch: main
base_url: https://sveltia-cms-auth.example.workers.dev
api_root: /admin/api/github
graphql_api_root: /admin/api/graphql
auth_methods: [oauth]
The GitHub OAuth App callback points to the Worker’s /callback URL. The Worker receives GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, and optionally ALLOWED_DOMAINS as environment variables.
This is separate from bot protection. Turnstile belongs around contact forms or comment APIs. CMS login should be handled through GitHub OAuth.
4. Decide the Media Folder Early
Sveltia CMS internal media storage writes uploaded assets into the repository. In Astro, public assets should usually live under public.
media_folder: public/uploads
public_folder: /uploads
If this is not explicit, media can end up relative to a content folder, or the saved file path can diverge from the public URL referenced by Markdown.
Acecore later fixed this in PR #116. The lesson is simple: decide the storage path and public URL together when the CMS is introduced.
We also keep external images and uploaded images as separate frontmatter fields.
- name: image
label: External image URL
widget: string
required: false
- name: uploadedImage
label: Uploaded image
widget: image
required: false
5. Split Editable Content Into Collections
The most important design decision is not the CMS itself, but which files are editable through it.
| collection | Target | Policy |
|---|---|---|
blog | src/content/blog/*.md | Edit only Japanese source articles |
authors | src/content/authors/*.json | Edit author profiles and localized names |
tags | src/content/tags/*.json | Edit tag names and localized labels |
| page text | src/i18n/source/ja/**/*.json | Edit Japanese source text for pages and shared UI |
The key is to avoid exposing all translated Markdown files in the CMS. For a nine-language site, that makes it harder to know which language is current and which translation should follow a Japanese source change.
Acecore treats Japanese content as the source of truth and sends translations through How to Run a Multilingual Blog with Sveltia CMS.
6. Use Relation and Select Widgets
The more free text fields you expose, the more small mistakes you will get.
Tags are relation fields, not manually typed strings.
- name: tags
label: Tags
widget: relation
collection: tags
value_field: name
display_fields: ['{{name}} ({{id}})']
search_fields: [name, id]
multiple: true
required: false
Authors use the same pattern. Announcement tone, link-card icons, and other constrained values are select fields.
This was improved over time. A CMS should not merely make editing possible; it should make invalid values hard to enter.
7. Make Japanese Source JSON Editable
CMS editing is also useful for fixed page text. Acecore stores Japanese page text under src/i18n/source/ja/**/*.json and exposes it in page-oriented groups.
The lesson from implementation was to avoid adding every field at once. After making page text editable, we had to stabilize existing value loading and label organization. Start with high-change areas like the blog, authors, tags, announcements, and key landing pages, then expand.
For multilingual sites, do not let CMS edits directly rewrite translation files. Keeping Japanese source edits and translation pull requests separate makes review much easier.
8. Keep main as the Publication Branch in Preview
The save proxy creates every short-lived branch from the latest main, so a Cloudflare Pages preview must not replace the backend branch with its PR branch. Use the generated CMS pull request’s Pages preview to inspect the result, while every edit starts from the production source of truth.
Acecore generates public/admin/runtime-config.js before builds to enable manual initialization only:
await writeFile(
'public/admin/runtime-config.js',
'window.CMS_MANUAL_INIT = true;\n',
'utf8',
)
Then init.js overrides only the backend branch.
CMS.init({
config: {
backend: {
branch: 'main',
},
},
})
This prevents production and preview from using different save bases, and lets the proxy reject any request whose base is not main.
9. Create Short-Lived Branches with a Save Proxy
Do not publish CMS saves directly. A same-origin save proxy creates a short-lived branch and pull request for each edit.
backend:
name: github
repo: owner/repository
branch: main
api_root: /admin/api/github
graphql_api_root: /admin/api/graphql
When it receives GraphQL createCommitOnBranch, the proxy does not commit to main. It combines allowed content and media in one commit, creates a cms/acecore/* branch from the latest main, and opens a pull request. Delete the branch automatically after merge so no permanent secondary source remains.
In a GitHub OAuth setup, the editor’s token provides identity and acts as the GitHub writer. In a Cloudflare Access setup, Access can provide identity while a site-specific GitHub App acts as the writer. The authentication mode can differ while path restrictions, short-lived branches, pull requests, and CI remain common policy.
The merge method matters. Acecore’s translation task detection relies on commit subjects such as cms: create ... and cms: update .... If a CMS PR is squash merged and those subjects disappear, the translation workflow may not detect the source change. For CMS PRs, keep the cms: commits through merge commit or rebase merge.
10. Trigger Translation Only for CMS Commits
If every Japanese source change triggers translation tasks, normal development commits can create unnecessary translation PRs.
PR #98 added the --cms-only guard so push-triggered translation tasks are created only for CMS commit subjects.
function isCmsCommitSubject(subject) {
return /^cms: (create|update|delete) /.test(subject || '')
}
This makes the commit subject part of the workflow contract. Normal code or article PRs should not use the cms: prefix. Only the CMS save flow should produce it.
11. Give /admin Its Own CSP
The admin screen needs different external connections than public pages: the Sveltia CMS CDN, GitHub API, the OAuth Worker, blob URLs, and status endpoints.
Acecore gives /admin/* a separate CSP and also marks the admin area as noindex.
/admin/*
X-Robots-Tag: noindex, nofollow
Content-Security-Policy: default-src 'self'; script-src 'self' https://unpkg.com; connect-src 'self' blob: data: https://unpkg.com https://api.github.com https://www.githubstatus.com https://sveltia-cms-auth.example.workers.dev; frame-ancestors 'self'
Keep Turnstile Separate
An older version of this article mixed CMS selection and Cloudflare Turnstile in one story. That was confusing.
Sveltia CMS setup is about GitHub backend, OAuth, collections, media paths, and pull request operations. Turnstile is about protecting contact forms or comment APIs from bot submissions. They both support safer operations, but they live in different layers and deserve separate articles.
Lessons From Pull Requests and Commits
The biggest lessons came after the first setup landed.
1. Update Articles When the CMS Changes
The site moved from an older Pages CMS assumption to Sveltia CMS, but the article text did not keep up. When the implementation changes, related articles, screenshots, and internal link text should be audited together.
2. Do Not Postpone OAuth
PAT-based testing is fine, but CMS exists so more people can edit content. OAuth should be part of the real setup, not a future improvement.
3. Fix Media Paths Before Editors Upload Images
Changing media paths later means auditing existing Markdown references and generated output. For Astro, public/uploads and /uploads are a practical default.
4. Expand CMS Fields Gradually
Putting every page text field into config.yml at once makes the config hard to review and maintain. Start with content that changes often, then add lower-frequency page text later.
5. Treat Commit Subjects as an API
cms: is not cosmetic. It is an input to automation. Using it outside the CMS flow can trigger unnecessary workflows; removing it from CMS merges can stop required workflows.
6. Do Not Switch the Publication Branch by Environment
Using a preview deployment branch as the CMS publication branch creates another source line and complicates proxy validation. Start every edit from main, then inspect the saved result in the pull request’s Cloudflare Pages preview.
Minimal Starting Point
For a new Astro site, start with this shape:
public/admin/index.html
public/admin/config.yml
public/admin/init.js
public/admin/runtime-config.js
backend:
name: github
repo: owner/repository
branch: main
base_url: https://your-auth-worker.example.workers.dev
api_root: /admin/api/github
graphql_api_root: /admin/api/graphql
auth_methods: [oauth]
commit_messages:
create: 'cms: create {{collection}} "{{slug}}"'
update: 'cms: update {{collection}} "{{slug}}"'
delete: 'cms: delete {{collection}} "{{slug}}"'
media_folder: public/uploads
public_folder: /uploads
collections:
- name: blog
label: Blog
folder: src/content/blog
slug: '{{fields._slug}}'
fields:
- { name: title, label: Title, widget: string }
- { name: description, label: Description, widget: text }
- { name: date, label: Published date, widget: datetime }
- { name: author, label: Author, widget: string }
- { name: body, label: Body, widget: markdown }
From there, add author relations, tag relations, uploaded images, source JSON editing, CMS PR automation, and translation PR tasks in that order.
References
- Sveltia CMS Getting Started
- Sveltia CMS GitHub Backend
- Sveltia CMS Editorial Workflow (not implemented)
- Sveltia CMS Internal Media Storage
- Sveltia CMS Manual Initialization
- Sveltia CMS Authenticator
Summary
Sveltia CMS is easy to place under public/admin, but a production setup needs more than an admin page.
Decide the save branch, OAuth flow, media folders, source-language policy, translation workflow, and merge strategy. Once those are explicit, a static Astro site can stay lightweight while gaining a usable editing workflow.
For dynamic AI features, see Adding an AI Chat to an Astro Site. For translation automation, see How to Run a Multilingual Blog with Sveltia CMS. The CMS is the content-update foundation that makes both workflows easier to operate.
Sveltia CMS setup flow
Treat the admin app, authentication, editable content, media, and pull request flow as separate design decisions.
Add the admin app
Place index.html and config.yml under public/admin and load the Sveltia CMS bundle.
Configure GitHub
Decide the repo, branch, OAuth Worker, and CMS commit messages before editors start saving content.
Limit editable scope
Expose only the blog, authors, tags, and Japanese source JSON files that should be edited through the CMS.
Automate operations
Use main as the publication branch and connect validated save-proxy branches, CMS edit PRs, and translation PR tasks.
Editing Markdown by hand
- Only people comfortable with GitHub or an editor can update content
- Image paths, author IDs, and tag names are typed manually
- Japanese source changes and translated files are easy to mix
- The save target and writable paths can be unclear
Editing with Sveltia CMS
- Markdown and JSON can be edited from a browser form
- relation, image, and select widgets reduce broken values
- Only CMS commits can trigger translation PR tasks
- A same-origin proxy writes only allowed paths to a short-lived branch and PR
- Done: Load Sveltia CMS from public/admin/index.html
- Done: Define the GitHub backend and collections in public/admin/config.yml
- Done: Use an OAuth Worker for multi-user editing
- Done: Align media_folder and public_folder with Astro's public directory
- Done: Decide how CMS commits trigger translation or publishing workflows
What kind of site is Sveltia CMS good for?
Can I use only a GitHub Personal Access Token?
Should every locale be editable in the CMS?
Comments
Gui
CEO of Acecore. Leads web, databases and infrastructure, quality assurance, and AI adoption from business problem framing through design, rollout, and post-launch improvement. Builds on hands-on C#/.NET capability while also covering PHP/JavaScript, SQL Server/PostgreSQL/MySQL, and Linux/Windows Server, designing requirements, technology choices, quality standards, and GitHub-based development operations as one coherent workflow. Uses generative AI across development, verification, and information organization, treating it as practical infrastructure that helps small teams deliver faster and more reliably.
Want to learn more about our services?
We provide comprehensive support including system development, web design, server operations, and graphic design.
Related Posts
Designing an Astro + Cloudflare Website That Can Grow Feature by FeatureJune 7, 2026 at 07:00 PM
Build Astro Blog Comments with Cloudflare OnlyJune 7, 2026 at 06:00 PM
What Cloudflare’s Former Paid SSL Option Really Is — From Dedicated SSL to Advanced Certificate ManagerMarch 31, 2026