Portfolio
title: Portfolio date: 2026-09-23T12:00:00 excerpt: Wielojęzyczna strona-portfolio z blogiem, zasilana przez Git-based CMS i w pełni statyczny potok renderowania treści. banner: /uploads/Gemini_Generated_Image_uqebh2uqebh2uqeb.jpg bannerAlt: Odręczny szkic na serwetce z napisem "The Art of Programming", otoczony rysunkami drzewa binarnego, pseudokodu i bramek logicznych bigBanner: /uploads/Gemini_Generated_Image_uqebh2uqebh2uqeb.jpg bigBannerAlt: Odręczny szkic na serwetce z napisem "The Art of Programming", otoczony rysunkami drzewa binarnego, pseudokodu i bramek logicznych github_link: ''
🗂️ Portfolio — strona, którą właśnie czytasz
Wielojęzyczne portfolio i blog techniczny w SvelteKit, redagowane jak repozytorium kodu — bo nim właśnie jest.
Co to jest
To strona, na której akurat czytasz ten opis — moje portfolio i blog techniczny, zbudowany od zera w SvelteKit (Svelte 5, tryb runes). Celowo nie jest to kolejna "wizytówka" z listą umiejętności, tylko miejsce na rozbudowane, przemyślane opisy projektów (takie jak ten) oraz notatki z procesu — czym różni się od repozytoriów typu AI Log Assistant, które mają pokazywać kod, podczas gdy ta strona ma pokazywać sposób myślenia o nim.
Treść — posty, opisy projektów, doświadczenie zawodowe, teksty stron — nie leży w bazie danych, tylko jako pliki Markdown/YAML wersjonowane razem z kodem w repozytorium. Redaguję ją przez wbudowany panel Sveltia CMS podpięty bezpośrednio pod GitHuba — commit z panelu = commit w repo = redeploy.
Architektura
Obsługa żądania — SSR na Cloudflare Workers, z rozwiązywaniem języka na poziomie middleware:
flowchart TB
Visitor((Odwiedzający)) -->|"GET /projects/portfolio"| Hooks
subgraph Worker["Cloudflare Worker — adapter-cloudflare"]
Hooks["hooks.server.ts<br/>paraglideMiddleware → locale"]
Reroute["hooks.ts: reroute()<br/>deLocalizeUrl"]
Load["+page.server.ts<br/>load()"]
Hooks --> Reroute --> Load
end
Load --> Content["contentService.ts"]
Content --> HTML["wyrenderowany HTML"]
HTML --> Client["hydracja w przeglądarce"]
Client -->|"lazy import"| MermaidJS["mermaid.js renderuje diagramy<br/>(m.in. ten, który widzisz)"]
style Worker fill:#f38020,color:#fff,stroke:#00000000Redagowanie treści — od commita w panelu CMS do renderowania Markdown w przeglądarce:
flowchart LR
Editor["Sveltia CMS<br/>/portfolio-content-management-system"] -->|commit| Repo[("GitHub<br/>content/*.md, *.yml")]
Repo -->|"push → redeploy"| Glob["import.meta.glob<br/>(eager, build-time)"]
Glob --> GM["gray-matter<br/>frontmatter + treść"]
GM --> Marked["marked + własny renderer"]
Marked -->|"blok kodu"| HLJS["highlight.js"]
Marked -->|"blok jęz. mermaid"| Tag["<pre class='mermaid'>"]Cała treść jest wciągana do bundla przez import.meta.glob w trybie eager, więc strona nie odpytuje żadnej bazy w runtime — to zwykłe pliki, sparsowane raz, w build-time.
Kluczowe decyzje
Kilka wyborów, które uważam za bardziej wartościowe niż sam kod:
- Rezygnacja z własnego backendu. Pierwotnie treść miał obsługiwać osobny moduł FastAPI. Po zbudowaniu pierwszej wersji CMS-a uznałem, że utrzymywanie własnego API do CRUD-owania markdownem nie ma uzasadnienia, skoro Git już jest bazą danych z pełną historią zmian — Sveltia CMS robi dokładnie to samo, commitując bezpośrednio do repo, bez dodatkowej usługi do hostowania, monitorowania i zabezpieczania. Najlepszy kod to często kod, którego się nie pisze.
- Bez publicznych komentarzy. Świadoma decyzja, nie zaniedbanie — strona ma być bezpieczną przestrzenią do publikowania niedopracowanych jeszcze przemyśleń, bez presji publicznej krytyki pod każdym wpisem.
- Statyczna treść, dynamiczny routing. Treść jest w pełni statyczna (wbudowana w bundle), ale routing językowy i strony detali są renderowane po stronie serwera — kompromis między prostotą prerenderowania a potrzebą poprawnego
Content-Languagei przekierowań w zależności od lokalizacji.
Wielojęzyczność i design system
Strona działa w dwóch językach (pl jako domyślny, en) przez Paraglide (inlang), z prefiksami URL (/pl/..., /en/...) zamiast ciasteczek — łatwiejsze do zindeksowania i debugowania niż język trzymany w sesji.
Zamiast Tailwinda czy gotowego UI-kita, motyw jasny/ciemny stoi na własnym zestawie tokenów CSS opartym o color-mix() w przestrzeni oklab — kolory tła i tekstu w trybie ciemnym są wyprowadzane z tych samych zmiennych bazowych, a nie duplikowane w osobnej palecie:
--neutral-lightest: color-mix(in oklab, var(--neutral) 12%, white 88%);
--neutral-darkest: color-mix(in oklab, var(--neutral) 28%, black 72%);
[data-theme='dark'] {
--surface: var(--neutral-darkest);
--ink: var(--neutral-lightest);
}Punkty przełamania są zdefiniowane raz, w breakpoints.css, i udostępnione do CSS-a przez postcss-custom-media — jedno źródło prawdy zamiast magicznych liczb rozsianych po komponentach.
Testy
Kod strony jest testowany, mimo że to "tylko portfolio" — traktuję to jako trening dyscypliny, nie formalność:
- Vitest podzielony na dwa profile: komponenty Svelte w prawdziwej przeglądarce (
@vitest/browser-playwright, Chromium headless) oraz testy jednostkowe/serwerowe w środowisku Node. - Playwright do testów end-to-end nawigacji.
- Testy dostępności (m.in.
aria-labelna menu hamburgerowym) pisane równolegle z komponentami, nie doklejane na końcu.
Co poszło dobrze
- Od pustego repo do wdrożonej, dwujęzycznej, testowanej strony z własnym CMS-em — solo, w około trzy tygodnie.
- Zejście z pomysłu na własny backend na rzecz Git-based CMS-a znacząco zmniejszyło powierzchnię do utrzymania, bez utraty wygody redagowania treści.
- Potok Markdown →
marked→highlight.js/mermaid(ten sam, który renderuje właśnie ten tekst) dał realną, "blogową" jakość pisania technicznego przy zerowej infrastrukturze serwerowej. - System tokenów kolorów oparty o
color-mix/oklabokazał się dużo łatwiejszy w utrzymaniu niż osobna paleta dla trybu ciemnego. - Pierwsza samodzielna transformacja layoutu z desktopu na mobile — zrobiona od podstaw, bez gotowego frameworka CSS, tylko na własnych breakpointach i tokenach.
Czego się nauczyłem
Największym wyzwaniem nie był żaden pojedynczy feature, tylko interakcja między i18n a renderowaniem po stronie serwera. Po przejściu z adapter-static na adapter-cloudflare (potrzebnego, żeby routing zależny od języka mógł być w pełni dynamiczny, a nie tylko prerenderowany) trasy z prefiksem językowym zaczęły się zapętlać na przekierowaniach. Historia commitów szczerze pokazuje, jak to rozwiązywałem — nie olśnieniem, tylko metodycznym dokładaniem logowania w każdej warstwie łańcucha (handle → paraglideMiddleware → reroute), aż udało się zawęzić problem do konfliktu między pozostawionym gdzieniegdzie export const prerender a dynamicznym rozwiązywaniem locale w SSR, i do zbędnej strategii ciasteczek w paraglideVitePlugin, która wprowadzała drugie, konkurencyjne źródło prawdy o języku. Usunięcie obu rzeczy naprawiło problem.
Wniosek, który zostaje mi na przyszłość: wybór adaptera i strategii i18n trzeba ustalić przed rozbudową routingu, nie w trakcie — zmiana pod ruchem kosztowała więcej niż gdybym poświęcił na to pół dnia na starcie.
Co dalej
Wersja 1.0 nie miała jeszcze układu mobilnego — dziś już go ma: to była moja pierwsza transformacja desktop → mobile od podstaw, którą świadomie zrobiłem sam, krok po kroku, zamiast delegować, żeby się tego faktycznie nauczyć, a nie tylko dostać gotowy wynik. Dalej strona będzie się rozrastać naturalnie, w miarę jak będę dopisywać kolejne opisy projektów i wpisy przez CMS.
Stos technologiczny
Frontend: SvelteKit 2, Svelte 5 (runes), TypeScript, Vite
Treść: Markdown + YAML w repo, gray-matter, marked, highlight.js, mermaid
i18n: Paraglide (inlang), routing oparty o URL (pl / en)
CMS: Sveltia CMS (git-based, backend: GitHub)
Testy: Vitest (@vitest/browser-playwright + Node), Playwright
Infrastruktura: Cloudflare Workers, Wrangler, Bun, Docker (dev)