diff --git a/DESIGN.md b/DESIGN.md index 9283e75..0ff4c2f 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -120,7 +120,18 @@ ctao.org drifts looping nebula videos under Galaxy vignettes; our translation: (Cherenkov .16 / Azure .12 / Indigo .10) drifting on a 36s alternate cycle. Opt-in via ``: **home only**. Body text NEVER sits on a moving gradient — cards, panels and bands are solid layers above it. -4. One-shot hero `rise` stagger; unified hover system (below). Nothing else. +4. One-shot hero `rise` stagger; unified hover system (below). +5. **CSS scroll-driven effects** (2026 pass; `animation-timeline`, no JS — + "no scroll-triggered motion" below means no JS/observer motion): card + reveal on viewport entry (`view()`, entry 0–35%), header shadow fading in + over the first 80px of scroll, article reading-progress bar (2px Cherenkov + under the header — functional navigation state, same budget line as the + TOC scrollspy). All behind `@supports (animation-timeline: view())`, + compositor-only (the header shadow animates a pseudo-element's OPACITY, + never box-shadow in keyframes), auto-killed by the reduce block. +6. Micro-interactions: nav underline-grow (2px white rule, `scaleX` 0→1 on + hover, held on `aria-current`) and 1px press-down on `.btn`/navlink + `:active`. Nothing else. **One hover language** (mid-pass audit: hovers were inconsistent per element; ctao.org uses a single quiet-fade token — we do the same): every interactive diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..46a0aa8 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/brand/hero.jpg b/public/brand/hero.jpg index a80bfaf..fdc915c 100644 Binary files a/public/brand/hero.jpg and b/public/brand/hero.jpg differ diff --git a/public/diagrams/architektura-cms-z-baza.svg b/public/diagrams/architektura-cms-z-baza.svg new file mode 100644 index 0000000..8b4db31 --- /dev/null +++ b/public/diagrams/architektura-cms-z-baza.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + MASZYNA · CMS Z BAZĄ DANYCH + np. WordPress, Strapi, Drupal — wspólny schemat tej klasy rozwiązań + + + + + + + + Redaktor + panel admina w przeglądarce + logowanie do aplikacji + + + + + + + + Czytelnik + publiczna strona portalu + HTML składany na żądanie + + + + + + APLIKACJA CMS + + NON STOP + + panel admina + publiczny adres + logowanie + z internetu + + renderowanie stron + każde wejście czytelnika = + praca aplikacji i bazy + + pluginy / rozszerzenia + własny cykl aktualizacji + i podatności + + + + + + BAZA DANYCH + + NON STOP + treść + konta użytkowników + wymaga backupów i testów odtwarzania + migracje schematu przy aktualizacjach + historia zmian: zależnie od produktu, + bywa funkcją płatną + + + + + + media/ + + WOLUMEN + obrazy i pliki poza bazą + osobny backup + + + + logowanie do panelu + + + HTML na żądanie + + + SQL + + + upload + + + + 2 procesy działają bez przerwy, także nocą + + aktualizacje bezpieczeństwa = stały obowiązek + diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..64c8443 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/robots.txt b/public/robots.txt index b89f538..7bc6508 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,5 +1,4 @@ +# Demo instance — keep out of search indexes until this becomes the real +# portal on its final domain (then: Allow: / and restore the Sitemap line). User-agent: * -Allow: / -Disallow: /admin/ - -Sitemap: https://portal.ctao.org/sitemap.xml +Disallow: / diff --git a/src/content.config.ts b/src/content.config.ts index 2ee77ab..e6850f8 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -15,6 +15,9 @@ const news = defineCollection({ // Public-path string, e.g. "/uploads/foo.jpg" — Sveltia uploads media to // public/uploads, which Astro's image() helper can't validate (src/ only). cover: z.string().optional(), + // BCP-47 tag when an article is not in English (e.g. "pl") — carried to + // so screen readers pick the right voice. + lang: z.string().optional(), draft: z.boolean().default(false), }), }); diff --git a/src/content/news/architektura-demo-git-cms.md b/src/content/news/architektura-demo-git-cms.md index 2bb52fb..73c73b3 100644 --- a/src/content/news/architektura-demo-git-cms.md +++ b/src/content/news/architektura-demo-git-cms.md @@ -5,6 +5,7 @@ date: 2026-07-23 category: demo author: Cyfronet cover: /diagrams/cover-git-cms.svg +lang: pl draft: false --- @@ -25,6 +26,13 @@ na stronie, bez żadnego ręcznego deployu. Każda zmiana to commit, więc ![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](/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](/diagrams/architektura-cms-z-baza.svg) + ## Gdzie co jest | Co | Gdzie | @@ -79,6 +87,40 @@ researchu: 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 | 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 | +| 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 | + +## 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: diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index 6d8dae0..e2ebd61 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -2,12 +2,14 @@ import '../styles/global.css'; // `ambient` opts a page into the whole-page drifting brand wash (home only — see DESIGN.md) // `type`/`image` feed the social meta: articles pass type="article" + their cover. -const { title = 'CTAO Science Portal', description = 'CTAO Science Portal — demo', ambient = false, type = 'website', image } = Astro.props; +const { title = 'CTAO Science Portal', description = 'CTAO Science Portal — demo', ambient = false, type = 'website', image, lang = 'en' } = Astro.props; // Absolute URLs for canonical/OG/RSS (head pattern from the official Astro blog // template). `site` is a placeholder domain until the portal has a real one. const site = Astro.site ?? new URL('https://portal.ctao.org'); const canonical = new URL(Astro.url.pathname, site); -const ogImage = new URL(image || '/brand/og-card.jpg', site); +// SVG covers never reach og:image — link-preview crawlers (Slack/Teams/ +// LinkedIn) don't render SVG, so those articles fall back to the brand card. +const ogImage = new URL(image && !image.endsWith('.svg') ? image : '/brand/og-card.jpg', site); const path = Astro.url.pathname; const isActive = (href) => (href === '/' ? path === '/' : path.startsWith(href)); // Navigation = services from the Science Portal spec (REQUIREMENTS.md §2/§5). @@ -24,7 +26,7 @@ const links = [ ]; --- - + @@ -42,6 +44,11 @@ const links = [ + + + {/* The home hero photo is a CSS background, which browsers discover late — + preloading it moves the page's LCP element to the front of the queue. */} + {path === '/' && } {/* Brand typography (BRAND D.3): Inter (body) + Space Grotesk (headlines), self-hosted latin woff2 (variable) — @font-face lives in global.css. */} {/* Both brand fonts ship inlined in the render-blocking stylesheet — diff --git a/src/pages/index.astro b/src/pages/index.astro index bde48cd..086e2b6 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -97,7 +97,7 @@ const planned = ['Scheduling', 'Science alerts'];

{post.data.title}

{post.data.description}

-
· {readMin(post)} min read
+
{readMin(post)} min read
))} diff --git a/src/pages/news/[...page].astro b/src/pages/news/[...page].astro index 10ef6ce..ded38c9 100644 --- a/src/pages/news/[...page].astro +++ b/src/pages/news/[...page].astro @@ -69,7 +69,7 @@ const hrefFor = (n) => (n === 1 ? '/news' : `/news/${n}`);

