Przejdź do treści
Michał Świąder — wizytówka artysty

Michał Świąder — wizytówka artysty
Dwujęzyczna one-page wizytówka gitarysty fingerstyle z Krakowa - Next.js 16 + Strapi v5, WCAG AAA dla tekstu, headless CMS na blog i nagrania, od strategii po produkcję.

KLIENTMichał Świąder
TERMIN2026-08
ROLAFull-Stack Developer / Architect / UX Designer
STATUSZakończone

"Strona, która nie krzyczy - subkomunikuje. Organiczna, ciepła, autentyczna wizytówka artysty-nauczyciela, gdzie design sam przekonuje, zanim ktokolwiek przeczyta słowo."

Michał Świąder — Osobista wizytówka artysty: dwujęzyczna one-page strona gitarzysty fingerstyle, nauczyciela i kompozytora z Krakowa. Projekt objął pełen cykl - od briefu strategicznego, przez persony i scenariusze UX, specyfikacje wizualne, po build, acceptance testing i wdrożenie produkcyjne na VPS z Dokploy.

📋 Metryki projektu

  • Status: Zakończone (produkcja, Dokploy)
  • Rola: Full-Stack Developer / Architect / UX Designer
  • Zakres: Pełny cykl - od discovery i strategii, przez UX/UI design, po implementację, CMS i deployment
  • Cel: Autentyczna, niezależna wizytówka artysty-nauczyciela, która subkomunikuje filozofię "guitar psychologist" przez design, nie przez hasła

🚀 Ewolucja projektu (Product Journey)

  1. Discovery i strategia — Zaczęliśmy od briefu: wizja, positioning, tone of voice, kierunek wizualny (Minimal + Local/Artisan), SEO keywords lokalne. Zbudowałem 3 persony (Piotr Poszukiwacz - uczeń na plateau, Natalia Nasłuchująca - fanka, Kasia Koncertowa - organizatorka), zmapowałem 18 driving forces i spriorytetyzowałem cechy przez scoring F×I×F. Każda decyzja na stronie ma uzasadnienie w konkretnej potrzebie konkretnej osoby.
  2. Scenariusze UX i specyfikacje — 3 scenariusze UX pokrywające 7 stron. Każdy widok dostał pełną specyfikację: Object Registry, nawigacja, sekcje Accessibility/Responsive/SEO.
  3. Visual Design i Design Delivery — Prototypy HTML (one-page, blog-list, blog-single), design tokens (kolory, typografia, spacing, cienie, radius), kontrakt dla builda, test scenario. Libre Caslon Text + Fira Sans, logo to odręczny podpis Michała w SVG (stroke 1.5).
  4. Build — Next.js 16.3 + React 19.2 + Tailwind v4 z tokenami w @theme. Warstwy app/lib/components/content/messages, PL bez prefiksu + EN pod /en, 6 tras (/, /blog, /blog/[slug] + odpowiedniki EN; wpisy bloga generowane na pierwsze żądanie przez ISR), JSON-LD (Person + BlogPosting + ItemList + BreadcrumbList), sitemap/robots, custom 404, mobile menu z focus trap, YouTube facade (youtube-nocookie, lazy load).
  5. Acceptance testing — 22 testy (HP, ER, EC, DS, AX), pierwsza runda 21/22. Dwa issue Low (hardkodowany hex w komponencie, touch targety poniżej 44px) - naprawione, retest PASS. Status: approved.
  6. Integracja CMS — Strapi v5.51 + PostgreSQL 16 w monorepo (cms/). Trzy content-type: blog-post, tag, nagranie. i18n PL/EN, webhook on-demand revalidation (revalidateTag dla blog-posts i nagran), ISR 300s jako fallback, paginacja listy 6 na stronę, tagi jako relacja manyToMany, YouTube facade dla nagrań (max 7, sortowane po order). Build przechodzi bez dostępu do CMS - po deployu pierwsze żądanie wyzwala ISR, a webhook z CMS odświeża treść natychmiast po publikacji.
  7. Wdrożenie produkcyjne — Docker Compose + Traefik (Dokploy), healthcheck + condition, noindex CMS, multi-stage Dockerfile (node:22-alpine).

🎯 Problem biznesowy

Gitarzysta fingerstyle, nauczyciel i kompozytor z Krakowa istnieje w sieci rozproszony przez social media. Bez strony, tak naprawdę bez swojego miejsca w sieci. Potrzebuje własnego, niezależnego miejsca, które łączy ludzi z nim bezpośrednio - przez lekcje lub zwykły kontakt zainspirowany muzyką i filozofią.

❌ "Bóle" i wyzwania

  • Brak własnego miejsca: Social media jako jedyne źródło informacji - zależność od algorytmów, brak kontroli nad narracją.
  • Subkomunikacja, nie eksplicja: Filozofia "guitar psychologist" / "psychoterapia gitarowa" ma być odczuwana, nigdy wypisana. Design, ton, zdjęcia, muzyka przekonują podświadomie.
  • Dwujęzyczność: PL/EN bez duplikacji kodu i treści, pełna parzystość wpisów bloga.
  • Zarządzanie treścią: Michał sam publikuje wpisy i nagrania - bez dotykania kodu, bez redeployu.
  • SEO dla niszy: "lekcje fingerstyle Kraków", "lekcje gitary Kraków" - konkurencja o nazwisko w Google wymaga structured data i dynamicznego sitemap.
  • Dostępność: Strona artysty-nauczyciela musi być dostępna dla wszystkich, w tym osób korzystających z czytników ekranu.
  • Współdzielony VPS: Izolacja zasobów Docker (porty, nazwy kontenerów, wolumeny, sieci) bez kolizji z innymi projektami.

💡 Dlaczego to działa? (Podejście inżynieryjne)

  • Każda decyzja designowa ma uzasadnienie w personach: Nie "ładna strona", tylko strona, która transformuje potencjalnych uczniów z "podbij i wyjdź" w ludzi, którzy czują połączenie i wybierają kontakt.
  • Subkomunikacja przez design: Split hero z autentycznym zdjęciem (Michał boso z gitarą), odręczny podpis SVG jako logo, organiczna estetyka (jasne tło, turkusowy akcent, humanistyczna typografia) - wszystko subkomunikuje "guitar psychologist" bez wypisywania tego.
  • Jedna ścieżka kontaktu, wiele intencji: "Napisz do mnie" / "Napisz wiadomość" obsługuje ucznia (lekcje), fana (wiadomość), organizatora (występy) - bez kategoryzacji, bez formularza rezerwacji, bez cennika, bez lejka sprzedażowego.
  • Headless CMS (Strapi v5): Michał sam dodaje wpisy bloga i nagrania YouTube - dwujęzycznie, przez panel, bez dotykania kodu. Gdy publikuje w panelu, webhook uderza na front i czyści cache blog-posts oraz nagran (revalidateTag). ISR 300s to bezpieczna siatka, gdyby webhook nie doszedł.
  • Build bez CMS: Strapi nie musi być włączone, żeby zbudować front. Guard NEXT_PHASE w lib/strapi.ts zwraca puste dane podczas builda, a po deployu pierwsze żądanie wyzwala ISR i webhook z CMS odświeża treść natychmiast po publikacji. Build jest niezależny od infrastruktury CMS.
  • Custom controllers Strapi v5: Okazało się, że REST API Strapi v5.51 nie serializuje atrybutów relacji (tagi, lokalizacje) dla żądań publicznych - pole relacji w ogóle nie trafia do odpowiedzi. Obejście: custom controllers nadpisują find/findOne i używają Documents API, które populuje relacje prawidłowo. Front dostaje płaski, własny format odpowiedzi.
  • WCAG AAA dla tekstu: Poszedł krok dalej niż wymaga standard - kontrast tekstu 7:1+ (standard AA wymaga 4.5:1). Tokeny w globals.css @theme pociemnione tak, żeby najgorsze tło (canvas #f9f8f6) trzymało 7.4:1+. WCAG 1.4.11 (non-text 3:1): obwódki CTA z border-ink/50.
  • YouTube facade: Zamiast ładować iframe'y na starcie, pokazuję miniaturę + przycisk play. iframe ładuje się dopiero po kliknięciu (youtube-nocookie, lazy load, pełna dostępność keyboard). Zero zbędnych skryptów na pierwszym paint.
  • Auto-hide header: Sticky + translate - scroll w dół ukrywa, scroll w górę pokazuje. Pauza przy otwartym menu, wyłączony przy prefers-reduced-motion.

📈 Wpływ na biznes (ROI)

MetrykaPrzedPo wdrożeniuZnaczenie
Obecność w sieciTylko social mediaWłasna domena + dwujęzyczna wizytówkaPełna kontrola nad narracją i brandingiem
Zarządzanie treściąRęczne / przez developeraSamodzielne via Strapi CMS (blog + nagrania)Niezależność, zero kosztów bieżących
Publikacja wpisuRedeploy aplikacjiWebhook on-demand (natychmiast)Czas publish → widoczność ≤ 1 min
SEOBrak structured dataJSON-LD + dynamic sitemap + hreflangWyższa widoczność w Google na nazwisko i lokalne keywords
DostępnośćNiezagospodarowanaWCAG 2.2 AA (AAA dla tekstu), focus trap, ARIA, skip linkStrona dostępna dla wszystkich
AutentycznośćCiemna fasada, dystansOrganiczna, ciepła, autentyczna (boso z gitarą, podpis SVG)Subkomunikacja "guitar psychologist" przez design
KontaktFormularz / social mediaJedna ścieżka "Napisz do mnie" (mailto)Bez presji, bez lejka, naturalne

Lighthouse Scores

Natywna optymalizacja obrazów (next/image, WebP/AVIF, lazy load), Server Components domyślnie i eliminacja zbędnych skryptów (YouTube facade) dają mocny fundament pod wynik wydajności. Finalne Lighthouse scores warto zmierzyć na produkcji i wstawić tu realne wartości.

"Strona, która nie krzyczy - subkomunikuje. Organiczna, ciepła, autentyczna wizytówka artysty-nauczyciela, gdzie design sam przekonuje, zanim ktokolwiek przeczyta słowo."

🛠️ Wyzwania techniczne (Engineering Deep Dive)

  • Dwujęzyczność bez biblioteki i18n: Zamiast ciągnąć bibliotekę, napisałem własne, lekkie i18n - PL domyślny (bez prefiksu), EN pod /en/. Każdy widoczny tekst przez t("klucz") (klient) lub import paczki (serwer). Paczki messages/pl.json i messages/en.json z kluczami kropkowymi. Mniej zależności, pełna kontrola.
  • Next 16.3 not-found przy wielu root layoutach: Bug (vercel/next.js #59180) - Turbopack nie kompiluje not-found przy wielu root layoutach. Obejście: pojedynczy root layout (lang="pl" serwerowo) + kliencki LangSetter (useLayoutEffect ustawia documentElement.lang po hydracji, WCAG 3.1.1, bez mismatch) w app/en/layout.tsx. Custom 404 rozpoznaje locale po ścieżce (usePathname) - 404 na /en jest po angielsku, z lang="en" i linkami /en.
  • og:type ginący na /en: Next shallow-merge liczy się z pól - ostatni segment nadpisuje cały openGraph. Fix: en/layout.tsx dostaje pełny obiekt openGraph, nie tylko wybrane pola.
  • Strapi v5.51 relacje w REST API: REST API nie serializuje atrybutów relacji (tags, localizations) dla żądań publicznych ani z tokenem read-only. Documents API (strapi.documents) populuje relacje prawidłowo. Decyzja: custom controllers nadpisują find/findOne, używają Documents API, wystawiają płaski format ({ data, meta: { page, pageSize, pageCount, total } }). Paginacja przez limit/start (nie page/pageSize), count liczy tylko opublikowane.
  • Webhook revalidation idempotentny: revalidateTag("blog-posts") i revalidateTag("nagran") czyszczą listę, strony paginacji, wpisy i sekcję Muzyka naraz (zamiast listy revalidatePath). Webhook rejestrowany idempotentnie w bootstrapie Strapi (REVALIDATE_URL), nagłówek x-revalidate-token z webhooks.defaultHeaders. Route handler force-dynamic, walidacja sekretu (brak/błędny → 401).
  • WCAG AAA na canvas #f9f8f6: Najgorszym tłem jest canvas #f9f8f6 (ciemniejsze niż biel), więc szarości musiały iść poniżej ~#565656. ink-muted #505050, accent-strong #005c55 - kontrasty AAA utrzymane także na papierowym tle i w przyciskach.
  • Stretched-link na kartach bloga: Overlay kart dostał z-index 1 - kontener obrazu (position:relative) malował się nad pseudo-elementem wg kolejności DOM i obraz w karcie featured nie był klikalny. Po poprawce klik w obraz prowadzi do wpisu (1 link na kartę dla screen readera).
  • Unikalność na współdzielonym VPS: Prefix ms- na wszystkie nazwy globalne Docker (sieci, wolumeny, container_name w dev). Porty hosta dev: 3200/1348/5434 (zero kolizji z innymi projektami na tym samym VPS). Prod: traefik (WEB_DOMAIN/CMS_DOMAIN), healthcheck + condition, noindex CMS, dokploy-network external.

🛠️ Architektura i stack techniczny

  • Frontend: Next.js 16.3 (App Router, ISR 300s + webhook), React 19.2, TypeScript (strict, noUncheckedIndexedAccess), Tailwind CSS v4 (@theme z tokenami)
  • Typografia: next/font (Libre Caslon Text + Fira Sans)
  • CMS: Strapi v5.51 + PostgreSQL 16 (headless, server-side only, monorepo cms/)
  • Content-types: blog-post (i18n, draftAndPublish), tag (i18n, manyToMany), nagranie (YouTube, sortowane po order, max 7)
  • Obrazy: next/image z remotePatterns (STRAPI_PUBLIC_URL + i.ytimg.com), WebP/AVIF, lazy load, responsywne rozmiary
  • i18n: Własne, lekkie (PL domyślny bez prefiksu, EN /en/), paczki messages/{pl,en}.json
  • Structured Data: JSON-LD (Person, BlogPosting, ItemList, BreadcrumbList)
  • Dostępność: WCAG 2.2 AA (AAA dla tekstu), SkipLink, focus-visible, focus trap (mobile menu), ARIA, prefers-reduced-motion, touch targety ≥ 44px
  • Publikacja: Webhook on-demand revalidation (revalidateTag blog-posts + nagran, /api/revalidate, REVALIDATE_SECRET) + ISR 300s fallback
  • Infra: Docker Compose (dev + prod), Traefik (Dokploy), multi-stage Dockerfile (node:22-alpine), unikalne nazwy ms-*, healthcheck + condition
  • SEO: hreflang (pl, en, x-default), dynamic sitemap (iteruje paginację obu locale), robots.txt, OpenGraph (jeden spójny obraz OG)

🎨 Kluczowe decyzje projektowe

  • Organiczna estetyka (Minimal + Local/Artisan): Jasne tło #f9f8f6 (kartka papieru), turkusowy akcent, humanistyczna typografia (Libre Caslon Text serif + Fira Sans). Ciepło, naturalność, akustyczny charakter - strona sama komunikuje filozofię Michała.
  • Split hero z autentycznym zdjęciem: Michał boso na podłodze z gitarą (nie korpo-portret). Odręczny podpis SVG jako logo (stroke 1.5). CTA "Poznajmy się" / "Posłuchaj utworów" (nie "Zapisz się"). First impression, które sprawia, że Piotr czuje "to jest to".
  • About w pierwszej osobie: Osobista, nie CV. Historia i filozofia - dlaczego Michał gra i uczy. Subkomunikacja "muzyka jako narzędzie rozwoju" (nie wypisana). Wyeksponowany portret, cytat o słuchaniu z border-l-4 accent.
  • Muzyka jako proof live: YouTube facade (nagrania z CMS, max 7, miniatura + przycisk play, iframe po kliknięciu, youtube-nocookie, lazy load, keyboard accessible). Dowód performerski dla Kasi (organizatorka), wspierający scroll dla Piotra.
  • Ciemna stopka Kontakt: Świadoma decyzja - kontrast z jasnym one-page, mailto michalswiadermusic@gmail.com, social links (YT/IG/FB), brak formularza rezerwacji. Twórca (czyli ja) w stopce.
  • Jedna ścieżka kontaktu: "Napisz do mnie" / "Napisz wiadomość" / "Porozmawiajmy o lekcjach" - bez kategoryzacji intencji, bez cennika, bez lejka sprzedażowego. Mailto + social links w stopce.
  • Design tokens jako jedyna prawda: Kolory, typografia, spacing, cienie, radius w design-tokens.md i globals.css @theme. Komponenty trzymają się tokenów (poza pojedynczym dekoracyjnym fill w SVG play). WCAG AAA kontrasty udokumentowane w tokenach.
  • Server Components domyślnie: "use client" tylko tam, gdzie niezbędne (stan, eventy, hooki przeglądarki - MobileMenu, LangSetter, YouTubeEmbed, 404). Warstwy: app/ (routing, metadata, JSON-LD), lib/ (czyste funkcje, dostęp do danych), components/ (ui/ sekcje/ chrome/), content/ (dane one-page), messages/ (stringi UI).

Artefakty

Kontakt

Masz podobne wyzwanie? Napisz do mnie — wrócę z propozycją kolejnych kroków.

Napisz wiadomość