świstak.codes staje się open‑source!
Jak możesz wiedzieć z artykułu „świstak.codes powraca!”, blog obecnie działa na moim autorskim rozwiązaniu. Jednak nie dzieliłem się do tej pory jego kodem. Z okazji 5-lecia bloga i 3-lecia jego przepisania postanowiłem udostępnić jego źródła na GitHubie. Jeśli jesteś ciekaw(a) więcej szczegółów, a także informacji o tym, dlaczego kod jest napisany tak, a nie inaczej, to zapraszam do lektury.
Gdzie jest kod?
Zacznijmy od najważniejszej rzeczy. Kod źródłowy znajdziesz tutaj:
Jest dostępny na licencji MIT, więc możesz go dowolnie wykorzystywać, modyfikować i rozwijać. Natomiast same artykuły oraz grafiki są objęte prawami autorskimi i nie możesz ich wykorzystywać bez mojej zgody, chyba że licencja stanowi inaczej. Więcej szczegółów znajdziesz w pliku LICENSE
w repozytorium.
Samo repozytorium jest jedynie automatyczną kopią gałęzi main
z prywatnego repozytorium bloga, bez zachowania historii pojedynczych commitów. Dlatego też nie będę przyjmować żadnych pull requestów. W przypadku sugestii poprawek proponuję skorzystać z dyskusji na GitHubie.
Uruchamianie
Aplikacja to zwykły projekt Next.js, więc aby go uruchomić, potrzebujesz Node.js. W momencie pisania artykułu serwer jest uruchamiany na wersji 22.12.0 (możesz zawsze to sprawdzić w Dockerfile
), ale powinien działać także na każdej późniejszej wersji LTS.
Aby uruchomić aplikację, wystarczy sklonować repozytorium i uruchomić polecenia:
npm install
npm start
Jeśli korzystasz z Visual Studio Code, możesz też uruchomić aplikację z poziomu edytora za pomocą zielonego przycisku uruchomienia aplikacji — skonfigurowałem to w .vscode/launch.json
.
Blog posiada także analitykę Matomo i wyszukiwarkę opartą na Typesense. Jeśli chcesz uruchomić je lokalnie, to musisz zainstalować Dockera (najprościej — Docker Desktop), a następnie uruchomić npm run docker:matomo
lub npm run docker:search
.
Dlaczego open source?
Zanim opowiem o kodzie, chcę poświęcić chwilę, dlaczego w ogóle po trzech latach stwierdziłem, że opublikuję kod. Powód jest dość prozaiczny — chcę zachować kopię treści, które przez lata napisałem w miejscu, gdzie powinno przetrwać długi czas. A jak zachowywać kopię, to pełną — z całą interaktywnością, która jest na blogu. Stąd też decyzja o publikacji kodu, a nie jedynie przeniesieniu treści na inną platformę.
Od razu też dodam, że absolutnie nie oznacza to końca bloga. Ostatnio zwolniłem tempo, ale nie zamierzam go zamykać. Dopóki widzę, że są czytelnicy, a także wszechobecne dziś AI crawlery nie zjadają mi całego transferu, blog będzie działać.
Struktura kodu i trochę historii
Technologia
Obecna wersja bloga została napisana na początku 2022 roku. Zestaw narzędzi, który wykorzystałem wtedy do jego stworzenia, to:
- Next.js w wersji 12
- styled-components do stylowania
- Nx do zarządzania monorepo
W momencie pisania tego artykułu wygląda to następująco:
- Next.js zaktualizowałem do wersji 15, aby pracować na najnowszej wersji frameworka.
- Zamiast styled-components używam CSS Modules z SCSS. Dzięki temu strona zajmuje mniej miejsca, szybciej się wczytuje, a tym samym osiąga lepsze wyniki w Lighthouse.
- Nie ma już monorepo, całość jest trzymana jako pojedynczy projekt. Nx jest świetnym narzędziem, ale praktycznie nie wykorzystywałem jego możliwości.
Zmiany te miałem okazję już opisywać, ogłaszając przejście na CSS Modules i update do Next.js 15.
Pozostałości po oryginalnych technologiach
Kod wciąż zawiera pozostałości po rozwiązaniach, które były używane w poprzednich wersjach, stąd:
- Mimo że nie ma już monorepo, kod dalej jest rozdzielony między foldery
/src
(główny kod aplikacji) a/libs
(prezentacje, narzędzia itp.). Jednak jest to wszystko zawarte w jednym projekcie i nie jest budowane oddzielnie. Możliwe, że z czasem struktura zostanie zmieniona. - Next.js korzysta z Pages Routera. Nie mam w planach tego zmieniać, bo zdecydowanie bardziej podoba mi się to rozwiązanie niż App Router.
- Aplikacja jest budowana za pomocą Webpacka, a nie Turbopacka, ponieważ nie udało mi się go skonfigurować tak, aby działał poprawnie. Dopóki Webpack działa, nie ma większej potrzeby zmiany.
- Tak samo korzystam wciąż ze starych podejść Next.js do obrazków (
next/legacy/image
) i linków (legacyBehavior
znext/link
). Są używane w wielu miejscach i może w przyszłości to przerobię. Na pewno będę do tego zmuszony, gdy tylko Next.js usunie kompatybilność z rozwiązaniami ze starszych wersji. - Część styli może być dziwnie porozdzielana, co wynika z tego, że wprost je kopiowałem ze styled-components do plików SCSS, dodając jedynie najbardziej konieczne zmiany.
Struktura stron
Blog ma dość nietypową strukturę stron wynikającą z tego, że jest to projekt, na którym uczyłem się Next.js po wielu latach pracy z czystym Reaktem. Podglądałem wtedy, jak inne blogi były napisane, ale nic mi nie pasowało do mojej idealnej architektury, więc stworzyłem własnego potworka, który wygląda fatalnie, ale działa tak jak chciałem.
- Artykuły są pisane w MDX z dość nietypową sekcją metadanych. Standardem jest stosowanie frontmatter, ale nie potrafiłem go doprowadzić do działania tak, jak chciałem, więc zdecydowałem się na własne rozwiązanie.
- Artykuły są rozdzielone między folderami
/src/_posts
(główna treść bloga),/src/_offtopic
(artykuły offtopicowe) i/src/_pages
(pozostałe strony, które chciałem pisać w MDX). Od razu rzuca się w oczy fakt, że nie jest to trzymane w standardowym dla Next.js/src/pages
(w przypadku używanego przeze mnie pages routera). Aby wygenerować strony widoczne przez routing Next.js, używam skryptunpm run generate-pages
(/tools/slugs-helper/generate-post-pages.js
), który generuje odpowiednie pliki.tsx
. - Tak przy okazji, dość nietypowo działają też zasilanie wyszukiwarki, generowanie RSS i sitemap.
- W przypadku wyszukiwarki aktualnie dane do niej generuję przez... scraping własnej strony. Nie jest to idealne rozwiązanie, ale działa. Znajduje się on w
./tools/scraper/scrape.mjs
, ale działa jedynie z lokalnie odpalaną wersją bloga, więc nie ściągniesz nim wszystkich danych (te i tak masz już tutaj dostępne). - RSS generuję za pomocą skryptu
public-fix.sh
. Dlaczego tak? Z którymś update Next.js przestało mi działać dynamiczne generowanie RSS i ich cache'owanie po uruchomieniu serwera. W ramach szybkiej poprawki napisałem ten skrypt i już tak zostało. Tak samo generowane są również sitemapy dla wyszukiwarek — dokładnie tym samym skryptem i dokładnie z tego samego powodu.
- W przypadku wyszukiwarki aktualnie dane do niej generuję przez... scraping własnej strony. Nie jest to idealne rozwiązanie, ale działa. Znajduje się on w
Unit testy i TypeScript
Blog został napisany w TypeScript, a w kodzie źródłowym możesz znaleźć nieco testów jednostkowych dla logiki. Jednak zarówno TypeScript, jak i testy nie działają:
- Ponownie jak wcześniej, któraś aktualizacja (już nie pamiętam, czy Nx, Jesta, czy Babela) zepsuła mi działanie testów jednostkowych na tyle, że nie byłem w stanie jej naprawić. Głównym problemem był hack, który używałem do wyciągania metadanych z MDX-ów na potrzeby testów (
./tools/testing/mdx-transform.js
). Na szczęście logiki praktycznie nie modyfikowałem od momentu napisania, więc nie miałem większej potrzeby przywracania testów. Możliwe, że kiedyś je przywrócę. - W kwestii TypeScript problemem jest napisana przeze mnie obsługa plików MDX, o której wspomniałem wcześniej. Stwierdziłem jednak, że nic się nie stanie, jeśli w momencie kompilacji zignoruję TypeScript. Wystarcza mi on jedynie do sprawdzania typów podczas pisania na bieżąco.
Dlaczego użyłeś X, a nie Y?
Teraz dla chętnych opiszę, dlaczego wybrałem takie, a nie inne rozwiązania. Częściowo już to opisałem w świstak.codes powraca!, ale teraz dodam też nieco kontekstu współczesnego.
Next.js
Chciałem napisać blog od zera, a przy okazji poznać jakiekolwiek rozwiązanie do Server-Side Rendering (SSR) lub Static Site Generation (SSG), ponieważ dotąd pracowałem jedynie z aplikacjami typu SPA. Next.js był mi najbliższy, bo pracuję głównie z Reaktem, a tak czy inaczej musiałbym się go nauczyć.
Jako alternatywy w tamtym czasie rozpatrywałem:
- Gatsby — jednak chciałem mieć też SSR, którego Gatsby nie ma. Gdybym jednak z góry postawił jedynie na generowanie stron w trakcie kompilacji, bardzo poważnie bym się nad nim zastanawiał.
- Jekyll — podobnie jak wyżej, a co więcej, tutaj niestety jeszcze bardziej zostałbym ograniczony. Do wizualizacji na blogu mocno wykorzystuję możliwości, jakie daje MDX, a Jekyll ograniczyłby mnie do czystego Markdown.
- Nuxt — ale nie umiałem (i dalej nie umiem) przekonać się do Vue. Jest to zdecydowanie najmniej podobający mi się framework front-endowy. Dla ewentualnych obrońców Vue dodam, że miałem okazję pracować z nim w jednym komercyjnym projekcie, więc nie jest to opinia wynikająca jedynie z czytania dokumentacji.
- Remix — ale chciałem jednak mieć SSG, którego Remix nie ma. Do tego, w czasie gdy pisałem bloga, była to nowość, więc miałem wątpliwości, czy warto w ogóle tworzyć na tym coś większego. Na swój sposób miałem dobre przeczucie, bo dzisiaj React Router przejął jego funkcje i nie wychodzą już nowe wersje Remiksa (aczkolwiek migracja jest bardzo prosta).
Dziś możliwe, że wybrałbym Astro do pisania od zera albo skorzystał z gotowca typu Docusaurus. Po czasie stwierdzam, że pisanie wszystkiego od zera nie dało mi szczególnej przewagi, a wręcz przeciwnie — sporo czasu poświęciłem na pisanie rzeczy, które są już gotowe w innych rozwiązaniach. Z drugiej strony nie miałbym okazji nauczyć się tak dogłębnie Next.js, więc nie żałuję tej decyzji.
CSS Modules z SCSS
Dwa powody:
- Lubię CSS Modules i najłatwiej było mi do niego przemigrować ze styled-components.
- W trakcie gdy przepisywałem style, jeszcze nie wszystkie przeglądarki obsługiwały zagnieżdżone style, a przy okazji też wykorzystuję mixiny i wbudowane w SCSS funkcje.
Z racji popularności Tailwinda odpowiem, dlaczego go nie użyłem — po prostu nie przepadam za nim, czysty CSS jest dla mnie znacznie czytelniejszy i nie czuję, żebym przez to tracił jakoś znacznie na szybkości programowania. Także prościej było mi przekopiować istniejące style do CSS Modules niż pisać je od nowa jako Tailwindowe klasy. Do tego kolejną trudnością byłby fakt, że musiałbym przemigrować wszystkie wartości typu wielkość czcionki, marginesy itd. do motywu Tailwindowego, a one nie są jakoś szczególnie dobrze ustrukturyzowane. Znowu, korzystając z domyślnych wartości Tailwinda, musiałbym zmieniać wszystkie style, aby do niego pasowały. Jak więc widać, tak było po prostu łatwiej.
A dlaczego oryginalnie korzystałem ze Styled Components? Bo jest bardzo wygodne i korzystałem wówczas z niego w projektach komercyjnych. Dziś jednak bym go nie wybrał. Pomijając fakt, że czysty CSS jest wydajniejszy, to w marcu 2025 roku zaprzestano aktywnego rozwoju tej biblioteki. Natomiast mógłbym rozważyć alternatywne rozwiązania, takie jak Vanilla Extract lub Stitches, które są znacznie bardziej wydajne, ponieważ mimo pisania styli w JavaScript, są one kompilowane do czystego CSS.
Matomo
Zależało mi na analityce szanującej prywatność użytkowników, która nie zbiera nadmiarowych danych, aby przesyłać je do reklamodawców. Matomo stawia przede wszystkim na bezpieczeństwo danych. Do tego jest darmowe w przypadku hostowania na własnym serwerze. Z racji tego, że dane, które zbiera Matomo, są bardzo mocno zanonimizowane, nie musisz klikać potwierdzeń, że zgadzasz się na zbieranie danych. Śledzenie aktywności nie opiera się na żadnych ciasteczkach, a do tego analityka przestrzega mechanizmu Do Not Track. Ten niestety z racji ogólnego braku adopcji jest już powoli wycofywany (np. Firefox zrobił to w grudniu 2024 roku).
W momencie wyboru Matomo nie rozpatrywałem żadnych alternatyw. Istotne było dla mnie pozbycie się Google Analytics, które dotychczas (z bólem serca) używałem, a Matomo miałem okazję poznać już nieco wcześniej. Dzisiaj możliwe, że zastanawiałbym się nad Litlyx albo Plausible. Jednak jestem bardzo zadowolony z Matomo, więc nie zamierzam go zmieniać.
Oprócz Matomo dodatkowo korzystam także z analiz dostarczanych przez Ahrefs, gdzie mam podsumowanie rzeczy, które łatwo przeoczyć (podstawowe błędy na stronie, błędne linki itp.), a także zagregowane dane z Google Search Console. Mogę tutaj powiedzieć, że do takich zastosowań jak prywatna strona darmowa wersja jest całkowicie wystarczająca.
Typesense
Zawsze byłem pod wrażeniem działania Algolii, jednak przed jej użyciem zniechęcało mnie to, że nie chciałem integrować zewnętrznej usługi niebędącej open source. Typesense to najbliższa funkcjonalnie, darmowa alternatywa, którą mogę lokalnie hostować. Chociaż warto dodać, że spokojnie zmieściłbym się w limitach darmowej wersji Algolii.
To, co dodatkowo mnie „kupiło”, to możliwość użycia Typesense jako bazy wektorowej, co daje mi możliwość przeszukiwania treści za pomocą „sztucznej inteligencji” (bardzo mocne uproszczenie, ale tak było najprościej to wytłumaczyć). W teorii mógłbym dzięki temu stworzyć chatbota bazującego na moich danych (RAG), ale w praktyce wykorzystuję to tylko i wyłącznie do automatycznego określania, które artykuły poruszają podobne tematy.
Inne alternatywy? Gdy wybierałem silnik wyszukiwania, to nie rozpatrywałem innych, ponieważ miałem okazję wcześniej pracować z Typesense i mi się spodobał. Dziś możliwe, że rozważałbym Meilisearch, szczególnie dlatego, że wykorzystuje mniej pamięci RAM. Aczkolwiek przy zbiorze danych z bloga nie ma to obecnie aż takiego znaczenia. Oczywiście istnieje też słynny ElasticSearch, ale użycie go byłoby strzelaniem z armaty do muchy. Prowadzę prostego bloga o programowaniu, nie zarządzam danymi wielkiej korporacji.
Giscus
Giscus to system komentarzy bazujący na dyskusjach GitHuba. Jest to jedyna usługa wykorzystywana na blogu, która używa zewnętrznego serwera, jednak w zupełnie innej formie niż inne systemy komentarzy.
Sam serwer Giscus również hostuję lokalnie, jedynie komentarze są trzymane zewnętrznie. Z GitHuba podczas logowania pobierany jest token użytkownika, który następnie wykorzystuje się do tworzenia i komentowania dyskusji w Twoim imieniu. Po swojej stronie nie trzymam ani komentarzy, ani danych użytkowników.
Giscus jest również otwartoźródłowy i można przeanalizować, że nie zbiera żadnych danych. Jedyna identyfikacja użytkownika po zalogowaniu się przez GitHuba to token użytkownika zapisany w localStorage
. GitHub nie śledzi Cię w żaden sposób. Jedynie wie, że zalogowałeś(-aś) się na blogu i ewentualnie skomentowałeś(-aś) wpis.
Self-hosting usług
Jak możesz zauważyć, wszystkie usługi, które do tej pory wymieniłem, są open source i mają możliwość lokalnego hostowania, co dokładnie robię. Dlaczego tak?
- Mam wtedy pewność, że usługi nie zbierają więcej danych, niż chcę, a także, że nie przekazują ich dalej. Teraz dzięki wypuszczeniu bloga jako open source możesz tym bardziej się upewnić, że nie zbieram danych, które nie są mi potrzebne.
- Hostowanie samodzielne Giscus, analityki i wyszukiwarki nie zużywa dużo zasobów na serwerze, więc nie ma większego problemu z ich uruchomieniem obok bloga.
- Także w ten sposób zabezpieczam się przed ewentualnym vendor lock-inem — nie jestem uzależniony od konkretnej usługi chmurowej. Nawet jeśli dana usługa przestanie być rozwijana, wciąż mogę ją samodzielnie hostować.
Oczywiście wymaga to więcej pracy. Zamiast kupić usługę i ją jedynie podpiąć, muszę skonfigurować serwer i odpowiednio go zabezpieczyć. To samo zresztą tyczy się hostowania całego bloga — mógłbym skorzystać z prostej w obsłudze chmury typu Vercel. Jednak nie przeszkadza mi to, chociaż przyznam, że administracja serwerem potrafi być momentami frustrująca. Co więcej, polecam każdemu programiście aplikacji webowych chociaż raz zrobić coś takiego od początku do końca — podstawy obsługi Linuksa, zrozumienie narzędzi typu Docker i prostych narzędzi CI/CD (np. Github Actions) mogą być bardzo przydatną wiedzą, nawet (albo tym bardziej) w erze vibe-codingu.
Podsumowanie
Na koniec chciałbym podziękować Ci za przeczytanie do końca. Mam nadzieję, że nawet jeśli nie potrzebujesz kodu mojego bloga, to zainteresowały Cię moje argumenty za i przeciw różnym technologiom oraz jak się one zmieniały w przeciągu lat. Jeśli masz jakieś pytania lub sugestie, to śmiało pisz w komentarzach pod tym artykułem lub na GitHubie.
Zdjęcie na okładce wygenerowane z użyciem Midjourney i ChatGPT.