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 Writer Credentials in Production Only
- 9. Validate and Publish Directly 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.
Updated July 28, 2026: Acecore now publishes CMS saves as validated direct commits to
main. GitHub OAuth verifies the editor and current write permission, while a repository-specific GitHub App performs repository operations. The proxy synchronously validates JSON and Markdown schemas, image signatures, active HTML and URLs, and the expected HEAD before writing.
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 saves should start publication immediately while code changes remain protected by pull requests
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 content, then writes one expected-HEAD cms: commit to main
.github/workflows/create-translation-prs.yml
-> creates translation PR tasks only for direct 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 every save, the proxy freshly verifies the GitHub user’s write permission, then uses an acecore-net-specific GitHub App for repository access. It validates changed paths, JSON and Markdown schemas, image signatures, active HTML and URLs, and the latest main HEAD before creating one direct cms: commit.
As of July 28, 2026, Editorial Workflow is not implemented in Sveltia CMS. Acecore does not depend on Decap CMS’s publish_mode: editorial_workflow; the same-origin proxy enforces scope, content validity, and conflicts.
A permanent branch such as cms-content creates ongoing synchronization, conflict, and deployment-source risks. Acecore keeps main as the only source of truth and rejects concurrent updates through expectedHeadOid.
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 Writer Credentials in Production Only
Configure the GitHub App client ID, installation ID, and private key only in the Cloudflare Pages production environment. Do not distribute writer credentials to previews; repository reads and writes remain disabled there. Editors save and publish from the production /admin/, while Pages previews remain for normal code and CMS-configuration pull requests.
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',
},
},
})
The production publication branch stays fixed to main, and the proxy rejects requests based on any other branch. Preview configuration still displays main for consistency checks, but cannot save without writer credentials.
9. Validate and Publish Directly with a Save Proxy
Treat CMS Save as the start of publication. A same-origin proxy synchronously validates the allowed scope and content, then creates exactly one commit on main.
backend:
name: github
repo: owner/repository
branch: main
api_root: /admin/api/github
graphql_api_root: /admin/api/graphql
The proxy does not blindly forward GraphQL createCommitOnBranch. GitHub OAuth freshly verifies the editor and write permission, while a short-lived installation token from an acecore-net-specific GitHub App performs repository reads and writes. Only allowlisted content and images are accepted; JSON and Markdown schemas, image magic bytes, and active HTML or URLs are checked before the commit. SVG and PDF uploads are rejected.
The save uses the editor’s starting HEAD as expectedHeadOid, so a concurrent update returns 409 instead of being overwritten. If GitHub’s response is lost, recovery succeeds only when the request marker, parent SHA, complete path set, and blob SHAs all match.
Direct commits keep subjects such as cms: create ... and cms: update .... The GitHub App push starts Cloudflare Pages deployment and the translation workflow in parallel. Code, schemas, workflows, CMS configuration, and translation files remain outside the CMS allowlist and still require pull requests and CI.
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. Only the direct-publish proxy should produce that prefix; using it outside the CMS flow can trigger unnecessary workflows.
6. Do Not Switch the Publication Branch by Environment
Giving a preview deployment a writer credential that can bypass the ruleset creates another route into production main. Restrict editing and saving to the production /admin/, then confirm the production deployment after saving. Use Cloudflare Pages previews without writer credentials for normal code or configuration pull requests.
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, synchronous direct-publish validation, 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, editor authentication, repository actor, editable content, media, and publication 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 direct commits, Pages deploys, 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 validates content and writes one direct commit to main
- 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