Jako nieprogramujący content writer i UX strategist, kiedy zaczynałem myśleć o własnej stronie, mój wybór siłą rzeczy ograniczał się do kilku opcji: WordPress, Substack, Ghost, Webflow. Każde z tych narzędzi ma swoją filozofię, swoje ograniczenia i swój sufit – punkt, do którego możesz dojść, zanim trafisz na ścianę możliwości lub na cennik, który zaczyna boleć.
Wtedy zdecydowałem inaczej: zbuduję własną aplikację webową od zera.
I zrobiłem to, nie będąc zawodowym programistą. A to tekst o tym, jak to wyszło, co po drodze poszło nie tak i dlaczego — mimo wszystko — uważam, że był to właściwy wybór.
Dlaczego nie gotowa platforma
Mam doświadczenie w projektowaniu doświadczeń użytkownika, które wynika z trudnego obcowania z systemami CMS. A przy tym pracuję z treścią – wiem, jak powinna wyglądać, jak się czytać, jak prowadzić czytelnika. Każda gotowa platforma prędzej czy później wymagałaby ode mnie kompromisu między tym, co chcę pokazać czytelnikom, a tym, na co pozwoli mi platforma.
Chciałem pełnej kontroli nad typografią. Chciałem własnego systemu tagów — nie wtyczki, nie zewnętrznego serwisu. Jestem z wykształcenia tłumaczem, więc chciałem, żeby strona była dwujęzyczna od samych fundamentów, nie jako nakładka tłumaczeniowa albo tłumaczenie „on the go”. I chciałem, żeby cały stack był mój: kod, baza danych, hosting, domena.
To był świadomy wybór metodyczny. Nie „bo WordPress jest zły”. Być może dla niektórych WordPress jest świetny. Ale dla kogoś, kto całymi dniami projektuje przepływy i pisze dokumentację produktową, budowanie własnego narzędzia to też rodzaj praktyki. Uczysz się inaczej, gdy nie masz wtyczki, do której możesz sięgnąć.
Jak zbudowałem stronę przy pomocy AI?
NEONEON powstał przy intensywnym wsparciu Claude — modelu językowego Anthropic. Chcę być z tym szczery, bo uważam, że model współpracy z AI zasługuje na osobny namysł, nie na ukrywanie go.
Nie używałem AI jako generatora kodu, który wklejam bez rozumienia. Używałem go jak bardzo cierpliwego, bardzo kompetentnego współpracownika technicznego — kogoś, kto rozumie architekturę, widzi konsekwencje decyzji i potrafi wyjaśnić, dlaczego coś działa tak, a nie inaczej. Ale też kogoś, kto czasami w pierwszym kroku najzwyczajniej myli się i wymaga skrupulatnego i świadomego nadzoru.
Konkretnie: Claude uczestniczył w projektowaniu architektury aplikacji, pisaniu komponentów, konfiguracji autoryzacji, debugowaniu błędów produkcyjnych i wdrażaniu moich kolejnych iteracji i usprawnień. Współpraca odbywała się przez Claude Cowork — narzędzie desktopowe, które daje modelowi bezpośredni dostęp do lokalnych plików projektu. Bez kopiowania fragmentów kodu do czatu, bez gubienia kontekstu między sesjami.
To podejście ma nazwę: AI-assisted development. I jest coraz bardziej realną ścieżką dla osób z doświadczeniem domenowym — designerów, strategów i redaktorów, którzy chcą budować własne narzędzia bez wieloletniego zaplecza programistycznego.
Stack: co i dlaczego
Next.js 16 z App Routerem — framework React do renderowania po stronie serwera. Decyzja częściowo techniczna, częściowo filozoficzna: zależało mi na tym, żeby strona była szybka i indeksowalna, a nie single-page application, która ładuje się przez trzy sekundy i pokazuje spinner.
Supabase jako baza danych i system autoryzacji. PostgreSQL w chmurze z panelem, który nie wymaga SQL do codziennej obsługi — idealne dla projektu, który będzie rozwijał filolog 🥲. Przechowuje artykuły, komentarze, wiadomości kontaktowe i tagi.
TipTap jako edytor treści w panelu admina. WYSIWYG z obsługą nagłówków, list, obrazów i osadzania filmów YouTube. To, co widzisz na stronie, wygląda niemal jeden do jeden jak w edytorze.
Vercel jako hosting. Automatyczny deploy po każdym git push do gałęzi main — co oznacza, że poprawka błędu jest na produkcji w dwie minuty od commita.
next-intl do dwujęzyczności. Polska wersja pod /pl/..., angielska pod /en/..., język wykrywany automatycznie z nagłówka Accept-Language przeglądarki. Interfejs tłumaczony przez pliki JSON, treści artykułów — osobno w bazie danych.
Tailwind CSS z customowymi tokenami kolorów: paper, ink, muted, border. Projekt używa Cormorant Garamond jako kroju szeryfowego i DM Sans do UI — żadnych domyślnych czcionek z create-next-app, które wylądowały w koszu od razu.
Framer Motion do animacji wejścia sekcji. Dyskretnych — strona nie tańczy, ale żyje.
Decyzje projektowe, które miały znaczenie
Typografia jako fundament
Strona jest tekstowa z założenia. Dlatego typografia nie była dekoracją — była pierwszą prawdziwą decyzją projektową. Cormorant Garamond to krój z charakterem, kojarzony z literaturą i prasą, nie z korporacyjnymi prezentacjami. DM Sans jako kontrast — neutralny, czytelny, dobry do UI.
Szczegóły, które robią różnicę: interlinia nagłówków H1–H3 w treści artykułu ustawiona na 1.2 — wartość standardowa dla dużego kroju szeryfowego. Domyślna interlinia 1.8 z body tekstu była na nagłówkach o 30–40% za duża, szczególnie widoczne na mobile, gdzie wieloliniowe nagłówki stawały się nieczytelne. Margines górny przed H2 i H3 zmniejszony o połowę — bo rytm tekstu na ekranie to nie rytm tekstu w druku.
System tagów w bazie danych, nie w plikach konfiguracyjnych
Pierwsza wersja tagów tłumaczyła nazwy przez pliki messages/pl.json i messages/en.json. Oznaczało to, że każdy nowy tag wymagał edycji kodu i deployu. Złe podejście — szczególnie dla projektu, który ma żyć i rosnąć.
Rozwiązanie: kolumna name_en w tabeli tags w Supabase. Panel admina ma teraz dwa pola przy dodawaniu tagu — polskie i angielskie. TagBadge czyta bieżący locale i wyświetla odpowiednią nazwę, z fallbackiem na polską, jeśli angielska nie jest wypełniona. Żadnego deployu przy nowym tagu.
Embedy z Instagrama: problem z nawigacją client-side
Embedy z Instagrama działały przy pełnym przeładowaniu strony, ale znikały przy nawigacji wewnątrz aplikacji — Next.js router nie przeładowuje strony, więc skrypt Instagrama uruchamiał się tylko raz.
Rozwiązanie nie było oczywiste: TiptapRenderer pozostał Server Componentem (żeby uniknąć błędu hydratacji), a osobny EmbedActivator — Client Component zwracający null — re-tworzy węzły <script> w useEffect po każdym zamontowaniu komponentu i wywołuje window.instgrm?.Embeds.process() jeśli biblioteka Instagrama była już w cache przeglądarki.
Dodatkowo: 43 piksele pustej przestrzeni pod embedem, które wynikały z tego, że znak \n w bloku kodu TipTap generował line-box przy line-height: 1.8. Reguła CSS :has(iframe) celuje tylko w bloki zawierające embed — nie dotyka zwykłych bloków kodu.
Favicon bez konfiguracji
Usunąłem domyślny favicon.ico generowany przez create-next-app i dodałem dwa pliki PNG (180×180) w katalogu app/. Next.js App Router automatycznie generuje odpowiednie tagi <link rel="icon"> i <link rel="apple-touch-icon"> — bez pliku konfiguracyjnego, bez żadnej dodatkowej logiki.
Panel admina bez lokalizacji
Strona logowania i cały panel admina żyją pod /login i /admin — bez prefiksu locale. To świadoma decyzja: panel admina ma jednego użytkownika (mnie), który zna język interfejsu niezależnie od ustawień przeglądarki. Dodawanie /pl/login i /en/login byłoby complexity bez wartości.
Błędy, które mnie czegoś nauczyły
Kilka rzeczy poszło nie tak w trakcie budowania. Wymieniam je, bo uważam, że lista błędów jest bardziej edukacyjna niż lista sukcesów.
Konflikt middleware.ts i proxy.ts. Next.js 16 zastąpił konwencję middleware.ts plikiem proxy.ts. Projekt już używał proxy.ts do detekcji języka, a ja omyłkowo utworzyłem middleware.ts na nowo — co powodowało błąd przy starcie serwera. Lekcja: przed tworzeniem nowego pliku sprawdź, czy projekt nie ma już jego odpowiednika.
Adres nadawcy w Resend. Resend pozwala wysyłać z adresu onboarding@resend.dev wyłącznie na email zarejestrowany na koncie. Wysyłka formularza kontaktowego na zewnętrzny adres wymagała użycia nadawcy z zweryfikowanej własnej domeny. Lekcja: dokumentacja usług mailowych kłamie przez przemilczenie — ograniczenia sandbox mode są pisane drobnym drukiem.
Async Server Component w Client Component. TagBadge był asynchronicznym Server Componentem używającym await getLocale(). Po tym jak ArticleCard dostał dyrektywę 'use client', Next.js zgłaszał błąd. Komponent musiał zostać przerobiony na Client Component z hookiem useLocale(). Lekcja: granica między Server a Client Components w Next.js App Router jest ostra i wymaga świadomości przy każdym refaktorze.
Zagnieżdżone linki. ArticleCard owijał cały artykuł w <Link>, a TagBadge wewnątrz też renderował <Link>. HTML nie pozwala na zagnieżdżanie <a> w <a> — przeglądarka zgłaszała błąd hydratacji. Rozwiązanie: tagi wyciągnięte poza główny <Link>. Lekcja: klikalne karty artykułów i klikalne elementy wewnątrz kart to dwa różne wzorce UI, które wymagają różnych rozwiązań strukturalnych.
Co to wszystko oznacza
NEONEON to dowód na to, że własna, w pełni customowa aplikacja webowa jest dziś w zasięgu osób, które nie są zawodowymi programistami — pod warunkiem że mają jasność co do tego, czego chcą, i cierpliwość do pracy iteracyjnej.
Każda poprawka, którą wdrożyłem, wynikała z obserwacji: co nie działa, co irytuje, co można zrobić lepiej. To jest właśnie praca UX — niezależnie od tego, czy robisz ją na cudzym produkcie, czy na własnym.
Strona nie jest skończona. Żadna dobra strona nie jest.
