<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title>holas.pl</title>
        <link>https://holas.pl/pl/</link>
        <description>Piece of web by Holas</description>
        <language>pl</language>
        <atom:link href="https://holas.pl/pl/feed/" rel="self" type="application/rss+xml"/>
                <lastBuildDate>Mon, 20 Apr 2026 00:00:00 +0000</lastBuildDate>
                        <item>
            <title><![CDATA[Drzewo decyzyjne narzędzi dla Claude Code z globalną pamięcią]]></title>
            <link>https://holas.pl/pl/wpisy/drzewo-decyzyjne-narzedzi-claude-code/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/drzewo-decyzyjne-narzedzi-claude-code/</guid>
                        <pubDate>Mon, 20 Apr 2026 00:00:00 +0000</pubDate>
                        <description><![CDATA[Claude Code może łączyć się z zewnętrznymi narzędziami przez serwery MCP (Model Context Protocol) — Sentry do błędów produkcyjnych, JetBrains do introspekcji IDE, Context7 do dokumentacji bibliotek, Perplexity do wyszukiwania w sieci. Problem: mając sześć serwerów MCP do dyspozycji, Claude nie zawsze wybiera właściwy. Potrafi przeszukiwać grepem 20 plików w poszukiwaniu route&#039;a Symfony, kiedy JetB…]]></description>
            <content:encoded><![CDATA[<p>Claude Code może łączyć się z zewnętrznymi narzędziami przez serwery MCP (Model Context Protocol) — Sentry do błędów produkcyjnych, JetBrains do introspekcji IDE, Context7 do dokumentacji bibliotek, Perplexity do wyszukiwania w sieci. Problem: mając sześć serwerów MCP do dyspozycji, Claude nie zawsze wybiera właściwy. Potrafi przeszukiwać grepem 20 plików w poszukiwaniu route'a Symfony, kiedy JetBrains zwróciłby go jednym wywołaniem, albo odpytywać Context7 o &quot;najnowszą wersję PHP&quot;, choć aktualne dane ma tylko Perplexity.</p>
<p>Rozwiązaniem jest globalny <code>CLAUDE.md</code> — trwały plik instrukcji, który uczy Claude'a, po które narzędzie sięgnąć w zależności od typu zapytania. Ten wpis opisuje mój setup, drzewo decyzyjne, które zbudowałem, i jak możesz stworzyć własne dla swojego stosu technologicznego.</p>
<h2>Problem: za dużo narzędzi, brak strategii<a id="problem-za-dużo-narzędzi-brak-strategii" href="#problem-za-dużo-narzędzi-brak-strategii" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Serwery MCP dają Claude Code supermoce: automatyzację przeglądarki, śledzenie błędów, wyszukiwanie dokumentacji, integrację z IDE. Ale więcej narzędzi oznacza więcej wyborów, a bez wskazówek Claude podejmuje rozsądne, ale nieoptymalne decyzje.</p>
<p>Typowe problemy, które zaobserwowałem:</p>
<ul>
<li><strong>Złe narzędzie do zadania</strong> — odpytywanie Context7 o &quot;najnowszą wersję Symfony&quot; (ma tylko dokumentację, nie metadane o wydaniach) zamiast Perplexity</li>
<li><strong>Droga ścieżka zamiast taniej</strong> — używanie Grepa do szukania route'ów Symfony w wielu plikach zamiast zapytania JetBrains MCP o strukturalną listę</li>
<li><strong>Pominięcie specjalisty</strong> — brak sprawdzenia Sentry przy błędzie produkcyjnym, kiedy stack trace natychmiast ujawniłby przyczynę</li>
<li><strong>Zbędne zapytania</strong> — próbowanie wielu narzędzi po kolei, kiedy pamięć mogłaby od razu skierować do właściwego</li>
</ul>
<p>Rozwiązaniem są jawne reguły routingu zapisane w globalnym <code>CLAUDE.md</code>. Claude czyta ten plik na początku każdej sesji, więc drzewo decyzyjne jest zawsze dostępne.</p>
<h2>Mój stos MCP<a id="mój-stos-mcp" href="#mój-stos-mcp" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Oto sześć serwerów MCP, których używam, i w czym każdy jest najlepszy.</p>
<h3>Context7 — dokumentacja bibliotek i frameworków<a id="context7--dokumentacja-bibliotek-i-frameworków" href="#context7--dokumentacja-bibliotek-i-frameworków" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h3>
<p>Context7 serwuje wersjonowaną dokumentację z przykładami kodu. Podajesz nazwę biblioteki i pytanie, a on zwraca odpowiednią sekcję z oficjalnej dokumentacji.</p>
<p><strong>Workflow:</strong> <code>resolve-library-id</code> (znajdź bibliotekę) → <code>query-docs</code> (pobierz dokumentację do konkretnego pytania). Obsługuje wersjonowane zapytania — mogę odpytać <code>/sylius/sylius/v1.14.6</code> bezpośrednio, nie tylko &quot;latest&quot;.</p>
<p><strong>Najlepszy do:</strong></p>
<ul>
<li>Sygnatur metod i przykładów konfiguracji</li>
<li>Poradników migracji między wersjami frameworków</li>
<li>Wzorców z oficjalnej dokumentacji (formularze Symfony, mapowania Doctrine, filtry API Platform)</li>
</ul>
<p><strong>Nie radzi sobie z:</strong></p>
<ul>
<li>Aktualnymi numerami wersji i datami wydań (ma dokumentację, nie metadane)</li>
<li>Funkcjami języka PHP (PHP nie jest biblioteką z wersjonowaną dokumentacją w Context7)</li>
<li>Poradnikami bezpieczeństwa i CVE</li>
<li>Czymkolwiek wymagającym informacji w czasie rzeczywistym</li>
</ul>
<h3>Perplexity — aktualne fakty i research<a id="perplexity--aktualne-fakty-i-research" href="#perplexity--aktualne-fakty-i-research" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h3>
<p>Perplexity to wyszukiwarka z AI w czterech trybach: <code>search</code> (wyniki z cytatami ze źródeł), <code>ask</code> (odpowiedzi AI przez sonar-pro), <code>research</code> (dogłębna analiza wieloźródłowa, 30+ sekund) i <code>reason</code> (logiczne rozumowanie krok po kroku).</p>
<p><strong>Najlepszy do:</strong></p>
<ul>
<li>Najnowszych numerów wersji, dat wydań, harmonogramów EOL</li>
<li>Poradników bezpieczeństwa i szczegółów CVE</li>
<li>Funkcji języka PHP i RFC (property hooks, asymmetric visibility — tego nie ma w Context7)</li>
<li>Cen usług zewnętrznych (koszty API, porównania hostingów)</li>
<li>Ogólnych dobrych praktyk programistycznych i benchmarków</li>
</ul>
<p><strong>Kluczowa rola:</strong> Perplexity wypełnia każdą lukę Context7. Kiedy Context7 nic nie zwraca albo zwraca nieaktualne dane, Perplexity prawie zawsze ma odpowiedź.</p>
<h3>JetBrains — inteligencja kodu na poziomie IDE<a id="jetbrains--inteligencja-kodu-na-poziomie-ide" href="#jetbrains--inteligencja-kodu-na-poziomie-ide" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h3>
<p>To serwer MCP, który oszczędza najwięcej tokenów. JetBrains MCP łączy Claude Code z indeksem Twojego IDE — tym samym, który napędza autouzupełnianie, go-to-definition i refaktoryzację. Bazowy JetBrains MCP dostarcza generyczne narzędzia (wyszukiwanie plików, lookup symboli, wyszukiwanie tekstowe, komendy terminala), a pluginy frameworkowe rozszerzają go o specjalistyczne narzędzia — plugin Symfony dodaje listowanie route'ów, wyszukiwanie serwisów, inspekcję encji Doctrine i analizę Twiga.</p>
<p><strong>Mój typowy workflow:</strong> Wklejam link do ticketu Jira do konwersacji. Claude czyta zadanie przez Atlassian MCP, a potem używa JetBrains MCP do szybkiego rekonesansu kodu — znajdując odpowiednie serwisy, sprawdzając definicje route'ów, inspekcjonując pola encji — zanim napisze choćby jedną linię kodu. Ten krok &quot;najpierw przeanalizuj&quot; wyłapuje nieporozumienia wcześnie i daje Claude'owi kontekst do zadawania lepszych pytań wyjaśniających.</p>
<p><strong>Możliwości specyficzne dla Symfony</strong> (przez plugin Symfony):</p>
<ul>
<li><code>list_symfony_routes_controllers</code> — wszystkie route'y z kontrolerem, ścieżką, metodami. Jedno wywołanie zamiast grepowania atrybutów w dziesiątkach plików</li>
<li><code>locate_symfony_service</code> — znajdź definicję dowolnego serwisu po pełnej nazwie klasy</li>
<li><code>list_doctrine_entity_fields</code> — pola encji, typy i relacje w ustrukturyzowanym formacie</li>
<li><code>list_symfony_commands</code>, <code>list_symfony_forms</code> — komendy konsolowe i typy formularzy w jednym widoku</li>
</ul>
<p><strong>Przykład oszczędności tokenów:</strong> Znalezienie wszystkich route'ów pasujących do <code>/api/</code> w projekcie Symfony:</p>
<ul>
<li><strong>Bez JetBrains:</strong> Grep po <code>#[Route</code> w <code>src/Controller/</code>, odczyt każdego pasującego pliku, parsowanie atrybutów route'ów, konfrontacja z <code>_routes.yaml</code>. Łatwo 5-10 wywołań narzędzi i tysiące tokenów treści plików.</li>
<li><strong>Z JetBrains:</strong> Jedno wywołanie <code>list_symfony_routes_controllers</code> zwraca strukturalną, filtrowalną listę. Gotowe.</li>
</ul>
<p><strong>Przydatny też do:</strong> Indeksowane wyszukiwanie tekstowe (<code>search_in_files_by_text</code>), wyszukiwanie plików po nazwie, lookup symboli, uruchamianie komend terminala w IDE, budowanie i testowanie.</p>
<h3>Chrome DevTools — automatyzacja przeglądarki<a id="chrome-devtools--automatyzacja-przeglądarki" href="#chrome-devtools--automatyzacja-przeglądarki" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h3>
<p>Chrome DevTools MCP pozwala Claude'owi sterować przeglądarką: nawigować po URL-ach, klikać elementy, wypełniać formularze, robić zrzuty ekranu, inspekcjonować żądania sieciowe, uruchamiać JavaScript i wykonywać audyty Lighthouse.</p>
<p><strong>Najlepszy do:</strong></p>
<ul>
<li>Wizualnego testowania zmian UI po modyfikacji szablonów lub CSS</li>
<li>Audytów wydajności i dostępności Lighthouse</li>
<li>Debugowania problemów frontendowych (błędy w konsoli, żądania sieciowe)</li>
<li>Weryfikacji responsywności przy różnych szerokościach viewportu</li>
</ul>
<h3>Sentry — śledzenie błędów produkcyjnych<a id="sentry--śledzenie-błędów-produkcyjnych" href="#sentry--śledzenie-błędów-produkcyjnych" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h3>
<p>Sentry MCP łączy Claude'a z systemem śledzenia błędów. Może wyszukiwać issues, pobierać stack trace'y, analizować błędy z AI Sentry (Seer) i sprawdzać informacje o wydaniach i wdrożeniach.</p>
<p><strong>Workflow, który sprawia, że to jest wartościowe:</strong></p>
<ol>
<li>Zauważasz błąd (lub zgłasza go użytkownik)</li>
<li>Claude odpytuje Sentry: &quot;wyszukaj błędy 500 z ostatnich 24 godzin&quot;</li>
<li>Sentry zwraca stack trace, liczbę dotkniętych użytkowników, timestampy first/last seen</li>
<li>Claude czyta odpowiedni plik źródłowy, identyfikuje przyczynę i proponuje fix</li>
<li>Cały cykl debugowania odbywa się bez opuszczania terminala</li>
</ol>
<p><strong>Najlepszy do:</strong></p>
<ul>
<li>Badania błędów produkcyjnych z pełnymi stack trace'ami</li>
<li>Rozumienia częstotliwości i wzorców błędów (czy jest nowy? czy się pogarsza?)</li>
<li>Korelowania błędów z ostatnimi wdrożeniami</li>
</ul>
<h3>Atlassian/Jira — zarządzanie zadaniami<a id="atlassianjira--zarządzanie-zadaniami" href="#atlassianjira--zarządzanie-zadaniami" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h3>
<p>Jira MCP zapewnia pełne zarządzanie cyklem życia issues: tworzenie, odczyt, edycję, przejścia statusów, komentowanie i wyszukiwanie JQL.</p>
<p><strong>Najlepszy do:</strong></p>
<ul>
<li>Czytania specyfikacji zadań przed rozpoczęciem pracy</li>
<li>Aktualizowania statusu issues w trakcie pracy</li>
<li>Dodawania technicznych komentarzy do issues dla widoczności zespołu</li>
<li>Wyszukiwania JQL do znajdowania powiązanych issues lub sprawdzania bieżącego sprintu</li>
</ul>
<h2>Drzewo decyzyjne<a id="drzewo-decyzyjne" href="#drzewo-decyzyjne" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Sercem globalnego <code>CLAUDE.md</code> jest tablica routingu, która mapuje typy zapytań na najlepsze narzędzie. Oto faktyczna treść z mojego pliku:</p>
<pre><code class="language-markdown">## Search &amp; Research — Tool Decision Tree

### When to use Context7
**Best for**: library/framework API docs with clean code examples
- Versioned library docs (Sylius, Doctrine, API Platform, Symfony, GitHub Actions)
- Official method signatures, configuration examples, how-to patterns
- Concise, authoritative answers directly from official source
- `resolve-library-id` first, then `query-docs`
- **Fails for**: current version/release info, PHP language features,
  security CVEs, pricing, general programming

### When to use Perplexity
**Best for**: anything current, factual, or not a library doc
- Latest versions, release dates, EOL schedules
- Security advisories, CVEs, vulnerability details
- PHP language features (property hooks, new syntax)
- External service pricing
- General programming best practices, benchmarks
- Supplement when Context7 fails or for real-world context

### When to use WebSearch
- Official blog posts / release announcements
- As last resort or to supplement
</code></pre>
<p>Kluczowy wzorzec: każda sekcja zaczyna się od &quot;best for&quot; (kiedy wybrać to narzędzie) i kończy &quot;fails for&quot; (kiedy je pominąć). Claude korzysta z obu sygnałów — routingu pozytywnego i negatywnego.</p>
<h3>Tablica benchmarków<a id="tablica-benchmarków" href="#tablica-benchmarków" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h3>
<p>Przepuściłem te same 10 typów zapytań przez Context7, Perplexity i WebSearch i oceniłem wyniki. Ta tablica znajduje się w globalnym <code>CLAUDE.md</code>, żeby Claude mógł się do niej odwoływać przy podejmowaniu decyzji:</p>
<table>
<thead>
<tr>
<th>Typ zapytania</th>
<th>Context7</th>
<th>Perplexity</th>
<th>WebSearch</th>
</tr>
</thead>
<tbody>
<tr>
<td>Wersjonowana dokumentacja bibliotek</td>
<td>★★★★★</td>
<td>★★★★</td>
<td>★★★</td>
</tr>
<tr>
<td>Aktualne wersje / daty wydań</td>
<td>✗</td>
<td>★★★★★</td>
<td>★★★★</td>
</tr>
<tr>
<td>Przykłady kodu z oficjalnej dokumentacji</td>
<td>★★★★★</td>
<td>★★★★★</td>
<td>★★★★</td>
</tr>
<tr>
<td>Funkcje języka PHP</td>
<td>✗</td>
<td>★★★★★</td>
<td>★★★★</td>
</tr>
<tr>
<td>Framework how-to (API Platform itp.)</td>
<td>★★★★★</td>
<td>★★★★★</td>
<td>★★★★</td>
</tr>
<tr>
<td>Poradniki bezpieczeństwa / CVE</td>
<td>✗</td>
<td>★★★★★</td>
<td>★★★★</td>
</tr>
<tr>
<td>Ogólne programowanie (benchmarki itp.)</td>
<td>✗</td>
<td>★★★★</td>
<td>★★★★</td>
</tr>
<tr>
<td>CI/DevOps workflows</td>
<td>★★★★</td>
<td>★★★★</td>
<td>★★★★</td>
</tr>
<tr>
<td>Release notes / nowe funkcje</td>
<td>★★★★</td>
<td>★★★</td>
<td>★★★★★</td>
</tr>
<tr>
<td>Ceny usług zewnętrznych</td>
<td>✗</td>
<td>★★★★★</td>
<td>★★★★★</td>
</tr>
</tbody>
</table>
<p>Wzorzec jest czytelny: Context7 jest doskonały do wersjonowanej dokumentacji, ale dostaje zero w czymkolwiek wymagającym aktualnych lub rzeczywistych danych. Perplexity pokrywa niemal wszystko. WebSearch jest najsilniejszy w blogpostach i ogłoszeniach o wydaniach.</p>
<p>Umieszczenie tej tabeli w globalnym <code>CLAUDE.md</code> daje Claude'owi ilościową podstawę do wyboru narzędzia, nie tylko reguły.</p>
<h2>Jak działa globalna pamięć<a id="jak-działa-globalna-pamięć" href="#jak-działa-globalna-pamięć" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Claude Code obsługuje globalny plik instrukcji w <code>~/.claude/CLAUDE.md</code>. Ten plik jest ładowany na początku każdej konwersacji, niezależnie od projektu. To właściwe miejsce na reguły routingu narzędzi, bo serwery MCP są konfigurowane globalnie, nie per projekt.</p>
<p>Porównanie z projektowym <code>AGENTS.md</code>:</p>
<table>
<thead>
<tr>
<th></th>
<th><code>AGENTS.md</code></th>
<th><code>~/.claude/CLAUDE.md</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>Zakres</td>
<td>Jeden projekt</td>
<td>Wszystkie projekty</td>
</tr>
<tr>
<td>Treść</td>
<td>Konwencje kodu, architektura, komendy</td>
<td>Routing narzędzi, preferencje osobiste</td>
</tr>
<tr>
<td>Przykład</td>
<td>&quot;Używaj <code>ddev exec</code> do komend PHP&quot;</td>
<td>&quot;Używaj Context7 do dokumentacji Symfony&quot;</td>
</tr>
</tbody>
</table>
<h3>Struktura globalnego CLAUDE.md<a id="struktura-globalnego-claudemd" href="#struktura-globalnego-claudemd" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h3>
<p>Globalny <code>CLAUDE.md</code> działa najlepiej, gdy jest ustrukturyzowany jak podręcznik referencyjny, nie narracja. Claude skanuje go na początku sesji — czytelne nagłówki i jawne reguły sprawiają, że ten skan jest skuteczny.</p>
<p><strong>Wskazówki z mojego doświadczenia:</strong></p>
<ol>
<li><strong>Zacznij od reguły decyzyjnej, nie od opisu.</strong> &quot;Best for: versioned library docs&quot; jest bardziej użyteczne niż &quot;Context7 is a documentation server that...&quot;</li>
<li><strong>Uwzględnij tryby awarii.</strong> &quot;Fails for: current versions&quot; zapobiega próbom użycia Context7 do zapytań, z którymi sobie nie radzi.</li>
<li><strong>Dodaj inwentarz serwerów.</strong> Wymień każdy serwer MCP z jego narzędziami — Claude może się do tego odwołać, gdy potrzebuje funkcji, której jeszcze nie używał.</li>
<li><strong>Używaj konkretnych przykładów.</strong> &quot;Latest Symfony version → Perplexity&quot; jest lepsze niż &quot;use Perplexity for current data.&quot;</li>
<li><strong>Aktualizuj przy zmianach narzędzi.</strong> Dodałeś nowy serwer MCP? Zaktualizuj plik. Usunąłeś jeden? Usuń jego wpis. Nieaktualne reguły routingu są gorsze niż brak reguł.</li>
</ol>
<h2>Zbuduj własne drzewo<a id="zbuduj-własne-drzewo" href="#zbuduj-własne-drzewo" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Konkretne serwery MCP nie mają znaczenia — liczy się wzorzec. Niezależnie od tego, czy używasz Cursora, Windsurf czy Claude Code, czy piszesz w Pythonie czy Go, zasada jest ta sama: naucz swojego asystenta AI, po które narzędzie sięgnąć.</p>
<p><strong>Krok po kroku:</strong></p>
<ol>
<li><strong>Wymień swoje serwery MCP</strong> (lub równoważne integracje narzędziowe) i do czego każdy służy</li>
<li><strong>Zidentyfikuj nakładanie się</strong> — gdzie dwa narzędzia mogą odpowiedzieć na to samo zapytanie? (Context7 i Perplexity obsługują dokumentację Symfony, ale z różnymi mocnymi stronami)</li>
<li><strong>Zbenchmarkuj</strong> — przepuść te same 5-10 reprezentatywnych zapytań przez każde nakładające się narzędzie. Oceń wyniki. To daje dane, nie przeczucie.</li>
<li><strong>Napisz reguły routingu</strong> — dla każdego narzędzia napisz sekcje &quot;best for&quot; i &quot;fails for&quot; z konkretnymi typami zapytań</li>
<li><strong>Dołącz benchmark</strong> — tablica daje Twojemu AI ilościowy punkt odniesienia, nie tylko instrukcje</li>
<li><strong>Iteruj</strong> — pierwsza wersja nie będzie idealna. Kiedy Claude wybierze złe narzędzie, zaktualizuj plik. Po kilku sesjach routing się doszlifuje.</li>
</ol>
<p>Mój globalny <code>CLAUDE.md</code> zaczął się jako lista serwerów MCP z jednolinijkowymi opisami. Po kilku tygodniach obserwacji, gdzie Claude podejmował nieoptymalne wybory, ewoluował w drzewo decyzyjne opisane powyżej. Tablica benchmarków była największą pojedynczą poprawą — zamieniła niejasne reguły &quot;preferuj X nad Y&quot; w konkretne dane, na podstawie których Claude mógł działać.</p>
<p>Inwestycja jest niewielka (godzina na konfigurację, kilka minut na aktualizację) a zwrot się kumuluje: mniej zmarnowanych tokenów, szybsze odpowiedzi i mniej czasu na korygowanie wyborów narzędzi w trakcie konwersacji.</p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/claude-code-mcp-memory/featured.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>porady</category>
                                    <category>ai</category>
                        <category>php</category>
                        <category>symfony</category>
                        <category>dev-tools</category>
                    </item>
                <item>
            <title><![CDATA[Zabezpieczenie jedynego dynamicznego endpointu strony statycznej — formularz kontaktowy]]></title>
            <link>https://holas.pl/pl/wpisy/bezpieczenstwo-formularza-kontaktowego/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/bezpieczenstwo-formularza-kontaktowego/</guid>
                        <pubDate>Tue, 14 Apr 2026 00:00:00 +0000</pubDate>
                        <description><![CDATA[To część 3 serii o migracji holas.pl z WordPressa do własnego generatora stron statycznych opartego na Symfony. Część 2 opisuje architekturę. holas.pl jest stroną statyczną z jednym wyjątkiem: formularzem kontaktowym. Każdy wpis blogowy, strona kategorii i strona statyczna to wstępnie wyrenderowany plik HTML serwowany przez nginx. Formularz kontaktowy to jedyny endpoint uruchamiający PHP. To jedno…]]></description>
            <content:encoded><![CDATA[<p><em>To część 3 serii o migracji holas.pl z WordPressa do własnego generatora stron statycznych opartego na Symfony. <a href="/pl/wpisy/symfony-jako-generator-statycznych-stron/">Część 2</a> opisuje architekturę.</em></p>
<hr />
<p>holas.pl jest stroną statyczną z jednym wyjątkiem: formularzem kontaktowym. Każdy wpis blogowy, strona kategorii i strona statyczna to wstępnie wyrenderowany plik HTML serwowany przez nginx. Formularz kontaktowy to jedyny endpoint uruchamiający PHP.</p>
<p>To jedno wyjście rodzi konkretne pytanie bezpieczeństwa: jak chronić formularz na stronie statycznej przed botami i atakami CSRF, gdy nie ma sesji?</p>
<h2>Problem CSRF na stronach statycznych<a id="problem-csrf-na-stronach-statycznych" href="#problem-csrf-na-stronach-statycznych" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Standardowa ochrona CSRF działa przez osadzenie tokena w formularzu powiązanego z sesją użytkownika. Przy przesyłaniu formularza serwer weryfikuje, czy token pasuje do tego z sesji.</p>
<p>Strony statyczne nie mają sesji. Strona kontaktowa jest generowana raz podczas <code>ddev build</code> i serwowana jako plik. Nie może generować tokena per użytkownik w czasie renderowania. Wbudowana <code>csrf_protection</code> Symfony jest więc jawnie wyłączona w formularzu kontaktowym:</p>
<pre><code class="language-php">public function configureOptions(OptionsResolver $resolver): void
{
    $resolver-&gt;setDefaults([
        'csrf_protection' =&gt; false,  // celowe — strona statyczna, brak sesji
    ]);
}
</code></pre>
<p>Wyłączenie ochrony CSRF to właściwa decyzja. Alternatywa — dodanie dynamicznego endpointu PHP tylko do generowania tokenów dla formularza — przywróciłaby problem PHP-przy-każdym-żądaniu dla strony, która poza tym go nie potrzebuje.</p>
<h2>Cloudflare Turnstile jako zamiennik<a id="cloudflare-turnstile-jako-zamiennik" href="#cloudflare-turnstile-jako-zamiennik" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p><a rel="nofollow noopener noreferrer" target="_blank" href="https://developers.cloudflare.com/turnstile/">Cloudflare Turnstile</a> to alternatywa dla CAPTCHA działająca po stronie klienta, potwierdzająca człowieczeństwo użytkownika i dostarczająca jednorazowy token weryfikowalny po stronie serwera. Zastępuje CSRF jako warstwa zapobiegania botom.</p>
<p>Przepływ formularza:</p>
<ol>
<li>Strona kontaktowa jest serwowana jako statyczny HTML z osadzonym widżetem Turnstile (klucz strony jest wbudowany podczas buildu)</li>
<li>Turnstile uruchamia swoje wyzwanie niewidocznie; po sukcesie wywołuje <code>window.onTurnstileSuccess(token)</code>, który zapisuje token do ukrytego pola</li>
<li><code>contact.js</code> przechwytuje zdarzenie <code>submit</code> formularza i wysyła dane przez <code>fetch()</code> zamiast standardowego wysłania formularza</li>
<li>Endpoint PHP otrzymuje przesłanie, weryfikuje pola formularza, następnie weryfikuje token Turnstile przez API Cloudflare</li>
<li>Tylko jeśli oba sprawdzenia przejdą, e-mail jest wysyłany</li>
</ol>
<p>Weryfikacja po stronie serwera to kluczowy krok:</p>
<pre><code class="language-php">final class TurnstileValidator
{
    public function verify(string $token, string $remoteIp): bool
    {
        try {
            $response = $this-&gt;httpClient-&gt;request('POST', 'https://challenges.cloudflare.com/turnstile/v0/siteverify', [
                'body' =&gt; [
                    'secret'   =&gt; $this-&gt;secretKey,
                    'response' =&gt; $token,
                    'remoteip' =&gt; $remoteIp,
                ],
            ]);

            $data = $response-&gt;toArray();

            return true === ($data['success'] ?? false);
        } catch (\Throwable $e) {
            $this-&gt;logger-&gt;error('Weryfikacja Turnstile nie powiodła się: '.$e-&gt;getMessage());

            return false;
        }
    }
}
</code></pre>
<p>Walidator zawodzi bezpiecznie — każdy wyjątek zwraca <code>false</code> i blokuje przesłanie. Pusty lub brakujący token również zwraca <code>false</code> natychmiast.</p>
<h2>Minimalna powierzchnia ataku PHP<a id="minimalna-powierzchnia-ataku-php" href="#minimalna-powierzchnia-ataku-php" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Cała powierzchnia PHP aplikacji w produkcji to dwa wzorce URL:</p>
<pre><code class="language-nginx">location ~ ^/(api|pl/api)/ {
    fastcgi_pass $php_upstream;
    fastcgi_read_timeout 30;
}
</code></pre>
<p>Wszystko inne — każdy wpis blogowy, każda strona kategorii, mapa strony, kanał RSS, strona wyszukiwania — jest obsługiwane przez nginx serwujący pliki statyczne. PHP-FPM nie jest nigdy wywoływane przy dostarczaniu treści. Powierzchnia ataku warstwy PHP to jeden endpoint POST.</p>
<h2>Content Security Policy<a id="content-security-policy" href="#content-security-policy" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Konfiguracja nginx stosuje rygorystyczny CSP przy każdej odpowiedzi:</p>
<pre><code class="language-nginx">add_header Content-Security-Policy
    &quot;default-src 'self';
     script-src  'self' challenges.cloudflare.com;
     style-src   'self' 'unsafe-inline';
     img-src     'self' data:;
     frame-src   challenges.cloudflare.com;
     connect-src 'self' challenges.cloudflare.com;&quot;
    always;
</code></pre>
<p>Jedyną dozwoloną zewnętrzną domeną jest <code>challenges.cloudflare.com</code>, której Turnstile wymaga dla swojego skryptu, iframe i wywołania API. Brak Google Analytics, skryptów CDN, piksela Facebooka. <code>script-src</code> nie ma <code>'unsafe-inline'</code> ani <code>'unsafe-eval'</code> — cały JavaScript jest ładowany z plików z hashami przez Symfony AssetMapper.</p>
<p><code>'unsafe-inline'</code> w <code>style-src</code> to celowy kompromis: widżet Turnstile wstrzykuje style inline, których nie można uniknąć bez nonce CSP, a AssetMapper aktualnie nie wstrzykuje nonces. Wszystko inne jest zablokowane.</p>
<h2>Dodatkowe nagłówki bezpieczeństwa<a id="dodatkowe-nagłówki-bezpieczeństwa" href="#dodatkowe-nagłówki-bezpieczeństwa" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<pre><code class="language-nginx">add_header X-Frame-Options        &quot;SAMEORIGIN&quot;                       always;
add_header X-Content-Type-Options &quot;nosniff&quot;                          always;
add_header Referrer-Policy        &quot;strict-origin-when-cross-origin&quot;  always;
add_header Permissions-Policy     &quot;camera=(), microphone=(), geolocation=()&quot; always;
</code></pre>
<p>Ukryte pliki są blokowane:</p>
<pre><code class="language-nginx">location ~ /\. { deny all; }
</code></pre>
<p>HSTS jest obsługiwane przez Cloudflare, więc nie ma nagłówka <code>Strict-Transport-Security</code> w konfiguracji nginx — byłby zbędny.</p>
<h2>Wynik<a id="wynik" href="#wynik" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Formularz kontaktowy działa bez sesji, bez tokenów CSRF i bez JavaScriptu wymaganego do czegokolwiek poza samym wysłaniem formularza. Strona renderuje się w pełni jako statyczny HTML. Przesłania botów są blokowane przez weryfikację Turnstile po stronie serwera. Endpoint PHP jest nieosiągalny dla czegokolwiek poza żądaniami POST do <code>/api/contact</code>.</p>
<p>W porównaniu z WordPressem z wtyczką formularza kontaktowego, powierzchnia ataku zmalała z &quot;PHP działającego przy każdym żądaniu, wp-admin wystawione, XML-RPC włączone, 22 wtyczki, każda mogąca mieć lukę&quot; do &quot;jeden endpoint POST z weryfikacją Cloudflare&quot;.</p>
<p><a href="/pl/wpisy/dev-experience-dwa-kontenery/">Następny wpis</a> opisuje środowisko deweloperskie — DDEV, pipeline buildu i deployment do produkcji w dwóch kontenerach Docker.</p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/contact-form-security/contact-form-security.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>porady</category>
                                    <category>symfony</category>
                        <category>php</category>
                        <category>bezpieczenstwo</category>
                        <category>cloudflare</category>
                        <category>nginx</category>
                    </item>
                <item>
            <title><![CDATA[# notACMS — generator statycznych stron na Symfony]]></title>
            <link>https://holas.pl/pl/wpisy/notacms-generator-statycznych-stron/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/notacms-generator-statycznych-stron/</guid>
                        <pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
                        <description><![CDATA[notACMS to oparty na Symfony generator statycznych stron który zbudowałem na początku 2026 roku żeby zastąpić 19 lat WordPressa na holas.pl. Bez bazy danych, bez panelu admina CMS, bez PHP zaangażowanego w serwowanie treści. Cała strona — posty z bloga, strony kategorii, listy tagów, miesiące archiwum, wyszukiwarka, feed RSS, sitemap — jest pre-renderowana do statycznych plików HTML i serwowana pr…]]></description>
            <content:encoded><![CDATA[<p>notACMS to oparty na Symfony generator statycznych stron który zbudowałem na początku 2026 roku żeby zastąpić 19 lat WordPressa na holas.pl. Bez bazy danych, bez panelu admina CMS, bez PHP zaangażowanego w serwowanie treści. Cała strona — posty z bloga, strony kategorii, listy tagów, miesiące archiwum, wyszukiwarka, feed RSS, sitemap — jest pre-renderowana do statycznych plików HTML i serwowana przez nginx.</p>
<p>Kod jest open-source na licencji Apache 2.0 — zobacz <a rel="nofollow noopener noreferrer" target="_blank" href="https://github.com/holas1337/notACMS">GitHub / notACMS</a>. Zawiera 368 testów PHPUnit, CI/CD przez GitHub Actions i wzorzec lokalnych nadpisań który pozwala użytkownikom customizować szablony, CSS i tłumaczenia bez forkowania.</p>
<h2>Architektura<a id="architektura" href="#architektura" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Cała treść żyje w plikach Markdown z YAML frontmatterem:</p>
<pre><code>content/
├── blog/
│   └── tutorials/
│       └── my-post/
│           ├── en.md   ← wersja angielska
│           ├── pl.md   ← wersja polska
│           └── files/  ← obrazki, serwowane z /media/my-post/
└── pages/
    └── about/
        ├── en.md
        └── pl.md
</code></pre>
<p><code>ContentTreeBuilder</code> skanuje system plików, parsuje każdy plik przez <code>league/commonmark</code> (GitHub Flavoured Markdown + YAML frontmatter) i buduje typowane <code>ContentTree</code> — indeks w pamięci wszystkich postów i stron dla danego locale. Tagi, kategorie, miesiące archiwum i mapy URL są obliczane z tego indeksu.</p>
<p>Statyczny build używa sub-requestów Symfony: <code>HttpKernelInterface::handle()</code> z <code>SUB_REQUEST</code> renderuje każdy URL przez pełny kernel bez dotykania sieci. Jeśli URL działa w development, będzie w buildzie statycznym. Brak osobnego silnika szablonów, brak konfiguracji buildu.</p>
<h2>Kluczowe funkcje<a id="kluczowe-funkcje" href="#kluczowe-funkcje" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<ul>
<li><strong>Wielojęzyczność</strong> — sparowane <code>en.md</code> + <code>pl.md</code> w tym samym katalogu, automatyczne mapowanie tłumaczeń, tagi <code>hreflang</code>, przełącznik języka</li>
<li><strong>Wyszukiwarka</strong> — statyczny indeks oparty na WASM przez Pagefind, po stronie klienta, bez Elasticsearch ani Algolia</li>
<li><strong>Responsywne obrazki</strong> — automatyczne generowanie srcset przez ImageMagick, konfigurowalne szerokości wariantów</li>
<li><strong>Drafty i zaplanowane posty</strong> — dev-only przełączniki podglądu, zaplanowane posty publikowane automatycznie w czasie buildu</li>
<li><strong>Formularz kontaktowy</strong> — Cloudflare Turnstile CAPTCHA, ciasny CSP, pojedynczy endpoint POST</li>
<li><strong>Styleguide</strong> — strona tylko dla dev dokumentująca każdy komponent z prawdziwymi klasami CSS</li>
<li><strong>Wzorzec lokalnych nadpisań</strong> — katalog <code>local/</code> scala się na bazę w czasie buildu, customizacja bez forkowania</li>
</ul>
<h2>Stos technologiczny<a id="stos-technologiczny" href="#stos-technologiczny" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<ul>
<li>PHP 8.5, Symfony 7.x</li>
<li>Szablony Twig, SCSS (dart-sass przez symfonycasts/sass-bundle)</li>
<li>Symfony AssetMapper (bez webpacka, bez Vite, bez pipeline'u Node.js)</li>
<li>nginx + PHP-FPM (dwa kontenery Docker na produkcji)</li>
<li>Pagefind do wyszukiwania</li>
<li>PHPUnit 13, PHPStan level 6, Rector, PHP CS Fixer</li>
</ul>
<h2>Dlaczego go zbudowałem?<a id="dlaczego-go-zbudowałem" href="#dlaczego-go-zbudowałem" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Hugo, Jekyll i Eleventy były oczywistymi kandydatami. Wybrałem budowę customowej aplikacji Symfony bo znam Symfony dobrze, a posiadanie pełnego stacka okazało się mieć realne zalety: te same szablony, kontrolery i pipeline treści obsługują zarówno development jak i produkcję. Nie ma &quot;silnika szablonów build-time&quot; oddzielonego od &quot;silnika szablonów runtime.&quot; To po prostu Symfony.</p>
<p>Pełna historia dlaczego WordPress musiał odejść jest w <a href="/wpisy/dlaczego-odszedlem-od-wordpressa/">Dlaczego odszedłem od WordPressa</a>. Głębokie zanurzenie w architekturę jest w <a href="/wpisy/symfony-jako-generator-statycznych-stron/">Symfony jako generator statycznych stron</a>.</p>
<h2>Open Source<a id="open-source" href="#open-source" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>notACMS jest dostępny na <a rel="nofollow noopener noreferrer" target="_blank" href="https://github.com/holas1337/notACMS">GitHubie</a> na licencji Apache 2.0. Przygotowanie do wydania — testy, audyt AI, poprawki bezpieczeństwa, wzorzec lokalnych nadpisań — jest udokumentowane w <a href="/wpisy/369-testow-dla-statycznego-generatora-stron/">serii o open-source</a>.</p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/notacms/featured.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>realizacje</category>
                                    <category>php</category>
                        <category>symfony</category>
                        <category>static-site</category>
                        <category>nginx</category>
                        <category>architektura</category>
                    </item>
                <item>
            <title><![CDATA[Symfony jako generator stron statycznych — jak działa holas.pl]]></title>
            <link>https://holas.pl/pl/wpisy/symfony-jako-generator-statycznych-stron/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/symfony-jako-generator-statycznych-stron/</guid>
                        <pubDate>Thu, 02 Apr 2026 00:00:00 +0000</pubDate>
                        <description><![CDATA[To część 2 serii o migracji holas.pl z WordPressa do własnego generatora stron statycznych opartego na Symfony. Część 1 opisuje, dlaczego WordPress musiał odejść. Po podjęciu decyzji o odejściu od WordPressa pytanie brzmiało: czym go zastąpić? Hugo, Jekyll i Eleventy były oczywistymi kandydatami. Zdecydowałem się zbudować własną aplikację Symfony — nie dlatego, że istniejące narzędzia są niewystar…]]></description>
            <content:encoded><![CDATA[<p><em>To część 2 serii o migracji holas.pl z WordPressa do własnego generatora stron statycznych opartego na Symfony. <a href="/pl/wpisy/dlaczego-odszedlem-od-wordpressa/">Część 1</a> opisuje, dlaczego WordPress musiał odejść.</em></p>
<hr />
<p>Po <a href="/pl/wpisy/dlaczego-odszedlem-od-wordpressa/">podjęciu decyzji o odejściu od WordPressa</a> pytanie brzmiało: czym go zastąpić? Hugo, Jekyll i Eleventy były oczywistymi kandydatami. Zdecydowałem się zbudować własną aplikację Symfony — nie dlatego, że istniejące narzędzia są niewystarczające, ale dlatego, że Symfony znam dobrze, a posiadanie pełnego stosu okazało się mieć realne zalety.</p>
<h2>Podstawowa idea<a id="podstawowa-idea" href="#podstawowa-idea" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Strona działa w dwóch trybach:</p>
<ul>
<li><strong>Deweloperski</strong> — Symfony obsługuje żądania dynamicznie. Edytujesz plik Markdown, odświeżasz przeglądarkę, widzisz wynik. Standardowy workflow developerski Symfony z paskiem profilera.</li>
<li><strong>Build produkcyjny</strong> — jedno polecenie konsolowe renderuje każdy URL do pliku HTML na dysku. nginx serwuje te pliki bezpośrednio. PHP nie jest wywoływane przy dostarczaniu treści.</li>
</ul>
<p>Te same szablony, kontrolery i pipeline treści obsługują oba tryby. Nie ma osobnego &quot;silnika szablonów do budowania&quot; oddzielnego od &quot;silnika szablonów do działania&quot;. To po prostu Symfony.</p>
<h2>Pipeline treści<a id="pipeline-treści" href="#pipeline-treści" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Cała treść mieszka w plikach Markdown z nagłówkiem YAML:</p>
<pre><code>content/
├── blog/
│   └── tutorials/
│       └── moj-wpis/
│           ├── en.md   ← wersja angielska
│           ├── pl.md   ← wersja polska
│           └── files/  ← obrazy, serwowane pod /media/moj-wpis/
└── pages/
    └── about/
        ├── en.md
        └── pl.md
</code></pre>
<p><code>ContentTreeBuilder</code> skanuje system plików, parsuje każdy plik przez <code>league/commonmark</code> (GitHub Flavoured Markdown + nagłówki YAML) i buduje typowany <code>ContentTree</code> — indeks w pamięci wszystkich wpisów i stron dla danego locale. Tagi, kategorie, miesiące archiwum i mapy URL są wyliczane z tego indeksu.</p>
<p><code>ContentItem</code> to obiekt wartości <code>final readonly</code>. Brak bazy danych, ORM, migracji. Dodanie wpisu oznacza stworzenie katalogu z dwoma plikami Markdown i uruchomienie buildu.</p>
<h2>Polecenie build — sub-żądania Symfony<a id="polecenie-build--sub-żądania-symfony" href="#polecenie-build--sub-żądania-symfony" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Build statyczny używa techniki specyficznej dla Symfony, która odróżnia to podejście od Hugo czy Jekylla: wywołuje <code>HttpKernelInterface::handle()</code> w celu tworzenia <strong>sub-żądań</strong> — wewnętrznych wywołań PHP przechodzących przez pełne jądro Symfony bez dotyku sieci.</p>
<pre><code class="language-php">$request = Request::create($url);
$request-&gt;attributes-&gt;set('_static_build', true);
$response = $this-&gt;kernel-&gt;handle($request, HttpKernelInterface::SUB_REQUEST, false);

if ($response-&gt;getStatusCode() &lt; 400) {
    file_put_contents($outputPath, $response-&gt;getContent());
}
</code></pre>
<p>Dla każdego URL — wpisów blogowych, stron kategorii, stron tagów, miesięcy archiwum, stron statycznych, stron błędów, kanałów RSS, mapy strony — polecenie build tworzy żądanie, przepuszcza je przez Symfony i zapisuje HTML na dysk. URL <code>/blog/</code> staje się <code>public/static/blog/index.html</code>. URL <code>/sitemap.xml</code> staje się <code>public/static/sitemap.xml</code>.</p>
<p>Brak osobnego silnika szablonów do nauki. Brak konfiguracji buildu. Jeśli URL działa w developmencie, będzie w statycznym buildzie.</p>
<h2>nginx serwuje wszystko<a id="nginx-serwuje-wszystko" href="#nginx-serwuje-wszystko" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>W produkcji nginx obsługuje całe dostarczanie treści:</p>
<pre><code class="language-nginx">location / {
    try_files /static$uri/index.html /static$uri/index.xml /static$uri $uri =404;
}

# PHP tylko dla formularza kontaktowego
location ~ ^/(api|pl/api)/ {
    fastcgi_pass $php_upstream;
}
</code></pre>
<p>Dla każdego przychodzącego URL nginx najpierw próbuje wstępnie wyrenderowanego pliku HTML. PHP-FPM jest wywoływane tylko dla <code>/api/contact</code> — jedynego dynamicznego endpointu. Każdy wpis blogowy, lista kategorii, strona tagów i strona statyczna to plik serwowany bezpośrednio z dysku. Raspberry Pi 5 obsługuje to banalnie.</p>
<h2>Bez bazy danych<a id="bez-bazy-danych" href="#bez-bazy-danych" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Drzewo treści jest budowane z plików Markdown podczas budowania. W produkcji jest cache'owane bez ograniczeń czasowych w cache'u systemu plików Symfony (unieważniane i przebudowywane przy każdym deployu). W developmencie jest przebudowywane przy każdym żądaniu, żeby zmiany plików były natychmiast widoczne.</p>
<p>Brak MySQL, schematu, migracji, connection poolingu, wolnych zapytań. Dodawanie treści oznacza tworzenie plików i uruchamianie <code>ddev build</code>. Usuwanie treści oznacza usuwanie plików. Cała historia strony jest w gicie.</p>
<h2>Wielojęzyczność bez bazy danych<a id="wielojęzyczność-bez-bazy-danych" href="#wielojęzyczność-bez-bazy-danych" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Zarówno polska, jak i angielska treść mieszka w tym samym katalogu:</p>
<pre><code>content/blog/tutorials/moj-wpis/
    en.md  → slug: &quot;blog/my-post&quot;     → /blog/my-post/
    pl.md  → slug: &quot;wpisy/moj-wpis&quot;   → /pl/wpisy/moj-wpis/
    files/ → obrazy serwowane pod /media/moj-wpis/
</code></pre>
<p>Współlokalizacja to link translacji. Dwa pliki locale w tym samym katalogu są automatycznie traktowane jako tłumaczenia siebie nawzajem. <code>TranslationMapBuilder</code> buduje <code>{directoryKey → {locale → url}}</code> dla tagów <code>hreflang</code> i przełącznika języka. Bez pola <code>translation_key</code>, bez tabeli łączącej, bez synchronizacji do zarządzania.</p>
<h2>Wyszukiwanie z Pagefind<a id="wyszukiwanie-z-pagefind" href="#wyszukiwanie-z-pagefind" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Wyszukiwanie to oparty na WASM statyczny indeks budowany przez <a rel="nofollow noopener noreferrer" target="_blank" href="https://pagefind.app/">Pagefind</a> po wygenerowaniu plików HTML:</p>
<pre><code class="language-bash">npx pagefind --site public/static --output-path public/pagefind
</code></pre>
<p>Pagefind czyta wstępnie wyrenderowany HTML, indeksuje regiony <code>data-pagefind-body</code> i generuje binarny indeks w <code>public/pagefind/</code>. Strona wyszukiwania ładuje ten indeks po stronie klienta przez dynamiczny <code>import()</code>. Brak Elasticsearch, Algolii, zapytań wyszukiwania po stronie serwera. Indeks to zbiór plików statycznych.</p>
<h2>Prostota redesignu<a id="prostota-redesignu" href="#prostota-redesignu" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Zmiana wyglądu strony oznacza edycję szablonów Twig i SCSS. Brak biblioteki komponentów React do aktualizacji. Brak pipeline'u Node.js. Brak hierarchii motywów WordPress. Cały frontend to:</p>
<ul>
<li><code>assets/styles/app.scss</code> — jeden punkt wejścia importujący pliki częściowe</li>
<li><code>templates/</code> — szablony Twig</li>
<li><code>symfonycasts/sass-bundle</code> — kompiluje SCSS przez dart-sass, bez Node.js</li>
</ul>
<p>Żeby zmienić schemat kolorów: edytuj <code>_variables.scss</code>. Żeby zmienić layout: edytuj szablon Twig. Uruchom <code>ddev build</code> i gotowe.</p>
<h2>Kompromisy<a id="kompromisy" href="#kompromisy" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Głównym kompromisem w porównaniu z Hugo czy Jekyllem jest to, że to własny kod — ja go utrzymuję. Zaletą jest to, że jest dokładnie tym, czego potrzebuję, niczym więcej. Brak ekosystemu wtyczek do nawigowania, brak ścieżki aktualizacji do martwienia się, brak feature flag dla rzeczy, których nigdy nie użyję.</p>
<p>Jedyna naprawdę dynamiczna funkcja — formularz kontaktowy — jest omówiona w <a href="/pl/wpisy/bezpieczenstwo-formularza-kontaktowego/">następnym wpisie</a>.</p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/symfony-static-site-generator/symfony-static-site-generator.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>porady</category>
                                    <category>symfony</category>
                        <category>php</category>
                        <category>statyczna-strona</category>
                        <category>nginx</category>
                        <category>architektura</category>
                        <category>performance</category>
                    </item>
                <item>
            <title><![CDATA[19 lat z WordPressem — dlaczego w końcu zrezygnowałem]]></title>
            <link>https://holas.pl/pl/wpisy/dlaczego-odszedlem-od-wordpressa/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/dlaczego-odszedlem-od-wordpressa/</guid>
                        <pubDate>Tue, 17 Mar 2026 00:00:00 +0000</pubDate>
                        <description><![CDATA[holas.pl działał na WordPressie od 2007 do początku 2026 roku. Dziewiętnaście lat. W tym czasie WordPress wyrósł z przyzwoitej platformy blogowej w coś, czego prawie nie rozpoznaję — a jego utrzymanie stało się większym wysiłkiem niż utrzymanie samej treści. To pierwszy wpis z serii dokumentującej migrację do własnego generatora stron statycznych opartego na Symfony. Kolejne wpisy omawiają archite…]]></description>
            <content:encoded><![CDATA[<p>holas.pl działał na WordPressie od 2007 do początku 2026 roku. Dziewiętnaście lat. W tym czasie WordPress wyrósł z przyzwoitej platformy blogowej w coś, czego prawie nie rozpoznaję — a jego utrzymanie stało się większym wysiłkiem niż utrzymanie samej treści.</p>
<p>To pierwszy wpis z serii dokumentującej migrację do własnego generatora stron statycznych opartego na Symfony. Kolejne wpisy omawiają <a href="/pl/wpisy/symfony-jako-generator-statycznych-stron/">architekturę</a>, <a href="/pl/wpisy/bezpieczenstwo-formularza-kontaktowego/">formularz kontaktowy</a>, <a href="/pl/wpisy/dev-experience-dwa-kontenery/">środowisko deweloperskie</a> i <a href="/pl/wpisy/budowanie-strony-z-ai-claude-code/">budowanie strony z pomocą AI</a>.</p>
<h2>Taśmociąg aktualizacji<a id="taśmociąg-aktualizacji" href="#taśmociąg-aktualizacji" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>WordPress regularnie wydaje aktualizacje bezpieczeństwa. Wtyczki wydają aktualizacje niezależnie. Motywy też. W spokojnym tygodniu miałem trzy pozycje w kolejce. W gorszy tydzień — piętnaście. Każda wymagała:</p>
<ol>
<li>Kopii zapasowej bazy danych i plików</li>
<li>Zastosowania aktualizacji</li>
<li>Sprawdzenia, czy nic się nie posypało</li>
</ol>
<p>Ten ostatni krok to największy problem. Wtyczki WordPressa wchodzą ze sobą w interakcje w sposób niemożliwy do przewidzenia. Aktualizacja wtyczki do cache'owania psuła wyniki wtyczki SEO. Aktualizacja wtyczki SEO zmieniała sposób generowania map strony. Każda aktualizacja to mały hazard.</p>
<p>Dla portfolio z wpisem co kilka miesięcy ten narzut na utrzymanie jest absurdalny.</p>
<h2>Dziesiątki wtyczek do podstawowych funkcji<a id="dziesiątki-wtyczek-do-podstawowych-funkcji" href="#dziesiątki-wtyczek-do-podstawowych-funkcji" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Świeża instalacja WordPressa nie robi prawie nic. Żeby prowadzić przyzwoite portfolio blogowe, potrzebowałem wtyczek do:</p>
<ul>
<li><strong>SEO</strong> — Yoast lub Rank Math, z własnym interfejsem, ustawieniami i cyklem aktualizacji</li>
<li><strong>Cache'owania</strong> — W3 Total Cache lub WP Super Cache, ze skomplikowaną konfiguracją</li>
<li><strong>Bezpieczeństwa</strong> — Wordfence lub podobna, skanowanie intruzji, blokowanie IP, codzienne raporty</li>
<li><strong>Formularza kontaktowego</strong> — Contact Form 7 lub Gravity Forms</li>
<li><strong>Kopii zapasowych</strong> — UpdraftPlus lub podobna, zwykle wysyłająca dane do zewnętrznego serwisu</li>
<li><strong>Wydajności</strong> — optymalizacja obrazów, lazy loading, minifikacja</li>
<li><strong>SMTP</strong> — bo WordPress domyślnie wysyła e-maile przez <code>mail()</code> w PHP, które trafiają do spamu</li>
</ul>
<p>Każda wtyczka to kod, którego nie kontroluję, utrzymywany przez kogoś innego, potencjalnie porzucony jutro, działający przy każdym wczytaniu strony.</p>
<p>Kiedy zacząłem planować migrację, miałem 22 aktywne wtyczki.</p>
<h2>Ataki botów i problemy z logowaniem<a id="ataki-botów-i-problemy-z-logowaniem" href="#ataki-botów-i-problemy-z-logowaniem" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Strona logowania do WordPressa jest dobrze znana botom. URL to zawsze <code>/wp-admin/</code> lub <code>/wp-login.php</code>. Każdy bot w internecie to wie. Efekt: ciągłe próby włamania przez brute-force, przez całą dobę.</p>
<p>Wordfence ograniczał próby logowania, blokował podejrzane IP i wysyłał mi codzienne raporty o atakach. Działało — ale to kolejny element pochłaniający zasoby serwera na stronie, która w ogóle nie potrzebuje logowania użytkowników. Jedyną osobą logującą się byłem ja, raz w miesiącu.</p>
<p>XML-RPC to kolejny wektor ataku. WordPress domyślnie go włącza dla pingbacków i aplikacji mobilnej. Boty ciągle go sondują. Rozwiązaniem była reguła nginx blokująca ten endpoint — ale dowiedziałem się o tym dopiero po zobaczeniu ruchu w logach.</p>
<h2>PHP i MySQL na Raspberry Pi<a id="php-i-mysql-na-raspberry-pi" href="#php-i-mysql-na-raspberry-pi" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>holas.pl działa na własnym sprzęcie — najpierw <a href="/pl/wpisy/wlasny-domowy-serwer-banana-pi/">Banana Pi</a>, teraz <a href="/pl/wpisy/raspberry-pi-5-migracja-na-nvme-i-konteneryzacje-uslug/">Raspberry Pi 5</a>. To sprawne płyty ARM, ale nie są maszynami serwerowymi. Uruchamianie PHP-FPM i MySQL jednocześnie dla bloga, który zmienia się raz w miesiącu, to czyste marnotrawstwo.</p>
<p>Typowe wczytanie strony WordPress na tym sprzęcie: PHP przetwarza żądanie, MySQL wykonuje kilka zapytań po treść wpisu, dane nawigacji, widżety paska bocznego i opcje strony, PHP renderuje szablony, każda aktywna wtyczka wykonuje swoje hooki. Wszystko to dla treści identycznej z poprzednim żądaniem i identycznej z następnym.</p>
<p>Ze stroną statyczną nginx serwuje wstępnie wyrenderowany plik HTML bezpośrednio z dysku. Procesor ARM ledwo się budzi. Strony ładują się szybciej niż WordPress zdążył przetworzyć żądanie.</p>
<h2>Baza danych jako ryzyko<a id="baza-danych-jako-ryzyko" href="#baza-danych-jako-ryzyko" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Dziewiętnaście lat WordPressa to baza danych z tysiącami wierszy metadanych wpisów, opcji, rewizji i transientów. Wymaga:</p>
<ul>
<li>Regularnych kopii zapasowych (i testowania, że przywracanie faktycznie działa)</li>
<li>Okazjonalnego czyszczenia — WordPress gromadzi śmieci: rewizje wpisów, wygasłe transienty, osierocone wiersze metadanych</li>
<li>Monitorowania pod kątem uszkodzeń po nieoczystym wyłączeniu</li>
<li>Ciągle działającego procesu MySQL, zużywającego pamięć</li>
</ul>
<p>Portfolio blog to zbiór tekstów i obrazów. Przechowywanie go w relacyjnej bazie danych dodaje złożoności operacyjnej bez dodawania wartości.</p>
<h2>Co zostało zachowane<a id="co-zostało-zachowane" href="#co-zostało-zachowane" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Treść. Wszystkie 26 wpisów zostało wyeksportowanych i przekonwertowanych do plików Markdown — tytuły, daty, kategorie, tagi, obrazy. Adresy URL zostały zachowane dokładnie: 25 indywidualnych przekierowań wpisów i 43 przekierowania ścieżek obrazów jest teraz wbudowanych w konfigurację nginx. Pozycje w wyszukiwarce przetrwały migrację nienaruszone.</p>
<p>Wszystko inne — baza danych, wtyczki, kolejka aktualizacji, strona logowania <code>/wp-admin/</code> — zniknęło.</p>
<p><a href="/pl/wpisy/symfony-jako-generator-statycznych-stron/">Następny wpis</a> opisuje, co zastąpiło WordPressa: własna aplikacja Symfony generująca statyczne pliki HTML, serwowane przez nginx bez udziału PHP w dostarczaniu treści do odwiedzających.</p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/why-i-left-wordpress/why-i-left-wordpress.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>porady</category>
                                    <category>wordpress</category>
                        <category>php</category>
                        <category>symfony</category>
                        <category>statyczna-strona</category>
                        <category>performance</category>
                    </item>
                <item>
            <title><![CDATA[Let&#039;s Encrypt z Dockerem i Traefik — automatyczne HTTPS dla każdej usługi]]></title>
            <link>https://holas.pl/pl/wpisy/lets-encrypt-docker-traefik/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/lets-encrypt-docker-traefik/</guid>
                        <pubDate>Sun, 08 Mar 2026 00:00:00 +0000</pubDate>
                        <description><![CDATA[To kontynuacja wpisu o Let&#039;s Encrypt z 2016 roku opartego na acme.sh i Apache. Tamto rozwiązanie działało, ale ręczne zarządzanie certyfikatami nie skaluje się, gdy prowadzisz wiele usług w Dockerze. Traefik rozwiązuje to w całości. Traefik to reverse proxy zaprojektowany dla środowisk kontenerowych. Obserwuje Docker socket, wykrywa uruchomione kontenery i automatycznie pobiera certyfikaty Let&#039;s E…]]></description>
            <content:encoded><![CDATA[<p><em>To kontynuacja <a href="/pl/wpisy/zabezpiecz-swoja-strone-www-za-darmo-certyfikatem-ssl-od-lets-encrypt/">wpisu o Let's Encrypt z 2016 roku</a> opartego na acme.sh i Apache. Tamto rozwiązanie działało, ale ręczne zarządzanie certyfikatami nie skaluje się, gdy prowadzisz wiele usług w Dockerze. Traefik rozwiązuje to w całości.</em></p>
<hr />
<p><a rel="nofollow noopener noreferrer" target="_blank" href="https://traefik.io/">Traefik</a> to reverse proxy zaprojektowany dla środowisk kontenerowych. Obserwuje Docker socket, wykrywa uruchomione kontenery i automatycznie pobiera certyfikaty Let's Encrypt — bez certbota, bez crona, bez ręcznej konfiguracji per domena.</p>
<p>Konfiguracja składa się z dwóch części: jedna instancja Traefik działająca cały czas na serwerze oraz etykiety przy każdej usłudze, które mówią Traefik jak routować ruch i dla jakiej domeny wystawić certyfikat.</p>
<h2>Część 1 — docker-compose.yml dla Traefik<a id="część-1--docker-composeyml-dla-traefik" href="#część-1--docker-composeyml-dla-traefik" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Uruchamiany raz na serwerze. Wszystkie pozostałe usługi przechodzą przez niego.</p>
<pre><code class="language-yaml">services:
    traefik:
        image: traefik:${TRAEFIK_VERSION}
        container_name: traefik
        command:
            - &quot;--providers.docker=true&quot;
            - &quot;--providers.docker.exposedbydefault=false&quot;
            - &quot;--entrypoints.web.address=:80&quot;
            - &quot;--entrypoints.websecure.address=:443&quot;
            # globalne przekierowanie HTTP → HTTPS
            - &quot;--entrypoints.web.http.redirections.entrypoint.scheme=https&quot;
            - &quot;--entrypoints.web.http.redirections.entrypoint.to=websecure&quot;
            # Let's Encrypt
            - &quot;--certificatesResolvers.le.acme.email=${ACME_EMAIL}&quot;
            - &quot;--certificatesResolvers.le.acme.storage=acme.json&quot;
            - &quot;--certificatesResolvers.le.acme.tlsChallenge=true&quot;
        restart: always
        ports:
            - 80:80
            - 443:443
        networks:
            - web
        volumes:
            - /etc/localtime:/etc/localtime:ro
            - /var/run/docker.sock:/var/run/docker.sock
            - ./acme.json:/acme.json
        labels:
            - traefik.http.middlewares.gzip.compress=true

networks:
    web:
        external: true
</code></pre>
<p>Kilka rzeczy wartych uwagi:</p>
<ul>
<li><strong><code>exposedbydefault=false</code></strong> — Traefik ignoruje kontenery, chyba że jawnie się zgłoszą przez etykiety. Nic nie zostaje przypadkowo wystawione na zewnątrz.</li>
<li><strong>TLS challenge</strong> — Traefik obsługuje weryfikację domeny bezpośrednio na porcie 443. Nie trzeba tymczasowo otwierać portu 80.</li>
<li><strong><code>acme.json</code></strong> — certyfikaty są zapisywane na dysku i przeżywają restarty kontenera. Przed pierwszym uruchomieniem utwórz plik i ustaw odpowiednie uprawnienia:</li>
</ul>
<pre><code class="language-bash">touch acme.json &amp;&amp; chmod 600 acme.json
</code></pre>
<p>Traefik odmówi uruchomienia przy zbyt szerokich uprawnieniach.</p>
<ul>
<li><strong>Zewnętrzna sieć <code>web</code></strong> — tworzysz ją raz:</li>
</ul>
<pre><code class="language-bash">docker network create web
</code></pre>
<p>Każda usługa potrzebująca routingu HTTP/S dołącza do tej sieci.</p>
<h2>Część 2 — Certyfikaty testowe (staging)<a id="część-2--certyfikaty-testowe-staging" href="#część-2--certyfikaty-testowe-staging" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Przed przejściem na produkcję przetestuj z serwerem staging Let's Encrypt, żeby nie trafić na limity:</p>
<pre><code class="language-yaml"># dodaj do sekcji command w traefik:
- &quot;--log.level=DEBUG&quot;
- &quot;--certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory&quot;
</code></pre>
<p>Certyfikaty stagingowe nie są zaufane przez przeglądarki, ale funkcjonalnie działają identycznie. Usuń te linie gdy wszystko działa.</p>
<h2>Część 3 — Dodanie usługi<a id="część-3--dodanie-usługi" href="#część-3--dodanie-usługi" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Tu widać zwrot z inwestycji. Dowolny kontener dostaje HTTPS przez dodanie czterech etykiet:</p>
<pre><code class="language-yaml">services:
    myapp:
        image: myapp:latest
        networks:
            - web
        labels:
            - traefik.enable=true
            - traefik.http.routers.myapp.rule=Host(`myapp.example.com`)
            - traefik.http.routers.myapp.entrypoints=websecure
            - traefik.http.routers.myapp.tls.certresolver=le

networks:
    web:
        external: true
</code></pre>
<p>Traefik wykrywa kontener, pobiera certyfikat od Let's Encrypt i zaczyna routować ruch — bez konfiguracji nginx, bez uruchamiania certbota, bez crona. Odnowienia dzieją się automatycznie w tle.</p>
<h2>Alternatywa bez Dockera<a id="alternatywa-bez-dockera" href="#alternatywa-bez-dockera" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Jeśli nie używasz Dockera, <a rel="nofollow noopener noreferrer" target="_blank" href="https://certbot.eff.org/">Certbot</a> z wtyczką nginx to standardowe podejście: <code>apt install certbot python3-certbot-nginx</code>, <code>certbot --nginx -d mojastrona.pl</code>, a timer systemd zajmuje się odnowieniami. Sprawdza się dobrze na jednym serwerze z kilkoma stronami. Gdy usług jest więcej, podejście z Traefik zaczyna się opłacać.</p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/lets-encrypt-modern/lets-encrypt-modern.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>porady</category>
                                    <category>bezpieczenstwo</category>
                        <category>linux</category>
                        <category>docker</category>
                        <category>traefik</category>
                        <category>ssl</category>
                    </item>
                <item>
            <title><![CDATA[Raspberry Pi 5: Migracja na NVMe i konteneryzację usług]]></title>
            <link>https://holas.pl/pl/wpisy/raspberry-pi-5-migracja-na-nvme-i-konteneryzacje-uslug/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/raspberry-pi-5-migracja-na-nvme-i-konteneryzacje-uslug/</guid>
                        <pubDate>Sat, 10 Jan 2026 00:00:00 +0000</pubDate>
                        <description><![CDATA[Mój poprzedni serwer — Banana Pi na Debianie — przez lata spełniał swoją funkcję, jednak ograniczenia wydajnościowe oraz brak wsparcia dla nowszych technologii skłoniły mnie do modernizacji. Głównym celem była przesiadka na architekturę opartą o kontenery oraz wyeliminowanie kart SD na rzecz standardu NVMe. Specyfikacja sprzętowa# Nowy zestaw został skompletowany z myślą o maksymalnej responsywnoś…]]></description>
            <content:encoded><![CDATA[<p>Mój poprzedni serwer — <a href="/pl/wpisy/wlasny-domowy-serwer-banana-pi/">Banana Pi</a> na Debianie — przez lata spełniał swoją funkcję, jednak ograniczenia wydajnościowe oraz brak wsparcia dla nowszych technologii skłoniły mnie do modernizacji. Głównym celem była przesiadka na architekturę opartą o kontenery oraz wyeliminowanie kart SD na rzecz standardu NVMe.</p>
<h2>Specyfikacja sprzętowa<a id="specyfikacja-sprzętowa" href="#specyfikacja-sprzętowa" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Nowy zestaw został skompletowany z myślą o maksymalnej responsywności i stabilności:</p>
<ul>
<li><strong>Jednostka:</strong> Raspberry Pi 5 (8GB RAM).</li>
<li><strong>Obudowa:</strong> Argon NEO 5 M.2 NVMe – zapewnia skuteczne chłodzenie i bezpośrednią obsługę dysków SSD przez dedykowany interfejs.</li>
<li><strong>Pamięć masowa:</strong> Dysk Lexar 1TB M.2 PCIe NVMe NM620. Rezygnacja z kart micro SD na rzecz NVMe znacząco redukuje opóźnienia i zwiększa trwałość nośnika.</li>
</ul>
<p><img data-full="/media/rpi5-migration/rpi5.webp" src="/media/rpi5-migration/rpi5.webp" alt="Raspberry Pi 5 z obudową Argon NEO 5 i dyskiem NVMe" /></p>
<h2>Energooszczędność<a id="energooszczędność" href="#energooszczędność" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Jednym z kluczowych argumentów za wyborem Raspberry Pi w roli serwera 24/7 jest niski pobór mocy. W obecnej konfiguracji parametry prezentują się następująco:</p>
<ul>
<li><strong>Idle (bezczynność):</strong> Zapotrzebowanie na poziomie <strong>3W</strong> (potwierdzone odczytem z zasilacza).</li>
<li><strong>Stress (obciążenie):</strong> Pobór mocy wzrasta do przedziału <strong>9-12W</strong>.</li>
</ul>
<p>Stosunek wydajności do zużytej energii czyni tę jednostkę niezwykle efektywnym rozwiązaniem pod kątem kosztów eksploatacji.</p>
<h2>System i usługi systemowe<a id="system-i-usługi-systemowe" href="#system-i-usługi-systemowe" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Całość pracuje pod kontrolą <strong>Raspberry Pi OS</strong>. Na poziomie systemu operacyjnego (poza Dockerem) skonfigurowałem kluczowe usługi zarządzania i dostępu:</p>
<ul>
<li><strong>Dostęp zdalny:</strong> VPN oparty na protokole Wireguard (<strong>PiVPN</strong>) zapewnia bezpieczny tunel do sieci domowej.</li>
<li><strong>Zarządzanie:</strong> Do obsługi graficznego interfejsu wykorzystuję <strong>VNC</strong>.</li>
<li><strong>Współdzielenie plików:</strong> Standardowa usługa <strong>Samba</strong> do szybkiego dostępu do danych w sieci lokalnej.</li>
</ul>
<h2>Środowisko Docker<a id="środowisko-docker" href="#środowisko-docker" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Kluczową zmianą w stosunku do poprzedniego serwera jest pełna konteneryzacja usług sieciowych. Za routing i automatyczne wystawianie certyfikatów SSL odpowiada <strong>Traefik</strong>.</p>
<p>W ramach Dockera utrzymuję obecnie:</p>
<ol>
<li><strong>holas.pl</strong> – moja strona internetowa.</li>
<li><strong>Nextcloud</strong> – prywatna chmura do synchronizacji danych.</li>
<li><strong>Immich</strong> – rozwiązanie do backupu i zarządzania biblioteką zdjęć.</li>
<li><strong>Traefik</strong> – reverse proxy zarządzające ruchem przychodzącym.</li>
</ol>
<h2>Potencjał rozwojowy<a id="potencjał-rozwojowy" href="#potencjał-rozwojowy" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Mimo uruchomienia kilku wymagających usług, Raspberry Pi 5 w wersji 8GB wykazuje <strong>spory zapas mocy obliczeniowej oraz pamięci RAM</strong>. Obecne obciążenie pozwala na swobodne wdrażanie kolejnych kontenerów bez ryzyka spadku wydajności działających już systemów. Przejście na standard NVMe sprawiło, że operacje na bazach danych (szczególnie w Immich i Nextcloud) są wykonywane natychmiastowo.</p>
<p><img data-full="/media/rpi5-migration/Zrzut-ekranu-2026-01-10-224632-1024x500.webp" src="/media/rpi5-migration/Zrzut-ekranu-2026-01-10-224632-1024x500.webp" alt="Obciążenie systemu – htop" /></p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/rpi5-migration/featured.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>porady</category>
                                    <category>raspberry-pi</category>
                        <category>docker</category>
                        <category>linux</category>
                        <category>homelab</category>
                        <category>self-hosted</category>
                    </item>
                <item>
            <title><![CDATA[ddev-sylius: Boilerplate DDEV dla Syliusa 2.x]]></title>
            <link>https://holas.pl/pl/wpisy/ddev-sylius-boilerplate/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/ddev-sylius-boilerplate/</guid>
                        <pubDate>Tue, 02 Dec 2025 00:00:00 +0000</pubDate>
                        <description><![CDATA[Projekt powstał przy okazji zdalnej konfiguracji Syliusa — całkowicie przez SSH, z telefonu. Kroki konfiguracyjne były na tyle powtarzalne, że warto było je zautomatyzować, więc złożyłem boilerplate DDEV. Zanim uznałem go za gotowy, ktoś udostępnił go publicznie — opublikowałem go więc jako wczesną alfę i rozwijałem dalej. Po roku użytkowania wewnętrznego wyszło v1.0.0 — z pełnym wsparciem dla Syl…]]></description>
            <content:encoded><![CDATA[<p>Projekt powstał przy okazji zdalnej konfiguracji Syliusa — całkowicie przez SSH, z telefonu. Kroki konfiguracyjne były na tyle powtarzalne, że warto było je zautomatyzować, więc złożyłem boilerplate DDEV. Zanim uznałem go za gotowy, ktoś udostępnił go publicznie — opublikowałem go więc jako wczesną alfę i rozwijałem dalej.</p>
<p>Po roku użytkowania wewnętrznego wyszło v1.0.0 — z pełnym wsparciem dla Syliusa 2.x, czystą strukturą i wszystkim, czego używam na co dzień. Następnego dnia pojawiło się v1.0.1 z poprawkami dla różnych platform.</p>
<h2>Dlaczego konfiguracja Syliusa bez narzędzi jest uciążliwa<a id="dlaczego-konfiguracja-syliusa-bez-narzędzi-jest-uciążliwa" href="#dlaczego-konfiguracja-syliusa-bez-narzędzi-jest-uciążliwa" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Sylius 2.x ma nietrywialną konfigurację lokalną. Wymaga PHP 8.4, Composera, bazy danych, serwera WWW z poprawnymi regułami rewrite, Node.js i Yarn do kompilacji assetów panelu admina (opartych na Webpack Encore) oraz binarki Symfony do zadań konsolowych. Sprawne uruchomienie tego wszystkiego na różnych maszynach — Windows, macOS, Linux — bez środowiska kontenerowego jest zawodne. Wersje się rozjeżdżają, zmienne środowiskowe różnią się, ścieżki kolidują.</p>
<p>DDEV rozwiązuje to, deklarując całe środowisko jako konfigurację. Boilerplate ma wbudowane właściwe wersje i łączy je razem, żeby nikt nie musiał tego rozgryzać ręcznie.</p>
<h2>Co jest konfigurowane<a id="co-jest-konfigurowane" href="#co-jest-konfigurowane" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Plik <code>.ddev/config.yaml</code> ustawia:</p>
<ul>
<li><strong>PHP 8.4</strong> z <code>apache-fpm</code> (Sylius 2.x wymaga PHP 8.2+; 8.4 to obecna stabilna wersja)</li>
<li><strong>MariaDB 11.8</strong> — najnowsza stabilna, z <code>upload_dirs</code> dostosowanym tak, żeby wykluczyć <code>media/</code>, <code>node_modules/</code> i <code>backups/</code> z synchronizacji Mutagen na macOS</li>
<li><strong>Composer 2</strong></li>
<li><strong>Porty</strong> 8123 (HTTP) i 8443 (HTTPS), żeby unikać konfliktów z innymi lokalnymi projektami</li>
<li><strong>Xdebug wyłączony</strong> domyślnie — włączany przez <code>ddev xdebug on</code> gdy potrzebny</li>
</ul>
<h2>Co jest w środku<a id="co-jest-w-środku" href="#co-jest-w-środku" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p><a rel="nofollow noopener noreferrer" target="_blank" href="https://github.com/holas1337/ddev-sylius">ddev-sylius</a> to szablon projektu oparty na DDEV dla Syliusa 2.x. Sklonuj, uruchom dwie komendy i masz działającą lokalną instancję Syliusa — bez ręcznej konfiguracji wersji PHP, bazy danych ani serwera WWW.</p>
<p>Niestandardowe komendy DDEV dołączone do boilerplate'u:</p>
<ul>
<li><code>ddev sylius-install</code> — pełna instalacja Syliusa od zera</li>
<li><code>ddev cc</code> — czyszczenie cache</li>
<li><code>ddev dist</code> — instalacja zależności i budowanie assetów (Composer + Yarn)</li>
<li><code>ddev yarn &lt;param&gt;</code> — komendy Yarn wewnątrz kontenera</li>
<li><code>ddev security-checker</code> — skanowanie znanych podatności</li>
<li><code>ddev code-check</code> — weryfikacja standardów kodowania</li>
<li><code>ddev backup</code> / <code>ddev database-import</code> / <code>ddev files-import</code> — backup i przywracanie bazy danych i mediów</li>
<li><code>ddev sylius-cleanup</code> — resetowanie wszystkich danych (przydatne przy testowaniu przepływu instalacji)</li>
<li><code>ddev build-site</code> / <code>ddev rebuild-site</code> — pełna lub częściowa przebudowa projektu</li>
</ul>
<p>Przetestowane na Windowsie 11 z WSL2 (Ubuntu 24.04), macOS Tahoe (Apple Silicon) i Linuksie z Dockerem.</p>
<h2>Jak zacząć<a id="jak-zacząć" href="#jak-zacząć" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<pre><code class="language-bash">git clone https://github.com/holas1337/ddev-sylius my-projekt
cd my-projekt
ddev start
ddev sylius-install
</code></pre>
<p>Tyle. Po kilku minutach masz działający sklep Sylius z panelem administracyjnym, dostępny pod lokalnym URL-em wygenerowanym przez DDEV (domyślnie <code>https://ddev-sylius-boilerplate.ddev.site:8443</code>).</p>
<h2>Codzienna praca<a id="codzienna-praca" href="#codzienna-praca" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Po pierwszej instalacji typowe komendy to:</p>
<pre><code class="language-bash">ddev start             # start środowiska
ddev cc                # czyszczenie cache po zmianach konfiguracji
ddev dist              # przebudowa assetów po zmianach frontendu
ddev code-check        # sprawdzenie standardów przed commitem
ddev backup            # snapshot bazy przed ryzykowną migracją
</code></pre>
<p>Do debugowania <code>ddev xdebug on</code> włącza Xdebug, a <code>ddev exec bin/console &lt;komenda&gt;</code> daje bezpośredni dostęp do konsoli Symfony wewnątrz kontenera.</p>
<h2>Co zmieniło się w v1.0.1<a id="co-zmieniło-się-w-v101" href="#co-zmieniło-się-w-v101" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Poprawka wydana następnego dnia naprawiła kilka rzeczy, które wyszły podczas testów na różnych platformach: dostosowania specyficzne dla macOS dla Mutagen i wyłączeń katalogów uploadu, aktualizacja MariaDB z 11.4 do 11.8 oraz aktualizacja phpMyAdmin do najnowszej wersji.</p>
<p>Repozytorium jest na GitHubie: <a rel="nofollow noopener noreferrer" target="_blank" href="https://github.com/holas1337/ddev-sylius">holas1337/ddev-sylius</a>. Issues i PR-y mile widziane.</p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/ddev-sylius/featured.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>realizacje</category>
                                    <category>ddev</category>
                        <category>sylius</category>
                        <category>symfony</category>
                        <category>open-source</category>
                        <category>e-commerce</category>
                    </item>
                <item>
            <title><![CDATA[Jak ograniczyć śledzenie przez Facebooka w przeglądarce]]></title>
            <link>https://holas.pl/pl/wpisy/facebook-nas-sledzi/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/facebook-nas-sledzi/</guid>
                        <pubDate>Tue, 27 Mar 2018 00:00:00 +0000</pubDate>
                        <description><![CDATA[Facebook śledzi cię w całej sieci przez przyciski &amp;quot;Lubię to&amp;quot;, widżety komentarzy i niewidoczne piksele osadzone na milionach stron — nawet jeśli nigdy w nic nie klikasz. Kilka rozszerzeń przeglądarki znacząco ogranicza ten zasięg. uBlock Origin# Niezbędna pierwsza warstwa ochrony. uBlock Origin (Firefox, Chrome) blokuje reklamy i śledzenie na poziomie sieciowym. Już z domyślnymi listami …]]></description>
            <content:encoded><![CDATA[<p>Facebook śledzi cię w całej sieci przez przyciski &quot;Lubię to&quot;, widżety komentarzy i niewidoczne piksele osadzone na milionach stron — nawet jeśli nigdy w nic nie klikasz. Kilka rozszerzeń przeglądarki znacząco ogranicza ten zasięg.</p>
<h2>uBlock Origin<a id="ublock-origin" href="#ublock-origin" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Niezbędna pierwsza warstwa ochrony. <strong>uBlock Origin</strong> (<a rel="nofollow noopener noreferrer" target="_blank" href="https://addons.mozilla.org/pl/firefox/addon/ublock-origin/">Firefox</a>, <a rel="nofollow noopener noreferrer" target="_blank" href="https://chromewebstore.google.com/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm">Chrome</a>) blokuje reklamy i śledzenie na poziomie sieciowym. Już z domyślnymi listami filtrów blokuje większość pikseli śledzących Facebooka i widżetów społecznościowych na stronach trzecich.</p>
<p><strong>Uwaga dla użytkowników Chrome:</strong> Przejście Google na Manifest V3 ogranicza niektóre możliwości uBlock Origin w Chrome. Pełna wersja pozostaje dostępna w Firefoksie.</p>
<h2>Privacy Badger<a id="privacy-badger" href="#privacy-badger" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p><a rel="nofollow noopener noreferrer" target="_blank" href="https://privacybadger.org/">Privacy Badger</a> od Electronic Frontier Foundation (<a rel="nofollow noopener noreferrer" target="_blank" href="https://addons.mozilla.org/en-US/firefox/addon/privacy-badger17/">Firefox</a>, <a rel="nofollow noopener noreferrer" target="_blank" href="https://chromewebstore.google.com/detail/privacy-badger/pkehgijcmpdhfbdbbnkijodmdjhbjlgp">Chrome</a>) działa inaczej niż klasyczne blokery: zamiast statycznej listy blokowania, uczy się, które śledziki podążają za tobą między stronami i stopniowo je blokuje. Dobrze współpracuje z uBlock Origin.</p>
<h2>Facebook Container (tylko Firefox)<a id="facebook-container-tylko-firefox" href="#facebook-container-tylko-firefox" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p><a rel="nofollow noopener noreferrer" target="_blank" href="https://addons.mozilla.org/en-US/firefox/addon/facebook-container/">Facebook Container</a> to rozszerzenie Mozilli, które izoluje sesję Facebooka w osobnej zakładce kontenera. Facebook nie widzi twojej aktywności na innych stronach, a widżety Facebooka na innych stronach nie mogą odczytać twoich ciasteczek sesji.</p>
<p>To najskuteczniejsze pojedyncze rozszerzenie wymierzone konkretnie w śledzenie Facebooka między stronami — stworzone i utrzymywane przez Mozillę.</p>
<h2>Wbudowana ochrona przeglądarek<a id="wbudowana-ochrona-przeglądarek" href="#wbudowana-ochrona-przeglądarek" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Zarówno <strong>Firefox</strong>, jak i <strong>Brave</strong> mają domyślnie włączoną rozszerzoną ochronę przed śledzeniem, która blokuje wielu znanych śledzicieli — w tym Facebooka. Jeśli korzystasz z jednej z tych przeglądarek, masz już solidną bazę — rozszerzenia powyżej dodają do niej bardziej szczegółową kontrolę.</p>
<p>Jeśli używasz Chrome, rozważ przejście na Firefoksa dla lepszej domyślnej prywatności.</p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/facebook-tracking-2018/featured.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>porady</category>
                                    <category>bezpieczenstwo</category>
                        <category>prywatnosc</category>
                    </item>
                <item>
            <title><![CDATA[Zabezpiecz swoją stronę za darmo certyfikatem SSL od Let&#039;s Encrypt]]></title>
            <link>https://holas.pl/pl/wpisy/zabezpiecz-swoja-strone-www-za-darmo-certyfikatem-ssl-od-lets-encrypt/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/zabezpiecz-swoja-strone-www-za-darmo-certyfikatem-ssl-od-lets-encrypt/</guid>
                        <pubDate>Wed, 24 Feb 2016 00:00:00 +0000</pubDate>
                        <description><![CDATA[To historyczny zapis konfiguracji z 2016 roku — acme.sh i Apache na Debianie Jessie. Działało bez zarzutu. Dla nowoczesnego podejścia z Dockerem i Traefik zapraszam do wpisu kontynuacji. Przed Let&#039;s Encrypt uzyskanie zaufanego certyfikatu SSL wymagało płacenia komerycjnemu CA (Comodo, DigiCert, GoDaddy) od 200 do kilku tysięcy złotych rocznie, ręcznej weryfikacji tożsamości i samodzielnej obsługi …]]></description>
            <content:encoded><![CDATA[<p><em>To historyczny zapis konfiguracji z 2016 roku — acme.sh i Apache na Debianie Jessie. Działało bez zarzutu. Dla nowoczesnego podejścia z Dockerem i Traefik zapraszam do wpisu kontynuacji.</em></p>
<hr />
<p>Przed Let's Encrypt uzyskanie zaufanego certyfikatu SSL wymagało płacenia komerycjnemu CA (Comodo, DigiCert, GoDaddy) od 200 do kilku tysięcy złotych rocznie, ręcznej weryfikacji tożsamości i samodzielnej obsługi odnowień. Let's Encrypt uruchomił się w 2015 roku, wyszedł z bety w kwietniu 2016 i zmienił to całkowicie: darmowe certyfikaty Domain Validation, automatyczne wydawanie przez protokół ACME, 90-dniowy czas ważności z wbudowaną obsługą odnowień. Dla małych serwerów i prywatnych stron działających dotychczas na HTTP była to pierwsza praktyczna droga do HTTPS.</p>
<p>Oficjalny klient Certbot nie był jeszcze dostępny w repozytoriach Debiana Jessie, więc skorzystałem z jednego z <a rel="nofollow noopener noreferrer" target="_blank" href="https://community.letsencrypt.org/t/list-of-client-implementations/2103">alternatywnych klientów</a> — prostego skryptu bash: <a rel="nofollow noopener noreferrer" target="_blank" href="https://github.com/Neilpang/acme.sh">https://github.com/Neilpang/acme.sh</a></p>
<p>Używając tego skryptu można wszystko zrobić dosłownie w 5 minut.</p>
<h2>Wymagania<a id="wymagania" href="#wymagania" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<ul>
<li>Dostęp root do serwera</li>
<li>Apache działający na Debianie Jessie</li>
<li>Domena wskazująca na publiczne IP serwera</li>
<li>Otwarty port 80 (używany przez challenge HTTP-01 do weryfikacji własności domeny)</li>
</ul>
<p>Na wstępie ustalmy, że wszystko wykonujemy z roota oraz:</p>
<pre><code>/root/.acme.sh/acme.sh     # miejsce składowania skryptów klienta
mojastrona.pl              # strona, dla której chcemy uzyskać certyfikat
/mnt/www/mojastrona.pl     # katalog na dysku naszej strony
/etc/apache2               # lokalizacja Apache wraz z plikami konfiguracji
</code></pre>
<h3>Krok 1 — Pobierz klienta<a id="krok-1--pobierz-klienta" href="#krok-1--pobierz-klienta" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h3>
<p>Przechodzimy do katalogu <code>/root/.acme.sh/acme.sh</code> (tworzymy jeśli nie istnieje) i wykonujemy:</p>
<pre><code class="language-bash">git clone https://github.com/Neilpang/acme.sh
</code></pre>
<p>Jeśli nie mamy gita, pobieramy pliki ręcznie ze strony projektu i wypakowujemy.</p>
<h3>Krok 2 — Symlink dla wygody<a id="krok-2--symlink-dla-wygody" href="#krok-2--symlink-dla-wygody" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h3>
<pre><code class="language-bash">ln -s /root/.acme.sh/ /etc/apache2/letsencrypt
</code></pre>
<h3>Krok 3 — Pobierz certyfikat<a id="krok-3--pobierz-certyfikat" href="#krok-3--pobierz-certyfikat" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h3>
<p>Upewniamy się, że strona jest dostępna z internetu, następnie:</p>
<pre><code class="language-bash">./acme.sh issue /mnt/www/mojastrona.pl/ mojastrona.pl
</code></pre>
<p>Lub jeśli mamy aliasy (np. <code>www.mojastrona.pl</code>):</p>
<pre><code class="language-bash">./acme.sh issue /mnt/www/mojastrona.pl/ mojastrona.pl www.mojastrona.pl
</code></pre>
<p>acme.sh umieszcza tymczasowy plik w webroocie, Let's Encrypt go pobiera, żeby zweryfikować że kontrolujesz domenę, i wydaje certyfikat. Pliki zapisane zostaną w:</p>
<pre><code>/root/.acme.sh/mojastrona.pl/
</code></pre>
<h3>Krok 4 — Konfiguracja Apache<a id="krok-4--konfiguracja-apache" href="#krok-4--konfiguracja-apache" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h3>
<p>Wskazujemy Apache na nowe certyfikaty w pliku virtual hosta:</p>
<pre><code class="language-apache">SSLCACertificateFile  /etc/apache2/letsencrypt/mojastrona.pl/ca.cer
SSLCertificateFile    /etc/apache2/letsencrypt/mojastrona.pl/mojastrona.pl.cer
SSLCertificateKeyFile /etc/apache2/letsencrypt/mojastrona.pl/mojastrona.pl.key
</code></pre>
<p>Następnie przeładowujemy Apache:</p>
<pre><code class="language-bash">service apache2 reload
</code></pre>
<p>Od tego momentu strona powinna serwować certyfikat Let's Encrypt.</p>
<h3>Krok 5 — Automatyczne odnawianie<a id="krok-5--automatyczne-odnawianie" href="#krok-5--automatyczne-odnawianie" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h3>
<p>Certyfikaty mają ważność 90 dni. Aby odnawiać automatycznie, tworzymy wykonywalny skrypt <code>acme_cron</code>:</p>
<pre><code class="language-sh">#!/bin/sh
/root/.acme.sh/acme.sh/acme.sh cron &gt;&gt; /var/log/le-renew.log
service apache2 reload
</code></pre>
<p>I przenosimy go do <code>/etc/cron.daily</code>.</p>
<p><img data-full="/media/lets-encrypt-howto/holas.pl-ssl.webp" src="/media/lets-encrypt-howto/holas.pl-ssl.webp" alt="holas.pl z certyfikatem Let's Encrypt" /></p>
<p>Proces nie jest skomplikowany — po pierwszej konfiguracji odnawianie przebiega w pełni automatycznie.</p>
<hr />
<p><em>Ta konfiguracja działała sprawnie, dopóki nie przeniosłem wszystkiego na Dockera. Nowoczesne podejście z Traefik obsługuje Let's Encrypt automatycznie — bez skryptów, bez crona, bez ręcznej konfiguracji. Więcej: <a href="/pl/wpisy/lets-encrypt-docker-traefik/">Let's Encrypt z Dockerem i Traefik</a>.</em></p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/lets-encrypt-howto/letsencrypt-b2.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>porady</category>
                                    <category>bezpieczenstwo</category>
                        <category>linux</category>
                        <category>ssl</category>
                    </item>
                <item>
            <title><![CDATA[Tymczasowy adres e-mail — zarejestruj się wszędzie bez śmiecenia w skrzynce]]></title>
            <link>https://holas.pl/pl/wpisy/tymczasowy-adres-e-mail/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/tymczasowy-adres-e-mail/</guid>
                        <pubDate>Mon, 15 Feb 2016 00:00:00 +0000</pubDate>
                        <description><![CDATA[Chcesz zajrzeć za ścianę rejestracyjną — pobrać PDF, obejrzeć demo, przeczytać wątek na forum. Konta tak naprawdę nie potrzebujesz, potrzebujesz tylko przejść przez weryfikację e-mail. Więc zakładasz kolejnego throwaway Gmaila, czekasz na link aktywacyjny i nigdy więcej tam nie zaglądasz. Jest prostsze wyjście: tymczasowe adresy e-mail — gotowe w kilka sekund, bez rejestracji, znikają po kilku god…]]></description>
            <content:encoded><![CDATA[<p>Chcesz zajrzeć za ścianę rejestracyjną — pobrać PDF, obejrzeć demo, przeczytać wątek na forum. Konta tak naprawdę nie potrzebujesz, potrzebujesz tylko przejść przez weryfikację e-mail. Więc zakładasz kolejnego throwaway Gmaila, czekasz na link aktywacyjny i nigdy więcej tam nie zaglądasz.</p>
<p>Jest prostsze wyjście: <strong>tymczasowe adresy e-mail</strong> — gotowe w kilka sekund, bez rejestracji, znikają po kilku godzinach.</p>
<h2>Guerrilla Mail<a id="guerrilla-mail" href="#guerrilla-mail" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p><a rel="nofollow noopener noreferrer" target="_blank" href="https://www.guerrillamail.com/">Guerrilla Mail</a> to najprostszy wybór. Wchodzisz na stronę, kopiujesz wygenerowany adres, używasz go gdzie trzeba. Poczta pojawia się od razu — bez odświeżania. Skrzynka działa w ramach sesji przeglądarki: zamkniesz kartę i adres znika.</p>
<p>Możesz też wysyłać wiadomości z tymczasowego adresu — przydaje się przy niektórych formularzach potwierdzających.</p>
<h2>Dropmail<a id="dropmail" href="#dropmail" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p><a rel="nofollow noopener noreferrer" target="_blank" href="https://dropmail.me/en/">Dropmail</a> oferuje więcej. To lepszy wybór, gdy adres ma przetrwać dłużej albo gdy zależy Ci na niezawodnym odbieraniu wiadomości.</p>
<p>Co wyróżnia Dropmail:</p>
<ul>
<li><strong>Przekierowanie na prawdziwy adres</strong> — maile trafiają do Twojej normalnej skrzynki, nie musisz trzymać otwartej karty z Dropmail. Przekierowanie możesz anulować w każdej chwili.</li>
<li><strong>Klucz odzyskiwania</strong> — zapisz go i możesz wrócić do tego samego adresu później, nawet z innego urządzenia lub sesji przeglądarki</li>
<li><strong>Dwa typy domen</strong> — domeny stałe dla długotrwałych adresów, rotujące dla jednorazowego użytku</li>
<li><strong>Bot na Telegrama</strong> — odbieraj maile bezpośrednio w Telegramie bez otwierania przeglądarki</li>
<li><strong>Aplikacja na Androida</strong> — zarządzaj tymczasowymi adresami z telefonu</li>
</ul>
<p>Żadna z powyższych funkcji nie wymaga rejestracji ani płatnego planu.</p>
<h2>Apple Hide My Email<a id="apple-hide-my-email" href="#apple-hide-my-email" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Jeśli korzystasz z urządzeń Apple i masz subskrypcję iCloud+ (dostępną w każdym płatnym planie iCloud od 50 GB), masz już wbudowaną opcję: <strong>Hide My Email</strong>.</p>
<p>Generuje unikalny, losowy adres, który przekierowuje pocztę na Twoją prawdziwą skrzynkę — na stałe, dopóki go nie dezaktywujesz. Żadnej osobnej aplikacji, żadnej strony do odwiedzania. Funkcja jest wbudowana bezpośrednio w iOS, iPadOS i macOS:</p>
<ul>
<li><strong>Autouzupełnianie w Safari</strong> — gdy wypełniasz pole e-mail na stronie, Safari proponuje automatyczne wygenerowanie ukrytego adresu</li>
<li><strong>Ustawienia → iCloud → Hide My Email</strong> — twórz adresy i zarządzaj nimi w jednym miejscu, sprawdzaj które są aktywne, dezaktywuj lub usuń dowolny z nich</li>
<li><strong>Zaloguj się przez Apple</strong> — gdy wybierasz opcję ukrycia adresu przy logowaniu przez Apple, Hide My Email generuje adres automatycznie</li>
</ul>
<p>Kluczowa różnica w stosunku do Guerrilla Mail i Dropmail: te adresy to stałe aliasy powiązane z Twoim Apple ID, nie jednorazowe skrzynki. Ty nimi zarządzasz, możesz je wyłączyć, a poczta zawsze trafia do Twojej prawdziwej skrzynki. To najbardziej bezproblemowa opcja jeśli jesteś w ekosystemie Apple — zero dodatkowych narzędzi, stałe i zarządzalne aliasy.</p>
<p>Ograniczenie: wymaga płatnego planu iCloud+ i działa tylko na urządzeniach Apple.</p>
<h2>Kiedy używać którego<a id="kiedy-używać-którego" href="#kiedy-używać-którego" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Używaj <strong>Guerrilla Mail</strong> gdy potrzebujesz tylko linku potwierdzającego i za dwie minuty zamkniesz kartę.</p>
<p>Używaj <strong>Dropmail</strong> gdy adres ma przeżyć zamknięcie przeglądarki albo gdy chcesz, żeby maile trafiały w miejsce, gdzie je faktycznie zobaczysz.</p>
<p>Używaj <strong>Apple Hide My Email</strong> gdy jesteś na iPhonie lub Macu, masz iCloud+, i chcesz generować adresy bez żadnego dodatkowego narzędzia — wbudowane w system, stałe i zarządzalne aliasy.</p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/temp-email/featured.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>porady</category>
                                    <category>bezpieczenstwo</category>
                    </item>
                <item>
            <title><![CDATA[Alfa Consilium – doradztwo finansowe i ubezpieczeniowe]]></title>
            <link>https://holas.pl/pl/wpisy/alfa-consilium-financial-insurance-consulting/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/alfa-consilium-financial-insurance-consulting/</guid>
                        <pubDate>Fri, 12 Feb 2016 00:00:00 +0000</pubDate>
                        <description><![CDATA[Serwis korporacyjny dla Alfa Consilium sp. z o.o. — licencjonowanego brokera ubezpieczeniowego i firmy doradztwa finansowego działającej pod nadzorem Komisji Nadzoru Finansowego. Projekt obejmował zarówno projekt graficzny, jak i wdrożenie WordPressa od zera. Co serwis miał robić# Pośrednictwo ubezpieczeniowe to branża regulowana. Strona brokera to nie tylko broszura — musi komunikować status regu…]]></description>
            <content:encoded><![CDATA[<p>Serwis korporacyjny dla <a rel="nofollow noopener noreferrer" target="_blank" href="https://alfaconsilium.eu/">Alfa Consilium sp. z o.o.</a> — licencjonowanego brokera ubezpieczeniowego i firmy doradztwa finansowego działającej pod nadzorem Komisji Nadzoru Finansowego. Projekt obejmował zarówno projekt graficzny, jak i wdrożenie WordPressa od zera.</p>
<h2>Co serwis miał robić<a id="co-serwis-miał-robić" href="#co-serwis-miał-robić" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Pośrednictwo ubezpieczeniowe to branża regulowana. Strona brokera to nie tylko broszura — musi komunikować status regulacyjny (numer licencji KNF, ubezpieczenie OC zawodowe), odróżniać firmę od agentów (broker reprezentuje klienta, nie ubezpieczyciela) i budować wystarczające zaufanie, żeby potencjalny klient korporacyjny zadzwonił.</p>
<p>Portfolio usług Alfa Consilium było szerokie: ubezpieczenia D&amp;O, cargo, OC spedytora i OC przewoźnika, ubezpieczenia maszyn, brokerstwo leasingowe, faktoring, zarządzanie wierzytelnościami i zarządzanie ryzykiem z wizytami audytowymi na miejscu. Każdy obszar usług wymagał czytelnego, samodzielnego opisu — nie ściany tekstu, ale treści wystarczającej, żeby decydent zrozumiał zakres i skontaktował się z zespołem.</p>
<h2>Projekt i wdrożenie<a id="projekt-i-wdrożenie" href="#projekt-i-wdrożenie" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Serwis został zaprojektowany w czystej estetyce korporacyjnej: ustrukturyzowany układ, czytelna nawigacja po usługach i dane kontaktowe dostępne z każdej podstrony. Klienci z sektora finansowego — firmy logistyczne, produkcyjne, floty — oczekują profesjonalnej prezentacji, nie startupowej landing page.</p>
<p>Wdrożenie na WordPressie z autorskim motywem PHP pisanym od podstaw — bez gotowego szablonu. Szablon obsługiwał trzy breakpointy responsywne: komputer, tablet i telefon. Responsywny design w 2016 roku wciąż nie był standardem dla małych polskich serwisów korporacyjnych; większość działała na stałej szerokości.</p>
<p>WordPress był właściwym wyborem dla klienta: zespół mógł aktualizować opisy usług i aktualności bez udziału dewelopera, a panel administracyjny nie wymagał żadnego szkolenia.</p>
<h2>Co zostało dostarczone<a id="co-zostało-dostarczone" href="#co-zostało-dostarczone" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<ul>
<li>Pełny projekt graficzny (desktop + tablet + mobile)</li>
<li>Autorski motyw WordPress w PHP</li>
<li>Podstrony usług dla wszystkich linii ubezpieczeniowych i finansowych</li>
<li>Sekcja kontaktowa z informacjami regulacyjnymi (dane nadzoru KNF, OC zawodowe)</li>
<li>Responsywny układ na wszystkich trzech breakpointach</li>
</ul>
<p><img data-full="/media/alfa-consilium/alfaconsilium_screenshot.webp" src="/media/alfa-consilium/alfaconsilium_screenshot_crop.webp" alt="Serwis Alfa Consilium" />
<em>Zrzut ekranu z działającego serwisu.</em></p>
<p>Firma działa nadal — pierwotna domena .pl przekierowuje teraz na <strong><a rel="nofollow noopener noreferrer" target="_blank" href="https://alfaconsilium.eu/">alfaconsilium.eu</a></strong>.</p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/alfa-consilium/ac.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>realizacje</category>
                                    <category>php</category>
                        <category>wordpress</category>
                        <category>rwd</category>
                    </item>
                <item>
            <title><![CDATA[Własny domowy serwer – Banana Pi]]></title>
            <link>https://holas.pl/pl/wpisy/wlasny-domowy-serwer-banana-pi/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/wlasny-domowy-serwer-banana-pi/</guid>
                        <pubDate>Wed, 08 Jul 2015 00:00:00 +0000</pubDate>
                        <description><![CDATA[To historyczny zapis mojej konfiguracji domowego serwera z 2015 roku, który działał niezawodnie przez kilka lat, zanim w 2026 przeniosłem się na Raspberry Pi 5. Jak to się potoczyło: Raspberry Pi 5: Migracja na NVMe i konteneryzację usług. W 2015 roku minikomputery jednopłytkowe zaczęły być praktyczną opcją dla energooszczędnego serwera domowego. Raspberry Pi 2 był oczywistym wyborem, ale Banana P…]]></description>
            <content:encoded><![CDATA[<p><em>To historyczny zapis mojej konfiguracji domowego serwera z 2015 roku, który działał niezawodnie przez kilka lat, zanim w 2026 przeniosłem się na Raspberry Pi 5. Jak to się potoczyło: <a href="/pl/wpisy/raspberry-pi-5-migracja-na-nvme-i-konteneryzacje-uslug/">Raspberry Pi 5: Migracja na NVMe i konteneryzację usług</a>.</em></p>
<hr />
<p>W 2015 roku minikomputery jednopłytkowe zaczęły być praktyczną opcją dla energooszczędnego serwera domowego. Raspberry Pi 2 był oczywistym wyborem, ale Banana Pi miał coś, czego Pi nie oferował: port SATA. Dla serwera przechowującego pliki multimedialne i obsługującego backupy prawdziwy dysk twardy był niepodważalną zaletą nad kartą SD.</p>
<h2>Sprzęt<a id="sprzęt" href="#sprzęt" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Banana Pi oparty jest na SoC Allwinner A20 — dwurdzeniowym ARM Cortex-A7 taktowanym 1 GHz. Kluczowe parametry, które przesądziły o wyborze:</p>
<ul>
<li><strong>CPU:</strong> Allwinner A20, 2× 1 GHz (Cortex-A7)</li>
<li><strong>RAM:</strong> 1 GB DDR3</li>
<li><strong>Gigabit Ethernet</strong> (vs 100 Mbit na RPi 2)</li>
<li><strong>Port SATA</strong> — decydujący czynnik. Dysk SATA jest szybszy, bardziej niezawodny i wielokrotnie pojemniejszy od jakiejkolwiek karty SD</li>
</ul>
<p>Konfiguracja: Banana Pi w obudowie, dysk 2,5&quot; SATA do przechowywania danych, pendrive 4 GB na backupy.</p>
<h2>Co działało na serwerze<a id="co-działało-na-serwerze" href="#co-działało-na-serwerze" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Na dystrybucji Bananian — dedykowanym Debianie dla Banana Pi (uwaga: projekt zakończył działalność w 2016 roku i został zastąpiony przez <a rel="nofollow noopener noreferrer" target="_blank" href="https://www.armbian.com/">Armbian</a>) — serwer obsługiwał:</p>
<ul>
<li><strong>Serwer multimediów</strong> (minidlna — strumieniowanie muzyki i wideo na TV przez DLNA bez włączania pełnego PC)</li>
<li><strong>SSH</strong> (zdalne zarządzanie bez monitora, z dowolnego miejsca)</li>
<li><strong>VPN</strong> (OpenVPN — bezpieczne korzystanie z publicznych sieci Wi-Fi, zdalny dostęp do sieci domowej)</li>
<li><strong>VNC</strong> (TightVNC — sesja graficzna gdy potrzeba, np. dla jDownloadera)</li>
<li><strong>Serwer WWW</strong> (stos LAMP — ta strona na nim działała, plus kilka projektów prywatnych)</li>
<li><strong>Backup</strong> (skrypty bash na cronie, bez dodatkowego oprogramowania)</li>
<li><strong>Pobieranie plików</strong> (aria2 i curl z konsoli; jDownloader w trybie graficznym gdy potrzeba)</li>
</ul>
<h2>W praktyce<a id="w-praktyce" href="#w-praktyce" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Całość działała szybko, sprawnie i stabilnie przez całą dobę. Główną zaletą było zużycie energii: Banana Pi pobiera około 4–8 W pod typowym obciążeniem. Uruchamianie starego desktopa z Core 2 Duo (TDP 65 W, plus reszta systemu) żeby posłuchać muzyki — nie miało sensu. Godzina tamtej maszyny to pewnie cały dzień pracy Banana Pi.</p>
<p><img data-full="/media/banana-pi-server/banana_pi_server.webp" src="/media/banana-pi-server/banana_pi_server-300x169.webp" alt="Banana Pi — gotowy zestaw" />
<em>Banana Pi w obudowie z podłączonym dyskiem SATA i starym pendrivem 4 GB na backupy.</em></p>
<p><img data-full="/media/banana-pi-server/banana_pi_server2.webp" src="/media/banana-pi-server/banana_pi_server2-300x151.webp" alt="Banana Pi — konsola administratora" />
<em>Banana Pi od strony konsoli administratora.</em></p>
<p>Jeśli masz podstawowe umiejętności zarządzania Linuksem i szukasz energooszczędnego serwera pracującego non-stop — ekosystem Debiana daje wszystko, czego potrzebujesz.</p>
<hr />
<p><em>Ten zestaw w końcu osiągnął swoje limity. W 2026 roku zastąpiłem go Raspberry Pi 5 z dyskiem NVMe i pełną konteneryzacją na Dockerze. Więcej: <a href="/pl/wpisy/raspberry-pi-5-migracja-na-nvme-i-konteneryzacje-uslug/">Raspberry Pi 5: Migracja na NVMe i konteneryzację usług</a>.</em></p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/banana-pi-server/featured.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>porady</category>
                                    <category>linux</category>
                        <category>sprzet</category>
                        <category>homelab</category>
                        <category>self-hosted</category>
                    </item>
                <item>
            <title><![CDATA[holas.pl – nowa wizualizacja RWD (2015)]]></title>
            <link>https://holas.pl/pl/wpisy/holas-pl-nowa-wizualizacja-rwd/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/holas-pl-nowa-wizualizacja-rwd/</guid>
                        <pubDate>Fri, 12 Jun 2015 00:00:00 +0000</pubDate>
                        <description><![CDATA[Po siedmiu latach ze starą wizualizacją stałoszerokościową w 2015 roku przebudowałem holas.pl od zera na w pełni responsywny motyw WordPress. Archiwalny zrzut tej wersji jest dostępny w Wayback Machine. Dlaczego przebudowa, a nie łatanie# Stary design korzystał z dwóch oddzielnych szablonów: pełnego układu desktopowego i skóry mobilnej serwowanej przez detekcję user-agenta. To podejście działało w…]]></description>
            <content:encoded><![CDATA[<p>Po siedmiu latach ze <a href="/pl/wpisy/holas-pl-stara-wizualizacja/">starą wizualizacją stałoszerokościową</a> w 2015 roku przebudowałem holas.pl od zera na w pełni responsywny motyw WordPress. Archiwalny zrzut tej wersji jest dostępny w <a rel="nofollow noopener noreferrer" target="_blank" href="https://web.archive.org/web/20161108123944/https://holas.pl/">Wayback Machine</a>.</p>
<h2>Dlaczego przebudowa, a nie łatanie<a id="dlaczego-przebudowa-a-nie-łatanie" href="#dlaczego-przebudowa-a-nie-łatanie" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Stary design korzystał z dwóch oddzielnych szablonów: pełnego układu desktopowego i skóry mobilnej serwowanej przez detekcję user-agenta. To podejście działało w 2008 roku, ale do 2015 stało się kulą u nogi — każda zmiana treści lub stylu wymagała podwójnej pracy, a rosnąca różnorodność urządzeń sprawiała, że binarne rozróżnienie desktop/mobile było coraz mniej adekwatne. Tablety, phablet i ekrany o wysokiej gęstości pikseli wpadały w niezręczne przypadki brzegowe.</p>
<p>Decyzja była prosta: pełna przebudowa zamiast doklejania responsywności do starej bazy kodu. Start od zera oznaczał czystszy SCSS, właściwą semantyczną strukturę HTML i brak dziedzictwa hacków do utrzymania.</p>
<h2>Implementacja techniczna<a id="implementacja-techniczna" href="#implementacja-techniczna" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Motyw zbudowałem od zera na WordPressie — bez gotowego szablonu, bez page buildera. Implementacja obejmowała:</p>
<ul>
<li><strong>Szablony PHP</strong> zgodne z hierarchią szablonów WordPress (<code>single.php</code>, <code>archive.php</code>, <code>page.php</code>, <code>functions.php</code>)</li>
<li><strong>SCSS</strong> kompilowany do jednego arkusza styli — zmienne dla kolorów, odstępów i breakpointów</li>
<li><strong>Płynna siatka</strong> z trzema breakpointami: telefon, tablet i desktop</li>
<li><strong>Jedna baza kodu</strong> — jeden zestaw szablonów, jeden arkusz styli, wszystkie rozmiary ekranów obsługiwane przez media queries w CSS</li>
</ul>
<h2>Co jeszcze zmieniło się przy okazji<a id="co-jeszcze-zmieniło-się-przy-okazji" href="#co-jeszcze-zmieniło-się-przy-okazji" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Przebudowa zbiegła się z migracją serwera — strona przeniosła się na własnoręcznie skonfigurowanego Banana Pi z Debianem działającego 24/7. Ten etap <a href="/pl/wpisy/wlasny-domowy-serwer-banana-pi/">opisałem osobno</a>.</p>
<h2>Co było później<a id="co-było-później" href="#co-było-później" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Responsywna przebudowa działała dobrze i wizualizacja służyła przez kilka kolejnych lat. Jednak praca z WordPressem coraz częściej oznaczała walkę z jego abstrakcjami zamiast budowania z nim — ekosystem pluginów, ograniczenia PHPowego szablonowania, kwestie deploymentu. W 2026 zastąpiłem go całkowicie — pełna historia w <a href="/pl/wpisy/dlaczego-odszedlem-od-wordpressa/">Dlaczego odszedłem od WordPressa</a>.</p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/holas-rwd-design/holas.pl_-_nowa_wersja_RWD.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>realizacje</category>
                                    <category>php</category>
                        <category>wordpress</category>
                        <category>rwd</category>
                        <category>scss</category>
                    </item>
                <item>
            <title><![CDATA[PHPers Toruń #1]]></title>
            <link>https://holas.pl/pl/wpisy/phpers-torun-1/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/phpers-torun-1/</guid>
                        <pubDate>Mon, 01 Jun 2015 00:00:00 +0000</pubDate>
                        <description><![CDATA[PHPers to jedna z największych społeczności programistów PHP w Polsce — sieć spotkań działająca w miastach takich jak Warszawa, Kraków, Wrocław, Poznań czy Trójmiasto. Każdy oddział skupia lokalnych PHPerów na prelekcjach, dyskusjach i networkingu wokół ekosystemu PHP. 27 lipca 2015 roku o 18:00 odbyło się pierwsze toruńskie spotkanie PHPers w Krainie Piva na Rynku Nowomiejskim 8. Współorganizował…]]></description>
            <content:encoded><![CDATA[<p><a rel="nofollow noopener noreferrer" target="_blank" href="https://phpers.pl/">PHPers</a> to jedna z największych społeczności programistów PHP w Polsce — sieć spotkań działająca w miastach takich jak Warszawa, Kraków, Wrocław, Poznań czy Trójmiasto. Każdy oddział skupia lokalnych PHPerów na prelekcjach, dyskusjach i networkingu wokół ekosystemu PHP.</p>
<p>27 lipca 2015 roku o 18:00 odbyło się pierwsze toruńskie spotkanie PHPers w Krainie Piva na Rynku Nowomiejskim 8. Współorganizowałem je razem z Szymonem Skowrońskim. Uruchomienie nowego oddziału oznaczało stworzenie regularnego miejsca spotkań dla programistów PHP w regionie — i na pierwszą edycję przybyły 57 osób, a kolejne 34 zaznaczyły swoje zainteresowanie.</p>
<h2>Prelekcje<a id="prelekcje" href="#prelekcje" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Na spotkaniu odbyły się trzy prelekcje obejmujące różnorodne tematy z PHP:</p>
<ul>
<li><strong>Leszek Prabucki</strong> — <em>Podstawy programowania obiektywnego w PHP</em></li>
<li><strong>Łukasz Lubosz</strong> — <em>Blackfire — inteligentny profiler</em></li>
<li><strong>Leszek Krupiński</strong> — <em>Co PHP? zepsuje w Twoim kodzie</em></li>
</ul>
<h2>Sponsorzy<a id="sponsorzy" href="#sponsorzy" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Wydarzenie odbyło się dzięki wsparciu <a rel="nofollow noopener noreferrer" target="_blank" href="https://cocoders.com/">Cocoders</a>, <a rel="nofollow noopener noreferrer" target="_blank" href="https://ecenter.pl/">ecenter.pl</a> oraz <a rel="nofollow noopener noreferrer" target="_blank" href="https://www.facebook.com/krajinapiv">Krainy Piva</a>, która nas ugościła.</p>
<h2>Dlaczego społeczność ma znaczenie<a id="dlaczego-społeczność-ma-znaczenie" href="#dlaczego-społeczność-ma-znaczenie" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Organizowanie takich wydarzeń to coś, co cenię sobie obok codziennej pracy deweloperskiej. Budowanie lokalnej sieci kontaktów oznacza lepszą wymianę wiedzy, silniejsze powiązania między programistami w regionie i bardziej aktywną lokalną scenę technologiczną. 91 odpowiedzi na pierwszą w historii edycję PHPers w Toruniu pokazało, że zainteresowanie było realne.</p>
<p>Pełne szczegóły wydarzenia dostępne są na <a rel="nofollow noopener noreferrer" target="_blank" href="https://www.facebook.com/events/krajina-piva/phpers-toru%C5%84-1/1606550216228709/">Facebooku</a>.</p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/phpers-torun-1/torun_night.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>spolecznosc</category>
                                    <category>php</category>
                        <category>community</category>
                    </item>
                <item>
            <title><![CDATA[Bifor – serwis lokalu, Gdańsk]]></title>
            <link>https://holas.pl/pl/wpisy/bifor-nowy-lokal-w-gdansku/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/bifor-nowy-lokal-w-gdansku/</guid>
                        <pubDate>Mon, 05 Jan 2015 00:00:00 +0000</pubDate>
                        <description><![CDATA[Bifor — pełna nazwa B!FOR FOOD &amp;amp; DRINK &amp;amp; FRIENDS — to gdański lokal, który pozycjonował się jako lokalne miejsce spotkań: jedzenie, napoje, sport, kultura, muzyka i wydarzenia, wszystko pod jednym dachem. Hasło na stronie mówiło wprost: „WIESZ, COŚ SIĘ DZIEJE_&amp;quot;. Co musiał robić serwis# Serwis miał komunikować pełen zakres oferty Biforu i zachęcić gości do odwiedzin. Nawigacja odzwierc…]]></description>
            <content:encoded><![CDATA[<p>Bifor — pełna nazwa <strong>B!FOR FOOD &amp; DRINK &amp; FRIENDS</strong> — to gdański lokal, który pozycjonował się jako lokalne miejsce spotkań: jedzenie, napoje, sport, kultura, muzyka i wydarzenia, wszystko pod jednym dachem. Hasło na stronie mówiło wprost: <em>„WIESZ, COŚ SIĘ DZIEJE_&quot;</em>.</p>
<h2>Co musiał robić serwis<a id="co-musiał-robić-serwis" href="#co-musiał-robić-serwis" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Serwis miał komunikować pełen zakres oferty Biforu i zachęcić gości do odwiedzin. Nawigacja odzwierciedlała tę różnorodność: <strong>Aktualności</strong>, <strong>O nas</strong>, <strong>Kuchnia</strong>, <strong>Kultura</strong>, <strong>Sport</strong>, <strong>Napoje i Alko</strong> i <strong>Kontakt</strong>. Na każdej podstronie widoczne było wyraźne CTA do rezerwacji stolika z numerem telefonu, a wtyczka Facebook Like Box integrowała obecność w mediach społecznościowych z serwisem.</p>
<h2>Wdrożenie<a id="wdrożenie" href="#wdrożenie" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Serwis zbudowałem na WordPressie z własnym motywem PHP, wdrożonym na podstawie dostarczonego projektu graficznego — bez gotowego szablonu. Wdrożenie obejmowało:</p>
<ul>
<li><strong>Trzy breakpointy RWD</strong> — komputer, tablet i telefon — z layoutem dostosowującym się na każdym z nich</li>
<li><strong>Szablony PHP</strong> zgodne z hierarchią szablonów WordPress</li>
<li><strong>Treści zarządzane przez CMS</strong> — klient mógł samodzielnie aktualizować aktualności, wydarzenia, menu i godziny otwarcia</li>
<li><strong>Integrację z Facebookiem</strong> przez widget Like Box</li>
</ul>
<p>Moją rolą było przełożenie statycznego projektu graficznego na w pełni działający motyw WordPress. Identyfikacja wizualna — ciemna kolorystyka, pogrubiona typografia, logo <em>B!FOR</em> — wynikała z briefu designerskiego; zadaniem była jego pikselowo-wierna implementacja w czystym, utrzymywalnym kodzie.</p>
<p><em>Serwis nie jest już dostępny.</em></p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/bifor-gdansk/bifor_mockup.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>realizacje</category>
                                    <category>php</category>
                        <category>wordpress</category>
                        <category>rwd</category>
                    </item>
                <item>
            <title><![CDATA[Fabryka Kształtów]]></title>
            <link>https://holas.pl/pl/wpisy/fabryka-ksztaltow/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/fabryka-ksztaltow/</guid>
                        <pubDate>Thu, 09 Oct 2014 00:00:00 +0000</pubDate>
                        <description><![CDATA[Statyczna strona wizytówka dla Fabryki Kształtów — firmy z Torunia/Chełmży specjalizującej się w materiałach reklamowych, cięciu CNC/3D i obróbce plexi. Hasło firmy: „Jesteśmy kreatywni dla Ciebie!&amp;quot;. Czym zajmuje się klient# Fabryka Kształtów produkuje materiały reklamowe i promocyjne: litery reklamowe, kasetony, półki i stojaki z plexi, dekoracje ścienne, wyklejanie samochodów oraz niestanda…]]></description>
            <content:encoded><![CDATA[<p>Statyczna strona wizytówka dla Fabryki Kształtów — firmy z Torunia/Chełmży specjalizującej się w materiałach reklamowych, cięciu CNC/3D i obróbce plexi. Hasło firmy: <em>„Jesteśmy kreatywni dla Ciebie!&quot;</em>.</p>
<h2>Czym zajmuje się klient<a id="czym-zajmuje-się-klient" href="#czym-zajmuje-się-klient" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Fabryka Kształtów produkuje materiały reklamowe i promocyjne: litery reklamowe, kasetony, półki i stojaki z plexi, dekoracje ścienne, wyklejanie samochodów oraz niestandardowe formy i płaskorzeźby 3D. W zakresie usług firma dysponuje urządzeniem CNC/3D (pole robocze 2000×3000×200 mm) do cięcia aluminium, metali miękkich, dibondu, alucobondu, tworzyw sztucznych (plexi, PCV, HIPS), sklejki i MDF — oraz ploter tnąco-rysujący (szerokość do 1250 mm) do folii standardowej, flex, welurowej, samochodowej i magnetycznej.</p>
<h2>Serwis<a id="serwis" href="#serwis" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Struktura była celowo minimalna — trzy podstrony odpowiadające na pytania, które zadaje potencjalny klient:</p>
<ul>
<li><strong>Kim jesteśmy?</strong> — informacje o firmie</li>
<li><strong>Co robimy?</strong> — pełna lista usług i produkcji</li>
<li><strong>Jak nas znaleźć?</strong> — kontakt i adres</li>
</ul>
<p>Wybór statycznej strony był celowy: wizytówka bez dynamicznych treści nie potrzebuje CMS-a. Czysty HTML/CSS/JS to szybszy czas ładowania, tańszy hosting i zerowy nakład na utrzymanie.</p>
<p>Dwa breakpointy — komputer i telefon — wystarczyły dla tego przypadku użycia bez nadmiernej inżynierii. W 2014 roku trzy wersje RWD nie były jeszcze domyślnym oczekiwaniem dla małej strony firmowej.</p>
<p><img data-full="/media/fabryka-ksztaltow/fk_komputer.webp" src="/media/fabryka-ksztaltow/fk_komputer_crop.webp" alt="Fabryka Kształtów — wersja desktopowa" />
<em>Wersja desktopowa.</em></p>
<p><img data-full="/media/fabryka-ksztaltow/fk_telefon.webp" src="/media/fabryka-ksztaltow/fk_telefon_crop.webp" alt="Fabryka Kształtów — wersja mobilna" />
<em>Wersja mobilna.</em></p>
<p><em>Projekt został ukończony, ale nigdy nie trafił na produkcję.</em></p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/fabryka-ksztaltow/fabryka_ksztaltow.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>realizacje</category>
                                    <category>js</category>
                        <category>rwd</category>
                    </item>
                <item>
            <title><![CDATA[EKO TREND INWEST]]></title>
            <link>https://holas.pl/pl/wpisy/eko-trend-inwest-sp-z-o-o/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/eko-trend-inwest-sp-z-o-o/</guid>
                        <pubDate>Wed, 04 Jul 2012 00:00:00 +0000</pubDate>
                        <description><![CDATA[Serwis korporacyjny dla EKO TREND INWEST Sp. z o.o. — warszawskiej firmy z branży budowlanej i inwestycji nieruchomościowych z blisko 30-letnim doświadczeniem w sektorze. Zbudowany na QuickCMS z własnym motywem. Czym zajmuje się firma# EKO TREND INWEST buduje i sprzedaje domy jednorodzinne na własnych działkach w okolicach Warszawy. Poza nowymi budowami firma zajmuje się również budową i remontami…]]></description>
            <content:encoded><![CDATA[<p>Serwis korporacyjny dla EKO TREND INWEST Sp. z o.o. — warszawskiej firmy z branży budowlanej i inwestycji nieruchomościowych z blisko 30-letnim doświadczeniem w sektorze. Zbudowany na <a rel="nofollow noopener noreferrer" target="_blank" href="https://opensolution.org/system-cms-quick-cms.html">QuickCMS</a> z własnym motywem.</p>
<h2>Czym zajmuje się firma<a id="czym-zajmuje-się-firma" href="#czym-zajmuje-się-firma" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>EKO TREND INWEST buduje i sprzedaje domy jednorodzinne na własnych działkach w okolicach Warszawy. Poza nowymi budowami firma zajmuje się również budową i remontami budynków mieszkalnych, użytkowych i przemysłowych, a także oferuje doradztwo kredytowe dla klientów finansujących inwestycje.</p>
<p>Strona główna pozycjonowała firmę na jakość i rzetelność: własna ekipa, sprawdzone firmy podwykonawcze, materiały najwyższej jakości, ceny zagwarantowane umowami. Przykładowe ceny realizacji domów jednorodzinnych w technologii tradycyjnej: od 350 zł/m² za stan surowy otwarty z dachem (2–3 miesiące), stan deweloperski (5–6 miesięcy), stan „pod klucz&quot; (ok. 8 miesięcy).</p>
<h2>Serwis<a id="serwis" href="#serwis" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p><img data-full="/media/eko-trend-inwest/ekotrendinwest_screenshot.webp" src="/media/eko-trend-inwest/ekotrendinwest_screenshot_crop.webp" alt="EKO TREND INWEST — strona główna" /></p>
<p>Serwis miał osiem sekcji odpowiadających na pytania potencjalnego kupującego: <strong>Osiedle</strong>, <strong>Lokalizacja</strong>, <strong>Plan osiedla</strong>, <strong>Technologia wykonania</strong>, <strong>Galeria / Realizacje</strong>, <strong>Doradztwo kredytowe</strong> i <strong>Kontakt</strong>. Ciemna kolorystyka grafitowo-zielona pasowała do marki „EKO&quot;, a fotografie nieruchomości wypełniały treść serwisu.</p>
<p>Wybór <a rel="nofollow noopener noreferrer" target="_blank" href="https://opensolution.org/system-cms-quick-cms.html">QuickCMS</a> dał klientowi lekki, edytowalny panel administracyjny bez rozbudowania WordPressa. Moją rolą było przygotowanie własnego motywu i pełne wdrożenie.</p>
<p><em>Serwis nie jest już dostępny — archiwalny zrzut dostępny w <a rel="nofollow noopener noreferrer" target="_blank" href="https://web.archive.org/web/20170515205757/http://ekotrendinwest.pl/">Wayback Machine</a>.</em></p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/eko-trend-inwest/ekotrendinwest.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>realizacje</category>
                                    <category>php</category>
                        <category>cms</category>
                    </item>
                <item>
            <title><![CDATA[HR Management]]></title>
            <link>https://holas.pl/pl/wpisy/hr-management/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/hr-management/</guid>
                        <pubDate>Tue, 24 Apr 2012 00:00:00 +0000</pubDate>
                        <description><![CDATA[Pełny serwis dla HR Management sp. z o.o. — jednej z najdłużej działających na polskim rynku firm consultingowych, łączącej pośrednictwo ubezpieczeniowe z bezpośrednio powiązaną kancelarią radców prawnych. Zbudowany na QuickCMS z własnym motywem, zastąpienie statycznej wizytówki dostarczonej dwa miesiące wcześniej. Czym zajmuje się firma# Wyróżnikiem HR Management była szerokość i głębokość zinteg…]]></description>
            <content:encoded><![CDATA[<p>Pełny serwis dla HR Management sp. z o.o. — jednej z najdłużej działających na polskim rynku firm consultingowych, łączącej pośrednictwo ubezpieczeniowe z bezpośrednio powiązaną kancelarią radców prawnych. Zbudowany na <a rel="nofollow noopener noreferrer" target="_blank" href="https://opensolution.org/system-cms-quick-cms.html">QuickCMS</a> z własnym motywem, zastąpienie <a href="/pl/wpisy/hr-management-wizytowka/">statycznej wizytówki</a> dostarczonej dwa miesiące wcześniej.</p>
<h2>Czym zajmuje się firma<a id="czym-zajmuje-się-firma" href="#czym-zajmuje-się-firma" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Wyróżnikiem HR Management była szerokość i głębokość zintegrowanej oferty: doradztwo ubezpieczeniowe, doradztwo prawne przez powiązaną kancelarię radców prawnych, pośrednictwo ubezpieczeniowe, likwidacja szkód (reprezentacja klientów przed towarzystwami ubezpieczeniowymi i w sporach sądowych) oraz consulting w zakresie oceny ryzyka. Jak podkreślał prezes dr Tomasz Kamiński — taka integracja prawników, brokerów ubezpieczeniowych i konsultantów pod jednym dachem była rzadkością na polskim rynku ubezpieczeniowym i prawnym.</p>
<p>Aktualności obejmowały tematykę ubezpieczeń D&amp;O dla zarządów spółek, studia przypadków z ubezpieczeń budowlanych oraz ubezpieczeń zdarzeń medycznych. W 2013 roku firma weszła na rynek niemiecki.</p>
<h2>Serwis<a id="serwis" href="#serwis" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p><img data-full="/media/hr-management/hr_screenshot.webp" src="/media/hr-management/hr_screenshot_crop.webp" alt="HR Management — strona główna" /></p>
<p>Strona główna prezentowała siatkę sześciu kafelków usług — <strong>O nas</strong>, <strong>Doradztwo prawne</strong>, <strong>Consulting</strong>, <strong>Pośrednictwo ubezpieczeniowe</strong>, <strong>Likwidacja szkód</strong>, <strong>Dokumenty</strong> — wraz z aktualnościami z datami. Nawigacja była minimalna: <strong>Strona główna</strong>, <strong>Aktualności</strong>, <strong>O nas</strong>, <strong>Kontakt</strong>. Białe i szare tło z akcentami w kolorach pomarańczowym i niebieskim, zgodnie z identyfikacją wizualną firmy.</p>
<p>Dwuetapowe podejście sprawdziło się: <a href="/pl/wpisy/hr-management-wizytowka/">wizytówka</a> dała klientowi natychmiastową obecność w internecie, a w tym czasie powstawał właściwy serwis oparty na CMS. QuickCMS zapewnił klientowi pełną samodzielność w zarządzaniu aktualnościami i treścią.</p>
<p><em>Serwis dostępny pod adresem <a rel="nofollow noopener noreferrer" target="_blank" href="http://hr.com.pl/">hr.com.pl</a>.</em></p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/hr-management/hr.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>realizacje</category>
                                    <category>php</category>
                        <category>cms</category>
                    </item>
                <item>
            <title><![CDATA[HR Management – wizytówka]]></title>
            <link>https://holas.pl/pl/wpisy/hr-management-wizytowka/</link>
            <guid isPermaLink="true">https://holas.pl/pl/wpisy/hr-management-wizytowka/</guid>
                        <pubDate>Sun, 19 Feb 2012 00:00:00 +0000</pubDate>
                        <description><![CDATA[Statyczna strona wizytówka zbudowana jako placeholder dla HR Management w czasie, gdy powstawała pełna wersja serwisu. Podejście placeholdera# Zakres był celowo minimalny: jednostronicowy serwis HTML/CSS/JS z czteroslajdowym slideshow — szybki do zbudowania i szybki w działaniu. Żadnego CMS-a, żadnego backendu. Cel: szybkie zaistnienie w internecie przed startem właściwego serwisu. Co pokazywały s…]]></description>
            <content:encoded><![CDATA[<p>Statyczna strona wizytówka zbudowana jako placeholder dla HR Management w czasie, gdy powstawała pełna wersja serwisu.</p>
<h2>Podejście placeholdera<a id="podejście-placeholdera" href="#podejście-placeholdera" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Zakres był celowo minimalny: jednostronicowy serwis HTML/CSS/JS z czteroslajdowym slideshow — szybki do zbudowania i szybki w działaniu. Żadnego CMS-a, żadnego backendu. Cel: szybkie zaistnienie w internecie przed startem właściwego serwisu.</p>
<h2>Co pokazywały slajdy<a id="co-pokazywały-slajdy" href="#co-pokazywały-slajdy" class="heading-anchor" aria-hidden="true" title="Permalink">#</a></h2>
<p>Cztery slajdy obejmowały: logo i dane rejestrowe firmy (<em>„Strona w przebudowie&quot;</em>), hasło (<em>„ROZWIJAMY SIĘ Z WAMI i dla Was&quot;</em>), dane kontaktowe do doradztwa prawnego i consultingu, oraz dane kontaktowe do pośrednictwa ubezpieczeniowego i dochodzenia roszczeń.</p>
<p><img data-full="/media/hr-management-card/HR_wizytowka.webp" src="/media/hr-management-card/HR_wizytowka.webp" alt="HR Management — wizytówka, wszystkie cztery slajdy" />
<em>Archiwalny zrzut ekranowy wszystkich czterech slajdów.</em></p>
<p>Dwa miesiące później zastąpiona pełnym serwisem na QuickCMS: <a href="/pl/wpisy/hr-management/">HR Management — pełny serwis</a>.</p>
]]></content:encoded>
                        <media:content url="https://holas.pl/media/hr-management-card/HR-wizytowka.webp" medium="image" type="image/webp" width="1280" height="720"/>
                                    <category>realizacje</category>
                                    <category>js</category>
                    </item>
            </channel>
</rss>