{post.data.title}

{post.data.description}

-
· {readMin(post)} min read
+
{readMin(post)} min read
))} diff --git a/src/pages/news/[slug].astro b/src/pages/news/[slug].astro index 192d1a3..36ba124 100644 --- a/src/pages/news/[slug].astro +++ b/src/pages/news/[slug].astro @@ -20,7 +20,10 @@ const toc = headings.filter((h) => h.depth === 2 || h.depth === 3); const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).format(d); const readMin = Math.max(1, Math.round((post.body ?? '').split(/\s+/).length / 220)); --- - + + {/* Reading progress (scroll-driven CSS, no JS) — styled only where + animation-timeline is supported; elsewhere it stays an empty div. */} + {/* TOC pattern (MDN/Stripe/NN-g): sticky right rail ≥1200px, collapsed
under the title below that. CSS shows exactly one of the two (DESIGN.md). */}
diff --git a/src/styles/global.css b/src/styles/global.css index 5034d8d..e08e0af 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -158,6 +158,7 @@ summary { cursor: pointer; list-style: none; } summary::-webkit-details-marker { display: none; } a { color: var(--link); text-decoration: underline; text-underline-offset: 3px; } +::selection { background: var(--galaxy); color: #fff; } a:hover { color: var(--indigo); } /* Placeholder links ( without href) don't pretend to be clickable */ a:not([href]) { text-decoration: none; cursor: default; } @@ -220,10 +221,19 @@ img { max-width: 100%; height: auto; display: block; } .nav .spacer { flex: 1; } .nav a.navlink { color: var(--on-galaxy-muted); font-weight: 500; font-size: var(--fs-s); - padding: 10px 10px; text-decoration: none; border-bottom: 2px solid transparent; + padding: 10px 10px; text-decoration: none; position: relative; +} +/* 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 { + 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[aria-current="page"] { color: #fff; border-bottom-color: #fff; } +.nav a.navlink:hover::after, .nav a.navlink[aria-current='page']::after { 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. */ .nav .login-mock { @@ -456,7 +466,9 @@ li.suggest-label { margin: 0; color: var(--muted); font-size: var(--fs-s); display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } -.card .meta { margin-top: auto; color: var(--muted); font-size: var(--fs-xs); } +.card .meta { margin-top: auto; color: var(--muted); font-size: var(--fs-xs); display: flex; align-items: center; gap: 12px; } +.card .meta > span { display: inline-flex; align-items: center; gap: 4px; } +.card .meta .ico { width: 13px; height: 13px; stroke-width: 1.8; } /* Lead story — larger, splits when the grid itself is wide enough (container query) */ @container (min-width: 640px) { .card--featured { grid-column: 1 / -1; display: grid; grid-template-columns: 3fr 2fr; } @@ -510,7 +522,12 @@ span.page-step { color: var(--muted); } /* disabled end stop — non-interactive .article .meta .ico { width: 15px; height: 15px; stroke-width: 1.8; } /* Lead image — borderless, breaking slightly out of the text column on wide screens (editorial breakout; the column stays 70ch for reading) */ -.article .cover { border-radius: var(--radius); margin-bottom: var(--space-l); } +.article .cover { + border-radius: var(--radius); margin-bottom: var(--space-l); + /* Reserved box = no layout shift while the cover loads; also normalizes + editor-uploaded images of any proportions to the editorial 16:9 lead. */ + width: 100%; aspect-ratio: 16 / 9; object-fit: cover; +} @media (min-width: 900px) { .article .cover { margin-inline: -40px; width: calc(100% + 80px); max-width: none; } } @@ -783,6 +800,32 @@ mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(va .card, .search-results li { transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease); } .suggest li { transition: background-color var(--dur) var(--ease); } /* same quiet fade as every hover */ .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); } + .btn:active, .nav a.navlink:active { transform: translateY(1px); } + + /* === Scroll-driven effects (CSS only, compositor-only, no JS) === + Progressive enhancement behind @supports; the reduce block above + force-disables all animation, so these obey the user preference too. */ + @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; + } + /* 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; + background: var(--cherenkov); transform-origin: 0 50%; + animation: progress linear both; animation-timeline: scroll(root); + } + } } @keyframes nebula { /* Gemini-style aurora: slow, ample, gentle breath */ from { transform: translate3d(-9%, -6%, 0) rotate(-5deg) scale(1); opacity: 0.85; } @@ -797,3 +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 fade-in { from { opacity: 0; } } +@keyframes progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }