DEV Community

Art
Art

Posted on

A CMS with no database and no build step: your site is just a Git repo


Recently Angela, a musician, sent me the link to her new website: music.angela-mahr.de. She built it herself. She didn't use a page builder or a WordPress theme. She told Claude Code what she wanted, and it wrote the pages for ForgeCMS, the small CMS I've been building since March. Then she built a second one: lebe-dich.info.

That's when I realized the design decisions behind ForgeCMS might be interesting to other developers too. This post explains how it works and why it's built this way.

The whole architecture in one line

Browser → Caddy (TLS) → forgecms → Codeberg (SML + Markdown)
                                   ↘ local cache
Enter fullscreen mode Exit fullscreen mode

ForgeCMS is a single Go binary. Your site is a folder in a public Git repo: pages in SML, longer texts in Markdown, images in static/. When a request comes in, ForgeCMS fetches the files it needs from Codeberg, caches them, and renders HTML.

That means:

  • No database. Your history, backups and reviews all happen in Git.
  • No build step. Commit a change (even in Codeberg's web editor), and it's live within a few minutes. There's no CI pipeline and nothing to regenerate.
  • No admin backend to patch, and no plugin ecosystem to keep compatible.
  • One process, many domains. Put one config per domain in sites/<domain>/app.sml, and ForgeCMS switches to multi-site mode. Our production box serves eight sites from one process.

The dependencies are the Go standard library, goldmark for Markdown (with chroma for code highlighting), and sml-go for the page language.

Pages in SML, not JSON or YAML

Pages are written in SML: elements with properties and children. Here's a complete page:

Page {
    title: "My Bakery"

    Heading { level: "1" text: "Fresh bread, every morning" }
    Markdown { src: "main.md" }

    Cards {
        Card { title: "Sourdough" text: "Baked daily from a 12-year-old starter." }
        Card { title: "Opening hours" text: "Tue–Sat, 7:00–13:00" }
    }

    ContactForm { }
}
Enter fullscreen mode Exit fullscreen mode

You don't need HTML, CSS or JavaScript. SML is also a format that language models get right on the first try. It has no indentation rules to break, no quoting puzzles like YAML, and no trailing-comma errors like JSON. Every element is written the same way, so a model that has seen three examples can write a fourth.

That's the real reason Angela could build her site. The repo contains an AI.md that explains the whole system to a coding agent: the elements, the folder layout, how languages work, and how to deploy. You point Claude Code at it and describe the site you want.

"Why not a static site generator?"

I get this question a lot, and it's a fair one. Hugo and friends are great. What bothered me is the part between "I changed a sentence" and "it's live": a build, a pipeline, an artifact, a deploy. For the people I build sites for (small associations, therapists, musicians), that pipeline is exactly where things break and where they need me.

With ForgeCMS the repo is the deployment. Anyone who can edit a file on Codeberg can update their site. If they break the syntax, the last good version keeps being served. A typo never takes the site down.

What I learned the hard way: caching against a Git host

Fetching content from Codeberg at request time sounds fragile, and the first version was. On September 25, after a routine restart, a burst of bot traffic made ForgeCMS fetch the same 404.sml about 800 times. Codeberg allows 250 raw-file requests per 10 minutes per IP. We got a 429 with Retry-After: 600, and three sites returned 503 for about 20 minutes.

The fix was three rules that I now think every "fetch from origin" system needs:

  1. Revalidate at most every 5 minutes per file, no matter how many requests come in.
  2. Use one shared backoff for the whole origin, and respect Retry-After. When Codeberg says "wait", every file waits, not just the one that got the 429.
  3. Always serve the stale copy when the origin fails. A slightly old page is better than an error page.

With those rules, Codeberg can have a bad day and visitors don't notice. The same fallback also works for static files: if an image isn't deployed on the server, ForgeCMS fetches it from the repo's static/ folder. This morning I added the banner at the top of this post to our developer page with nothing but a git push.

Modules for the boring-but-necessary parts

The core covers pages, menus, themes, several languages per site (a de/, es/, … subfolder with fallback), per-page SEO descriptions and embeds. The things that need server-side state are modules in the same binary. You switch them on with a block in app.sml:

  • Blog with photos, Atom feed and share buttons without trackers (live)
  • Events with flyers, event pages and .ics export (demo)
  • Contact form and newsletter with form fields defined in SML, double opt-in and mailings written in Markdown (live)
  • Shop paid in Ğ1 or euros, with digital delivery
  • Feature requests with voting and magic-link login (live)

Whatever these modules need to store, like orders or subscribers, is written as small SML files on the server. There's still no database.

Quickstart

You need Go 1.22 or newer and a public repo on Codeberg for your content.

git clone https://codeberg.org/CrowdWare/ForgeCMS.git
cd ForgeCMS
go build -o forgecms .

# point the Site { } block at your content repo
cp app-demo.sml app.sml

./forgecms --port 8080
Enter fullscreen mode Exit fullscreen mode

Put Caddy in front for HTTPS and you're done. For a real multi-site setup, our production repo is public: CrowdWare/sites contains the content, the per-domain configs and the deploy script for every site mentioned here. The page describing ForgeCMS for developers is one SML file in that repo.

License

ForgeCMS is free under the GPLv3 for non-commercial use. If you make money with it, for example by hosting sites for paying customers, there's a commercial license. Profits aren't paid out to individuals. They go into UBUNTU land projects.

Try it, break it, tell me

Everything in one place: crowdware.info/forgecms/developers

I'd especially like to hear from people who run sites for others: what would you need before you'd move a client off WordPress?

Top comments (0)