4×100 w Lighthouse Mobile — co daje statyczna strona
performance,nginx,symfony,statyczna-strona,accessibility,seoCzęść 7 z 7
- 19 lat z WordPressem — dlaczego w końcu zrezygnowałem
- Symfony jako generator stron statycznych — jak działa holas.pl
- Zabezpieczenie jedynego dynamicznego endpointu strony statycznej — formularz kontaktowy
- Drzewo decyzyjne narzędzi dla Claude Code z globalną pamięcią
- Środowisko deweloperskie — od lokalnego devu do produkcji w dwóch kontenerach
- Budowanie holas.pl z AI — Claude Code, MCP i lokalne generowanie obrazów
- 4×100 w Lighthouse Mobile — co daje statyczna strona
holas.pl osiąga 100 we wszystkich czterech kategoriach Lighthouse na urządzeniach mobilnych — Performance, Accessibility, Best Practices i SEO. Testy mobilne są ostrzejsze: wolniejszy symulowany procesor, ograniczona przepustowość sieci, wyższe progi punktacji. Osiągnięcie czterech setnych tam oznacza, że wyniki desktopowe dbają o siebie same.

Ten wpis omawia, co konkretnie napędza każdy wynik. Większość z tego nie jest wynikiem optymalizacji — to efekt uboczny sposobu, w jaki strona jest zbudowana.
Performance
Główny powód, dla którego wynik performance wynosi 100, jest taki, że nginx serwuje wstępnie wyrenderowane pliki HTML bez udziału PHP. Nie ma zapytania do bazy danych, renderowania szablonów ani bootstrapowania frameworka przy każdym żądaniu. Plik trafia z dysku do klienta. Raspberry Pi 5 obsługuje to bez najmniejszego wysiłku.
Reszta wynika z tego bezpośrednio:
Zasoby są hashowane i niezmienne. Pliki JavaScript i CSS skompilowane przez Symfony AssetMapper otrzymują hash zawartości w nazwie pliku (app-a1b2c3d4.css). nginx serwuje je z nagłówkiem Cache-Control: public, max-age=31536000, immutable — rok czasu, bez rewalidacji. Przy ponownych odwiedzinach przeglądarka obsługuje wszystko z cache. Przy deploymencie hash się zmienia i nowy plik jest pobierany.
JavaScript jest minimalny i nie blokuje renderowania. Strona używa natywnych modułów ES przez importmap — bez bundlera, bez webpacka, bez jQuery. Jest siedem małych plików JS: app.js, contact.js, cookie-banner.js, lightbox.js, locale-redirect.js, nav-toggle.js, tagline.js. Żaden z nich nie blokuje renderowania. Wyszukiwanie obsługuje Pagefind — statyczny indeks wyszukiwania oparty na WebAssembly, który ładuje się leniwie — tylko na stronie wyszukiwania, tylko gdy jest potrzebny.
Obrazy są w formacie WebP. Zdjęcia wyróżniające są zapisane jako WebP o wymiarach 1280×720. Żadnych dużych nieskompresowanych JPEGów.
Brak zasobów blokujących renderowanie. Nie ma <link rel="stylesheet"> do zewnętrznego CDN z fontami ani synchronicznego skryptu third-party ładowanego w <head>. CSS jest kompilowany lokalnie i serwowany jako hashowany zasób.
Accessibility
<html lang> ustawiany na podstawie locale. Każda strona ma poprawny atrybut języka — lang="en" dla stron angielskich, lang="pl" dla polskich. Jest ustawiany w bazowym szablonie Twig na podstawie aktualnego locale, nie na sztywno.
Semantyczny HTML w całości. Layout używa <nav>, <main>, <article>, <aside>, <footer> — nie sekwencji elementów <div>. Nagłówki zachowują logiczną hierarchię: jeden <h1> na stronę, <h2> dla sekcji najwyższego poziomu, <h3> poniżej.
Kontrast kolorów jest zachowany. Strona używa ciemnej palety opartej na Monokai: tekst #F8F8F2 na tle #242424. To współczynnik kontrastu 15,5:1, znacznie powyżej progu WCAG AA wynoszącego 4,5:1.
Wszystkie obrazy mają atrybuty alt. Jest to wymuszane w szablonach Twig — tag <img> zawsze wyprowadza tekst alt z frontmatter elementu treści.
Metatag viewport jest obecny. Każda strona zawiera <meta name="viewport" content="width=device-width, initial-scale=1">.
Best Practices
HTTPS. Strona działa za Cloudflare, który obsługuje zakończenie TLS. Wszystkie żądania HTTP są przekierowywane na HTTPS.
Nagłówki bezpieczeństwa. nginx ustawia pełny zestaw na każdej odpowiedzi:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; ..." always;
CSP wymagał pewnej uwagi — wasm-unsafe-eval dla paczki WASM Pagefinda oraz challenges.cloudflare.com jako dozwolone źródło ramki dla CAPTCHA Turnstile w formularzu kontaktowym. Wszystko inne to 'self'.
Brak przestarzałych API. Strona nie używa document.write, XMLHttpRequest, layoutów <table> ani niczego innego, co Lighthouse oznacza jako przestarzałą praktykę.
Brak mixed content. Każdy zewnętrzny zasób (skrypt Cloudflare Turnstile) jest ładowany przez HTTPS.
SEO
Wstępnie wyrenderowany HTML. Roboty wyszukiwarek otrzymują kompletny HTML — każdy nagłówek, akapit, blok kodu i link jest w źródle strony. Nie ma renderowania po stronie klienta, na które trzeba czekać, nie potrzeba JavaScript, żeby zobaczyć treść.
Sitemap z hreflang. Mapa strony pod adresem /sitemap.xml wyświetla wszystkie wpisy i strony dla obu wersji językowych. Każdy wpis zawiera pary <xhtml:link rel="alternate" hreflang="..."> wskazujące na wersje EN i PL. Jeśli wpis istnieje tylko w jednym języku, wpis alternatywny jest pomijany.
hreflang w <head>. Każda strona zawiera tagi <link rel="alternate" hreflang="..."> dla obu locale. Przełącznik języka używa tej samej mapy tłumaczeń — zbudowanej z współlokalizowanych plików en.md/pl.md w każdym katalogu wpisu.
Kanoniczne URL-e. Każda strona zawiera <link rel="canonical" href="..."> wskazujący na autorytatywny URL tej strony.
Metadane OpenGraph. Każda strona ma og:title, og:description, og:image i og:url. Są wypełniane z frontmatter — pola title, description i image mapują się bezpośrednio na tagi OG w bazowym szablonie.
Opisowe tytuły i meta opisy. Pola frontmatter title i description są wymagane. Strona nie ma żadnych stron z domyślnymi lub brakującymi meta opisami.
Co nie było automatyczne
Większość z powyższego wynika z architektury — pliki statyczne, minimalny JS, wstępnie wyrenderowany HTML. Ale kilka rzeczy wymagało świadomej pracy.
Atrybuty dostępności. Atrybut lang, wymuszanie tekstu alt, hierarchia nagłówków i wybór semantycznych elementów — wszystko to musiało zostać zapisane w szablonach. Nie pojawia się samo z siebie.
CSP. Prawidłowe skonfigurowanie Content-Security-Policy wymagało kilku iteracji. Pagefind używa WebAssembly, co wymaga wasm-unsafe-eval. Cloudflare Turnstile ładuje się z challenges.cloudflare.com i potrzebuje wyjątku frame-src. Każdy zewnętrzny zasób wymaga jawnego wyjątku w CSP — dodanie jednego bez sprawdzenia psuje wynik.
hreflang. Serwis TranslationMapBuilder buduje mapę {directoryKey → {locale → url}} ze współlokalizowanych plików treści. Jeśli wpis istnieje tylko w jednym locale, wpis hreflang dla brakującego locale jest pomijany zamiast wskazywać na nieistniejący URL. Wymagało to celowego fallbacku w szablonie, nie tylko pętli po wszystkich locale.
Wynik
Cztery setki to nie efekt sprintu optymalizacyjnego. To to, co dostaje się, gdy strona serwuje pliki statyczne, używa minimalnego JavaScript, ma właściwą strukturę HTML i ustawia nagłówki bezpieczeństwa, które powinny być na każdej stronie produkcyjnej.
Architektura jest szczegółowo opisana w części 2 tej serii (jak Symfony generuje statyczny HTML) i części 4 (jak nginx serwuje go na produkcji).