diff --git a/news/how-this-portal-works.md b/news/how-this-portal-works.md index 40c5960..55cf2fe 100644 --- a/news/how-this-portal-works.md +++ b/news/how-this-portal-works.md @@ -1,6 +1,6 @@ --- title: "How this portal works: static pages, a visual editor, git as the database" -description: "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." +description: "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." date: 2026-09-11 category: demo author: Cyfronet @@ -9,66 +9,125 @@ draft: 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 +git repository. Editors work in a **visual editor in their 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. +reader and the content there is only a file server. 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](/uploads/diagrams/stack-overview.svg) +![The three parts of the stack: Sveltia as the editor, git as the database, Astro as the generator](/uploads/diagrams/stack-overview.svg) -**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. +**Sveltia CMS is the editor.** A single JavaScript file, served as an ordinary +page under `/admin/`. It signs the editor in to the git server and writes +Markdown files and images straight into the repository, with no CMS server and +no database behind it. 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. +fixed set of fields, such as title, date and cover image, and uploaded images +sit next to it. Full history, and restoring any earlier version in one step, +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. +into a directory of plain HTML, CSS and images, so deploying means copying that +directory onto a file server, and nothing from Astro runs in production. If an +article has a broken field the build stops, so a faulty article never reaches +readers. Interactive parts, a dashboard for example, can be added later without +changing the technology. ## From a save to a published page -![Five steps of publishing: save in the editor, commit in the repository, change detected, site build, published](/uploads/diagrams/publish-flow.svg) +![Five steps of publishing: save, commit, change detected, build, published](/uploads/diagrams/publish-flow.svg) 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**. +later, with no manual deployment step in between. If a build fails, the +previous version simply keeps serving. ## 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](/uploads/diagrams/architecture-machine.svg) +![The demo machine: Gitea with two repositories, a one-shot Astro build, the releases directory and nginx](/uploads/diagrams/architecture-machine.svg) -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. +Two repositories, by design: the portal **code**, maintained by the developers, +and the editorial **content**, written by the CMS. Each build takes the newest +version of both, so a change in either one republishes the site. Editors never +touch the code, and development never mixes with 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. +Everything runs in three containers on one machine: the git server, the web +server, and a build container that starts only after a change and exits when it +is done. 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. +same shape on purpose: the application is up around the clock, editors and +readers use that same application, and its login page 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](/uploads/diagrams/architecture-cms-database.svg) +![A database-backed CMS: one always-on application with a public login, plus a database and a media volume](/uploads/diagrams/architecture-cms-database.svg) + +## Why a static site + +![Attack surface compared: a classic CMS exposes an application, a login panel and a database, this solution exposes files only](/uploads/diagrams/attack-surface.svg) + +- **Security.** What faces the internet is a file server and a set of files: no + admin panel to attack, no database to steal, no application code needing + urgent security fixes. +- **Maintenance.** Nothing on the reader's side has to be watched and upgraded. + What is left is the git server, which the team runs anyway, and a short build + script. +- **Speed.** A ready-made HTML file is the fastest thing a server can hand over, + which search engines reward as well. +- **History.** Every version of every article is kept, and going back is a + single step. + +## How it compares with a database-backed CMS + +The dividing line is not whether content sits in a database or in files. It is +**whether a running application stands between the reader and the content**. The +portal should last at least six years, so the last row matters as much as the +first ones. + +| | 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 | +| In production you run | a file server | an application, usually with a database | +| Maintenance | a build script plus a git service | patches, upgrades, database backups | +| Change history | git, built in | product-dependent, sometimes paid | +| Day one | few moving parts, a visual editor ready to use; simple permissions, no editorial roles | a richer panel at once: roles, workflow, structured content; more parts to set up and secure | +| Over six years | nothing to patch on the reader's side; content in an open format outlives the tools | major upgrades, database backups, a permanent attack surface | + +Two products blur the line: **Grav** keeps content in files but still serves +every page with a PHP application, and **TinaCMS** keeps content in git but +needs a backend with a database to edit it. + +In the free self-hosted editions we checked, Strapi keeps content version +history for paid plans only, while WordPress and Grav carry long records of +vulnerabilities. Features can move into paid plans over time; content and +history in git leave nothing a vendor can put behind a paywall. + +## Isn't Gitea just another CMS? + +A fair question, because Gitea also has a login, a database and security +updates, so maintenance here is not zero either. Three things differ: + +- **It is off the reader's path.** Gitea can be down, mid-upgrade or even broken + into while the portal keeps serving its files. The worst case is vandalised + content: visible, recorded, undone with one command. In a CMS that serves the + site, the same incident means a compromised public site. +- **The team already runs it.** The content repository can live on the instance + the team maintains anyway, so the extra cost is close to zero. +- **It is smaller software.** Gitea is one program with a small database, and an + upgrade is a restart. CMS platforms are frameworks with plugins and regular + major migrations. + +## No lock-in + +The content is plain Markdown, so each part can be swapped on its own: Sveltia +for Decap (one line of configuration), Astro for another generator, Gitea for +any other git hosting. Leaving the approach entirely is a plain export. + +That is the conclusion from this demo: start with the simplest solution that +meets the requirements, a visual editor for the editors and a public site that +needs no care, and add complexity only when a need appears that this approach +cannot serve. ## Where things are @@ -79,113 +138,3 @@ application, and its login panel faces the internet. | 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](/uploads/diagrams/attack-surface.svg) - -- **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.