Colophon / maszynownia, otwarta
Właściwa architektura to ta zwymiarowana pod problem.
Problem jest tu precyzyjny: osobiste archiwum, które ma być szybkie, kosztować zero, dać się czytać ludziom, crawlerom i agentom, i dać się utrzymywać na autopilocie. Cała reszta tej strony z tego wynika.
Wymagania → decyzje
Statyczny HTML z CDN, client JS tylko tam, gdzie jest interakcja. Wąskim gardłem ma być czytanie, nigdy ładowanie.
Żadnego serwera, bazy, CMS-a: nic do patchowania, nic, co można włamać, nic, co dzwoni o 3 w nocy. Osobiste archiwum nie może wymagać operacyjnej uwagi.
Statyczny build na planie hobby: koszt krańcowy wizyty to ~zero, koszt stały też. Pieniądze idą w treść, nie w hosting.
Strona utrzymuje się sama: ja mówię, co trzeba, code-os (si apre in una nuova scheda) to buduje, gate decyduje, czy się wdraża. agent-memory (si apre in una nuova scheda) z zasadami i definition of done, decyzje zapisane jako ADR: świeża sesja agentowa jest produktywna od pierwszego prompta, beze mnie w roli pamięci.
Architektura danych też jest dla agentów: typowane dane w src/content z przewidywalnymi ścieżkami, jeden komponent na odpowiedzialność, nazwy mówiące, co robią. Wszystko da się znaleźć bez palenia tokenów na eksplorację: minimalny wystarczający kontekst to właściwość repo, nie prompta.
Statyczne jest crawlowalne z definicji, a Next wykorzystuję do tego, co umie: Metadata API dla canonicali i OpenGraph, typowane sitemap i robots, OG images generowane w build time przez ImageResponse, fonty self-hosted przez next/font (zero layout shift), JSON-LD w layoutach. Dla szukających przez AI: llms.txt i /api/dossier, z tego samego źródła co strony.
Żadnych funneli, popupów, trackerów firm trzecich (cookieless analytics i tyle). To archiwum do czytania, nie landing do konwersji.
// ta strona też: powiedziałem, czego chcę, harness ją zbudował, gate zatwierdził.
Architektura
CONTENT / SSoT
src/content/*.ts: eseje, zasady, work: typowane dane, 3 języki
REACT SERVER COMPONENTS
cienkie route, sekcje SRP, client JS tylko przy interakcji
STATIC BUILD
każda route to HTML w build time, także OG, llms.txt, JSON
EDGE / CDN
Vercel: zero compute w runtime, koszt krańcowy ~0
ta sama treść wychodzi przez /llms.txt i /api/dossier
GATE
- eslint
- tsc --noEmit
- next build
zielone, albo nie ma commita
INWARIANTY
- a11y AA
- reduced motion
- jeden akcent
- SSoT-derived
- parytet i18n
regresja = bug
Decision record
Zero zależności, plus jedna
Runtime: Next + React + react-aria-components (dostępność widgetów: focus management, press semantics, focus trap: rzeczy, które psujesz, jeśli piszesz je od nowa). Bez Tailwinda, CMS-a, UI kitów, bibliotek animacji. Konsekwencja: minimalna powierzchnia update'ów, każda linia w bundlu jest uzasadnialna.
Wszystko statyczne, zawsze
Każda route (HTML, OG image, llms.txt, /api/dossier) jest generowana w build time i serwowana z CDN. Zero compute w runtime: bez cold startów, powierzchnia ataku zredukowana do minimum, ~zerowy koszt krańcowy requesta. Ograniczenie zaakceptowane: nowa treść = nowy deploy.
Treść to typ, nie strona
Eseje, zasady, work żyją w src/content jako typowane obiekty (SSoT). Archiwum, strona główna, sitemap, OG, llms.txt i /api/dossier pochodzą z jednego źródła. Konsekwencja: treść pisze się raz i nie może się rozjechać między formatami.
Jeden akcent
#E35B2D, wszędzie; na śniegu ciemniejszy odcień (#B8481C) dla kontrastu AA na małym tekście: ta sama barwa, nie drugi kolor. Obowiązuje też w easter eggach: każda śnieżyca ma dokładnie jeden pomarańczowy płatek.
Motion jako dekoracja, nigdy jako gate
Ukryty stan scroll-reveala istnieje tylko przy włączonym JS (@media scripting) i dozwolonym motion. JS wyłączony albo prefers-reduced-motion: treść nietknięta. Animacje są warstwą nad treścią, nie warunkiem jej czytania.
Dostępność jako inwariant
Skip-link z fokusowalnym celem, widoczny focus ring, aria-current w nawigacji, kontrasty AA, linki zewnętrzne ogłaszane, aria-live nawet dla easter eggów. Traktowana jak testy: regresja to bug, nie kompromis.
Czytelne dla maszyn
/llms.txt dla agentów, /api/dossier dla curla, JSON-LD dla crawlerów: wszystko generowane z tego samego SSoT co strony, więc nigdy rozsynchronizowane. Kontekst o mnie serwuję sam, w formacie odbiorcy.
Trzy języki, jeden SSoT
IT, EN, PL jako statyczne route (/it /en /pl), hreflang per strona, wspólne slugi. Bez middleware: detekcja to statyczny redirect. Parytet między językami sprawdzany w build time: brakujące tłumaczenie psuje build, nie zaufanie.
Świat to baner
Strona staje się SimoFree: baner z LinkedIna, na żywo. Canvas rysowany raz na rozmiar (bez pętli); wszystko, co da się przeczytać lub kliknąć, to DOM nad nim, w tej samej kamerze. Śnieg, tusz, jeden akcent. Easter egg to prawdziwa, opcjonalna gra.
Dwóch czytelników: ludzie i agenci
Każda strona ma bliźniaka w Markdownie z tego samego źródła, do tego llms.txt, llms-full.txt i serwer MCP tylko do odczytu. HTML pozostaje kanoniczny; Markdown ma noindex. Przełącznik Człowiek / Agent pokazuje, co czyta agent.
Zjazdy i trasy
Case'y to zjazdy (wspólne slugi, historie bez liczb klientów); sposoby współpracy to trasy (slugi w każdym języku, jedna wyszukiwana fraza na stronę, trudność kodowana kształtem, nigdy samym kolorem).
Ranking SimoFree weryfikowany przez powtórkę
Serwer nie ufa liczbom. Wydaje seed, przeglądarka zapisuje wejścia ze stałym krokiem (120 Hz), a serwer odtwarza zjazd tą samą czystą symulacją i sam liczy wynik. Jednorazowe tickety, minimalny czas rzeczywisty, limity na IP zapisane jako HMAC, tylko dodatnie wyniki, top 100 w Upstash.
Jeden ciągły ruch: niebo w stopce
Zjazd kończy się nocą: w stopce jest niebo z kropek (dither 1-bit) z yeti na orbicie. Canvas rysuje się raz na rozmiar; yeti to warstwa DOM przesuwana tylko przez transform, zapisywana tylko gdy poza zmienia się o piksel. Pętla działa tylko, gdy niebo jest na ekranie i nikt jej nie zatrzymał (WCAG 2.2.2). Przy reduced motion yeti stoi, a pętla się nie uruchamia. To wyjątek od zasady zero nieskończonych animacji; ADR-014 rozszerza ten sam kontrakt na resztę strony.
Świat się rusza: jedna choreografia, jeden przełącznik
Nagłówek to żywy baner: Simone zjeżdża, skacze z nawisu (inny trik na każdym okrążeniu, wywrotka co czwarte), a yeti go goni i przegrywa. Choreografia to czysta, przetestowana funkcja czasu i zaczyna się dokładnie od klatki banera. Canvas stoi; ruszają się tylko warstwy DOM przez transform. Każda animacja tła przechodzi przez jeden hook (tylko na ekranie, nigdy w tle) i jeden przełącznik pauzy dla całej strony. Przewijanie to zjazd: jedna bramka na sekcję w nawigacji. Turyn, Mediolan i Warszawa na horyzoncie.
Pasek przewijania też należy do stoku
Na myszy i gładziku natywny pasek staje się cienką szyną: suwak w trakcie używania zamienia się w box HUD w kolorze akcentu, a każda numerowana sekcja to nazwana bramka, do której można przeciągnąć albo kliknąć. Natywne przewijanie zostaje nietknięte (klawiatura, kółko, dotyk, technologie asystujące); szyna tylko je odzwierciedla i jest aria-hidden. Natywny pasek znika dopiero, gdy szyna się zamontuje, więc bez JS albo na dotyku zostaje systemowy. Podąża za przewijaniem, to nie ruch tła: jedna zmienna CSS na klatkę.
Dla maszyn
Treść o mnie serwuję sam, w formacie tego, kto ją konsumuje. Agenci czytają /llms.txt; kto woli terminal, ma endpoint JSON generowany z tego samego SSoT co strony:
curl simonebova.com/api/dossier
// jedno źródło, trzy formaty: HTML dla ciebie, llms.txt dla agentów, JSON dla curla.