demo article in English: stack overview and redesigned architecture diagrams

This commit is contained in:
2026-09-11 13:58:49 +02:00
parent 43e2bb4061
commit 9ca89f3e40
13 changed files with 573 additions and 524 deletions
+3 -3
View File
@@ -1,18 +1,18 @@
# CTAO Science Portal — content # CTAO Science Portal: content
Editorial content of the CTAO Science Portal, separated from the portal code Editorial content of the CTAO Science Portal, separated from the portal code
so editorial history stays clean and publishing never mixes with development. so editorial history stays clean and publishing never mixes with development.
| Path | What | | Path | What |
|---|---| |---|---|
| `news/*.md` | Articles — Markdown + frontmatter | | `news/*.md` | Articles: Markdown with frontmatter |
| `pages/*.md` | Static pages (contact, privacy, disclaimer) | | `pages/*.md` | Static pages (contact, privacy, disclaimer) |
| `uploads/` | Editor-uploaded media (the CMS converts images to WebP) | | `uploads/` | Editor-uploaded media (the CMS converts images to WebP) |
## How to edit ## How to edit
Editors use the visual editor at the portal's `/admin/` page (sign in with a Editors use the visual editor at the portal's `/admin/` page (sign in with a
Gitea account) — every save is a commit here, and the site republishes Gitea account). Every save is a commit here, and the site republishes
automatically within seconds. Direct git commits work exactly the same way. automatically within seconds. Direct git commits work exactly the same way.
The portal code lives in the `ctao-portal` repository; its build overlays The portal code lives in the `ctao-portal` repository; its build overlays
-175
View File
@@ -1,175 +0,0 @@
---
title: "Jak działa to demo: portal statyczny + CMS oparty o git"
description: "Architektura rozwiązania, przydatne linki i porównanie klas rozwiązań: strona w 100% statyczna z graficznym edytorem treści vs CMS z własną bazą danych."
date: 2026-07-23
category: demo
author: Cyfronet
cover: /uploads/diagrams/cover-git-cms.svg
lang: pl
draft: false
---
Ten portal to **statyczne pliki HTML** generowane z plików Markdown trzymanych
w repozytorium git. Redaktor dostaje **graficzny edytor w przeglądarce** —
a mimo to **po stronie publicznej** nie działa żadna aplikacja ani baza:
między czytelnikiem a treścią jest wyłącznie serwer plików. Jedyny działający
serwis (Gitea) obsługuje edycję i stoi poza ścieżką czytelnika — wracamy do
tego uczciwie niżej. Ten artykuł został napisany i opublikowany dokładnie tą
ścieżką.
## Od zapisu do publikacji
![Pięć kroków publikacji: zapis w edytorze → commit w repo → detekcja zmiany → build strony → publikacja](/uploads/diagrams/od-zapisu-do-publikacji.svg)
Publikacja jest automatyczna — zapis w edytorze po chwili sam pojawia się
na stronie, bez żadnego ręcznego deployu. Każda zmiana to commit, więc
**pełna historia treści i możliwość cofnięcia są w gicie za darmo**.
## Jak to wygląda na maszynie
![Architektura na maszynie demo: redaktor zapisuje przez Sveltię do Gitei, timer systemd uruchamia jednorazowy kontener builda Astro, wynik trafia do wolumenu releases, a nginx serwuje czytelnikom wyłącznie statyczne pliki](/uploads/diagrams/architektura-maszyna.svg)
Dla porównania — ta sama maszyna z klasycznym CMS-em. Układ diagramu jest
celowo identyczny, żeby różnice było widać na pierwszy rzut oka: tu każdy
element działa bez przerwy, a panel logowania i aplikacja są wystawione
do internetu.
![Architektura klasycznego CMS z bazą danych: aplikacja z publicznym panelem admina i renderowaniem na żądanie działa non stop, obok baza danych wymagająca backupów i wolumen mediów](/uploads/diagrams/architektura-cms-z-baza.svg)
## Gdzie co jest
| Co | Gdzie |
|---|---|
| Portal | strona główna tego serwisu |
| Edytor treści | `/admin/` (link „Content editor" w stopce) |
| Gitea (repo `ctao/portal`) | port 3000 na maszynie demo |
| Konto demo | użytkownik `ctao` (hasło u prowadzącego demo) |
| Dokumentacja wdrożenia | katalog `deploy/` w repo kodu: `README.md` (runbook) |
Całość działa w podmanie na jednej maszynie: kontener Gitei, kontener nginx
i jednorazowy kontener builda odpalany przez systemd timer.
## Dlaczego strona statyczna
![Porównanie powierzchni ataku: CMS z bazą wystawia publicznie aplikację, panel logowania, bazę i pluginy — tu publiczne są tylko pliki HTML, a edycja idzie przez git dla zalogowanych](/uploads/diagrams/powierzchnia-ataku.svg)
- **Bezpieczeństwo**: publicznie wystawione są tylko pliki. Nie istnieje
panel admina dostępny z internetu, baza do wykradzenia ani runtime z CVE.
- **Utrzymanie**: nie ma aplikacji, która musi być monitorowana i
aktualizowana pod presją łatek bezpieczeństwa. Do utrzymania zostaje git
(który i tak utrzymujemy) i krótki skrypt builda.
- **Szybkość**: statyczny HTML to najszybsza możliwa strona — istotne też
dla SEO.
- **Historia**: wersjonowanie treści to naturalna cecha gita, nie płatna
funkcja produktu.
## Dwie klasy rozwiązań
Linia podziału jest często mylona: nie chodzi o to, czy treść leży w bazie
czy w plikach, tylko o to, **czy między czytelnikiem a treścią stoi
działająca aplikacja**.
| | Statyczne + git-CMS *(to demo)* | CMS serwujący stronę aplikacją |
|---|---|---|
| Przykłady | Sveltia, Decap (+ Astro / Hugo / Eleventy) | Strapi, WordPress, Drupal, Grav, TinaCMS |
| Treść żyje w | plikach Markdown w gicie | zwykle w bazie; Grav — w plikach, Tina — w gicie |
| W produkcji działa | serwer plików | aplikacja (zwykle + baza), non stop |
| Utrzymanie | skrypt builda + serwis gitowy | patche, aktualizacje, backupy bazy |
| Historia zmian | git, wbudowana | zależnie od produktu (bywa płatna) |
Dwa doprecyzowania, żeby tabela nie upraszczała: **Grav** trzyma treść
w plikach bez żadnej bazy — ale stronę i tak serwuje aplikacja PHP, więc pod
względem powierzchni ataku i utrzymania należy do prawej kolumny. **TinaCMS**
trzyma treść wręcz w Markdownie w gicie, ale do działania edytora wymaga
stale działającego backendu z bazą — jest hybrydą, która dziedziczy koszty
obu światów.
Ograniczenia poszczególnych rozwiązań, które zweryfikowaliśmy podczas
researchu:
- **Strapi** — historia wersji treści nie istnieje w darmowym self-hosted
(tylko płatne plany Growth/Enterprise). Wymaga stale działającego Node +
Postgresa i regularnych aktualizacji.
- **WordPress** — największy ekosystem, ale i największa historia podatności
(głównie pluginy); publiczny panel logowania i PHP do ciągłego patchowania.
- **Grav** (PHP, flat-file — „lekka" alternatywa) — poważne podatności RCE
w ostatnich latach; nadal wymaga serwera PHP wystawionego do internetu.
- **TinaCMS** — edytuje Markdown, ale wymaga backendu: chmura Tina jest
darmowa tylko do 2 użytkowników, a self-hosting to własny serwer Node +
baza (Mongo/Postgres) + auth — wracamy do utrzymywania aplikacji.
- **Ryzyko open-core**: funkcje potrafią z czasem przechodzić do płatnych
planów (przykład wyżej). Gdy treść i jej historia leżą w gicie, nie ma
funkcji, którą dostawca mógłby przenieść do płatnego planu.
## Plusy i minusy obu podejść
Portal ma być utrzymywany przez co najmniej 6 lat, więc bilans trzeba robić
w dwóch horyzontach. Poniżej ocena podejść jako klas rozwiązań, nie
konkretnych produktów.
**Krótkoterminowo (uruchomienie i pierwsze miesiące)**
| | Statyczne + git | CMS z bazą |
|---|---|---|
| Plusy | mało ruchomych części od pierwszego dnia; edytor graficzny gotowy; historia i rollback od razu, w gicie | bogatszy panel od ręki: role i uprawnienia, workflow redakcyjny, relacje między treściami, więcej gotowych integracji |
| Minusy | uprawnienia proste (dziedziczone z gita — bez ról redakcyjnych); przeglądanie historii na razie w interfejsie gita, nie w edytorze | więcej komponentów do postawienia, spięcia i zabezpieczenia od pierwszego dnia (aplikacja, baza, backupy) |
**W horyzoncie 6 lat**
| | Statyczne + git | CMS z bazą |
|---|---|---|
| Plusy | strona publiczna bez aplikacji = brak presji łatek na ścieżce czytelnika; do utrzymania zostaje serwis gitowy (najlepiej ten, który zespół i tak ma); treść w otwartym formacie przetrwa każdą wymianę narzędzi | jeśli z czasem będą potrzebne funkcje aplikacyjne (konta, personalizacja, treści silnie strukturalne), platforma już je ma |
| Minusy | przy bardzo dużej skali (tysiące stron) buildy rosną i wymagają optymalizacji; zaawansowane funkcje redakcyjne zależą od tempa rozwoju narzędzi open-source | kilka dużych migracji wersji w 6 lat (zmiany łamiące); baza wymaga backupów i testów odtwarzania; stała powierzchnia ataku; ryzyko przenoszenia funkcji do płatnych planów |
## „Czy Gitea to nie jest po prostu drugi CMS?"
Uczciwe pytanie: Gitea też ma logowanie, bazę i wymaga aktualizacji
bezpieczeństwa, a dla edytorów musi być dostępna spoza VPN. Czy różnica nie
sprowadza się więc do tego, że zamiast CMS-a utrzymujemy Giteę?
Częściowo tak — i dlatego nie twierdzimy, że utrzymanie jest zerowe. Różnica
polega na trzech rzeczach:
- **Gitea stoi poza ścieżką czytelnika.** Może mieć przerwę, aktualizację,
a nawet zostać przejęta — portal dalej stoi i serwuje bezpieczne pliki.
Najgorszy scenariusz włamania to wandalizm treści: widoczny, z pełną
historią, odwracalny jednym revertem. W CMS-ie serwującym stronę ten sam
incydent oznacza przejętą stronę publiczną.
- **Zespół już utrzymuje Giteę.** Docelowo repozytorium treści może żyć na
istniejącej instancji zespołu — wtedy krańcowy koszt utrzymania jest
bliski zera, a logowanie załatwia jedna integracja z AAI.
- **Klasa oprogramowania.** Gitea to pojedynczy program w Go z SQLite:
aktualizacja to podmiana obrazu i restart, bez ekosystemu pluginów i bez
drzewa zależności npm po stronie serwera. Platformy CMS to frameworki
aplikacyjne z regularnymi migracjami głównych wersji.
## Gdzie jeszcze może się przydać
Ten sam wzorzec — edytor graficzny nad repozytorium Markdown — nie jest
ograniczony do portalu. Naturalni kandydaci u nas:
- **Projekt Mickiewicza** — treści redagowane przez osoby nietechniczne,
a publikowane jako strona statyczna.
- **Dokumentacja Episodes Platform** — już dziś jest w Markdownie; podpięcie
edytora dałoby wygodną edycję w przeglądarce zamiast edytora plików
w Gitei, bez żadnych zmian w istniejącym repozytorium.
Koszt wdrożenia w takich miejscach jest niewielki: edytor to jeden statyczny
plik HTML plus konfiguracja wskazująca repozytorium i strukturę treści.
## Brak lock-inu
Każdy element jest wymienialny osobno, bo treść to czysty Markdown:
- **Sveltia ↔ Decap** — ten sam format konfiguracji; podmiana = jedna linijka
`<script>`.
- **Astro ↔ Hugo / Eleventy** — Markdown zostaje bez zmian, wymieniamy tylko
szablony.
- **Gitea ↔ dowolny hosting gitowy** — GitHub, GitLab i inne.
- Nawet rezygnacja z całego podejścia = eksport trywialny, bo treść od
początku leży w otwartym formacie w naszym repozytorium.
Wniosek z tego demo: zaczynamy od najprostszego rozwiązania, które spełnia
wymagania — graficzny edytor dla redaktorów i bezpieczna, bezobsługowa strona
publiczna. Złożoność dodajemy dopiero wtedy, gdy pojawi się potrzeba, której
to podejście nie obsłuży.
+191
View File
@@ -0,0 +1,191 @@
---
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."
date: 2026-09-11
category: demo
author: Cyfronet
cover: /uploads/diagrams/cover-git-cms.svg
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
**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](/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.
**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](/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**.
## 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)
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](/uploads/diagrams/architecture-cms-database.svg)
## 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](/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.
@@ -0,0 +1,101 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" role="img" aria-label="Architecture of a database-backed CMS: one always-on application serves both the editor's admin panel and the reader's pages, backed by a database that needs backups and a separate media volume">
<defs>
<marker id="arrB" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="#00004a"/>
</marker>
<filter id="softB" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="5" flood-color="#00004a" flood-opacity="0.10"/>
</filter>
<style>
.t { font-family: Inter, "Segoe UI", Arial, sans-serif; }
.box { fill: #ffffff; stroke: #00004a; stroke-width: 1.5; }
.sub { fill: #f5f5f5; stroke: #e2e5ee; stroke-width: 1; }
.ttl { font-weight: 700; font-size: 19px; fill: #00004a; letter-spacing: .04em; }
.act { font-weight: 700; font-size: 18px; fill: #00004a; }
.cap { font-size: 15px; fill: #46536a; }
.sml { font-size: 14px; fill: #46536a; }
.lbl { font-size: 14px; fill: #46536a; paint-order: stroke; stroke: #f8f9fc; stroke-width: 5; }
.chipbg { fill: #e9ebf3; }
.chip { font-size: 12.5px; font-weight: 700; fill: #00004a; letter-spacing: .06em; }
.flow { stroke: #00004a; stroke-width: 1.8; fill: none; marker-end: url(#arrB); }
.name { font-size: 16px; font-weight: 700; fill: #101228; }
.pill { fill: #f5f5f5; stroke: #e2e5ee; }
.pilltxt { font-size: 14px; fill: #101228; }
</style>
</defs>
<!-- actor: editor -->
<g filter="url(#softB)"><rect class="box" x="49" y="20" width="380" height="86" rx="16"/></g>
<circle cx="80" cy="48" r="11" fill="none" stroke="#00004a" stroke-width="1.8"/>
<path d="M64 78 q16 -18 32 0" fill="none" stroke="#00004a" stroke-width="1.8"/>
<text class="t act" x="108" y="54">Editor</text>
<text class="t sml" x="108" y="80">admin panel in the browser, signs in</text>
<!-- actor: reader -->
<g filter="url(#softB)"><rect class="box" x="395" y="20" width="380" height="86" rx="16"/></g>
<circle cx="426" cy="48" r="11" fill="none" stroke="#00004a" stroke-width="1.8"/>
<path d="M410 78 q16 -18 32 0" fill="none" stroke="#00004a" stroke-width="1.8"/>
<text class="t act" x="454" y="54">Reader</text>
<text class="t sml" x="454" y="80">the public portal, assembled live</text>
<!-- machine boundary (same vertical skeleton as the static diagram) -->
<rect x="30" y="158" width="740" height="560" rx="18" fill="#f8f9fc" stroke="#e2e5ee" stroke-width="1.5"/>
<text class="t" x="400" y="194" text-anchor="middle" font-size="14" font-weight="700" fill="#00004a" letter-spacing=".18em">MACHINE · DATABASE-BACKED CMS</text>
<text class="t sml" x="400" y="216" text-anchor="middle">WordPress, Strapi, Drupal and the like</text>
<path class="flow" d="M239 106 V232"/>
<text class="t lbl" x="253" y="150">sign in to the panel</text>
<path class="flow" d="M585 232 V110"/>
<text class="t lbl" x="599" y="150">HTML built per request</text>
<!-- the application: always on, serves everybody -->
<g filter="url(#softB)"><rect class="box" x="60" y="238" width="680" height="210" rx="16"/></g>
<text class="t ttl" x="88" y="278">CMS APPLICATION</text>
<rect class="chipbg" x="290" y="258" width="110" height="24" rx="12"/>
<text class="t chip" x="345" y="275" text-anchor="middle">ALWAYS ON</text>
<rect class="sub" x="84" y="298" width="204" height="130" rx="12"/>
<text class="t name" x="104" y="330">admin panel</text>
<text class="t cap" x="104" y="356">public address</text>
<text class="t cap" x="104" y="378">and login, open</text>
<text class="t cap" x="104" y="400">to the internet</text>
<rect class="sub" x="298" y="298" width="204" height="130" rx="12"/>
<text class="t name" x="318" y="330">page rendering</text>
<text class="t cap" x="318" y="356">every visit is</text>
<text class="t cap" x="318" y="378">work for the app</text>
<text class="t cap" x="318" y="400">and the database</text>
<rect class="sub" x="512" y="298" width="204" height="130" rx="12"/>
<text class="t name" x="532" y="330">plugins</text>
<text class="t cap" x="532" y="356">each with its own</text>
<text class="t cap" x="532" y="378">update cycle and</text>
<text class="t cap" x="532" y="400">its own bugs</text>
<path class="flow" d="M239 448 V504"/>
<text class="t lbl" x="253" y="484">upload</text>
<path class="flow" d="M585 448 V504"/>
<text class="t lbl" x="599" y="484">SQL</text>
<!-- media volume -->
<g filter="url(#softB)"><rect class="sub" x="60" y="508" width="310" height="170" rx="16"/></g>
<text class="t ttl" x="88" y="546">media/</text>
<rect class="chipbg" x="180" y="526" width="86" height="24" rx="12"/>
<text class="t chip" x="223" y="543" text-anchor="middle">VOLUME</text>
<text class="t cap" x="88" y="582">images and files</text>
<text class="t cap" x="88" y="606">outside the database</text>
<text class="t cap" x="88" y="630">backed up separately</text>
<!-- database -->
<g filter="url(#softB)"><rect class="box" x="430" y="508" width="310" height="170" rx="16"/></g>
<text class="t ttl" x="458" y="546">DATABASE</text>
<rect class="chipbg" x="590" y="526" width="110" height="24" rx="12"/>
<text class="t chip" x="645" y="543" text-anchor="middle">ALWAYS ON</text>
<text class="t cap" x="458" y="582">content and accounts</text>
<text class="t cap" x="458" y="606">backups and restore drills</text>
<text class="t cap" x="458" y="630">schema migrations</text>
<text class="t cap" x="458" y="654">on every upgrade</text>
<!-- summary pills -->
<rect class="pill" x="60" y="740" width="330" height="46" rx="23"/>
<text class="t pilltxt" x="225" y="768" text-anchor="middle">two processes run non stop, day and night</text>
<rect class="pill" x="410" y="740" width="330" height="46" rx="23"/>
<text class="t pilltxt" x="575" y="768" text-anchor="middle">security updates are a standing duty</text>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

+105
View File
@@ -0,0 +1,105 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 1040" role="img" aria-label="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 that overlays content onto the code repository, the result lands in the releases volume and nginx serves readers static files only">
<defs>
<marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="#00004a"/>
</marker>
<filter id="soft" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="5" flood-color="#00004a" flood-opacity="0.10"/>
</filter>
<style>
.t { font-family: Inter, "Segoe UI", Arial, sans-serif; }
.box { fill: #ffffff; stroke: #00004a; stroke-width: 1.5; }
.sub { fill: #f5f5f5; stroke: #e2e5ee; stroke-width: 1; }
.ttl { font-weight: 700; font-size: 19px; fill: #00004a; letter-spacing: .04em; }
.act { font-weight: 700; font-size: 18px; fill: #00004a; }
.cap { font-size: 15px; fill: #46536a; }
.sml { font-size: 14px; fill: #46536a; }
.lbl { font-size: 14px; fill: #46536a; paint-order: stroke; stroke: #f8f9fc; stroke-width: 5; }
.chipbg { fill: #d2faf7; }
.chip { font-size: 12.5px; font-weight: 700; fill: #00004a; letter-spacing: .06em; }
.flow { stroke: #00004a; stroke-width: 1.8; fill: none; marker-end: url(#arr); }
.mono { font-family: "SF Mono", Consolas, Menlo, monospace; font-size: 14px; fill: #101228; }
.name { font-size: 16px; font-weight: 700; fill: #101228; }
</style>
</defs>
<!-- actor: editor -->
<g filter="url(#soft)"><rect class="box" x="49" y="20" width="380" height="86" rx="16"/></g>
<circle cx="80" cy="48" r="11" fill="none" stroke="#00004a" stroke-width="1.8"/>
<path d="M64 78 q16 -18 32 0" fill="none" stroke="#00004a" stroke-width="1.8"/>
<text class="t act" x="108" y="54">Editor</text>
<text class="t sml" x="108" y="80">browser · <tspan font-weight="600">/admin/</tspan> (Sveltia), visual editing</text>
<!-- machine boundary -->
<rect x="30" y="158" width="740" height="730" rx="18" fill="#f8f9fc" stroke="#e2e5ee" stroke-width="1.5"/>
<text class="t" x="400" y="194" text-anchor="middle" font-size="14" font-weight="700" fill="#00004a" letter-spacing=".18em">DEMO MACHINE · PODMAN</text>
<text class="t sml" x="400" y="216" text-anchor="middle">separate containers under systemd</text>
<path class="flow" d="M239 106 V232"/>
<text class="t lbl" x="253" y="150">save through the API (OAuth)</text>
<!-- gitea: two repositories -->
<g filter="url(#soft)"><rect class="box" x="60" y="238" width="680" height="190" rx="16"/></g>
<text class="t ttl" x="88" y="278">GITEA</text>
<rect class="chipbg" x="170" y="258" width="62" height="24" rx="12"/>
<text class="t chip" x="201" y="275" text-anchor="middle">:3000</text>
<rect class="sub" x="84" y="298" width="310" height="108" rx="12"/>
<text class="t name" x="106" y="330">ctao/content</text>
<text class="t cap" x="106" y="356">Markdown and images,</text>
<text class="t cap" x="106" y="380">written by the CMS</text>
<rect class="sub" x="414" y="298" width="302" height="108" rx="12"/>
<text class="t name" x="436" y="330">ctao/portal</text>
<text class="t cap" x="436" y="356">portal code,</text>
<text class="t cap" x="436" y="380">maintained by the team</text>
<path class="flow" d="M400 428 V454"/>
<text class="t lbl" x="414" y="447">git fetch, both repos</text>
<!-- build (ephemeral) -->
<g filter="url(#soft)">
<rect x="60" y="460" width="680" height="190" rx="16" fill="#ffffff" stroke="#00004a" stroke-width="1.5" stroke-dasharray="7 5"/>
</g>
<text class="t ttl" x="88" y="500">BUILD · ASTRO</text>
<rect class="chipbg" x="250" y="480" width="190" height="24" rx="12"/>
<text class="t chip" x="345" y="497" text-anchor="middle">ONE-SHOT CONTAINER</text>
<text class="t cap" x="88" y="536">a systemd timer checks both repositories every 10 seconds</text>
<text class="t cap" x="88" y="564">on a change: fetch both, overlay content onto code</text>
<text class="t cap" x="88" y="592" font-weight="700" fill="#101228">Markdown becomes static HTML</text>
<text class="t cap" x="88" y="620">the container exits when the build is done</text>
<path class="flow" d="M400 650 V676"/>
<text class="t lbl" x="414" y="669">new version of the site</text>
<!-- releases volume -->
<g filter="url(#soft)"><rect class="sub" x="60" y="682" width="310" height="176" rx="16"/></g>
<text class="t ttl" x="88" y="720">releases/</text>
<rect class="chipbg" x="196" y="700" width="86" height="24" rx="12"/>
<text class="t chip" x="239" y="717" text-anchor="middle">VOLUME</text>
<text class="t mono" x="88" y="754">9f2c1d-43e2bb/ previous</text>
<text class="t mono" x="88" y="778">71b980-8c1a2f/ <tspan font-weight="700" fill="#00004a">← current</tspan></text>
<text class="t cap" x="88" y="812">atomic swap,</text>
<text class="t cap" x="88" y="836">zero downtime</text>
<!-- nginx -->
<g filter="url(#soft)"><rect class="box" x="430" y="682" width="310" height="176" rx="16"/></g>
<text class="t ttl" x="458" y="720">NGINX</text>
<rect class="chipbg" x="540" y="700" width="62" height="24" rx="12"/>
<text class="t chip" x="571" y="717" text-anchor="middle">:8080</text>
<text class="t cap" x="458" y="754">serves <tspan font-weight="600">releases/current</tspan></text>
<text class="t cap" x="458" y="782">static files only</text>
<text class="t cap" x="458" y="810">no application, no database,</text>
<text class="t cap" x="458" y="836">no PHP, no plugins</text>
<path class="flow" d="M372 770 H424"/>
<text class="t lbl" x="398" y="756" text-anchor="middle">current/</text>
<path class="flow" d="M585 860 V930"/>
<text class="t lbl" x="599" y="900">static HTML</text>
<!-- actor: reader -->
<g filter="url(#soft)"><rect class="box" x="395" y="940" width="380" height="86" rx="16"/></g>
<circle cx="426" cy="968" r="11" fill="none" stroke="#00004a" stroke-width="1.8"/>
<path d="M410 998 q16 -18 32 0" fill="none" stroke="#00004a" stroke-width="1.8"/>
<text class="t act" x="454" y="974">Reader</text>
<text class="t sml" x="454" y="1000">the public portal, plain and fast HTML</text>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

@@ -1,110 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 640" role="img" aria-label="Architektura klasycznego CMS z bazą danych: aplikacja CMS z publicznym panelem admina działa non stop, obok baza danych i wolumen mediów; czytelnik dostaje strony renderowane na żądanie">
<defs>
<marker id="arrB" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="#00004a"/>
</marker>
<filter id="softB" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="5" flood-color="#00004a" flood-opacity="0.10"/>
</filter>
<style>
.t { font-family: Inter, "Segoe UI", Arial, sans-serif; }
.box { fill: #ffffff; stroke: #00004a; stroke-width: 1.5; }
.sub { fill: #f5f5f5; stroke: #e2e5ee; stroke-width: 1; }
.ttl { font-weight: 700; font-size: 15.5px; fill: #00004a; letter-spacing: .04em; }
.cap { font-size: 12.5px; fill: #46536a; }
.lbl { font-size: 12px; fill: #46536a; paint-order: stroke; stroke: #ffffff; stroke-width: 4; }
.chipbg { fill: #e9ebf3; }
.chip { font-size: 11.5px; font-weight: 700; fill: #00004a; letter-spacing: .06em; }
.flow { stroke: #00004a; stroke-width: 1.8; fill: none; marker-end: url(#arrB); }
.pill { fill: #f5f5f5; stroke: #e2e5ee; }
.pilltxt { font-size: 12.5px; fill: #101228; }
</style>
</defs>
<!-- machine boundary (mirrors the static-demo diagram 1:1) -->
<rect x="340" y="40" width="830" height="570" rx="18" fill="#f8f9fc" stroke="#e2e5ee" stroke-width="1.5"/>
<text class="t" x="366" y="76" font-size="13" font-weight="700" fill="#00004a" letter-spacing=".18em">MASZYNA · CMS Z BAZĄ DANYCH</text>
<text class="t cap" x="366" y="96">np. WordPress, Strapi, Drupal — wspólny schemat tej klasy rozwiązań</text>
<!-- actor: redaktor -->
<g filter="url(#softB)">
<rect class="box" x="52" y="120" width="220" height="120" rx="16"/>
</g>
<circle cx="88" cy="156" r="11" fill="none" stroke="#00004a" stroke-width="1.8"/>
<path d="M72 186 q16 -18 32 0" fill="none" stroke="#00004a" stroke-width="1.8"/>
<text class="t ttl" x="116" y="162">Redaktor</text>
<text class="t cap" x="70" y="212">panel admina w przeglądarce</text>
<text class="t cap" x="70" y="228">logowanie do aplikacji</text>
<!-- actor: czytelnik -->
<g filter="url(#softB)">
<rect class="box" x="52" y="400" width="220" height="120" rx="16"/>
</g>
<circle cx="88" cy="436" r="11" fill="none" stroke="#00004a" stroke-width="1.8"/>
<path d="M72 466 q16 -18 32 0" fill="none" stroke="#00004a" stroke-width="1.8"/>
<text class="t ttl" x="116" y="442">Czytelnik</text>
<text class="t cap" x="70" y="492">publiczna strona portalu</text>
<text class="t cap" x="70" y="508">HTML składany na żądanie</text>
<!-- CMS application (tall box, always running) -->
<g filter="url(#softB)">
<rect class="box" x="400" y="120" width="320" height="400" rx="16"/>
</g>
<text class="t ttl" x="424" y="152">APLIKACJA CMS</text>
<rect class="chipbg" x="590" y="136" width="106" height="22" rx="11"/>
<text class="t chip" x="643" y="151" text-anchor="middle">NON STOP</text>
<rect class="sub" x="420" y="176" width="280" height="88" rx="12"/>
<text class="t" x="436" y="202" font-size="13.5" font-weight="700" fill="#101228">panel admina</text>
<text class="t cap" x="436" y="224">publiczny adres + logowanie</text>
<text class="t cap" x="436" y="242">z internetu</text>
<rect class="sub" x="420" y="278" width="280" height="88" rx="12"/>
<text class="t" x="436" y="304" font-size="13.5" font-weight="700" fill="#101228">renderowanie stron</text>
<text class="t cap" x="436" y="326">każde wejście czytelnika =</text>
<text class="t cap" x="436" y="344">praca aplikacji i bazy</text>
<rect class="sub" x="420" y="380" width="280" height="88" rx="12"/>
<text class="t" x="436" y="406" font-size="13.5" font-weight="700" fill="#101228">pluginy / rozszerzenia</text>
<text class="t cap" x="436" y="428">własny cykl aktualizacji</text>
<text class="t cap" x="436" y="446">i podatności</text>
<!-- database -->
<g filter="url(#softB)">
<rect class="box" x="790" y="120" width="340" height="185" rx="16"/>
</g>
<text class="t ttl" x="814" y="152">BAZA DANYCH</text>
<rect class="chipbg" x="972" y="136" width="106" height="22" rx="11"/>
<text class="t chip" x="1025" y="151" text-anchor="middle">NON STOP</text>
<text class="t cap" x="814" y="188">treść + konta użytkowników</text>
<text class="t cap" x="814" y="212">wymaga backupów i testów odtwarzania</text>
<text class="t cap" x="814" y="236">migracje schematu przy aktualizacjach</text>
<text class="t cap" x="814" y="266">historia zmian: zależnie od produktu,</text>
<text class="t cap" x="814" y="284">bywa funkcją płatną</text>
<!-- media volume -->
<g filter="url(#softB)">
<rect class="sub" x="790" y="400" width="340" height="120" rx="16"/>
</g>
<text class="t ttl" x="814" y="432">media/</text>
<rect class="chipbg" x="896" y="416" width="82" height="22" rx="11"/>
<text class="t chip" x="937" y="431" text-anchor="middle">WOLUMEN</text>
<text class="t cap" x="814" y="462">obrazy i pliki poza bazą</text>
<text class="t cap" x="814" y="480">osobny backup</text>
<!-- flows -->
<path class="flow" d="M272 180 H396"/>
<text class="t lbl" x="334" y="170" text-anchor="middle">logowanie do panelu</text>
<path class="flow" d="M400 460 H276"/>
<text class="t lbl" x="338" y="450" text-anchor="middle">HTML na żądanie</text>
<path class="flow" d="M720 212 H786"/>
<text class="t lbl" x="753" y="202" text-anchor="middle">SQL</text>
<path class="flow" d="M720 440 H786"/>
<text class="t lbl" x="753" y="430" text-anchor="middle">upload</text>
<!-- summary pills -->
<rect class="pill" x="380" y="552" width="360" height="36" rx="18"/>
<text class="t pilltxt" x="560" y="575" text-anchor="middle">2 procesy działają bez przerwy, także nocą</text>
<rect class="pill" x="770" y="552" width="360" height="36" rx="18"/>
<text class="t pilltxt" x="950" y="575" text-anchor="middle">aktualizacje bezpieczeństwa = stały obowiązek</text>
</svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

-111
View File
@@ -1,111 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 640" role="img" aria-label="Architektura demo na maszynie: redaktor i czytelnik łączą się z kontenerami podmana — Gitea, build Astro, wolumen releases i nginx">
<defs>
<marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="#00004a"/>
</marker>
<filter id="soft" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="5" flood-color="#00004a" flood-opacity="0.10"/>
</filter>
<style>
.t { font-family: Inter, "Segoe UI", Arial, sans-serif; }
.box { fill: #ffffff; stroke: #00004a; stroke-width: 1.5; }
.sub { fill: #f5f5f5; stroke: #e2e5ee; stroke-width: 1; }
.ttl { font-weight: 700; font-size: 15.5px; fill: #00004a; letter-spacing: .04em; }
.cap { font-size: 12.5px; fill: #46536a; }
.lbl { font-size: 12px; fill: #46536a; paint-order: stroke; stroke: #ffffff; stroke-width: 4; }
.chipbg { fill: #d2faf7; }
.chip { font-size: 11.5px; font-weight: 700; fill: #00004a; letter-spacing: .06em; }
.flow { stroke: #00004a; stroke-width: 1.8; fill: none; marker-end: url(#arr); }
.mono { font-family: "SF Mono", Consolas, Menlo, monospace; font-size: 12.5px; fill: #101228; }
</style>
</defs>
<!-- machine boundary -->
<rect x="340" y="40" width="830" height="570" rx="18" fill="#f8f9fc" stroke="#e2e5ee" stroke-width="1.5"/>
<text class="t" x="366" y="76" font-size="13" font-weight="700" fill="#00004a" letter-spacing=".18em">MASZYNA DEMO · PODMAN</text>
<text class="t cap" x="366" y="96">osobne kontenery pod systemd · wszystko w jednym katalogu użytkownika</text>
<!-- actor: redaktor -->
<g filter="url(#soft)">
<rect class="box" x="52" y="120" width="220" height="120" rx="16"/>
</g>
<circle cx="88" cy="156" r="11" fill="none" stroke="#00004a" stroke-width="1.8"/>
<path d="M72 186 q16 -18 32 0" fill="none" stroke="#00004a" stroke-width="1.8"/>
<text class="t ttl" x="116" y="162">Redaktor</text>
<text class="t cap" x="70" y="212">przeglądarka · <tspan font-weight="600">/admin/</tspan> (Sveltia)</text>
<text class="t cap" x="70" y="228">edycja WYSIWYG, podgląd 1:1</text>
<!-- actor: czytelnik -->
<g filter="url(#soft)">
<rect class="box" x="52" y="400" width="220" height="120" rx="16"/>
</g>
<circle cx="88" cy="436" r="11" fill="none" stroke="#00004a" stroke-width="1.8"/>
<path d="M72 466 q16 -18 32 0" fill="none" stroke="#00004a" stroke-width="1.8"/>
<text class="t ttl" x="116" y="442">Czytelnik</text>
<text class="t cap" x="70" y="492">publiczna strona portalu</text>
<text class="t cap" x="70" y="508">czysty, szybki HTML</text>
<!-- gitea -->
<g filter="url(#soft)">
<rect class="box" x="400" y="120" width="320" height="185" rx="16"/>
</g>
<text class="t ttl" x="424" y="152">GITEA</text>
<rect class="chipbg" x="490" y="136" width="52" height="22" rx="11"/>
<text class="t chip" x="516" y="151" text-anchor="middle">:3000</text>
<rect class="sub" x="420" y="172" width="280" height="112" rx="12"/>
<text class="t" x="436" y="198" font-size="13.5" font-weight="700" fill="#101228">repo ctao/portal</text>
<text class="t cap" x="436" y="220">treść: Markdown + obrazy (WebP)</text>
<text class="t cap" x="436" y="238">każda zmiana = commit</text>
<text class="t cap" x="436" y="256">pełna historia + rollback za darmo</text>
<!-- build (ephemeral) -->
<g filter="url(#soft)">
<rect x="790" y="120" width="340" height="185" rx="16" fill="#ffffff" stroke="#00004a" stroke-width="1.5" stroke-dasharray="7 5"/>
</g>
<text class="t ttl" x="814" y="152">BUILD · ASTRO</text>
<rect class="chipbg" x="948" y="136" width="164" height="22" rx="11"/>
<text class="t chip" x="1030" y="151" text-anchor="middle">KONTENER JEDNORAZOWY</text>
<text class="t cap" x="814" y="188">timer systemd (co ~10 s) sprawdza,</text>
<text class="t cap" x="814" y="206">czy w repo jest nowy commit</text>
<text class="t cap" x="814" y="234">jeśli tak: git fetch → checkout →</text>
<text class="t cap" x="814" y="252"><tspan font-weight="700" fill="#101228">Markdown → statyczny HTML</tspan></text>
<text class="t cap" x="814" y="280">po buildzie kontener znika</text>
<!-- releases volume -->
<g filter="url(#soft)">
<rect class="sub" x="790" y="400" width="340" height="150" rx="16"/>
</g>
<text class="t ttl" x="814" y="432">releases/</text>
<rect class="chipbg" x="908" y="416" width="82" height="22" rx="11"/>
<text class="t chip" x="949" y="431" text-anchor="middle">WOLUMEN</text>
<text class="t mono" x="814" y="466">9f2c1d/ poprzednia wersja</text>
<text class="t mono" x="814" y="488">71b980/ <tspan font-weight="700" fill="#00004a">← current (symlink)</tspan></text>
<text class="t cap" x="814" y="518">podmiana atomowa — zero przestoju</text>
<!-- nginx -->
<g filter="url(#soft)">
<rect class="box" x="400" y="400" width="320" height="150" rx="16"/>
</g>
<text class="t ttl" x="424" y="432">NGINX</text>
<rect class="chipbg" x="500" y="416" width="52" height="22" rx="11"/>
<text class="t chip" x="526" y="431" text-anchor="middle">:8080</text>
<text class="t cap" x="424" y="466">serwuje <tspan font-weight="600">releases/current</tspan></text>
<text class="t cap" x="424" y="484">wyłącznie statyczne pliki</text>
<text class="t cap" x="424" y="502">brak aplikacji · brak bazy · brak PHP</text>
<!-- flows -->
<path class="flow" d="M272 180 H396"/>
<text class="t lbl" x="334" y="170" text-anchor="middle">zapis przez API (OAuth)</text>
<path class="flow" d="M790 212 H724"/>
<text class="t lbl" x="757" y="202" text-anchor="middle">git fetch</text>
<path class="flow" d="M960 305 V396"/>
<text class="t lbl" x="968" y="354" text-anchor="start">nowa wersja strony</text>
<path class="flow" d="M790 475 H724"/>
<text class="t lbl" x="757" y="465" text-anchor="middle">current/</text>
<path class="flow" d="M400 475 H276"/>
<text class="t lbl" x="338" y="465" text-anchor="middle">statyczny HTML</text>
</svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

+48
View File
@@ -0,0 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 540" role="img" aria-label="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">
<defs>
<filter id="soft3" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="5" flood-color="#00004a" flood-opacity="0.10"/>
</filter>
<marker id="arr3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="#00004a"/>
</marker>
<style>
.t { font-family: Inter, "Segoe UI", Arial, sans-serif; }
.hd { font-weight: 800; font-size: 16px; fill: #00004a; letter-spacing: .14em; }
.cap { font-size: 14.5px; fill: #46536a; }
.item { font-size: 15.5px; fill: #101228; }
.side { font-size: 15px; fill: #101228; }
.bad { fill: #fdf0f0; stroke: #e8c5c5; stroke-width: 1.5; }
.good { fill: #ffffff; stroke: #00004a; stroke-width: 1.5; }
.flow { stroke: #00004a; stroke-width: 1.8; fill: none; marker-end: url(#arr3); }
</style>
</defs>
<!-- top: classic CMS -->
<text class="t hd" x="40" y="48">DATABASE-BACKED CMS</text>
<text class="t side" x="40" y="173">internet</text>
<path class="flow" d="M120 168 H176"/>
<g filter="url(#soft3)"><rect class="bad" x="180" y="68" width="580" height="200" rx="16"/></g>
<text class="t item" x="208" y="106">▪ CMS application (PHP / Node), public</text>
<text class="t item" x="208" y="140">▪ login panel, public</text>
<text class="t item" x="208" y="174">▪ database holding the content</text>
<text class="t item" x="208" y="208">▪ plugins and dependencies</text>
<text class="t" x="208" y="242" font-size="14.5" font-weight="700" fill="#8f3b3b">all of it runs non stop and needs patching</text>
<!-- divider -->
<line x1="40" y1="300" x2="760" y2="300" stroke="#e2e5ee" stroke-width="1.5"/>
<!-- bottom: this solution -->
<text class="t hd" x="40" y="334">THIS SOLUTION</text>
<text class="t side" x="40" y="389">internet</text>
<path class="flow" d="M120 384 H176"/>
<g filter="url(#soft3)"><rect class="good" x="180" y="346" width="580" height="76" rx="16"/></g>
<text class="t item" x="208" y="378">▪ HTML files served by nginx</text>
<text class="t cap" x="208" y="404">no application, no database, no login panel</text>
<text class="t side" x="40" y="483">editing</text>
<path class="flow" d="M120 478 H176"/>
<g filter="url(#soft3)"><rect class="good" x="180" y="440" width="580" height="76" rx="16"/></g>
<text class="t item" x="208" y="472">▪ git (Gitea), signed-in users only</text>
<text class="t cap" x="208" y="498">off the reader's path, so an outage is invisible</text>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 900" role="img" aria-label="Grafika: graf commitów w gicie przechodzący w statyczną stronę WWW"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 900" role="img" aria-label="Illustration: a graph of git commits flowing into a static web page">
<defs> <defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"> <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#00004a"/> <stop offset="0" stop-color="#00004a"/>

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -1,76 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 430" role="img" aria-label="Pięć kroków od zapisu w edytorze do publikacji: zapis, commit, detekcja, build, publikacja">
<defs>
<marker id="arr2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="#00004a"/>
</marker>
<filter id="soft2" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="5" flood-color="#00004a" flood-opacity="0.10"/>
</filter>
<style>
.t { font-family: Inter, "Segoe UI", Arial, sans-serif; }
.card { fill: #ffffff; stroke: #00004a; stroke-width: 1.5; }
.ttl { font-weight: 700; font-size: 15px; fill: #00004a; }
.cap { font-size: 12.3px; fill: #46536a; }
.num { font-weight: 800; font-size: 15px; fill: #00004a; }
.pill { fill: #f5f5f5; stroke: #e2e5ee; }
.pilltxt { font-size: 12.5px; fill: #101228; }
.flow { stroke: #00004a; stroke-width: 1.8; fill: none; marker-end: url(#arr2); }
</style>
</defs>
<!-- cards -->
<g filter="url(#soft2)"><rect class="card" x="55" y="100" width="200" height="175" rx="16"/></g>
<g filter="url(#soft2)"><rect class="card" x="285" y="100" width="200" height="175" rx="16"/></g>
<g filter="url(#soft2)"><rect class="card" x="515" y="100" width="200" height="175" rx="16"/></g>
<g filter="url(#soft2)"><rect class="card" x="745" y="100" width="200" height="175" rx="16"/></g>
<g filter="url(#soft2)"><rect class="card" x="975" y="100" width="200" height="175" rx="16"/></g>
<!-- connectors -->
<path class="flow" d="M255 187 H281"/>
<path class="flow" d="M485 187 H511"/>
<path class="flow" d="M715 187 H741"/>
<path class="flow" d="M945 187 H971"/>
<!-- step numbers -->
<circle cx="155" cy="100" r="17" fill="#00e4d8"/><text class="t num" x="155" y="106" text-anchor="middle">1</text>
<circle cx="385" cy="100" r="17" fill="#00e4d8"/><text class="t num" x="385" y="106" text-anchor="middle">2</text>
<circle cx="615" cy="100" r="17" fill="#00e4d8"/><text class="t num" x="615" y="106" text-anchor="middle">3</text>
<circle cx="845" cy="100" r="17" fill="#00e4d8"/><text class="t num" x="845" y="106" text-anchor="middle">4</text>
<circle cx="1075" cy="100" r="17" fill="#00e4d8"/><text class="t num" x="1075" y="106" text-anchor="middle">5</text>
<!-- card 1 -->
<text class="t ttl" x="155" y="152" text-anchor="middle">Zapis w edytorze</text>
<text class="t cap" x="155" y="182" text-anchor="middle">Sveltia · WYSIWYG</text>
<text class="t cap" x="155" y="200" text-anchor="middle">podgląd artykułu 1:1</text>
<text class="t cap" x="155" y="218" text-anchor="middle">obrazy → WebP przy uploadzie</text>
<!-- card 2 -->
<text class="t ttl" x="385" y="152" text-anchor="middle">Commit w repo</text>
<text class="t cap" x="385" y="182" text-anchor="middle">Gitea · logowanie OAuth</text>
<text class="t cap" x="385" y="200" text-anchor="middle">zapis przez API = commit</text>
<text class="t cap" x="385" y="218" text-anchor="middle">historia każdej zmiany</text>
<!-- card 3 -->
<text class="t ttl" x="615" y="152" text-anchor="middle">Detekcja zmiany</text>
<text class="t cap" x="615" y="182" text-anchor="middle">timer systemd</text>
<text class="t cap" x="615" y="200" text-anchor="middle">sprawdza repo co ~10 s</text>
<text class="t cap" x="615" y="218" text-anchor="middle">brak zmian = brak pracy</text>
<!-- card 4 -->
<text class="t ttl" x="845" y="152" text-anchor="middle">Build strony</text>
<text class="t cap" x="845" y="182" text-anchor="middle">jednorazowy kontener</text>
<text class="t cap" x="845" y="200" text-anchor="middle">Astro: Markdown →</text>
<text class="t cap" x="845" y="218" text-anchor="middle">statyczny HTML</text>
<!-- card 5 -->
<text class="t ttl" x="1075" y="152" text-anchor="middle">Publikacja</text>
<text class="t cap" x="1075" y="182" text-anchor="middle">atomowa podmiana symlinka</text>
<text class="t cap" x="1075" y="200" text-anchor="middle">nginx od razu serwuje</text>
<text class="t cap" x="1075" y="218" text-anchor="middle">nową wersję</text>
<!-- reassurance pills -->
<rect class="pill" x="240" y="330" width="330" height="40" rx="20"/>
<text class="t pilltxt" x="405" y="355" text-anchor="middle">awaria builda? strona działa dalej na starej wersji</text>
<rect class="pill" x="630" y="330" width="330" height="40" rx="20"/>
<text class="t pilltxt" x="795" y="355" text-anchor="middle">rollback = git revert — jedna komenda</text>
</svg>

Before

Width:  |  Height:  |  Size: 4.5 KiB

-48
View File
@@ -1,48 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 360" role="img" aria-label="Porównanie powierzchni ataku: CMS z bazą wystawia publicznie aplikację, bazę i pluginy; rozwiązanie statyczne wystawia tylko pliki HTML, a edycja idzie przez git">
<defs>
<filter id="soft3" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="5" flood-color="#00004a" flood-opacity="0.10"/>
</filter>
<marker id="arr3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="#00004a"/>
</marker>
<style>
.t { font-family: Inter, "Segoe UI", Arial, sans-serif; }
.hd { font-weight: 800; font-size: 14px; fill: #00004a; letter-spacing: .14em; }
.cap { font-size: 12.5px; fill: #46536a; }
.item{ font-size: 13px; fill: #101228; }
.bad { fill: #fdf0f0; stroke: #e8c5c5; stroke-width: 1.5; }
.good{ fill: #ffffff; stroke: #00004a; stroke-width: 1.5; }
.flow{ stroke: #00004a; stroke-width: 1.8; fill: none; marker-end: url(#arr3); }
.lbl { font-size: 12px; fill: #46536a; paint-order: stroke; stroke: #ffffff; stroke-width: 4; }
</style>
</defs>
<!-- left: classic CMS -->
<text class="t hd" x="300" y="52" text-anchor="middle">CMS Z BAZĄ DANYCH</text>
<text class="t item" x="60" y="150">internet</text>
<path class="flow" d="M130 145 H186"/>
<g filter="url(#soft3)"><rect class="bad" x="190" y="80" width="360" height="200" rx="16"/></g>
<text class="t item" x="218" y="126">▪ aplikacja CMS (PHP / Node) — publiczna</text>
<text class="t item" x="218" y="158">▪ panel logowania — publiczny</text>
<text class="t item" x="218" y="190">▪ baza danych z treścią</text>
<text class="t item" x="218" y="222">▪ pluginy i zależności</text>
<text class="t cap" x="218" y="256" font-weight="700" fill="#8f3b3b">wszystko działa non stop i wymaga patchowania</text>
<!-- divider -->
<line x1="600" y1="60" x2="600" y2="320" stroke="#e2e5ee" stroke-width="1.5"/>
<!-- right: static -->
<text class="t hd" x="900" y="52" text-anchor="middle">TO ROZWIĄZANIE</text>
<text class="t item" x="646" y="130">internet</text>
<path class="flow" d="M716 125 H772"/>
<g filter="url(#soft3)"><rect class="good" x="776" y="88" width="360" height="76" rx="16"/></g>
<text class="t item" x="804" y="120">▪ pliki HTML — i nic więcej</text>
<text class="t cap" x="804" y="146">nie ma czego zhakować ani patchować</text>
<text class="t item" x="646" y="252">edycja</text>
<path class="flow" d="M716 247 H772"/>
<g filter="url(#soft3)"><rect class="good" x="776" y="210" width="360" height="76" rx="16"/></g>
<text class="t item" x="804" y="242">▪ git (Gitea) — tylko zalogowani</text>
<text class="t cap" x="804" y="268">poza ścieżką czytelnika — awaria nie kładzie strony</text>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

+54
View File
@@ -0,0 +1,54 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 620 600" role="img" aria-label="Five steps from a save in the editor to a published page: save, commit, change detected, site build, published">
<defs>
<filter id="soft2" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="5" flood-color="#00004a" flood-opacity="0.10"/>
</filter>
<style>
.t { font-family: Inter, "Segoe UI", Arial, sans-serif; }
.ttl { font-weight: 700; font-size: 18px; fill: #00004a; }
.cap { font-size: 15px; fill: #46536a; }
.num { font-weight: 800; font-size: 16px; fill: #00004a; }
.pill { fill: #f5f5f5; stroke: #e2e5ee; }
.pilltxt { font-size: 14px; fill: #101228; }
</style>
</defs>
<!-- rail -->
<line x1="60" y1="70" x2="60" y2="470" stroke="#c7cde9" stroke-width="2"/>
<!-- step 1 -->
<circle cx="60" cy="70" r="20" fill="#00e4d8"/>
<text class="t num" x="60" y="76" text-anchor="middle">1</text>
<text class="t ttl" x="100" y="66">Save in the editor</text>
<text class="t cap" x="100" y="92">Sveltia, visual editing with a preview that matches the page</text>
<!-- step 2 -->
<circle cx="60" cy="170" r="20" fill="#00e4d8"/>
<text class="t num" x="60" y="176" text-anchor="middle">2</text>
<text class="t ttl" x="100" y="166">Commit in the repository</text>
<text class="t cap" x="100" y="192">Gitea signs the editor in; each save becomes one commit</text>
<!-- step 3 -->
<circle cx="60" cy="270" r="20" fill="#00e4d8"/>
<text class="t num" x="60" y="276" text-anchor="middle">3</text>
<text class="t ttl" x="100" y="266">Change detected</text>
<text class="t cap" x="100" y="292">a systemd timer checks both repositories every 10 seconds</text>
<!-- step 4 -->
<circle cx="60" cy="370" r="20" fill="#00e4d8"/>
<text class="t num" x="60" y="376" text-anchor="middle">4</text>
<text class="t ttl" x="100" y="366">Site build</text>
<text class="t cap" x="100" y="392">a one-shot container runs Astro: Markdown becomes static HTML</text>
<!-- step 5 -->
<circle cx="60" cy="470" r="20" fill="#00e4d8"/>
<text class="t num" x="60" y="476" text-anchor="middle">5</text>
<text class="t ttl" x="100" y="466">Published</text>
<text class="t cap" x="100" y="492">an atomic symlink swap; nginx serves the new version at once</text>
<!-- reassurance -->
<rect class="pill" x="40" y="524" width="310" height="46" rx="23"/>
<text class="t pilltxt" x="195" y="552" text-anchor="middle">a failed build leaves the old version up</text>
<rect class="pill" x="370" y="524" width="220" height="46" rx="23"/>
<text class="t pilltxt" x="480" y="552" text-anchor="middle">rollback is one git revert</text>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+70
View File
@@ -0,0 +1,70 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 700" role="img" aria-label="The three parts of the stack: Sveltia CMS as the editor, git and Gitea as the database, Astro as the build-time generator; when a reader opens a page only nginx and the generated files are involved">
<defs>
<marker id="arr0" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="#00004a"/>
</marker>
<filter id="soft0" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="5" flood-color="#00004a" flood-opacity="0.10"/>
</filter>
<style>
.t { font-family: Inter, "Segoe UI", Arial, sans-serif; }
.card { fill: #ffffff; stroke: #00004a; stroke-width: 1.5; }
.ttl { font-weight: 700; font-size: 19px; fill: #00004a; letter-spacing: .04em; }
.cap { font-size: 15.5px; fill: #46536a; }
.chipbg { fill: #d2faf7; }
.chip { font-size: 12.5px; font-weight: 700; fill: #00004a; letter-spacing: .06em; }
.gain { font-size: 15.5px; font-weight: 700; fill: #101228; }
.flow { stroke: #00004a; stroke-width: 1.8; fill: none; marker-end: url(#arr0); }
.lbl { font-size: 14px; fill: #46536a; }
.band { fill: #f5f5f5; stroke: #e2e5ee; }
.bandtxt { font-size: 14px; fill: #101228; }
</style>
</defs>
<!-- row 1: the editor -->
<g filter="url(#soft0)"><rect class="card" x="20" y="20" width="760" height="160" rx="16"/></g>
<text class="t ttl" x="48" y="68">SVELTIA CMS</text>
<rect class="chipbg" x="48" y="84" width="78" height="26" rx="13"/>
<text class="t chip" x="87" y="102" text-anchor="middle">EDITOR</text>
<text class="t gain" x="48" y="142">every save is</text>
<text class="t gain" x="48" y="164">a commit</text>
<line x1="284" y1="46" x2="284" y2="154" stroke="#e2e5ee" stroke-width="1"/>
<circle cx="312" cy="64" r="3" fill="#00e4d8"/><text class="t cap" x="330" y="69">visual editing in the browser at /admin/</text>
<circle cx="312" cy="98" r="3" fill="#00e4d8"/><text class="t cap" x="330" y="103">one JavaScript file, no server to run</text>
<circle cx="312" cy="132" r="3" fill="#00e4d8"/><text class="t cap" x="330" y="137">signs in to Gitea over OAuth</text>
<path class="flow" d="M400 180 V220"/>
<text class="t lbl" x="414" y="205">commit</text>
<!-- row 2: the storage -->
<g filter="url(#soft0)"><rect class="card" x="20" y="224" width="760" height="160" rx="16"/></g>
<text class="t ttl" x="48" y="272">GIT · GITEA</text>
<rect class="chipbg" x="48" y="288" width="86" height="26" rx="13"/>
<text class="t chip" x="91" y="306" text-anchor="middle">STORAGE</text>
<text class="t gain" x="48" y="346">the repository</text>
<text class="t gain" x="48" y="368">is the database</text>
<line x1="284" y1="250" x2="284" y2="358" stroke="#e2e5ee" stroke-width="1"/>
<circle cx="312" cy="268" r="3" fill="#00e4d8"/><text class="t cap" x="330" y="273">articles as Markdown files</text>
<circle cx="312" cy="302" r="3" fill="#00e4d8"/><text class="t cap" x="330" y="307">images stored next to them</text>
<circle cx="312" cy="336" r="3" fill="#00e4d8"/><text class="t cap" x="330" y="341">history, diff and revert built in</text>
<path class="flow" d="M400 384 V424"/>
<text class="t lbl" x="414" y="409">build</text>
<!-- row 3: the generator -->
<g filter="url(#soft0)"><rect class="card" x="20" y="428" width="760" height="160" rx="16"/></g>
<text class="t ttl" x="48" y="476">ASTRO</text>
<rect class="chipbg" x="48" y="492" width="110" height="26" rx="13"/>
<text class="t chip" x="103" y="510" text-anchor="middle">BUILD ONLY</text>
<text class="t gain" x="48" y="550">nothing of it runs</text>
<text class="t gain" x="48" y="572">in production</text>
<line x1="284" y1="454" x2="284" y2="562" stroke="#e2e5ee" stroke-width="1"/>
<circle cx="312" cy="472" r="3" fill="#00e4d8"/><text class="t cap" x="330" y="477">Markdown becomes static HTML</text>
<circle cx="312" cy="506" r="3" fill="#00e4d8"/><text class="t cap" x="330" y="511">article fields checked at build time</text>
<circle cx="312" cy="540" r="3" fill="#00e4d8"/><text class="t cap" x="330" y="545">React islands where they are needed</text>
<!-- what the reader actually touches -->
<rect class="band" x="20" y="616" width="760" height="64" rx="32"/>
<text class="t bandtxt" x="400" y="645" text-anchor="middle">When a reader opens a page, only nginx and the generated files are involved.</text>
<text class="t bandtxt" x="400" y="667" text-anchor="middle">No application, no database, no admin login.</text>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB