Files
content/news/how-this-portal-works.md
T

11 KiB

title, description, date, category, author, cover, draft
title description date category author cover draft
How this portal works: static pages, a visual editor, git as the database The architecture behind this demo: a visual editor in the browser, content as Markdown in git, and a public site that is nothing but static files. With an honest comparison against database-backed CMS platforms. 2026-09-11 demo Cyfronet /uploads/diagrams/cover-git-cms.svg false

This portal is a set of static HTML files generated from Markdown kept in a git repository. Editors get a visual editor in the browser, and still nothing on the public side runs an application or a database: between the reader and the content there is only a file server. The one service that does run, the git server, stands behind the editor and off the reader's path, and we come back to that honestly below. This article was written and published through exactly the path it describes.

The stack in three parts

The three parts of the stack: Sveltia CMS as the editor, git and Gitea as the database, Astro as the build-time generator, with only nginx and static files involved when a reader opens a page

Sveltia CMS is the editor. It is one JavaScript file served as a static page under /admin/. It signs the editor in to Gitea and writes Markdown files and images straight into the repository through the git API. There is no CMS server and no database behind it, and every save is a commit.

Git, served by Gitea, is the database. An article is a Markdown file with a defined set of fields, and uploaded images sit next to it. History, diffs and a one-command rollback come from git itself rather than from a paid tier of a product.

Astro is a generator, not a server. Its build turns those Markdown files into a directory of plain HTML, CSS and images, so deployment is copying that directory onto a file server. Nothing from Astro runs in production. Pages ship without JavaScript by default, while components, React among them, are rendered to HTML during the build, so an interactive dashboard can later be added as an island on the same site without changing the stack. The build also checks every article against the defined fields, which means a malformed article stops the build instead of reaching the public site.

On the reader's side that leaves one moving part: nginx handing over files.

From a save to a published page

Five steps of publishing: save in the editor, commit in the repository, change detected, site build, published

Publishing is automatic. A save in the editor shows up on the site moments later, with no manual deployment step in between. Every change is a commit, so a full content history and a one-command rollback come for free.

How it runs on the machine

Architecture on the demo machine: the editor writes through Sveltia into the Gitea content repository, a systemd timer runs a one-shot Astro build container, the result lands in the releases volume and nginx serves readers static files only

Two repositories, by design. The portal code is developed by the team, the editorial content is written by the CMS. Every build takes the newest commit from both, overlays content onto code and renders the site, so a push to either repository republishes. Editors never touch the code repository, and development never mixes with the editorial history.

Everything runs rootless in podman on a single machine: a Gitea container, an nginx container, and a one-shot build container started by a systemd timer. A build that fails changes nothing, because the previous release keeps serving.

For comparison, the same machine running a classic CMS. The diagram keeps the same skeleton on purpose, so the difference is visible at a glance: every element is up around the clock, the editor and the reader talk to the very same application, and its login panel faces the internet.

Architecture of a database-backed CMS: an application with a public admin panel and on-demand rendering runs non stop, next to a database that needs backups and a separate media volume

Where things are

What Where
Portal this site
Content editor /admin/, the "Content editor" link in the footer
Content repository ctao/content on the demo machine's Gitea, written by the CMS
Code repository ctao/portal, maintained by the team
Deployment documentation deploy/README.md in the code repository

Why a static site

Attack surface compared: a database-backed CMS exposes an application, a login panel, a database and plugins, while this solution exposes static HTML files only and keeps editing behind a git sign-in

  • Security. What faces the internet is a file server and a directory of files. There is no admin panel to attack, no database to exfiltrate and no application runtime carrying its own vulnerabilities.
  • Maintenance. There is no application that has to be monitored and upgraded under patch pressure. What remains is a git service, which the team runs anyway, and a short build script.
  • Speed. Static HTML is as fast as a page gets, which search engines reward as well.
  • History. Versioning is a property of git, not a feature tier of a product.

Two classes of solution

The dividing line is often misread. The question is not whether content sits in a database or in files. It is whether a running application stands between the reader and the content.

Static + git CMS (this demo) CMS that serves the site itself
Examples Sveltia, Decap (+ Astro / Hugo / Eleventy) Strapi, WordPress, Drupal, Grav, TinaCMS
Content lives in Markdown files in git usually a database; Grav in files, Tina in git
In production you run a file server an application, usually with a database, non stop
Maintenance a build script plus a git service patches, upgrades, database backups
Change history git, built in product-dependent, sometimes paid

Two clarifications, so the table is not read too simply. Grav keeps content in files with no database at all, yet a PHP application still serves every page, which puts it in the right-hand column for attack surface and maintenance. TinaCMS keeps content as Markdown in git, but its editor needs a backend with a database in order to work, so it inherits the costs of both worlds.

Limits of the individual products, verified during our research:

  • Strapi: content version history does not exist in the free self-hosted edition (Growth and Enterprise plans only). It needs Node and PostgreSQL running at all times, plus regular upgrades.
  • WordPress: the largest ecosystem, and also the longest vulnerability history, mostly through plugins; a public login panel and PHP to patch continuously.
  • Grav (PHP, flat file, the "light" alternative): serious remote code execution issues in recent years, and still a PHP server facing the internet.
  • TinaCMS: it edits Markdown, but it requires a backend. The Tina cloud is free for up to two users, and self-hosting means your own Node server, a database and authentication, which is an application to maintain again.
  • Open-core risk: features can move into paid plans over time, as above. When the content and its history live in git, there is no feature a vendor can move behind a paywall.

Trade-offs over six years

The portal is meant to be maintained for at least six years, so the balance has to be drawn over two horizons. What follows judges the two approaches as classes of solution, not individual products.

Short term: launch and the first months

Static + git CMS with a database
Strengths few moving parts from day one; a visual editor ready to use; history and rollback immediately, in git a richer panel out of the box: roles and permissions, editorial workflow, relations between content types, more ready-made integrations
Weaknesses simple permissions, inherited from git, without editorial roles; browsing history happens in the git interface rather than in the editor more components to stand up, connect and secure from day one: application, database, backups

Over six years

Static + git CMS with a database
Strengths no application on the reader's path, so no patch pressure there; what stays is a git service, ideally the one the team already runs; content in an open format survives any change of tooling if application features are eventually needed (accounts, personalisation, strongly structured content), the platform already has them
Weaknesses at very large scale (thousands of pages) build times grow and need attention; advanced editorial features depend on the pace of open-source tools several major version migrations in six years, with breaking changes; the database needs backups and restore drills; a permanent attack surface; features can move into paid tiers

"Isn't Gitea just another CMS?"

A fair question. Gitea also has a login, a database and security updates, and it has to be reachable by editors. Does the difference come down to maintaining Gitea instead of a CMS?

Partly yes, and we do not claim that maintenance is zero. The difference rests on three things:

  • Gitea is off the reader's path. It can be down, mid-upgrade, or even compromised while the portal keeps serving safe files. The worst case of a break-in is content vandalism: visible, fully recorded, and undone with a single revert. In a CMS that serves the site, the same incident means a compromised public site.
  • The team already runs Gitea. The content repository can live on the instance the team maintains anyway, which brings the marginal cost close to zero and lets one AAI integration handle sign-in.
  • Class of software. Gitea is a single Go program with SQLite: an upgrade is swapping an image and restarting, with no plugin ecosystem and no server-side npm dependency tree. CMS platforms are application frameworks with regular major-version migrations.

No lock-in

Every element can be replaced on its own, because the content is plain Markdown:

  • Sveltia to Decap: the same configuration format, so switching is one script tag.
  • Astro to Hugo or Eleventy: the Markdown stays untouched, only templates change.
  • Gitea to any git hosting: GitHub, GitLab, or an internal service.
  • Even dropping the whole approach is a trivial export, because the content has been sitting in an open format in our own repository from the start.

The conclusion we draw from this demo: start with the simplest solution that meets the requirements, which means a visual editor for the editors and a public site that needs no care. Add complexity only when a need appears that this approach cannot serve.