Skip to content
Acecore

Sveltia CMS Setup Guide

by Gui
Table of Contents
Sveltia CMS Setup Guide

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.

collectionTargetPolicy
blogsrc/content/blog/*.mdEdit only Japanese source articles
authorssrc/content/authors/*.jsonEdit author profiles and localized names
tagssrc/content/tags/*.jsonEdit tag names and localized labels
page textsrc/i18n/source/ja/**/*.jsonEdit 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

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.

Before and after adding the CMS

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
Setup checklist
  • 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
Frequently asked questions
What kind of site is Sveltia CMS good for?
It works well for static sites where Markdown or JSON lives in the repository, such as Astro, Hugo, and VitePress projects. You can add a CMS without adding an external database.
Can I use only a GitHub Personal Access Token?
Yes, but for multiple editors or non-engineers, an OAuth Worker is safer and easier to explain. Acecore uses a Cloudflare Worker as the OAuth client and sets it as backend.base_url.
Should every locale be editable in the CMS?
For a small team, it is safer to edit only the Japanese source in the CMS and update translations through pull requests. Exposing every locale makes review and stale-translation detection harder.

Comments

Loading comments...

Links, email addresses, and promotional text cannot be posted.

G

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.

Business problem framingTechnology selectionSystem designC#/.NETDatabase/infrastructure designGitHub development operationsGenerative AIAI workflow designQuality designOn-site integration

Want to learn more about our services?

We provide comprehensive support including system development, web design, server operations, and graphic design.

Related Posts

Search this site

After you enter at least two characters, Related content automatically sends your search terms to Cloudflare Workers AI. Do not enter personal or confidential information.How search data is handled