Wróć do projektów
Wystawa obrazów jako reprezentacja portfolio

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.

SvelteKit

TypeScript

Cloudflare

i18n

Testy


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:#00000000

Redagowanie 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["&lt;pre class='mermaid'&gt;"]

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-Language i 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-label na 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 → markedhighlight.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/oklab okazał 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 (handleparaglideMiddlewarereroute), 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)