From a74eef0e47b84b710ff7d3782a79f0cdfbf04854 Mon Sep 17 00:00:00 2001 From: Mieszko Makuch Date: Wed, 29 Jul 2026 07:55:22 +0200 Subject: [PATCH] ui iteration: nav underline moved to ::before (restores external arrow on Data), card reveal tuned (entry 75%, 28px), reading progress 3px, permanent lifted header shadow; article honesty pass: app-in-path class line (Grav/Tina clarified), 'is Gitea a second CMS' section, no-VPN claim removed --- public/diagrams/powierzchnia-ataku.svg | 2 +- src/content/news/architektura-demo-git-cms.md | 52 ++++++++++++++++--- src/styles/global.css | 28 +++++----- 3 files changed, 59 insertions(+), 23 deletions(-) diff --git a/public/diagrams/powierzchnia-ataku.svg b/public/diagrams/powierzchnia-ataku.svg index 839e6f3..b887430 100644 --- a/public/diagrams/powierzchnia-ataku.svg +++ b/public/diagrams/powierzchnia-ataku.svg @@ -44,5 +44,5 @@ ▪ git (Gitea) — tylko zalogowani - docelowo za VPN, poza publicznym internetem + poza ścieżką czytelnika — awaria nie kładzie strony diff --git a/src/content/news/architektura-demo-git-cms.md b/src/content/news/architektura-demo-git-cms.md index 73c73b3..1b7517b 100644 --- a/src/content/news/architektura-demo-git-cms.md +++ b/src/content/news/architektura-demo-git-cms.md @@ -11,8 +11,11 @@ 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 w produkcji nie działa żadna aplikacja, żadna baza danych. -Ten artykuł został napisany i opublikowany dokładnie tą ścieżką. +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 @@ -62,14 +65,25 @@ i jednorazowy kontener builda odpalany przez systemd timer. ## Dwie klasy rozwiązań -| | Statyczne + git-CMS *(to demo)* | CMS z własną bazą | +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, Grav, TinaCMS | -| Treść żyje w | plikach Markdown w gicie | bazie danych | -| W produkcji działa | serwer plików | aplikacja + baza, non stop | -| Utrzymanie | skrypt builda | patche, aktualizacje, backupy bazy | +| 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: @@ -104,9 +118,31 @@ konkretnych produktów. | | Statyczne + git | CMS z bazą | |---|---|---| -| Plusy | brak publicznie działającej aplikacji = brak presji łatek bezpieczeństwa; koszt utrzymania bliski zera; 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 | +| 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 diff --git a/src/styles/global.css b/src/styles/global.css index e08e0af..29b56e5 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -193,7 +193,9 @@ img { max-width: 100%; height: auto; display: block; } /* Header — Galaxy Blue band, reverse (white) logo per BRAND B.1.2; translucent over content where backdrop blur is supported (AA computed: worst underlying white → bar (31,31,96), muted nav text 9.4:1). Solid Galaxy fallback. */ -.site-header { background: var(--galaxy); position: sticky; top: 0; z-index: 10; } +/* Lifted shadow separates the bar (and the mobile dropdown panels + underneath it) from content — the header floats above the page. */ +.site-header { background: var(--galaxy); position: sticky; top: 0; z-index: 10; box-shadow: var(--shadow-lifted); } @supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) { .site-header { background: color-mix(in srgb, var(--galaxy) 88%, transparent); @@ -225,14 +227,15 @@ img { max-width: 100%; height: auto; display: block; } } /* Underline-grow (the current nav-hover convention): a 2px rule scales in from the left on hover and stays put on the active page. Transform-only; - its transition lives in the no-preference block like every other motion. */ -.nav a.navlink::after { + its transition lives in the no-preference block like every other motion. + ::before by codebase convention — ::after belongs to the external arrow. */ +.nav a.navlink::before { content: ""; position: absolute; left: 10px; right: 10px; bottom: 4px; height: 2px; border-radius: 1px; background: #fff; transform: scaleX(0); transform-origin: 0 50%; } .nav a.navlink:hover { color: #fff; } -.nav a.navlink:hover::after, .nav a.navlink[aria-current='page']::after { transform: scaleX(1); } +.nav a.navlink:hover::before, .nav a.navlink[aria-current='page']::before { transform: scaleX(1); } .nav a.navlink[aria-current='page'] { color: #fff; } /* AAI sign-in entry — ghost/outline: Cherenkov fill is reserved for the page's primary CTA, the header only whispers. */ @@ -802,7 +805,7 @@ mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(va .card:hover, .search-results li:hover { transform: translateY(-2px); } /* the one elevate gesture (no cover zoom) */ /* Nav underline-grow + press feedback (micro-interaction pair) */ - .nav a.navlink::after { transition: transform var(--dur) var(--ease); } + .nav a.navlink::before { transition: transform var(--dur) var(--ease); } .btn:active, .nav a.navlink:active { transform: translateY(1px); } /* === Scroll-driven effects (CSS only, compositor-only, no JS) === @@ -811,17 +814,14 @@ mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(va @supports (animation-timeline: view()) { /* Cards drift in as they enter the viewport; items already on screen start past the entry range, so nothing flashes at load. */ - .card { animation: card-in linear both; animation-timeline: view(); animation-range: entry 0% entry 35%; } - /* Header casts its shadow only once the page is actually scrolled. */ - .site-header::before { - content: ""; position: absolute; inset: 0; z-index: -1; - box-shadow: var(--shadow-lifted); opacity: 0; - animation: fade-in linear both; animation-timeline: scroll(); animation-range: 0 80px; - } + /* Range tuned on a real trackpad: entry 0–35% completed within a flick + and read as "no effect"; 0–75% keeps the drift visible at normal + scrolling speed while still finishing before the card is fully in. */ + .card { animation: card-in linear both; animation-timeline: view(); animation-range: entry 0% entry 75%; } /* Article reading progress — functional state, same Cherenkov budget line as the TOC scrollspy rule (navigation feedback, not decoration). */ .read-progress { - position: fixed; top: 56px; left: 0; width: 100%; height: 2px; z-index: 9; + position: fixed; top: 56px; left: 0; width: 100%; height: 3px; z-index: 9; background: var(--cherenkov); transform-origin: 0 50%; animation: progress linear both; animation-timeline: scroll(root); } @@ -840,6 +840,6 @@ mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(va to { transform: translate3d(5%, 5%, 0) rotate(10deg) scale(1.16); opacity: 1; } } @keyframes rise { from { opacity: 0; transform: translateY(14px); } } -@keyframes card-in { from { opacity: 0; transform: translateY(14px); } } +@keyframes card-in { from { opacity: 0; transform: translateY(28px); } } @keyframes fade-in { from { opacity: 0; } } @keyframes progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }