FastAPI + HTMX: full stack bez procesu budowania
# Tworzenie produkcyjnych aplikacji internetowych bez Reacta i webpacka: FastAPI, HTMX, Alpine.js, Jinja2, czysty CSS, wzorce Bootstrap, i18n, wdrażanie, SEO i wydajność.
TL;DR: FastAPI + HTMX + Alpine.js + Jinja2 + zwykły CSS pozwala tworzyć produkcyjne aplikacje webowe bez narzędzi budowania, bez
node_modules/i z idealnymi wynikami Lighthouse. Ten przewodnik omawia cały system, od architektury po wdrożenie, wykorzystując blakecrosley.com jako produkcyjny przykład referencyjny, który obsługuje 210 wpisów blogowych, interaktywne komponenty JavaScript, 11 głównych przewodników, 48 studiów projektowych oraz język angielski i 9 przetłumaczonych lokalizacji bez ani jednego bundlera, kompilatora czy transpiler.1
Współczesny stos tworzenia aplikacji webowych zakłada, że potrzebne są React, webpack, TypeScript i potok budowania. Dla dużej kategorii aplikacji — serwisów opartych na treści, narzędzi wewnętrznych, aplikacji CRUD, stron portfolio, platform dokumentacyjnych — to założenie jest błędne. Stos opisany w tym przewodniku eliminuje cały frontendowy łańcuch narzędzi budowania, a jednocześnie pozwala tworzyć strony osiągające w Lighthouse wynik 100/100/100/100.2
To nie jest agitacja. To pomiar. Opisana tutaj architektura działa produkcyjnie, obsługuje rzeczywistych użytkowników w dziesięciu językach, a liczby można zweryfikować.
Kluczowe wnioski
- Renderowany po stronie serwera HTML eliminuje trzy całe kategorie problemów: zarządzanie stanem klienta, granice serializacji JSON oraz niezgodności hydratacji. HTMX sprawia, że odpowiedzi serwera stają się ostatecznym wynikiem — bez kroku renderowania po stronie klienta.
- Brak narzędzi do budowania oznacza brak błędów budowania. Żadnych konfliktów zależności peer przy
npm install, żadnych błędów kompilatora TypeScript w plikach, których się nie tknęło, żadnych PR-ów Dependabota dla zależności tranzytywnych, których nigdy się nie importowało. Pipeline wdrożeniowy togit push. - Alpine.js obsługuje stan wyłącznie kliencki, którego HTMX obsłużyć nie potrafi. Rozwijane menu, modale, przełączniki nawigacji mobilnej oraz wszelki stan UI istniejący wyłącznie w przeglądarce należą do Alpine.js. Granica jest jasna: jeśli stan wymaga serwera, należy użyć HTMX. Jeśli nie wymaga, należy użyć Alpine.js.
- Zwykły CSS z właściwościami niestandardowymi zastępuje Sass i Tailwind. Właściwości niestandardowe CSS kaskadują, dziedziczą i reagują na zapytania medialne w czasie wykonywania. Zmienne preprocesora kompilują się do statycznych wartości i znikają. Przeglądarka odczytuje właściwości niestandardowe bezpośrednio — bez kroku kompilacji.
- Podejście to ma jasne granice. Jest błędne dla dużych zespołów współdzielących interfejsy komponentów, produktów SaaS ze złożonym stanem po stronie klienta oraz aplikacji zależnych od bibliotek z ekosystemu npm. Framework decyzyjny w sekcji 15 precyzyjnie identyfikuje tę granicę.
- blakecrosley.com jest dowodem. Podstawowe wzorce z tego przewodnika (HTMX, Alpine.js, Jinja2, zwykły CSS) działają w środowisku produkcyjnym na blakecrosley.com. Sekcje dotyczące Bootstrap i SQLAlchemy obejmują standardowe wzorce dla stosu, które nie są używane na tej konkretnej witrynie. Każde stwierdzenie ma ścieżkę pliku, blok konfiguracyjny lub audyt Lighthouse, który można samodzielnie zweryfikować na PageSpeed Insights.2
Jak korzystać z tego przewodnika
To kompleksowe źródło informacji. Warto rozpocząć od miejsca pasującego do poziomu doświadczenia:
| Doświadczenie | Zacznij tutaj | Następnie zgłęb |
|---|---|---|
| Programista Python, nowy w HTMX | Teza no-build → Przegląd architektury → Dogłębna analiza HTMX | Wzorce Alpine.js, Bezpieczeństwo |
| Programista React/Vue oceniający alternatywy | Teza no-build → Framework decyzyjny | Przegląd architektury, Wydajność |
| Programista FastAPI dodający interaktywność | Dogłębna analiza HTMX → Wzorce Alpine.js | i18n i lokalizacja, Wdrażanie |
| Programista full-stack budujący od podstaw | Czytanie sekwencyjne od Przegląd architektury | Karta szybkiego odniesienia do bieżącego użytku |
Można użyć Ctrl+F / Cmd+F do wyszukania konkretnych wzorców lub atrybutów. Karta szybkiego odniesienia na końcu zawiera zwięzłe podsumowanie do przeglądania.
Teza no-build
Teza jest wąska i precyzyjna: dla witryn opartych na treści, prowadzonych przez pojedynczego programistę lub mały zespół, narzędzia do budowania rozwiązują problemy, których się nie ma, jednocześnie tworząc te, które się ma.
Oto rzeczywiste metryki z blakecrosley.com:
| Metryka | blakecrosley.com (no-build) | Typowy projekt Next.js3 |
|---|---|---|
| Zależności | 17 pakietów Python | 311+ pakietów npm |
| Pliki konfiguracyjne budowania | 0 | 5-8 (next.config, tsconfig, postcss, tailwind itd.) |
Rozmiar node_modules/ |
Nie istnieje | 187 MB bazowo, 250-400 MB z dodatkami |
| Czas instalacji | pip install: 8 sekund |
npm install: 30-90 sekund |
| Krok budowania | Brak | next build: 15-60 sekund |
| Pipeline wdrożeniowy | git push → na żywo w ~40 sekund |
Instalacja → budowanie → wdrożenie: 2-5 minut |
| Wydajność Lighthouse | 100 | 70-90 bez jawnej optymalizacji4 |
17 pakietów Python obejmuje FastAPI, Jinja2, Pydantic, uvicorn, nh3 i 12 innych. Żaden nie jest narzędziem do budowania. Żaden nie jest kompilatorem. Żaden nie jest bundlerem.5
Z czego się rezygnuje
Uczciwość wymaga wyliczenia rzeczywistych kosztów:
Brak TypeScript. Każdy plik .js to czysty JavaScript. Błędy typów wychwytywane są przez testy i analizę kodu, a nie przez kompilator. Sprawdza się to dla pojedynczego programisty. Nie sprawdziłoby się dla zespołu 10 osób współdzielących interfejsy komponentów.
Brak Hot Module Replacement. Zmiany w CSS wymagają ręcznego odświeżenia przeglądarki. hx-boost z HTMX sprawia, że nawigacja jest na tyle szybka, by pełne odświeżenia były tolerowalne, jednak przy intensywnych cyklach iteracji wizualnych HMR oszczędza czas.
Brak Tree Shaking. Każdy bajt JavaScript, który zostanie napisany, trafia do przeglądarki. Ograniczenie to wymusza dyscyplinę: małe, skupione pliki zamiast dużych modułów narzędziowych.
Brak bibliotek komponentów z npm. Żadnego Radix, żadnego shadcn/ui, żadnego Headless UI. Każdy element interaktywny jest budowany ręcznie lub korzysta z wbudowanych komponentów Bootstrap 5.
Brak tokenów systemu projektowego z npm. System projektowy żyje w niestandardowych właściwościach CSS. Nie można go zaimportować jako pakietu w innym projekcie.
Te kompromisy są akceptowalne dla witryny opartej na treści z jednym do trzech programistów. Byłyby nie do przyjęcia dla produktu SaaS z 15-osobowym zespołem inżynieryjnym. Sekcja 15 zawiera framework decyzyjny.
Co się zyskuje
Brak błędów budowania. Żaden npm install nie może zawieść z powodu konfliktów zależności peer. Żaden next build nie może zawieść z powodu błędu TypeScript w pliku, którego się nie tknęło.6
Debugowanie przez View Source. JavaScript działający w przeglądarce to ten sam JavaScript, który został napisany. Source mapy nie są wymagane.
Natychmiastowy start lokalny. uvicorn app.main:app --reload uruchamia się w mniej niż 2 sekundy.
Konkretny waterfall żądań. Pierwsza wizyta ładuje: jeden dokument HTML (~15KB po gzip), jeden plik CSS (~8KB), HTMX (~16KB, w cache), Alpine.js (~15KB, w cache) oraz interaktywny JS strony (~4-8KB). Łącznie: około 55-65KB przy pierwszej wizycie.1
Frontend odporny na przyszłość. Kod po stronie klienta używa HTML, CSS i JavaScript — standardów, które utrzymują wsteczną kompatybilność od 30 lat.7 Żadnej migracji Webpack 4 → 5, żadnego wycofywania Create React App, żadnej migracji do Next.js App Router.
Porównanie stosów
Jak stos no-build wypada w porównaniu z popularnymi alternatywami w mierzalnych wymiarach:
| Wymiar | FastAPI+HTMX (ten przewodnik) | Next.js (React) | Astro | 11ty |
|---|---|---|---|---|
| JS wysyłany do przeglądarki | 35-40KB (HTMX+Alpine+małe skrypty stron) | 85-250KB+ (środowisko uruchomieniowe React) | 0KB domyślnie, wyspy opt-in | 0KB domyślnie |
| Krok budowania | Brak | Wymagany (webpack/turbopack) | Wymagany (Vite) | Wymagany (niestandardowy) |
| Pliki konfiguracyjne | 0 | 5-8 (next.config, tsconfig itd.) | 1-3 (astro.config, tsconfig) | 1-2 (.eleventy.js) |
| Pipeline wdrożeniowy | git push (40s) |
Instalacja+budowanie+wdrożenie (2-5min) | Instalacja+budowanie+wdrożenie (1-3min) | Instalacja+budowanie+wdrożenie (1-2min) |
| Interaktywność po stronie serwera | Natywna (HTMX) | Trasy API + fetch klienta | Ograniczona (akcje formularzy) | Brak (statyczne wyjście) |
| Zarządzanie stanem klienta | Alpine.js (15KB) | Stan/context/Redux w React | Wyspy framework | Ręczny JS |
| Język backendu | Python | JavaScript/TypeScript | JavaScript/TypeScript | JavaScript |
| Podejście do i18n | Po stronie serwera (middleware) | next-intl lub podobny pakiet | @astrojs/i18n | Ręczne |
| Wydajność Lighthouse | 100 (zmierzone) | 70-90 typowo4 | 95-100 typowo | 95-100 typowo |
| Najlepsze dla | Witryny treściowe, CRUD, dashboardy | Złożone SPA, duże zespoły | Witryny treściowe, marketing | Statyczne blogi, dokumentacja |
Astro i 11ty to najbliżsi konkurenci dla witryn treściowych. Oba produkują doskonałe statyczne wyjście, ale wymagają kroku budowania i łańcucha narzędzi JavaScript. Stos FastAPI+HTMX wymienia wydajność strony statycznej na interaktywność po stronie serwera (filtrowanie kategorii, obsługa formularzy, wyszukiwanie w czasie rzeczywistym) bez dodawania kroku budowania. Jeśli witryna jest czysto statyczna i nie ma interakcji z serwerem, Astro lub 11ty mogą okazać się lepszym wyborem.
Przegląd architektury
Przepływ żądań
Każde żądanie przechodzi przez pojedynczą ścieżkę obejmującą cztery warstwy:
Browser FastAPI Jinja2 HTMX/Alpine
| | | |
|--- GET /about ------>| | |
| |-- render template ->| |
| | |-- base.html ------->|
| | | + about.html |
| |<-- full HTML -------| |
|<--- HTML response ---| | |
| |
|--- hx-get /search ------------------------------------------------>|
| |<-- HTMX request ----| |
| |-- render partial -->| |
| | |-- _results.html |
| |<-- HTML fragment ---| |
|<--- HTML fragment ---| | |
|--- DOM swap -------------------------------------------------------->|
Pełne ładowanie strony zwraca kompletne dokumenty HTML (szablon bazowy + szablon strony). Żądania HTMX zwracają fragmenty HTML (cząstkowe szablony). Serwer decyduje, co wyrenderować, na podstawie typu żądania. Alpine.js zarządza stanem po stronie klienta, który nigdy nie trafia na serwer.
Role komponentów
| Komponent | Rola | Zakres |
|---|---|---|
| FastAPI | Routing, logika biznesowa, dostęp do danych, walidacja | Serwer |
| Jinja2 | Renderowanie szablonów, dziedziczenie, makra | Serwer |
| HTMX | Interaktywność sterowana przez serwer (formularze, paginacja, wyszukiwanie) | Klient ↔ Serwer |
| Alpine.js | Stan wyłącznie po stronie klienta (rozwijane menu, modale, przełączniki) | Tylko klient |
| Bootstrap 5 | System siatki, klasy narzędziowe, responsywny układ | Klient (CSS) |
| Czysty CSS | Własności niestandardowe, style komponentów, tokeny projektowe | Klient (CSS) |
| Pydantic | Walidacja żądań/odpowiedzi, ustawienia | Serwer |
Struktura projektu
app/
├── main.py # FastAPI app, middleware, templates
├── config.py # Pydantic settings management
├── routes/
│ ├── pages.py # Page routes (HTML responses)
│ └── api.py # API routes (JSON/HTML fragment responses)
├── content.py # Markdown loading, blog post parsing
├── security/
│ ├── headers.py # CSP, HSTS, security headers middleware
│ ├── csrf.py # HMAC-signed CSRF tokens
│ ├── rate_limit.py # 3-tier rate limiting
│ └── logging.py # Security event logging
├── i18n/
│ ├── config.py # Supported locales, mappings
│ ├── middleware.py # URL-based locale detection
│ ├── jinja.py # Translation functions for templates
│ └── d1_client.py # Cloudflare D1 translation storage
├── cache_assets.py # Content-hash asset versioning
└── templates/
├── base.html # Base layout with Alpine.js state
├── components/ # Reusable partials (_language_switcher.html, etc.)
└── pages/ # Page templates (home.html, about.html, etc.)
content/
├── blog/ # Markdown blog posts with YAML frontmatter
└── guides/ # Multi-section guide markdown
static/
├── css/ # Plain CSS (no preprocessors)
├── js/ # Vanilla JavaScript (no bundlers)
│ └── vendor/ # Self-hosted HTMX, Alpine.js
└── images/ # Optimized images with WebP srcset
Struktura opiera się na jednej zasadzie: każdy katalog zawiera jeden typ rzeczy. Trasy znajdują się w routes/. Szablony znajdują się w templates/. Zasoby statyczne znajdują się w static/. Żaden etap budowania nie przekształca jednego w drugie.
Porównanie z architekturą SPA
W projekcie React + Next.js odpowiadająca struktura obejmowałaby:
src/
├── components/ # React components (JSX)
├── pages/ # Route handlers (also JSX)
├── api/ # API routes (also in pages/)
├── hooks/ # Custom React hooks
├── context/ # React context providers
├── lib/ # Utility functions
├── styles/ # CSS modules or Tailwind config
└── types/ # TypeScript type definitions
# Plus build configuration
next.config.js
tsconfig.json
postcss.config.js
tailwind.config.js
eslint.config.js
package.json
package-lock.json
node_modules/ # 187+ MB of dependencies
Architektura SPA wymaga koordynacji między tymi katalogami na etapie budowania. TypeScript kompiluje pliki .tsx do JavaScript. PostCSS przetwarza dyrektywy Tailwind na CSS. Webpack (lub Turbopack) łączy wynik w paczki. Każdy krok może zakończyć się niepowodzeniem niezależnie.
Architektura bez etapu budowania nie wymaga żadnej koordynacji. Szablon odwołuje się do pliku CSS. Plik CSS istnieje w static/css/. Przeglądarka ładuje go bezpośrednio. Zmiana nazwy pliku powoduje błąd odwołania w szablonie w czasie wykonania — nie w czasie budowania. To przenosi błędy z etapu kompilacji do etapu wykonania, co stanowi rzeczywisty kompromis. Dla samodzielnego programisty uruchamiającego uvicorn --reload podczas rozwoju, błędy czasu wykonania pojawiają się natychmiast w przeglądarce. W przypadku dużego zespołu błędy kompilacji wychwycone przez TypeScript zapobiegają kategorii błędów, których błędy czasu wykonania nie są w stanie wykryć.
Wzorce FastAPI
Konfiguracja aplikacji
Aplikacja jest inicjalizowana w pliku main.py z jawnie określoną kolejnością middleware:
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.middleware.gzip import GZipMiddleware
app = FastAPI(
title="Blake Crosley",
docs_url=None, # Disable docs in production
redoc_url=None,
openapi_url=None, # Prevent /openapi.json exposure
)
# Middleware order matters: last added = first executed
app.add_middleware(SecurityHeadersMiddleware)
app.add_middleware(GZipMiddleware, minimum_size=500)
app.add_middleware(LocaleMiddleware)
app.add_middleware(RateLimitMiddleware)
app.add_middleware(SecurityLogMiddleware, site_name="blakecrosley.com")
# Static files
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
# Templates
templates = Jinja2Templates(directory=TEMPLATES_DIR)
Istotne są tutaj trzy decyzje projektowe. Po pierwsze, docs_url=None i openapi_url=None wyłączają automatyczne endpointy dokumentacji API. Publicznie dostępna witryna z treściami nie wymaga udostępniania w internecie /docs ani /openapi.json.8 Po drugie, kolejność middleware ma znaczenie — rejestrowanie zdarzeń związanych z bezpieczeństwem jest wykonywane jako pierwsze (ponieważ zostało dodane jako ostatnie), dzięki czemu obejmuje każde żądanie, również odrzucone wskutek ograniczenia częstotliwości. Po trzecie, GZipMiddleware kompresuje wszystkie odpowiedzi większe niż 500 bajtów, co zwykle zmniejsza rozmiar przesyłanych danych HTML o 70–80%.
Routing
Trasy dzielą się na dwie kategorie: trasy stron zwracają pełne dokumenty HTML, natomiast trasy API zwracają fragmenty JSON lub HTML.
# routes/pages.py — full HTML responses
from fastapi import APIRouter, Request
router = APIRouter()
@router.get("/about")
async def about(request: Request):
templates = request.app.state.templates
return templates.TemplateResponse("pages/about.html", {
"request": request,
"page_title": "About — Blake Crosley",
"page_description": "Designer, developer, dad.",
})
# routes/api.py — JSON or HTML fragment responses
@router.get("/api/quiz/{quiz_id}/step")
async def quiz_step(request: Request, quiz_id: str, answers: str = ""):
# Parse answers, compute next question or result
question = get_next_question(quiz_id, answers)
templates = request.app.state.templates
return templates.TemplateResponse("components/_quiz_step.html", {
"request": request,
"question": question,
"answers": answers,
"step": len(answers.split(",")) if answers else 0,
})
To rozróżnienie jest istotne dla HTMX. Trasy pełnych stron zwracają dokumenty rozszerzające base.html. Trasy API zwracają fragmenty HTML, które HTMX umieszcza w istniejących elementach DOM. Oba rodzaje renderuje ten sam silnik szablonów Jinja2 — nie ma osobnej warstwy API.
Wstrzykiwanie zależności
System Depends() w FastAPI zapewnia przejrzyste rozdzielenie funkcji obsługujących trasy od współdzielonej logiki:
from fastapi import Depends, Request
def get_templates(request: Request):
"""Get templates from app state."""
return request.app.state.templates
def get_current_locale(request: Request) -> str:
"""Get locale from middleware-set request state."""
return getattr(request.state, "locale", "en")
@router.get("/blog/{slug}")
async def blog_post(
request: Request,
slug: str,
templates=Depends(get_templates),
locale: str = Depends(get_current_locale),
):
post = load_post_by_slug(slug)
if not post:
raise HTTPException(404, "Post not found")
return templates.TemplateResponse("pages/blog/post.html", {
"request": request,
"post": post,
"locale": locale,
})
Zależności można ze sobą łączyć. Zależność get_db może zależeć od get_current_locale, która z kolei zależy od żądania. FastAPI automatycznie rozwiązuje cały łańcuch.
Ustawienia Pydantic
Konfiguracja korzysta z BaseSettings biblioteki Pydantic, przy czym zmienne środowiskowe mają pierwszeństwo:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
D1_WORKER_URL: str = ""
D1_AUTH_SECRET: str = ""
CLOUDFLARE_ACCOUNT_ID: str = ""
CLOUDFLARE_API_TOKEN: str = ""
ANALYTICS_PASSKEY: str = ""
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
settings = Settings()
Zmienne środowiskowe zastępują wartości z pliku .env. W środowisku produkcyjnym (Railway) sekrety ustawia się jako zmienne środowiskowe. Lokalnie wartości domyślne zapewnia plik .env. Klasa Settings weryfikuje typy podczas uruchamiania — brak wymaganego pola natychmiast powoduje błąd, zamiast ujawniać się dopiero w czasie działania.
Wzorce async
Trasy FastAPI są domyślnie asynchroniczne. W przypadku operacji zależnych od wejścia/wyjścia (zapytań do bazy danych, żądań HTTP, odczytu plików) async zapobiega blokowaniu pętli zdarzeń:
@asynccontextmanager
async def lifespan(app: FastAPI):
# Load translations into memory cache at startup
async with httpx.AsyncClient() as client:
for locale in SUPPORTED_LOCALES:
resp = await client.post(f"{D1_URL}/query", ...)
TRANSLATIONS[locale] = resp.json()["results"]
yield
# Cleanup on shutdown (if needed)
app = FastAPI(lifespan=lifespan)
Lifespan jest obecnie jedynym mechanizmem uruchamiania i zamykania aplikacji. Pierwsza stabilna wersja Starlette, 1.0, ukazała się w marcu 2026 roku (wersja 1.3.1 była aktualna 12 czerwca) i usunęła od dawna oznaczone jako przestarzałe hooki on_event, on_startup oraz on_shutdown — lifespan (powyżej) jest jedynym mechanizmem, a @app.route() i @app.websocket_route() zostały zastąpione przez Route oraz WebSocketRoute na liście routes. FastAPI 0.137.0 (14 czerwca 2026) przebudowuje również wewnętrzną strukturę własnego routera: router.routes nie jest już płaską listą obiektów APIRoute, lecz drzewem węzłów pośrednich, dlatego należy traktować ją jako szczegół implementacyjny, a nie strukturę przeznaczoną do iterowania. Zaletą jest to, że trasy dodane do routera po wywołaniu include_router() są teraz uwzględniane na bieżąco, a router podrzędny można dołączyć przed zdefiniowaniem jego tras. Sam FastAPI nie wymusza wersji Starlette z linii 1.x: od wersji 0.136.3 jego wymaganie uruchomieniowe określa jedynie dolną granicę, starlette>=0.46.0, i pozostaje niezmienione aż do wersji 0.140.7 — bez górnej granicy, więc Starlette 0.4x nadal je spełnia. Numery wersji 1.x wymienione w informacjach o wydaniu 0.137.0 są aktualizacjami dependabot w pliku blokady testów samego repozytorium, a nie ograniczeniem uruchomieniowym aplikacji.24 Żadna z tych zmian nie wpływa na wzorce przedstawione w tym przewodniku — wszędzie używa on lifespan i standardowego deklarowania tras — jednak dla narzędzi przechodzących po router.routes albo starszych procedur obsługi @app.on_event wersje 0.137.0 / Starlette 1.0 wprowadzają zmiany niezgodne wstecznie. FastAPI 0.137.2 (18 czerwca 2026) dodaje iter_route_contexts(), czyli obecnie wspierany sposób wyliczania tras, odkąd router.routes stało się elementem wewnętrznym. Następnie FastAPI 0.138.0 (20 czerwca 2026) wprowadza app.frontend("/", directory="dist") / router.frontend(...) do udostępniania zbudowanego statycznego frontendu — przydatne przy publikowaniu osobnej kompilacji SPA, lecz niezwiązane z opisanym w tym przewodniku podejściem bez procesu budowania i z renderowaniem po stronie serwera (mechanizm montuje folder dist/, zamiast renderować HTML na serwerze).25 FastAPI 0.139.0 (1 lipca 2026) rozszerza tę funkcję o obsługę zależności w app.frontend() — na przykład automatyczne uwierzytelnianie za pomocą plików cookie dla udostępnianego frontendu — udostępniając statycznemu frontendowi ten sam mechanizm Depends(), którego używa się w trasach API.26 FastAPI 0.141.0 (29 lipca 2026) dodaje app.frontend(check_dir="auto"), dzięki czemu fastapi dev nie kończy się błędem, gdy folder kompilacji jeszcze nie istnieje — co jest typowe, gdy serwer uruchamia się przed zbudowaniem frontendu. Wydany tego samego dnia FastAPI 0.141.1 naprawia błąd, przez który zależności w app.frontend() po cichu odrzucały zadania w tle i nagłówki odpowiedzi: praca zależności ustawiającej plik cookie lub planującej BackgroundTask była pomijana w punkcie montowania frontendu, choć działała prawidłowo w trasach API. Jeśli wdrożono obsługę zależności z wersji 0.139.0, dopiero wydanie 0.141.1 zapewnia zachowanie zgodne z resztą aplikacji.28
FastAPI 0.140.0 usuwa regresję zużycia pamięci obecną w każdym wydaniu od listopada 2025 roku — należy zaktualizować wersję. Wydanie z 24 lipca 2026 roku zawiera pojedynczą refaktoryzację o wyjątkowo dużym wpływie. Dependant, wewnętrzny obiekt tworzony przez FastAPI dla każdego węzła grafu zależności każdej trasy, od wersji 0.121.0 (3 listopada 2025) gromadził atrybuty functools.cached_property — w wersji 0.139.2 było ich już dziesięć. Właściwość przechowująca wynik w pamięci podręcznej wymaga osobnego __dict__ dla każdej instancji, aby móc ten wynik zapisać, więc koszt mnożył się przez każdy węzeł każdego grafu w aplikacji. PR #16049 przenosi tę logikę z klasy do pomocniczych funkcji na poziomie modułu (_get_cache_key(), _get_oauth_scopes(), _uses_scopes()) i deklaruje Dependant jako @dataclass(slots=True), pozostawiając go wyłącznie kontenerem danych. Wynik testu CodSpeed przeprowadzonego przez zespół FastAPI po scaleniu PR pokazuje w benchmarku pamięci test_dependency_graph spadek z 17,5 MB do 1,1 MB, czyli 16-krotną redukcję; w zgłoszeniu, które zapoczątkowało te prace, opisano usługę produkcyjną zużywającą mniej niż około 400 MB w wersji 0.120.4, lecz kończącą działanie z powodu OOM w wersji 0.121.3. Każda wersja polecana od tamtej pory w tym przewodniku — 0.137.x, 0.138.0, 0.139.2 — zawierała tę regresję. Jeśli aplikacja ma głębokie lub rozbudowane drzewo zależności (zagnieżdżone Depends(), schematy zabezpieczeń, wiele dołączonych routerów), wersja 0.140.0 pozwala bez dodatkowych kosztów zmniejszyć zużycie pamięci i nie wymaga zmian w kodzie aplikacji.27
Wersja 0.140.0 była początkiem, a nie pełnym rozwiązaniem — należy przypiąć wersję 0.140.7 lub nowszą. Trzy dni po tamtym wydaniu, 27 lipca 2026 roku, FastAPI opublikował siedem kolejnych wersji w ciągu pięciu i pół godziny: od 0.140.1 do 0.140.7, z których każda refaktoryzowała ten sam mechanizm zależności. Prace dzielą się na dwa obszary. Pierwszy to spłaszczone drzewo zależności: wcześniej FastAPI tworzył i przechowywał spłaszczoną kopię grafu zależności każdej trasy, a wersja 0.140.2 przestaje ją zachowywać; wersje 0.140.3, 0.140.5, 0.140.6 i 0.140.7 usuwają natomiast pozostałe miejsca, w których nadal ją odtwarzano — generowanie OpenAPI, pola treści, parametry żądania i ponownie OpenAPI. Wersja 0.140.4 usuwa niewykorzystywaną ewidencję powtarzających się zależności. Drugi obszar obejmuje jedyną zmianę z zauważalnym progiem: wersja 0.140.1 zwiększa lru_cache pomocniczych funkcji klasyfikujących wywoływalne obiekty w fastapi/dependencies/models.py z 1 024 do 4 096 wpisów, wykorzystując nazwaną stałą _CALLABLE_CLASSIFICATION_CACHE_SIZE, ponieważ użytkownicy zgłaszali aplikacje z ponad 1 024 różnymi zależnościami, które powodowały ciągłe wypieranie wpisów z pamięci podręcznej. Żadna z tych zmian nie wpływa na wywoływane API, więc aktualizacja sprowadza się do zmiany numeru wersji. Warto jasno wskazać dwa zastrzeżenia: tempo wydań oznacza, że ta linia nadal się rozwija, dlatego należy czytać informacje o wydaniach, zamiast zakładać, że wersja 0.140.7 kończy prace; ponadto FastAPI dodał benchmarki zależności OpenAPI, które mierzą te zmiany, w tym samym okresie (PR #16075), dlatego opublikowane wyniki obejmują kilka ostatnich wydań, a nie cały cykl siedmiu wersji.29
Operacje zależne od CPU (renderowanie Markdown, wyodrębnianie CSS) mogą korzystać z funkcji synchronicznych. FastAPI automatycznie uruchamia je w puli wątków, gdy funkcja obsługująca trasę nie została zadeklarowana jako async:
# Sync function — FastAPI runs it in a thread pool
@router.get("/blog/{slug}")
def blog_post(slug: str):
post = load_post_by_slug(slug) # CPU-bound Markdown parsing
return templates.TemplateResponse(...)
Zasada jest następująca: jeśli funkcja oczekuje na operacje wejścia/wyjścia, należy oznaczyć ją jako async. Jeśli wykonuje obliczenia zależne od CPU, powinna pozostać synchroniczna. Nie należy łączyć await z wywołaniami blokującymi w tej samej funkcji.9
Szablony Jinja2
Dziedziczenie szablonów
System dziedziczenia Jinja2 zastępuje kompozycję komponentów z React prostszym modelem. Jeden szablon bazowy definiuje szkielet strony. Szablony potomne wypełniają nazwane bloki:
<!-- base.html — the skeleton -->
<!DOCTYPE html>
<html lang="{{ lang_attr() }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ page_title | default("Blake Crosley") }}</title>
<meta name="description" content="{{ page_description | default('...') }}">
<!-- CSS — single file, no preprocessor -->
<link rel="stylesheet" href="{{ asset('css/styles.css') }}">
<!-- JSON-LD structured data -->
<script type="application/ld+json">
{ "@context": "https://schema.org", "@graph": [...] }
</script>
{% block head %}{% endblock %}
</head>
<body>
<header class="header">...</header>
<main id="main" role="main">
{% block content %}{% endblock %}
</main>
<footer class="footer">...</footer>
<!-- Scripts deferred for performance -->
<script defer src="{{ asset('js/vendor/alpine.min.js') }}"></script>
<script defer src="{{ asset('js/vendor/htmx.min.js') }}"></script>
<script defer src="{{ asset('js/main.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
<!-- pages/about.html — fills the blocks -->
{% extends "base.html" %}
{% block head %}
<script type="application/ld+json">
{ "@type": "AboutPage", "name": "About Blake Crosley", ... }
</script>
{% endblock %}
{% block content %}
<section class="hero">
<h1>About</h1>
<p>Designer, developer, dad.</p>
</section>
{% endblock %}
Dyrektywa {% extends %} ustanawia relację rodzic-potomek. Szablon potomny definiuje jedynie te bloki, które chce nadpisać. Cała reszta — <head>, nagłówek, stopka, tagi skryptów — pochodzi z szablonu bazowego. To kompozycja przez odejmowanie, a nie budowanie.
Funkcja globalna asset()
Zasoby statyczne wykorzystują wersjonowanie oparte na haszu zawartości do wymuszania odświeżenia pamięci podręcznej:
# cache_assets.py
def build_asset_map(static_dir: Path) -> dict[str, str]:
"""Compute MD5 hashes of all static files at startup."""
asset_map = {}
for filepath in static_dir.rglob("*"):
if filepath.is_file():
rel_path = str(filepath.relative_to(static_dir))
content_hash = hashlib.md5(filepath.read_bytes()).hexdigest()[:10]
asset_map[rel_path] = content_hash
return asset_map
def make_asset_url(asset_map: dict, path: str) -> str:
"""Generate versioned URL: /static/css/styles.css?v=a3f8b2c1d0"""
clean_path = path.lstrip("/")
version = asset_map.get(clean_path, "0")
return f"/static/{clean_path}?v={version}"
W szablonie: {{ asset('css/styles.css') }} renderuje się jako /static/css/styles.css?v=a3f8b2c1d0. Hasz zmienia się wraz ze zmianą pliku, wymuszając odświeżenie pamięci podręcznej CDN. To zastępuje strategię nazw plików [contenthash] z webpack za pomocą 30 linii Python obliczanych przy starcie.
Include dla komponentów wielokrotnego użytku
Komponenty powtarzające się na wielu stronach wykorzystują {% include %}:
<!-- base.html -->
{% include "components/_language_switcher.html" %}
<!-- components/_language_switcher.html -->
{%- set current = current_locale() -%}
{%- set locales = all_locales() -%}
<div class="language-switcher"
x-data="{ open: false }"
@click.away="open = false">
<button @click="open = !open" :aria-expanded="open">
{{ current_locale_native() }}
</button>
<ul class="language-switcher-menu"
:class="{ 'is-open': open }"
x-cloak>
{% for locale in locales %}
<li>
<a href="{{ locale_url(request.url.path, locale.code) }}"
hreflang="{{ locale.code }}">
{{ locale.native }}
</a>
</li>
{% endfor %}
</ul>
</div>
Prefiks z podkreślnikiem (_language_switcher.html) to konwencja oznaczająca partial — fragment szablonu nieprzeznaczony do samodzielnego renderowania. Ten komponent wykorzystuje zarówno Alpine.js (do przełączania rozwijanego menu), jak i Jinja2 (do listy lokalizacji). Granica jest wyraźna: Alpine.js zarządza stanem otwierania/zamykania, Jinja2 zarządza danymi.
Makra jako komponenty wielokrotnego użytku
Makra to funkcje Jinja2 — bloki szablonów wielokrotnego użytku z parametrami:
<!-- components/_macros.html -->
{% macro card(title, description, href, badge=None) %}
<article class="card">
<a href="{{ href }}" class="card__link">
{% if badge %}
<span class="card__badge">{{ badge }}</span>
{% endif %}
<h3 class="card__title">{{ title }}</h3>
{% if description %}
<p class="card__description">{{ description }}</p>
{% endif %}
</a>
</article>
{% endmacro %}
{% macro optimized_image(image_config, loading="lazy") %}
{% if image_config.get("svg") %}
<img src="{{ image_config.svg }}"
width="{{ image_config.width }}"
height="{{ image_config.height }}"
alt="{{ image_config.alt }}">
{% else %}
<picture>
<source type="image/webp"
srcset="{{ image_config.webp_srcset }}"
sizes="(max-width: 768px) 100vw, 50vw">
<img src="{{ image_config.fallback }}"
width="{{ image_config.width }}"
height="{{ image_config.height }}"
alt="{{ image_config.alt }}"
loading="{{ loading }}">
</picture>
{% endif %}
{% endmacro %}
Importowanie i używanie makr w szablonach stron:
{% from "components/_macros.html" import card, optimized_image %}
<section class="projects">
{% for project in projects %}
{{ card(
title=project.title,
description=project.description,
href=project.link,
badge="New" if project.is_new else None
) }}
{% endfor %}
</section>
Makra zastępują komponenty React w przypadku wzorców prezentacyjnych. Przyjmują parametry, obsługują wartości domyślne i komponują się z innymi makrami. Kluczowa różnica: makra renderują się jednokrotnie na serwerze i generują statyczny HTML. Komponenty React renderują się po stronie klienta i utrzymują stan. Do wyświetlania treści makra są odpowiednim narzędziem.
Kontekst szablonów i zmienne globalne
Zmienne globalne Jinja2 to funkcje dostępne w każdym szablonie bez konieczności ich jawnego przekazywania:
# In main.py — register globals
templates.env.globals["asset"] = lambda path: make_asset_url(_asset_map, path)
templates.env.globals["csrf_token"] = generate_csrf_token
templates.env.globals["analytics_script"] = analytics.tracking_script
Funkcja globalna asset() generuje wersjonowane adresy URL. Funkcja globalna csrf_token() generuje świeże tokeny CSRF. Funkcja globalna analytics_script() wstrzykuje fragment kodu śledzenia. Wszystkie te funkcje można wywoływać w dowolnym szablonie, bez konieczności jawnego przekazywania ich przez handler trasy.
W przypadku i18n konfiguracja jest bardziej złożona — funkcje tłumaczące potrzebują dostępu do lokalizacji bieżącego żądania:
# i18n/jinja.py
def setup_i18n_jinja(env):
"""Register translation functions as Jinja2 globals."""
env.globals["_"] = get_translation # _('ui.nav.about')
env.globals["locale_prefix"] = get_locale_prefix # '/ja' or ''
env.globals["current_locale"] = get_current_locale
env.globals["all_locales"] = get_all_locales
env.globals["alternate_urls"] = get_alternate_urls
env.globals["lang_attr"] = get_lang_attr # 'ja' for HTML lang
env.globals["og_locale"] = get_og_locale # 'ja_JP' for og:locale
env.globals["jsonld_lang"] = get_jsonld_lang # 'ja-JP' for JSON-LD
Każda funkcja odczytuje lokalizację ze zmiennej kontekstowej żądania ustawianej przez middleware lokalizacji. Szablon wywołuje {{ _('ui.nav.about') }} i otrzymuje przetłumaczony ciąg znaków dla lokalizacji bieżącego żądania, bez konieczności jawnego przekazywania parametru lokalizacji.
Bloki warunkowe
System bloków Jinja2 obsługuje warunkowe nadpisywanie:
<!-- base.html -->
{% block head %}{% endblock %}
<!-- pages/blog/post.html -->
{% block head %}
<script type="application/ld+json">
{
"@type": "Article",
"headline": "{{ post.meta.title }}",
"author": { "@id": "https://blakecrosley.com/#person" },
"datePublished": "{{ post.meta.date.isoformat() }}",
"dateModified": "{{ post.meta.updated.isoformat() if post.meta.updated else post.meta.date.isoformat() }}"
}
</script>
{% if post.meta.scripts %}
{% for script in post.meta.scripts %}
<script defer src="{{ asset(script.lstrip('/static/')) }}"></script>
{% endfor %}
{% endif %}
{% if post.meta.styles %}
{% for style in post.meta.styles %}
<link rel="stylesheet" href="{{ asset(style.lstrip('/static/')) }}">
{% endfor %}
{% endif %}
{% endblock %}
Wpisy blogowe deklarują swoje zależności w frontmatter YAML (scripts: ["/static/js/boids.js"]). Szablon warunkowo je dołącza. Strony, które nie potrzebują dodatkowych skryptów ani stylów, nie ładują żadnych — bez martwego kodu, bez nieużywanych importów.
Filtry niestandardowe
Filtry Jinja2 przekształcają dane podczas renderowania. Filtr sanitize zapobiega atakom XSS w treściach generowanych przez użytkowników:
import nh3
ALLOWED_TAGS = {"a", "b", "blockquote", "br", "code", "em", "h1", "h2",
"h3", "h4", "h5", "h6", "hr", "i", "img", "li", "ol",
"p", "pre", "span", "strong", "table", "td", "th", "tr", "ul"}
def sanitize_html(value: str) -> str:
"""Sanitize HTML to prevent XSS attacks."""
if not value:
return ""
return nh3.clean(
value,
tags=ALLOWED_TAGS,
attributes={"a": {"href", "title"}, "img": {"src", "alt"}},
link_rel="noopener noreferrer",
)
templates.env.filters["sanitize"] = sanitize_html
W szablonach: {{ user_content | sanitize }}. Biblioteka nh3 to napisany w Rust sanitizer HTML — szybki i bezpieczny. Usuwa wszelkie tagi i atrybuty spoza listy dozwolonych, zapobiegając stored XSS nawet wtedy, gdy treść pochodzi z niezaufanego źródła.10
HTMX — szczegółowe omówienie
HTMX umożliwia dowolnemu elementowi HTML wysyłanie żądań HTTP i podmianę odpowiedzi w drzewie DOM. Kluczowa idea ma charakter architektoniczny: renderowany po stronie serwera HTML stanowi API. Serwer zwraca finalną reprezentację. Bez renderowania po stronie klienta, bez serializacji JSON, bez hydratacji.
Podstawowe atrybuty
| Atrybut | Przeznaczenie | Przykład |
|---|---|---|
hx-get |
Wysłanie żądania GET | hx-get="/search?q=term" |
hx-post |
Wysłanie żądania POST | hx-post="/contact" |
hx-target |
Miejsce umieszczenia odpowiedzi | hx-target="#results" |
hx-swap |
Sposób wstawienia odpowiedzi | hx-swap="innerHTML" (domyślnie), outerHTML, beforeend |
hx-trigger |
Zdarzenie wyzwalające żądanie | hx-trigger="click", keyup changed delay:300ms, load |
hx-indicator |
Element wyświetlany podczas żądania | hx-indicator="#spinner" |
hx-push-url |
Aktualizacja adresu URL przeglądarki | hx-push-url="true" |
hx-replace-url |
Zamiana URL bez wpisu w historii | hx-replace-url="true" |
Wzorzec 1: Interaktywny quiz (wieloetapowy stan na serwerze)
Strona blakecrosley.com zawiera interaktywny quiz, który prowadzi użytkowników przez wybór narzędzi. Cały stan quizu znajduje się na serwerze — bez zarządzania stanem po stronie klienta:
<!-- _quiz_container.html — initial load -->
<div hx-get="/api/quiz/claude-vs-codex/step?answers="
hx-trigger="load"
hx-swap="innerHTML"
id="quiz-wrapper">
<p>Loading quiz...</p>
</div>
<!-- _quiz_step.html — each question -->
<div class="quiz-step" id="quiz-container">
<p>Question {{ step }} of {{ total }}</p>
<h3>{{ question.question }}</h3>
<div class="quiz-step__options">
{% for opt in question.options %}
<button class="quiz-step__btn"
hx-get="/api/quiz/claude-vs-codex/step?answers={{ answers }},{{ opt.value }}"
hx-target="#quiz-container"
hx-swap="outerHTML">
{{ opt.label }}
</button>
{% endfor %}
</div>
</div>
Każde kliknięcie przycisku wysyła dotychczas zebrane odpowiedzi jako parametr zapytania. Serwer na podstawie historii odpowiedzi oblicza kolejne pytanie lub wynik końcowy. Stan kumuluje się w adresie URL — bez ciasteczek, bez sesji, bez JavaScript po stronie klienta. Quiz postępuje poprzez podmiany outerHTML: każda odpowiedź zastępuje cały element kroku quizu.
Wzorzec 2: Stronicowana lista wpisów na blogu
Strona z wpisami wykorzystuje HTMX do płynnej paginacji z aktualizacją adresu URL:
<!-- Pagination link -->
<a href="/writing?page=2&category=Engineering"
hx-get="/writing?page=2&category=Engineering"
hx-target="#writing-content"
hx-swap="innerHTML"
hx-replace-url="true"
hx-indicator="#writing-loading"
aria-label="Go to page 2">
2
</a>
Cztery atrybuty współpracujące ze sobą:
hx-getwysyła żądanie pod ten sam adres URL cohref(stopniowe wzbogacanie — działa bez JavaScript)hx-targetumieszcza odpowiedź w kontenerze#writing-contenthx-replace-url="true"aktualizuje adres URL przeglądarki bez dodawania wpisu do historiihx-indicatorwyświetla wskaźnik ładowania podczas trwania żądania
Serwer wykrywa żądania HTMX za pomocą nagłówka HX-Request i zwraca jedynie fragment z listą wpisów zamiast pełnej strony. Dlatego middleware nagłówków bezpieczeństwa dodaje Vary: HX-Request — aby pamięci podręczne CDN przechowywały pełną stronę i fragment osobno.11
Wzorzec 3: Wyszukiwanie z opóźnieniem (debounce)
<input type="search" name="q"
hx-get="/api/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results"
hx-indicator="#search-spinner" />
<div id="results"></div>
Atrybut hx-trigger łączy trzy modyfikatory:
keyup— wyzwalany przy zwolnieniu klawiszachanged— wyzwalany tylko wtedy, gdy wartość faktycznie się zmieniła (zapobiega duplikowaniu żądań przez klawisze modyfikujące)delay:300ms— opóźnienie (debounce) — czeka 300 ms od ostatniego zdarzenia keyup przed wysłaniem żądania
Serwer zwraca wyrenderowany fragment HTML:
@router.get("/api/search")
async def search(request: Request, q: str = ""):
results = search_content(q)
return templates.TemplateResponse("components/_search_results.html", {
"request": request,
"results": results,
"query": q,
})
Bez stanu po stronie klienta. Bez biblioteki debounce. Bez useEffect. Szablon renderuje wyniki, HTMX podmienia je w DOM, a serwer pozostaje jedynym źródłem prawdy.
Wzorzec 4: Podmiany poza głównym celem (OOB Swaps)
Zdarza się, że pojedyncza akcja serwera musi zaktualizować wiele elementów DOM jednocześnie. Mechanizm podmian OOB (out-of-band) w HTMX obsługuje to bez orkiestracji po stronie klienta:
<!-- Server returns multiple elements in one response -->
<!-- Primary target: swapped normally via hx-target -->
<div id="cart-items">
<ul>
<li>Widget A — $29.99</li>
<li>Widget B — $14.99</li>
</ul>
</div>
<!-- OOB target: swapped independently via hx-swap-oob -->
<span id="cart-count" hx-swap-oob="true">2 items</span>
<span id="cart-total" hx-swap-oob="true">$44.98</span>
Atrybut hx-swap-oob="true" instruuje HTMX, aby znalazł element po id w dowolnym miejscu drzewa DOM i zastąpił go, niezależnie od wartości hx-target. Zastępuje to wzorzec „podnoszenia stanu” (lift state up) z React — serwer oblicza cały stan pochodny i wysyła finalny HTML dla każdego elementu w jednej odpowiedzi.
Formularz kontaktowy dobrze to ilustruje: wysłanie formularza może zastąpić jego treść komunikatem o sukcesie, a jednocześnie zaktualizować plakietkę powiadomień poprzez podmianę OOB:
Wzorzec 5: Wzmocnione linki (Boosted Links)
HTMX potrafi „wzmocnić” standardowe linki nawigacyjne, aby korzystały z AJAX zamiast pełnego przeładowania strony:
<nav hx-boost="true">
<a href="/about">About</a>
<a href="/writing">Writing</a>
<a href="/guides">Guides</a>
</nav>
Dzięki hx-boost="true" kliknięcie w link pobiera stronę przez AJAX, podmienia zawartość <body> i aktualizuje adres URL — bez pełnego przeładowania strony. Historia przeglądarki działa normalnie (przyciski wstecz/dalej). Jeśli JavaScript nie zadziała, linki funkcjonują jako standardowa nawigacja.
Korzyścią jest postrzegana szybkość: wzmocniona nawigacja sprawia wrażenie natychmiastowej, ponieważ przeglądarka nie musi ponownie parsować CSS, ponownie ewaluować skryptów ani ponownie renderować układu. Zmienia się jedynie zawartość <body>. Wzmocnione linki sprawdzają się doskonale w głównych elementach nawigacyjnych, dzięki czemu przejścia między stronami przypominają aplikację jednostronicową — bez architektury SPA.
Wzorzec 6: Nagłówki żądań HTMX
HTMX wysyła niestandardowe nagłówki z każdym żądaniem:
| Nagłówek | Wartość | Zastosowanie |
|---|---|---|
HX-Request |
true |
Wykrywanie żądań HTMX po stronie serwera |
HX-Target |
ID elementu | Informacja, który element otrzyma odpowiedź |
HX-Trigger |
ID elementu | Informacja, który element wyzwolił żądanie |
HX-Current-URL |
Pełny URL | Informacja o bieżącej stronie użytkownika |
Serwer może wykorzystać HX-Request do zwracania różnych odpowiedzi:
@router.get("/writing")
async def writing(request: Request, page: int = 1, category: str = None):
posts = load_all_posts(page=page, category=category)
context = {"request": request, "posts": posts, "current_page": page}
# HTMX request: return only the post list fragment
if request.headers.get("HX-Request"):
return templates.TemplateResponse(
"pages/writing/_post_list.html", context
)
# Normal request: return the full page
return templates.TemplateResponse("pages/writing/index.html", context)
Ten wzorzec podwójnej odpowiedzi stanowi rdzeń architektury. Pełne załadowanie strony zwraca kompletny dokument (szablon bazowy + zawartość strony). Nawigacja przez HTMX zwraca jedynie zmienioną treść. Decyduje serwer, nie klient.
Wzorzec 7: Stopniowe wzbogacanie (Progressive Enhancement)
Każdy link HTMX na blakecrosley.com zawiera standardowy atrybut href:
<a href="/writing?page=2"
hx-get="/writing?page=2"
hx-target="#writing-content"
hx-swap="innerHTML">
Next Page
</a>
Jeśli JavaScript nie załaduje się, href działa jako zwykły link. Jeśli HTMX się załaduje, przechwytuje kliknięcie i wykonuje podmianę AJAX. To właśnie stopniowe wzbogacanie: strona działa bez JavaScript, a HTMX wzbogaca doświadczenie, gdy jest dostępny.
Wzorzec 8: Stany ładowania
<button hx-post="/api/contact"
hx-target="#form-result"
hx-indicator="#submit-spinner">
<span id="submit-spinner" class="htmx-indicator">Sending...</span>
<span>Send Message</span>
</button>
HTMX dodaje klasę htmx-request do elementu wyzwalającego na czas trwania żądania. Atrybut hx-indicator wskazuje element, który staje się widoczny podczas żądania. Stylowanie odbywa się za pomocą CSS:
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
display: inline;
}
Bez zarządzania stanami ładowania. Bez useState(false). Bez setLoading(true). CSS obsługuje widoczność, HTMX przełącza klasę.
Wzorce Alpine.js
Alpine.js wypełnia lukę, którą pozostawia HTMX: stan istniejący wyłącznie po stronie klienta, który nigdy nie musi komunikować się z serwerem. Gdy użytkownik klika rozwijane menu i ono się otwiera, ten stan istnieje tylko w przeglądarce. Alpine.js zarządza nim za pomocą atrybutów HTML.
Zasada granicy
Granica między HTMX a Alpine.js jest precyzyjna:
| Typ stanu | Narzędzie | Przykład |
|---|---|---|
| Wymaga danych z serwera | HTMX | Wyniki wyszukiwania, walidacja formularzy, paginacja |
| Istnieje tylko w przeglądarce | Alpine.js | Otwieranie/zamykanie menu, przełącznik menu mobilnego, widoczność modala |
| Łączy oba podejścia | Oba | Przełącznik języka (przełączanie Alpine.js, nawigacja w stylu HTMX) |
Nawigacja mobilna
Szablon bazowy opakowuje cały nagłówek w komponent Alpine.js:
<div x-data="{ navOpen: false, langOpen: false }"
@keydown.escape.window="navOpen = false; langOpen = false">
<!-- Mobile hamburger button -->
<button @click="navOpen = !navOpen; langOpen = false"
:aria-expanded="navOpen"
:class="navOpen ? 'nav__toggle is-open' : 'nav__toggle'"
aria-label="Toggle navigation">
<span class="nav__toggle-icon">
<span class="nav__toggle-bar"></span>
<span class="nav__toggle-bar"></span>
<span class="nav__toggle-bar"></span>
</span>
</button>
<!-- Mobile menu panel -->
<div class="mobile-menu" x-show="navOpen" x-cloak>
<nav class="mobile-menu__nav">
<a href="/about" @click="navOpen = false">About</a>
<a href="/#work" @click="navOpen = false">Work</a>
<a href="/writing" @click="navOpen = false">Writing</a>
</nav>
</div>
</div>
Kluczowe wzorce Alpine.js:
x-datadeklaruje zakres komponentu i stan początkowyx-showprzełącza widoczność na podstawie stanu (wykorzystuje CSSdisplay: none)x-cloakukrywa element do momentu inicjalizacji Alpine.js (zapobiega migotaniu niestylizowanej treści)@clickwiąże procedury obsługi kliknięć z wyrażeniami:aria-expanded(skrót odx-bind:aria-expanded) dynamicznie ustawia atrybuty@keydown.escape.windownasłuchuje klawisza Escape globalnie, aby zamknąć panele
Komponent rozwijany
Przełącznik języka wykorzystuje Alpine.js do zarządzania stanem z @click.away do zamykania przy kliknięciu poza komponentem:
<div x-data="{ open: false }"
@click.away="open = false"
@keydown.escape.window="open = false">
<button @click="open = !open"
:aria-expanded="open"
aria-haspopup="listbox">
English
<svg :class="{ 'rotated': open }">...</svg>
</button>
<ul :class="{ 'is-open': open }"
:aria-hidden="!open"
role="listbox"
x-cloak>
<li role="option">
<a href="/ja/about">日本語</a>
</li>
<!-- more languages -->
</ul>
</div>
Modyfikator @click.away zamyka menu rozwijane przy kliknięciu na zewnątrz. Alpine.js obsługuje to za pomocą jednego atrybutu — bez rejestrowania nasłuchiwania zdarzeń, bez czyszczenia, bez zarządzania referencjami.
Kiedy używać Alpine.js, a kiedy czystego JavaScript
Alpine.js sprawdza się, gdy:
- Stan jest ograniczony do pojedynczego elementu DOM (menu rozwijane, modal, przełącznik)
- Interakcje są binarne lub proste (otwórz/zamknij, pokaż/ukryj, przełącz)
- Wiele elementów musi reagować na tę samą zmianę stanu
- Atrybuty dostępności muszą być zsynchronizowane z widocznością
Czysty JavaScript sprawdza się, gdy:
- Interakcja wymaga złożonych obliczeń (wizualizacje, symulacje)
- Komponent ma własną pętlę renderowania (canvas, animacje)
- Wydajność jest kluczowa (Alpine.js dodaje narzut na każdy komponent
x-data) - Logika przekracza 20–30 linii wyrażeń Alpine.js
blakecrosley.com używa Alpine.js do nawigacji, przełączania języków i przełączników treści. 20 interaktywnych komponentów bloga (symulacja boidów, wizualizator kodu Hamminga itd.) korzysta z czystego JavaScript, ponieważ wymaga renderowania na canvas i złożonych maszyn stanów.
Kompleksowy przykład: filtrowanie kategorii na /writing
Ta sekcja śledzi rzeczywistą funkcję z produkcyjnego kodu przez wszystkie warstwy: trasę, szablon, interakcję HTMX, bezpieczeństwo, cachowanie i wyrenderowany wynik. Funkcja: zakładki kategorii na stronie z wpisami, które filtrują posty bloga bez pełnego przeładowania strony.
Trasa (app/routes/pages.py:508)
async def writing_listing(request: Request, page: int = 1, category: str | None = None):
"""Writing page — blog posts and external publications."""
templates = get_templates(request)
markdown_posts = load_all_posts(published_only=True)
all_posts = CUSTOM_BLOG_POSTS + markdown_posts
# Filter by category if specified
if category and category in CATEGORY_MAP:
display_name = CATEGORY_MAP[category]
all_posts = [
p for p in all_posts
if _get_post_category(p).lower() == display_name.lower()
]
# Pagination
total_pages = max(1, (len(all_posts) + POSTS_PER_PAGE - 1) // POSTS_PER_PAGE)
page = max(1, min(page, total_pages))
paginated = all_posts[(page - 1) * POSTS_PER_PAGE : page * POSTS_PER_PAGE]
template_context = {
"request": request,
"posts": paginated,
"categories": categories,
"current_category": category,
"current_page": page,
"total_pages": total_pages,
# ... SEO: canonical, prev/next URLs
}
# HTMX partial: return just the post list fragment
if request.headers.get("HX-Request"):
return templates.TemplateResponse(
"pages/writing/_post_list.html",
template_context,
)
# Full page for direct navigation
return templates.TemplateResponse(
"pages/writing/index.html",
template_context,
)
Sprawdzenie nagłówka HX-Request to kluczowy wzorzec: ta sama trasa, te same dane, inny szablon. HTMX otrzymuje fragment. Przeglądarki otrzymują pełną stronę.
Zakładki kategorii (HTMX)
<!-- Category filter tabs -->
<nav class="writing-categories">
<a href="/writing"
hx-get="/writing"
hx-target="#post-list"
hx-push-url="true"
class="category-tab {% if not current_category %}active{% endif %}">
All ({{ total_posts }})
</a>
{% for cat in categories %}
<a href="/writing?category={{ cat.slug }}"
hx-get="/writing?category={{ cat.slug }}"
hx-target="#post-list"
hx-push-url="true"
class="category-tab {% if current_category == cat.slug %}active{% endif %}">
{{ cat.name }} ({{ cat.count }})
</a>
{% endfor %}
</nav>
<div id="post-list">
{% include "pages/writing/_post_list.html" %}
</div>
Każda zakładka ma zarówno href (działa bez JavaScript), jak i hx-get (podmienia tylko listę postów). hx-push-url aktualizuje adres URL przeglądarki, dzięki czemu przefiltrowany widok można udostępniać i dodawać do zakładek.
Fragment (pages/writing/_post_list.html)
Fragment renderuje się identycznie niezależnie od tego, czy jest dołączony przy ładowaniu strony, czy podmieniony przez HTMX:
{% for post in posts %}
<article class="post-card">
<a href="{{ locale_prefix() }}/blog/{{ post.meta.slug }}">
<h3>{{ post.meta.title }}</h3>
<p>{{ post.meta.description }}</p>
<time>{{ post.meta.date }}</time> · {{ post.reading_time }}m
</a>
</article>
{% endfor %}
Brak specjalnego znacznika HTMX we fragmencie. Brak logiki renderowania po stronie klienta. Ten sam HTML obsługuje zarówno początkowe ładowanie strony, jak i każde kolejne filtrowanie.
Bezpieczeństwo
Wartości kategorii są walidowane względem CATEGORY_MAP (słownika po stronie serwera) przed filtrowaniem. Nieprawidłowe kategorie są ignorowane, a nie zwracane w odpowiedzi. Żadne dane wejściowe użytkownika nie są interpolowane do SQL ani HTML. Nagłówek CSP blokuje skrypty inline.
Cachowanie
Odpowiedzi kategorii są dynamiczne (bez cache CDN). Natomiast zasoby statyczne (CSS, HTMX, Alpine.js) mają hash w nazwie i są cachowane bezterminowo po pierwszym pobraniu. Kolejne przełączenia kategorii przesyłają jedynie fragment HTML (~3–5 KB) — bez ponownego pobierania CSS, JS ani obrazów.
Co to demonstruje
Jedna funkcja, prawdziwy kod produkcyjny, zero narzędzi do budowania. Serwer filtruje i renderuje HTML. HTMX podmienia listę postów. Alpine.js nie jest zaangażowany (nie potrzeba stanu po stronie klienta). URL aktualizuje się, umożliwiając udostępnianie. Progresywne ulepszanie: zakładki działają jako zwykłe linki bez JavaScript. Łączna ilość niestandardowego JavaScript w tej funkcji: zero linii.
Opcjonalne rozszerzenia
Poniższe sekcje opisują wzorce uzupełniające podstawowy stos, które nie są używane na blakecrosley.com. Zostały uwzględnione, ponieważ reprezentują najczęstsze dodatki wprowadzane przez zespoły adoptujące tę architekturę.
Bootstrap 5 bez Sass
Uwaga: blakecrosley.com używa zwykłego CSS z właściwościami niestandardowymi — bez Bootstrap. Ta sekcja opisuje Bootstrap 5 jako opcję dla zespołów, które chcą korzystać z frameworka narzędziowego bez etapu budowania. Skompilowany CSS Bootstrap można załadować z CDN lub dołączyć do arkusza stylów. Poniższe wzorce są uniwersalne i działają równolegle z podejściem HTMX + Alpine.js opisanym w poprzednich sekcjach.
Bootstrap 5 zrezygnował z jQuery jako zależności i obsługuje samodzielne użycie CSS. Nie potrzeba Sass, PostCSS ani żadnego narzędzia budowania, aby korzystać z systemu siatki i klas narzędziowych Bootstrap.
Samodzielny hosting bez CDN
blakecrosley.com hostuje wszystkie biblioteki zewnętrzne lokalnie:
<!-- base.html — no CDN, no external requests -->
<script defer src="{{ asset('js/vendor/alpine.min.js') }}"></script>
<script defer src="{{ asset('js/vendor/htmx.min.js') }}"></script>
Samodzielny hosting eliminuje zależności zewnętrzne, zapobiega awariom CDN wpływającym na działanie witryny i umożliwia niezmienne buforowanie z adresami URL zawierającymi skrót zawartości. Należy pobrać skompilowany CSS Bootstrap (nie źródła Sass) i umieścić go w static/css/vendor/.
System siatki
System siatki Bootstrap działa ze zwykłymi klasami HTML:
<div class="container">
<div class="row">
<div class="col-12 col-md-8">
<article>Main content</article>
</div>
<div class="col-12 col-md-4">
<aside>Sidebar</aside>
</div>
</div>
</div>
Bez mixinów Sass. Bez @include make-col(). Skompilowany CSS zawiera responsywne klasy siatki. W przypadku niestandardowych punktów przełamania wykraczających poza domyślne wartości Bootstrap wystarczy napisać zwykłe zapytania medialne CSS.
Nadpisywanie za pomocą zwykłego CSS
Domyślne style Bootstrap można nadpisać za pomocą właściwości niestandardowych CSS i standardowych selektorów:
/* Custom design tokens — no Sass, no Tailwind */
:root {
--color-bg-dark: #000000;
--color-text-primary: #ffffff;
--color-text-secondary: rgba(255, 255, 255, 0.65);
--color-text-tertiary: rgba(255, 255, 255, 0.40);
--spacing-sm: 1rem;
--spacing-md: 1.5rem;
--spacing-lg: 2rem;
--gutter: 48px;
--font-size-lg: 1.25rem;
}
/* Responsive override — the browser reads this at runtime */
@media (max-width: 768px) {
:root {
--gutter: var(--spacing-md); /* 48px → 24px on mobile */
}
}
/* Override Bootstrap's default body styles */
body {
background: var(--color-bg-dark);
color: var(--color-text-primary);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}
Właściwości niestandardowe CSS kaskadowo przechodzą przez DOM, są dziedziczone z elementów nadrzędnych i reagują na zapytania medialne w czasie wykonywania. Zmienne Sass kompilują się do wartości statycznych i znikają. To rozróżnienie ma znaczenie przy tworzeniu motywów: pojedyncza zmiana właściwości niestandardowej może zaktualizować każdą wartość pochodną bez ponownej kompilacji.12
Klasy narzędziowe a CSS komponentów
Klasy narzędziowe Bootstrap sprawdzają się przy jednorazowych odstępach i układach. CSS komponentów należy stosować do powtarzających się wzorców:
<!-- Bootstrap utility for one-off spacing -->
<div class="mt-4 mb-3 px-2">One-off layout</div>
<!-- Component class for repeated patterns -->
<article class="writing__item">
<h3 class="writing__item-title">Post Title</h3>
<p class="writing__item-description">Description</p>
</article>
/* Component CSS — BEM naming, reusable */
.writing__item {
padding: var(--spacing-md);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
transition: background 0.15s ease;
}
.writing__item:hover {
background: rgba(255, 255, 255, 0.03);
}
.writing__item-title {
font-size: var(--font-size-lg);
margin-bottom: 0.5rem;
}
Zasada jest następująca: klasy narzędziowe Bootstrap do mechaniki układu (marginesy, wypełnienia, flexbox). Niestandardowy CSS do tożsamości wizualnej (kolory, typografia, animacje). Nie należy mieszać klas narzędziowych ze stylowaniem komponentów w zakresie tego samego zagadnienia.
Internacjonalizacja i lokalizacja
blakecrosley.com udostępnia treści w 10 językach: angielskim, japońskim, koreańskim, chińskim uproszczonym, chińskim tradycyjnym, niemieckim, francuskim, hiszpańskim, polskim i portugalskim (brazylijskim).
Routing locale oparty na URL
Locale znajduje się w ścieżce URL: /about (angielski), /ja/about (japoński), /zh-Hans/about (chiński uproszczony). Angielski jest domyślny i nie ma prefiksu.
# i18n/config.py
SUPPORTED_LOCALES = [
"en", "zh-Hans", "zh-Hant", "fr", "de", "ja", "ko", "pl", "pt-BR", "es"
]
DEFAULT_LOCALE = "en"
Middleware locale wyodrębnia locale ze ścieżki URL:
# i18n/middleware.py
class LocaleMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
path = request.url.path
# Check if path starts with a supported locale
for locale in SUPPORTED_LOCALES:
if path.startswith(f"/{locale}/") or path == f"/{locale}":
request.state.locale = locale
# Strip locale prefix for route matching
request.scope["path"] = path[len(f"/{locale}"):]
break
else:
request.state.locale = DEFAULT_LOCALE
response = await call_next(request)
return response
Middleware usuwa prefiks locale przed dopasowaniem tras. Oznacza to, że handlery tras nie potrzebują ścieżek specyficznych dla locale — /about obsługuje zarówno angielski (/about), jak i japoński (/ja/about), ponieważ middleware normalizuje ścieżkę.
Funkcje tłumaczeń w szablonach
Zmienne globalne Jinja2 udostępniają funkcje tłumaczeń:
<!-- Template usage -->
<h3>{{ _('ui.footer.navigate') | default('Navigate') }}</h3>
<a href="{{ locale_prefix() }}/about">
{{ _('ui.nav.about') | default('About') }}
</a>
Funkcja _() wyszukuje klucz tłumaczenia w pamięci podręcznej. Filtr | default() zapewnia angielski fallback w przypadku braku tłumaczenia. Funkcja locale_prefix() zwraca prefiks URL dla bieżącego locale ("" dla angielskiego, "/ja" dla japońskiego).
Tagi hreflang
Każda strona zawiera tagi hreflang dla wszystkich obsługiwanych locale:
<!-- Generated in base.html -->
{% for alt in alternate_urls(request.url.path) %}
<link rel="alternate" hreflang="{{ alt.hreflang }}" href="{{ alt.url }}">
{% endfor %}
Generuje to:
<link rel="alternate" hreflang="en" href="https://blakecrosley.com/about">
<link rel="alternate" hreflang="ja" href="https://blakecrosley.com/ja/about">
<link rel="alternate" hreflang="zh-Hans" href="https://blakecrosley.com/zh-Hans/about">
<!-- ... all 10 locales -->
<link rel="alternate" hreflang="x-default" href="https://blakecrosley.com/about">
Wyszukiwarki wykorzystują hreflang do wyświetlania prawidłowej wersji językowej w wynikach wyszukiwania. Wpis x-default wskazuje na wersję angielską jako fallback.13
Przechowywanie tłumaczeń i pamięć podręczna
Tłumaczenia są przechowywane w Cloudflare D1 (SQLite na brzegu sieci) i ładowane do pamięci podręcznej za pośrednictwem handlera lifespan:
@asynccontextmanager
async def lifespan(app: FastAPI):
# Load translations into memory at startup
for locale in SUPPORTED_LOCALES:
data = await fetch_translations(locale)
TRANSLATIONS[locale] = data
yield
app = FastAPI(lifespan=lifespan)
Pamięć podręczna eliminuje zapytania do bazy danych przy każdym renderowaniu strony. Aktualizacja tłumaczeń wymaga odświeżenia cache (wyzwalanego przez endpoint administracyjny lub wdrożenie). Taka architektura poświęca świeżość danych na rzecz wydajności — tłumaczenia zmieniają się rzadko, natomiast renderowanie stron odbywa się przy każdym żądaniu.
Monitorowanie stanu
blakecrosley.com zawiera endpoint sprawdzania stanu i18n, który monitoruje pokrycie tłumaczeń dla poszczególnych locale:
@app.get("/health/i18n")
async def health_i18n():
cache = get_translation_cache()
result = {
"status": "healthy",
"cache_loaded": cache.is_loaded,
"locales": {},
"alerts": [],
}
# Check coverage for each locale
for locale in SUPPORTED_LOCALES:
coverage = await calculate_coverage(locale, en_count)
result["locales"][locale] = {"coverage": round(coverage, 2)}
if coverage < 99.5:
result["alerts"].append(
f"{locale}: {coverage:.1f}% coverage (threshold: 99.5%)"
)
result["status"] = "warning"
return result
Próg pokrycia 99,5% wykrywa brakujące tłumaczenia, zanim użytkownicy napotkają nieprzetłumaczone ciągi tekstowe. Endpoint stanu integruje się z monitoringiem Railway, aby alertować o spadku pokrycia — na przykład po dodaniu nowych ciągów UI, które nie zostały jeszcze przetłumaczone.
Renderowanie treści z uwzględnieniem locale
Wpisy blogowe i przewodniki obsługują tłumaczenia metadanych i treści dla poszczególnych locale:
# In route handler
translated = get_blog_translation(post.meta.slug, locale)
return templates.TemplateResponse("pages/blog/post.html", {
"request": request,
"post": post,
"translated_title": translated.title if translated else post.meta.title,
"translated_description": translated.description if translated else post.meta.description,
})
<!-- In template -->
<h1>{{ translated_title }}</h1>
<p class="post__description">{{ translated_description }}</p>
<!-- Body content falls back to English if translation unavailable -->
{{ post.html | sanitize | safe }}
Wzorzec jest spójny: najpierw próba wyświetlenia przetłumaczonej treści, w razie jej braku — fallback do angielskiego. Umożliwia to częściowe tłumaczenie — japoński użytkownik widzi przetłumaczone tytuły i opisy, nawet jeśli pełna treść artykułu pozostaje w języku angielskim. Filtr | default() Jinja2 koduje ten wzorzec w jednym pipe:
{{ translated.title if translated else post.meta.title }}
Tłumaczenie danych locale
Treści statyczne, takie jak opisy projektów i etykiety nawigacji, są tłumaczone za pomocą funkcji pomocniczych, które zachowują tę samą strukturę danych, podmieniając ciągi tekstowe specyficzne dla danego locale:
# i18n/data.py
def translate_projects(projects: list, locale: str) -> list:
"""Return projects with translated titles and descriptions."""
if locale == "en":
return projects
translated = []
for project in projects:
t = get_translation(f"project.{project['slug']}.title", locale)
d = get_translation(f"project.{project['slug']}.description", locale)
translated.append({
**project,
"title": t or project["title"],
"description": d or project["description"],
})
return translated
Takie podejście utrzymuje warstwę tłumaczeń oddzieloną od warstwy danych. Trasy przekazują tę samą listę projects niezależnie od locale. Funkcje tłumaczeń opakowują dane w sposób transparentny.
Mapa witryny z alternatywami hreflang
Dynamiczna mapa witryny zawiera wszystkie strony we wszystkich locale z wzajemnymi odwołaniami:
@app.get("/sitemap.xml")
async def sitemap():
for page in static_pages:
for locale in SUPPORTED_LOCALES:
# Each URL entry includes alternates for all locales
locale_path = f"/{locale}{path}" if locale != "en" else path
xml_parts.append(f"<loc>{base_url}{locale_path}</loc>")
# Add xhtml:link alternates
for alt_locale in SUPPORTED_LOCALES:
alt_path = f"/{alt_locale}{path}" if alt_locale != "en" else path
hreflang = LOCALE_TO_HREFLANG[alt_locale]
xml_parts.append(
f'<xhtml:link rel="alternate" hreflang="{hreflang}" '
f'href="{base_url}{alt_path}"/>'
)
Generuje to 10 wpisów URL na stronę (po jednym na locale), z których każdy zawiera 11 linków alternatywnych (10 locale + x-default). Dla witryny z 50 stronami mapa witryny zawiera 500 wpisów URL z 5500 linkami hreflang. Mapa witryny jest generowana dynamicznie i cache’owana na jedną godzinę.
Wzorce baz danych
Uwaga: blakecrosley.com używa Cloudflare D1 (serverless SQLite) przez HTTP dla wszystkich trwałych danych, a nie SQLAlchemy. Ta sekcja omawia standardowy wzorzec async SQLAlchemy dla projektów FastAPI, które potrzebują relacyjnej bazy danych — najczęstszej konfiguracji produkcyjnej dla tego stosu.
Async w SQLAlchemy 2.0
W aplikacjach, które potrzebują relacyjnej bazy danych, obsługa async w SQLAlchemy 2.0 dobrze integruje się z FastAPI:
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, DeclarativeBase
engine = create_async_engine("sqlite+aiosqlite:///./data.db")
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
class Base(DeclarativeBase):
pass
Uwaga dotycząca instalacji (SQLAlchemy 2.0.50+): od wersji 2.0.50 zależność greenlet dla stosu async nie jest już instalowana domyślnie. Należy użyć dodatku asyncio, aby została pobrana, w przeciwnym razie pierwsze await względem silnika zakończy się błędem braku greenlet:23
pip install "sqlalchemy[asyncio]" aiosqlite
SQLAlchemy 2.0.50 wymaga także Python 3.10+ (porzucono obsługę 3.7–3.9) i dodaje koła dla free-threaded (3.13t).23
Dependency Injection dla sesji bazy danych
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
@router.get("/users/{user_id}")
async def get_user(request: Request, user_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(404, "User not found")
return templates.TemplateResponse("pages/user.html", {
"request": request, "user": user
})
Zależność get_db zarządza cyklem życia sesji: otwiera sesję, przekazuje ją do route handlera, zatwierdza po powodzeniu i wycofuje w razie wyjątku. Każda operacja na bazie danych używa zapytań parametryzowanych — nigdy interpolacji ciągów znaków.
Integracja z Pydantic
Modele Pydantic walidują dane wejściowe na granicy API i serializują dane wyjściowe dla szablonów:
from pydantic import BaseModel, EmailStr
class ContactForm(BaseModel):
name: str
email: EmailStr
message: str
@router.post("/contact")
async def submit_contact(request: Request, form: ContactForm):
# form.name, form.email, form.message are validated
await send_email(form)
return templates.TemplateResponse("components/_contact_success.html", {
"request": request
})
Pydantic waliduje typy, formaty (email, URL) oraz ograniczenia (minimalna/maksymalna długość), zanim route handler zostanie wykonany. Nieprawidłowe dane wejściowe automatycznie zwracają odpowiedź 422. Zastępuje to biblioteki walidacji formularzy po stronie klienta — serwer waliduje dane, a HTMX podmienia albo komunikat sukcesu, albo informację zwrotną o błędzie.
Migracje z Alembic
Alembic zarządza zmianami schematu bazy danych:
# Generate a migration from model changes
alembic revision --autogenerate -m "add user preferences table"
# Apply migrations
alembic upgrade head
# Roll back one migration
alembic downgrade -1
Funkcja autogenerate porównuje modele SQLAlchemy z bieżącym schematem bazy danych i generuje skrypty migracji. Te skrypty to wersjonowane pliki Python, które znajdują się w repozytorium:
# alembic/versions/001_add_user_preferences.py
def upgrade():
op.create_table(
"user_preferences",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id")),
sa.Column("locale", sa.String(10), default="en"),
sa.Column("theme", sa.String(20), default="dark"),
)
def downgrade():
op.drop_table("user_preferences")
Migracje są uruchamiane podczas wdrożenia (przed startem aplikacji). Dzięki temu schemat bazy danych odpowiada kodowi aplikacji. W przypadku blakecrosley.com większość danych znajduje się w Cloudflare D1 (dostęp przez HTTP), dlatego migracje Alembic dotyczą lokalnej bazy SQLite lub PostgreSQL używanej do danych sesji i analityki.
Wzorzec Cloudflare D1
blakecrosley.com używa Cloudflare D1 jako zdalnej bazy danych dostępnej przez proxy Cloudflare Worker:
class D1Client:
"""HTTP client for Cloudflare D1 via Worker proxy."""
def __init__(self, worker_url: str, auth_secret: str):
self.worker_url = worker_url
self.auth_secret = auth_secret
async def fetch_all(self, sql: str, params: list = None) -> list[dict]:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.worker_url}/query",
json={"sql": sql, "params": params or []},
headers={"Authorization": f"Bearer {self.auth_secret}"},
)
return response.json()["results"]
Ten wzorzec sprawdza się w aplikacjach, które potrzebują bazy danych, ale nie chcą zarządzać serwerem bazy danych. D1 to SQLite na brzegu sieci Cloudflare, dostępne przez HTTP. Proxy Worker obsługuje uwierzytelnianie i ograniczanie liczby żądań. Kompromisem jest opóźnienie: każde zapytanie jest żądaniem HTTP (~50-100 ms), podczas gdy lokalne połączenie z bazą danych zajmuje ~1-5 ms. Pamięć podręczna w pamięci operacyjnej inicjalizowana przy starcie łagodzi ten problem w obciążeniach z przewagą odczytu, takich jak tłumaczenia.
Bezpieczeństwo
Middleware nagłówków bezpieczeństwa
blakecrosley.com implementuje wzmocnione nagłówki bezpieczeństwa przez niestandardowe middleware:
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
CSP_DIRECTIVES = {
"default-src": "'self'",
"script-src": "'self' 'unsafe-inline' 'unsafe-eval'",
"style-src": "'self' 'unsafe-inline'",
"img-src": "'self' data: https:",
"connect-src": "'self'",
"frame-ancestors": "'self'",
"base-uri": "'self'",
"form-action": "'self'",
"upgrade-insecure-requests": "",
}
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "SAMEORIGIN"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Strict-Transport-Security"] = (
"max-age=31536000; includeSubDomains"
)
response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
response.headers["Content-Security-Policy"] = self.csp
response.headers["Permissions-Policy"] = self.PERMISSIONS_POLICY
return response
CSP zawiera 'unsafe-inline' i 'unsafe-eval', ponieważ Alpine.js wymaga ich do ewaluacji wyrażeń. Alternatywą jest zgodna z CSP kompilacja Alpine.js, która ma ograniczenia.14 Każda inna funkcja jest zablokowana: frame-ancestors zapobiega clickjackingowi, form-action ogranicza przesyłanie formularzy do tego samego źródła, a upgrade-insecure-requests wymusza HTTPS.
Bezpieczne cache’owanie CDN z HTMX
Middleware nagłówków bezpieczeństwa dodaje Vary: HX-Request do odpowiedzi HTMX:
if request.headers.get("HX-Request"):
existing_vary = response.headers.get("Vary", "")
if "HX-Request" not in existing_vary:
parts = [v.strip() for v in existing_vary.split(",") if v.strip()]
parts.append("HX-Request")
response.headers["Vary"] = ", ".join(parts)
Bez tego nagłówka CDN mógłby zapisać w cache odpowiedź fragmentu HTMX i zwrócić ją jako pełną stronę dla żądania innego niż HTMX (albo odwrotnie). Nagłówek Vary informuje CDN, aby przechowywać osobne wpisy cache na podstawie wartości nagłówka HX-Request.11
Ochrona CSRF
Formularze HTMX używają bezstanowych tokenów CSRF podpisanych HMAC:
# csrf.py
def generate_csrf_token() -> str:
"""Token format: timestamp:random:HMAC-SHA256-signature"""
timestamp = str(int(time.time()))
random_value = secrets.token_hex(16)
payload = f"{timestamp}:{random_value}"
signature = hmac.new(
CSRF_SECRET.encode(), payload.encode(), hashlib.sha256
).hexdigest()
return f"{payload}:{signature}"
def validate_csrf_token(token: str) -> bool:
"""Verify signature and check expiration (1 hour)."""
timestamp, random_value, signature = token.split(":")
if int(time.time()) - int(timestamp) > 3600:
return False
expected = hmac.new(
CSRF_SECRET.encode(),
f"{timestamp}:{random_value}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Token jest generowany w szablonie przez globalny obiekt Jinja2 i dołączany do żądań formularzy HTMX:
<form hx-post="/contact" hx-target="#form-result">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<!-- form fields -->
</form>
Tokeny bezstanowe eliminują przechowywanie sesji po stronie serwera. Podpis HMAC gwarantuje, że token został wygenerowany przez serwer. Znacznik czasu zapobiega atakom replay. hmac.compare_digest zapobiega atakom timingowym.15
Sanityzacja HTML
Treści generowane przez użytkowników przechodzą przez nh3 przed renderowaniem:
templates.env.filters["sanitize"] = sanitize_html
# In templates: {{ content | sanitize }}
Biblioteka nh3 usuwa tagi i atrybuty spoza listy dozwolonych. Linki automatycznie otrzymują rel="noopener noreferrer". Ta ochrona jest niezależna od CSP — zapobiega stored XSS na warstwie renderowania, podczas gdy CSP zapobiega wstrzykniętym skryptom na warstwie przeglądarki. Obrona warstwowa.
Walidacja danych wejściowych
Modele Pydantic walidują wszystkie dane wejściowe na granicy API:
from pydantic import BaseModel, Field, EmailStr
class ContactRequest(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
email: EmailStr
message: str = Field(..., min_length=10, max_length=5000)
FastAPI automatycznie zwraca 422 Unprocessable Entity dla nieprawidłowych danych wejściowych. W połączeniu z parametryzowanymi zapytaniami do bazy danych (SQLAlchemy nigdy nie interpoluje stringów) zapobiega to SQL injection i zapewnia bezpieczeństwo typów na granicach.
Wydajność
Lighthouse 100/100/100/100
blakecrosley.com uzyskuje wynik 100 we wszystkich czterech kategoriach Lighthouse: Performance, Accessibility, Best Practices i SEO. Można to zweryfikować w PageSpeed Insights.2
Kluczowe optymalizacje:
Strategia ładowania CSS
blakecrosley.com ładuje CSS za pomocą jednego tagu <link> i adresów URL z hashem treści do niezmiennego cache’owania:
<link rel="stylesheet" href="{{ asset('css/styles.css') }}">
Helper asset() dodaje hash treści (?v=a3b2c1d4), dzięki czemu przeglądarka przechowuje plik w cache bezterminowo, dopóki treść się nie zmieni. Bez wyodrębniania krytycznego CSS, bez sztuczki z print-media, bez ładowania opartego na JavaScript. Plik CSS ma około 8 KB po gzip — wystarczająco mało, aby podejście z pojedynczym żądaniem uzyskiwało 100 punktów w Lighthouse Performance bez optymalizacyjnej gimnastyki.
Kompresja GZip
app.add_middleware(GZipMiddleware, minimum_size=500)
Odpowiedzi powyżej 500 bajtów są kompresowane. HTML kompresuje się o 70-80%, zmniejszając dokument 15 KB do 3-4 KB.
Niezmienne cache’owanie statycznych zasobów
# In security headers middleware
if request.url.path.startswith("/static/"):
if os.environ.get("RAILWAY_ENVIRONMENT"):
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
Zasoby statyczne z adresami URL zawierającymi hash treści (?v=a3f8b2c1d0) są cache’owane przez rok z immutable. Hash zmienia się, gdy zmienia się plik, co wymusza na przeglądarkach i CDN pobranie nowej wersji.
Odroczone ładowanie skryptów
<script defer src="{{ asset('js/vendor/alpine.min.js') }}"></script>
<script defer src="{{ asset('js/vendor/htmx.min.js') }}"></script>
<script defer src="{{ asset('js/main.js') }}"></script>
Atrybut defer pobiera skrypty równolegle z parsowaniem HTML, ale wykonuje je dopiero po sparsowaniu dokumentu. Zapobiega to blokowaniu renderowania bez złożoności ładowania async i zarządzania kolejnością wykonywania.
Optymalizacja obrazów
Obrazy używają WebP z responsywnym srcset i jawnymi wymiarami:
OPTIMIZED_IMAGES = {
"vision-sprint": {
"webp_srcset": (
"/static/images/optimized/vision-sprint-400w.webp 400w, "
"/static/images/optimized/vision-sprint-800w.webp 800w, "
"/static/images/optimized/vision-sprint-1200w.webp 1200w"
),
"fallback": "/static/images/optimized/vision-sprint-fallback.jpg",
"width": 1200,
"height": 1045,
},
}
<picture>
<source type="image/webp"
srcset="{{ image.webp_srcset }}"
sizes="(max-width: 768px) 100vw, 50vw">
<img src="{{ image.fallback }}"
width="{{ image.width }}"
height="{{ image.height }}"
alt="{{ image.alt }}"
loading="lazy">
</picture>
Jawne atrybuty width i height zapobiegają Cumulative Layout Shift (CLS). Atrybut loading="lazy" odracza obrazy poza ekranem. WebP zapewnia pliki o 25-35% mniejsze niż JPEG przy równoważnej jakości.16
Early Hints
# In main.py
app.state.preload_links = [
f'<{make_asset_url(_asset_map, "css/styles.css")}>; rel=preload; as=style',
]
# In security headers middleware
if "text/html" in content_type:
preload_links = getattr(request.app.state, "preload_links", [])
if preload_links:
response.headers["Link"] = ", ".join(preload_links)
Nagłówek Link z rel=preload informuje Cloudflare, aby wysłać odpowiedź 103 Early Hints, co pozwala przeglądarce rozpocząć pobieranie CSS, zanim serwer zakończy generowanie odpowiedzi HTML.17
Minimalny JavaScript
Całkowity rozmiar JavaScript:
| Biblioteka | Rozmiar (zminifikowany + gzip) |
|---|---|
| HTMX | ~16 KB |
| Alpine.js | ~15 KB |
| JS specyficzny dla strony | 4-8 KB |
| Razem | 35-39 KB |
Typowa aplikacja React wysyła 100-300 KB frameworkowego JavaScript przed kodem aplikacji.18 Podejście bez builda wysyła mniej JavaScript, ponieważ do wysłania jest mniej JavaScript.
Wdrożenie
Railway
Serwis blakecrosley.com jest wdrażany na platformę Railway za pomocą polecenia git push:
# railway.toml
[build]
builder = "nixpacks"
[deploy]
startCommand = "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}"
healthcheckPath = "/health"
healthcheckTimeout = 300
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10
Kreator Nixpacks platformy Railway wykrywa projekt Python na podstawie pliku requirements.txt, instaluje zależności i uruchamia polecenie startowe. Plik Docker nie jest wymagany. Punkt końcowy kontroli kondycji zapewnia, że aplikacja odpowiada, zanim zacznie przyjmować ruch:
@app.get("/health")
async def health():
return {"status": "healthy"}
Proces wdrażania
git push origin main
→ Railway detects push
→ Nixpacks installs Python + requirements.txt (cached)
→ uvicorn starts
→ Health check passes
→ Traffic routes to new deployment
→ ~40 seconds total
Bez npm install. Bez npm run build. Bez kompilacji przez webpack. Bez kompilacji TypeScript. Jedynym etapem instalacji jest pip install -r requirements.txt, którego wynik jest przechowywany w pamięci podręcznej między wdrożeniami.
Procfile
web: uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}
Plik Procfile stanowi alternatywę zgodną z Heroku. Railway obsługuje zarówno railway.toml, jak i Procfile. Składnia ${PORT:-8000} korzysta z portu udostępnionego przez platformę lub domyślnie z portu 8000 podczas programowania lokalnego.
Konfiguracja Uvicorn dla środowiska produkcyjnego
W przypadku wdrożeń obsługujących większy ruch należy użyć wielu procesów roboczych:
uvicorn app.main:app \
--host 0.0.0.0 \
--port ${PORT:-8000} \
--workers 4 \
--loop uvloop \
--http httptools
--workers 4uruchamia cztery procesy robocze (ogólna zasada: 2 * liczba rdzeni CPU + 1)--loop uvloopkorzysta z szybszej pętli zdarzeń uvloop (bezpośredniego zamiennika asyncio)--http httptoolskorzysta z szybszego parsera HTTP httptools
Każdy proces roboczy jest osobnym procesem przechowującym własną kopię aplikacji, dlatego zużycie pamięci przez proces jest mnożone przez liczbę procesów roboczych — i właśnie tutaj poprawka grafu zależności w FastAPI 0.140.0 przynosi korzyści: w aplikacji z dużą liczbą zależności cztery procesy robocze w wersji 0.139.2 czterokrotnie ponoszą wcześniejszy narzut związany z Dependant.27
Podczas programowania opcja --reload monitoruje zmiany w plikach:
uvicorn app.main:app --reload --port 8000
Alternatywa z Docker
Na platformach wymagających Docker:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Odchudzony obraz bazowy pozwala zachować niewielki rozmiar kontenera. Opcja --no-cache-dir zapobiega zapisywaniu przez pip pobranych pakietów w warstwie obrazu.
CDN Cloudflare
Serwis blakecrosley.com korzysta z Cloudflare do buforowania CDN, obsługi DNS i Workers:
# Cache headers for HTML pages (set in security middleware)
response.headers["Cache-Control"] = (
"public, max-age=300, s-maxage=3600, "
"stale-while-revalidate=86400"
)
max-age=300— przeglądarka przechowuje dane w pamięci podręcznej przez 5 minuts-maxage=3600— CDN przechowuje dane w pamięci podręcznej przez 1 godzinęstale-while-revalidate=86400— nieaktualna zawartość jest udostępniana podczas jej ponownej walidacji przez 24 godziny
Zasoby statyczne otrzymują ustawienie max-age=31536000, immutable, ponieważ adresy URL z hashem zawartości gwarantują aktualność.
Ramy decyzyjne
Czy potrzebne są narzędzia do budowania?
Należy odpowiedzieć na cztery pytania:
1. Czy więcej niż pięciu programistów współdzieli interfejsy JavaScript? Jeśli tak, sprawdzanie typów podczas kompilacji zapewniane przez TypeScript zapobiega błędom integracji, które testy wykonywane w czasie działania wykrywają zbyt późno. Należy dodać etap budowania.
2. Czy aplikacja zarządza złożonym stanem po stronie klienta? Jeśli przeciąganie i upuszczanie, współpraca w czasie rzeczywistym lub obsługa danych w pierwszej kolejności offline są podstawowymi funkcjami, a nie jedynie dodatkami, złożoność frameworka takiego jak React lub Svelte jest uzasadniona. Należy dodać etap budowania.
3. Czy wiele produktów korzysta ze wspólnej biblioteki komponentów? Jeśli tak, biblioteka ta wymaga pakowania za pomocą npm, wersjonowania semantycznego i tree shaking. Należy dodać etap budowania.
4. Czy aplikacja zależy od bibliotek ekosystemu npm, które zakładają użycie bundlera? Jeśli Radix, Framer Motion, TanStack Query lub podobne biblioteki stanowią podstawę produktu, proces budowania jest niezbędny.
Jeśli odpowiedź na wszystkie cztery pytania brzmi „nie”, podejście bez etapu budowania jest dobrym rozwiązaniem. Jeśli choć jedna odpowiedź brzmi „tak”, narzędzia do budowania rozwiązują rzeczywisty problem. Błędem jest dodawanie ich, gdy wszystkie cztery odpowiedzi brzmią „nie” — oznacza to rozwiązywanie nieistniejących problemów przy jednoczesnym tworzeniu rzeczywistego narzutu związanego z zarządzaniem zależnościami.1
Porównanie stosów technologicznych
| Kategoria | Bez etapu budowania (ten przewodnik) | React + narzędzia do budowania |
|---|---|---|
| Najlepsze zastosowanie | Serwisy z treścią, portfolio, narzędzia wewnętrzne, aplikacje CRUD | Produkty SaaS, złożone SPA, rozwiązania korzystające z systemów projektowych |
| Wielkość zespołu | 1–5 programistów | 5–50+ programistów |
| Zarządzanie stanem | Serwer (HTMX) + klient (Alpine.js) | Klient (stan React, Redux, Zustand) |
| Bezpieczeństwo typów | W czasie działania (Pydantic po stronie serwera) | Podczas kompilacji (TypeScript) |
| Ponowne wykorzystanie komponentów | Dołączane fragmenty + makra Jinja2 | Pakiety npm, współdzielone biblioteki |
| SEO | Domyślnie renderowane po stronie serwera | Wymaga konfiguracji SSR/SSG |
| Minimalna wydajność | Wysoka (minimum JS, renderowanie po stronie serwera) | Zmienna (narzut frameworka) |
| Górna granica złożoności | Niższa (brak trybu offline i rozbudowanego stanu klienta) | Wyższa (możliwa dowolna interakcja po stronie klienta) |
| Zależności | 17 pakietów Python | Ponad 300 pakietów npm |
| Czas budowania | 0 sekund | 15–60 sekund |
Kiedy HTMX nie jest właściwym rozwiązaniem
HTMX zastępuje stan klienta komunikacją z serwerem. Działa to, dopóki opóźnienie nie zaczyna mieć znaczenia:
- Interfejsy z funkcją przeciągania i upuszczania — 200 ms komunikacji z serwerem dla każdego zdarzenia przeciągania jest nieakceptowalne
- Współpraca w czasie rzeczywistym — stan obsługiwany przez WebSocket wymaga rozwiązywania konfliktów po stronie klienta
- Aplikacje działające w pierwszej kolejności offline — bez serwera nie ma HTMX
- Złożone animacje powiązane ze stanem — Framer Motion i React Spring zakładają model uzgadniania React
- Aplikacje Canvas/WebGL — pętla renderowania z natury działa po stronie klienta
W tych zastosowaniach framework działający po stronie klienta jest właściwym narzędziem. Podejście bez etapu budowania nie próbuje go zastępować.
Skrócona karta referencyjna
FastAPI
# Development
source venv/bin/activate
uvicorn app.main:app --reload --port 8000
# Production
uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}
# Testing
python -m pytest -v --cov=app
# Database migrations
alembic upgrade head
alembic revision --autogenerate -m "description"
Atrybuty HTMX
hx-get="/url" <!-- GET request -->
hx-post="/url" <!-- POST request -->
hx-target="#element" <!-- Where to put response -->
hx-swap="innerHTML" <!-- How to insert (innerHTML, outerHTML, beforeend) -->
hx-trigger="click" <!-- What triggers request -->
hx-trigger="keyup changed delay:300ms" <!-- Debounced input -->
hx-trigger="load" <!-- Fire on element load -->
hx-indicator="#spinner" <!-- Show during request -->
hx-push-url="true" <!-- Update browser URL -->
hx-replace-url="true" <!-- Replace URL (no history) -->
Atrybuty Alpine.js
x-data="{ open: false }" <!-- Component scope + state -->
x-show="open" <!-- Toggle visibility -->
x-cloak <!-- Hide until Alpine inits -->
@click="open = !open" <!-- Event handler -->
@click.away="open = false" <!-- Outside click -->
@keydown.escape="open = false" <!-- Keyboard event -->
:class="{ 'active': open }" <!-- Dynamic class -->
:aria-expanded="open" <!-- Dynamic attribute -->
x-text="count" <!-- Dynamic text content -->
x-init="fetchData()" <!-- Run on init -->
Właściwości niestandardowe CSS
:root {
--color-bg: #000000;
--color-text: #ffffff;
--spacing-sm: 1rem;
--spacing-md: 1.5rem;
--font-size-lg: 1.25rem;
}
@media (max-width: 768px) {
:root { --gutter: 24px; }
}
Nagłówki bezpieczeństwa
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Cross-Origin-Opener-Policy: same-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Lista kontrolna konfiguracji projektu
[ ] FastAPI app with Jinja2Templates
[ ] Security headers middleware (CSP, HSTS, X-Frame-Options)
[ ] CSRF token generation and validation
[ ] GZip middleware (minimum_size=500)
[ ] Content-hash asset versioning (cache busting)
[ ] HTMX self-hosted in /static/js/vendor/
[ ] Alpine.js self-hosted in /static/js/vendor/
[ ] CSS custom properties for design tokens
[ ] Health check endpoint (/health)
[ ] Error handlers (404, 500)
[ ] robots.txt, sitemap.xml, llms.txt
[ ] JSON-LD structured data in base template
[ ] Hreflang tags for i18n (if multi-language)
[ ] HTML sanitization filter (nh3)
[ ] Rate limiting middleware
[ ] Deferred script loading
Często zadawane pytania
Czy HTMX nadaje się do użycia produkcyjnego w rzeczywistych aplikacjach internetowych?
Tak. HTMX pozostaje stabilny od 2020 roku i jest używany produkcyjnie w wielu branżach. Carson Gross, twórca biblioteki, traktuje zgodność wsteczną jako jedną z podstawowych zasad projektowych — dokumentacja HTMX stanowi, że biblioteka nie spowoduje niezgodności z istniejącymi aplikacjami w ramach tej samej wersji głównej.19 Biblioteka zajmuje około 16 KB po minifikacji i kompresji gzip, nie ma żadnych zależności i stosuje wersjonowanie semantyczne. Serwis blakecrosley.com korzysta z HTMX produkcyjnie od 3 lat i w tym czasie nie wystąpił żaden błąd związany z HTMX.20
Czy można używać TypeScript bez etapu kompilacji?
Częściowo. Pliki TypeScript można sprawdzać pod kątem typów za pomocą tsc --noEmit bez generowania plików wyjściowych, dzięki czemu kontrola na etapie kompilacji działa podobnie do lintera. Przeglądarki nie mogą jednak bezpośrednio wykonywać plików .ts, dlatego do udostępniania TypeScript nadal potrzebny jest etap kompilacji. Alternatywą są adnotacje typów JSDoc w zwykłych plikach .js, które TypeScript może sprawdzać bez kompilacji. Zapewnia to bezpieczeństwo typów podczas programowania, a jednocześnie pozwala dostarczać standardowy JavaScript.
Jak to podejście wypada na tle Astro lub 11ty?
Astro i 11ty to generatory witryn statycznych, które tworzą zwykły HTML z minimalną ilością JavaScript po stronie klienta, ale wymagają etapu kompilacji (Node.js, npm install i polecenia kompilacji). Podejście bez kompilacji eliminuje ten etap — serwer renderuje HTML przy każdym żądaniu. Wiąże się to z kompromisem: Astro/11ty tworzą szybsze strony statyczne (bez obliczeń po stronie serwera), natomiast FastAPI + HTMX natywnie obsługuje treści dynamiczne (dane właściwe dla użytkownika, przesyłanie formularzy i aktualizacje w czasie rzeczywistym) bez osobnej warstwy API.
A co z renderowaniem po stronie serwera (SSR) w React?
SSR w Next.js oraz podejście FastAPI + HTMX mają wspólny cel: przesyłanie do przeglądarki kodu HTML wyrenderowanego po stronie serwera. Różnica polega na tym, co dzieje się po początkowym renderowaniu. Next.js przeprowadza hydratację strony za pomocą React, dostarczając klientowi środowisko uruchomieniowe frameworka i kod komponentów. FastAPI + HTMX nie przeprowadza hydratacji — HTML stanowi ostateczny wynik. HTMX obsługuje kolejne interakcje, żądając od serwera nowych fragmentów HTML. W efekcie FastAPI + HTMX dostarcza łącznie około 35–40 KB kodu JavaScript, podczas gdy aplikacja Next.js — od 100 do 300 KB.18
Jak obsługiwać walidację formularzy w tym stosie?
Po stronie serwera. Pydantic sprawdza dane wejściowe po przesłaniu formularza. Jeśli walidacja zakończy się niepowodzeniem, serwer zwraca formularz z komunikatami o błędach. HTMX podmienia odpowiedź w DOM:
<form hx-post="/contact" hx-target="#form-container" hx-swap="outerHTML">
<input type="email" name="email" required>
<button type="submit">Send</button>
</form>
@router.post("/contact")
async def contact(request: Request, email: str = Form(...)):
if not validate_email(email):
return templates.TemplateResponse("components/_contact_form.html", {
"request": request,
"error": "Please enter a valid email address",
"email": email, # Preserve input
})
await send_email(email)
return templates.TemplateResponse("components/_contact_success.html", {
"request": request
})
Serwer przeprowadza walidację i renderuje stany błędów, a HTMX podmienia wynik. Biblioteka do walidacji po stronie klienta nie jest potrzebna. Atrybut HTML required zapewnia podstawową walidację na poziomie przeglądarki jako pierwszą linię ochrony.
Czy można dodać funkcje czasu rzeczywistego (WebSockets)?
Tak. FastAPI ma wbudowaną obsługę WebSocket:
from fastapi import WebSocket
@app.websocket("/ws/notifications")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
data = await get_notification()
await websocket.send_text(render_notification_html(data))
HTMX oferuje rozszerzenie WebSocket (hx-ws), które łączy elementy z punktami końcowymi WebSocket:
<!-- HTMX 2.x WebSocket extension syntax -->
<div hx-ext="ws" ws-connect="/ws/notifications">
<div id="notifications" ws-send></div>
</div>
Uwaga: HTMX 1.x korzystał ze składni
hx-ws="connect:...". W HTMX 2.x obsługę WebSocket przeniesiono do osobnego rozszerzenia (htmx-ext-ws) z przedstawionymi powyżej atrybutamiws-connectiws-send. W przypadku korzystania z HTMX 1.x stara składniahx-wsnadal działa.Wersja beta HTMX 4.0: htmx 4.0.0-beta6 jest już dostępny w npm pod tagiem
next, podobnie jak dokumentacja wersji 4.0 (beta6 opublikowano 23 lipca 2026), podczas gdy szybki start na htmx.org i tag npmlatestnadal dotyczą wersji 2.0.10. Ten przewodnik wciąż jest przeznaczony dla HTMX 2.x, który pozostaje zalecaną wersją do zastosowań produkcyjnych do czasu ustabilizowania się wersji 4.0; migracja z 2.x do 4.x stanowi zmianę generacyjną, a nie aktualizację punktową wersji 2.x. Schemat wersjonowania big-skies-software pomija nieparzyste wersje główne, dlatego 4.0 jest kolejnym krokiem po 2.x.2122Co warto obserwować w dokumentacji wersji 4.0. Przed wydaniem 4.0 GA na szczególną uwagę podczas przeglądu bezpieczeństwa i architektury zasługują 2 nowości: nowe rozszerzenie
hx-livewprowadza reaktywne względem DOM wyrażenia, które są ponownie obliczane po zmianie stanu, do którego się odwołują, natomiast nowe rozszerzeniehx-nonceuzależnia przetwarzanie atrybutów htmx od nonce CSP. Przewodnik migracji do wersji 4.0 przenosi także kilka koncepcji konfiguracyjnych, przywraca lub zmienia część zachowań związanych ze zdarzeniami i historią oraz usuwa z rdzenia niektóre funkcje pomocnicze JavaScript. Wersję 4.0 należy traktować jako projekt migracyjny, a nie bezpośrednią aktualizację 2.x.21
Wiadomości z serwera są podmieniane w DOM przy użyciu tych samych mechanizmów wskazywania celu i podmiany co odpowiedzi HTTP. Serwer przesyła fragmenty HTML przez WebSocket, a HTMX je wstawia.
Jak ten stos obsługuje SEO?
Renderowany po stronie serwera HTML jest z natury przyjazny dla SEO, ponieważ roboty indeksujące otrzymują pełną treść strony bez wykonywania JavaScript. Serwis blakecrosley.com dodaje kilka warstw SEO:
- Dane strukturalne JSON-LD w
<head>każdej strony (schematy Person, Article, WebSite i FAQPage) - Dynamiczna mapa witryny z alternatywnymi adresami hreflang dla wszystkich 10 wersji językowych
- Kanał RSS pod adresem
/blog/feed.xml llms.txtw katalogu głównym, ułatwiający robotom AI wykrywanie treści- Kanoniczne adresy URL i tagi Open Graph w szablonie bazowym
- Semantyczny HTML:
<article>,<section>,<main>oraz prawidłowa hierarchia nagłówków
Konfiguracja SSR nie jest potrzebna. Nie ma getStaticProps. Nie ma ISR. HTML jest renderowany przy każdym żądaniu — to zachowanie domyślne, a nie optymalizacja.
Jak wygląda krzywa uczenia się w porównaniu z React?
Dla programistów Python krzywa uczenia się jest znacznie łagodniejsza. Język jest już znany. Procedury obsługi tras w FastAPI zwracają odpowiedzi z szablonów — to ten sam model mentalny co widoki Flask lub Django. HTMX dodaje kilka atrybutów HTML (hx-get, hx-target, hx-swap). Alpine.js dodaje jeszcze kilka (x-data, x-show, @click). Nie trzeba poznawać JSX, wirtualnego DOM, systemu hooków, biblioteki do zarządzania stanem ani konfiguracji narzędzi do kompilacji.
Dokumentacja HTMX mieści się na jednej długiej stronie. Dokumentacja Alpine.js zajmuje kilka stron. Dokumentacja React obejmuje setki stron poświęconych hookom, kontekstowi, referencjom, efektom, Suspense, komponentom serwerowym i strumieniowemu SSR.
Dla programistów JavaScript/React zmiana ma charakter bardziej koncepcyjny niż składniowy. Kluczowe jest zrozumienie, że stan należy do serwera i to serwer renderuje HTML. Zarządzanie stanem po stronie klienta staje się obsługą tras po stronie serwera. Pobieranie danych po stronie klienta zastępują atrybuty HTMX w elementach HTML. Składnia jest prostsza — model mentalny wymaga jednak odejścia od założenia właściwego aplikacjom SPA, że za renderowanie odpowiada klient.
Historia zmian
| Data | Zmiana | Źródło |
|---|---|---|
| 2026-07-29 | FastAPI 0.141.0 + 0.141.1 (obie wersje z 29 lipca). W wersji 0.141.0 dodano app.frontend(check_dir="auto"), dzięki czemu fastapi dev nie kończy się już niepowodzeniem, gdy katalog kompilacji nie istnieje — co zwykle ma miejsce przy uruchamianiu serwera przed wykonaniem kompilacji frontendu. Wydana kilka godzin później wersja 0.141.1 naprawia odrzucanie zadań w tle i nagłówków odpowiedzi przez zależności w app.frontend(); jeśli zależność ustawiała plik cookie lub planowała BackgroundTask, operacje te były pomijane podczas montowania frontendu, choć działały prawidłowo na trasach API. Usuwa to rzeczywistą lukę w obsłudze zależności dodanej w wersji 0.139.0. Obie zmiany opisano w istniejącym omówieniu app.frontend(), zamiast tworzyć nową sekcję, ponieważ prezentowane w tym przewodniku podejście z renderowaniem po stronie serwera nie montuje katalogu dist/. Wersja 0.141.1 dokumentuje także FASTAPI_ENV w przewodniku CLI dotyczącym FastAPI (zmiana wyłącznie w dokumentacji, bez zmian w treści głównej). |
28 |
| 2026-07-27 | 27 lipca w ciągu pięciu i pół godziny wydano wersje FastAPI od 0.140.1 do 0.140.7 — łącznie 7 wydań, z których wszystkie refaktoryzują mechanizm zależności zapoczątkowany w wersji 0.140.0. Zmiany obejmują 2 obszary: usunięto spłaszczoną kopię grafu zależności, którą FastAPI wcześniej tworzył i przechowywał (0.140.2), wraz ze wszystkimi pozostałymi miejscami, które ją odtwarzały — generowaniem OpenAPI (0.140.3, 0.140.7), polami treści żądania (0.140.5) i parametrami żądania (0.140.6) — natomiast wersja 0.140.4 usuwa nieużywane mechanizmy ewidencjonowania powtórzeń. Jedyną zmianą z zauważalnym progiem jest 0.140.1: rozmiar lru_cache w funkcjach pomocniczych klasyfikujących wywoływalne obiekty w fastapi/dependencies/models.py zwiększono z 1 024 do 4 096 wpisów (stała o nazwie _CALLABLE_CLASSIFICATION_CACHE_SIZE). Zmianę wprowadzono po zgłoszeniach dotyczących aplikacji z ponad 1 024 odrębnymi zależnościami, które powodowały ciągłą wymianę zawartości pamięci podręcznej. Bez zmian w API; zalecenie w akapicie dotyczącym pamięci zajmowanej przez zależności zaktualizowano z wersji 0.140.0 do 0.140.7 lub nowszej, zaznaczając, że ta seria wciąż się rozwija, a testy wydajności zależności OpenAPI (PR #16075) trafiły dopiero do jej ostatniego wydania. |
29 |
| 2026-07-25 | FastAPI 0.140.0 (24 lipca, 21:16 UTC) naprawia regresję wykorzystania pamięci przez system zależności, obecną od wersji 0.121.0 (3 listopada 2025). PR #16049 usuwa z Dependant 10 atrybutów functools.cached_property, przenosi je do funkcji pomocniczych na poziomie modułu i przekształca klasę w @dataclass(slots=True); oficjalny pomiar CodSpeed dla scalonego PR wskazuje spadek zużycia pamięci w teście test_dependency_graph z 17,5 MB do 1,1 MB (16-krotnie), a pierwotne zgłoszenie opisywało błędy OOM w środowisku produkcyjnym przy wersji 0.121.3, podczas gdy wersja 0.120.4 utrzymywała zużycie poniżej około 400 MB. Dodano fragment dotyczący wersji 0.140.0 do sekcji poświęconej wzorcom async oraz wzmiankę o pamięci procesów roboczych do konfiguracji produkcyjnej Uvicorn. Poprawiono także istniejący wcześniej błąd: w przewodniku twierdzono, że wersja 0.137.0 „przypina Starlette do linii 1.x” — tak nie jest. Wymaganiem uruchomieniowym FastAPI jest starlette>=0.46.0 (dolna granica bez górnego limitu, nadal spełniana przez Starlette 0.4x), jednakowa w wersjach 0.136.3, 0.137.0, 0.138.0, 0.139.2 i 0.140.0; numery 1.x w informacjach o wydaniu 0.137.0 dotyczą aktualizacji dependabot w pliku blokady testów repozytorium (PR #15722 modyfikuje wyłącznie uv.lock). Poprawiono zarówno twierdzenie w treści, jak i 24. Wskazano również 2 elementy, które nie powodują zmian w przewodniku: wewnętrzne elementy Dependant wprowadzają teraz niezgodności w narzędziach (oauth_scopes, cache_key, _uses_scopes i _is_security_scheme usunięto jako atrybuty i zastąpiono funkcjami modułu _get_oauth_scopes() / _get_cache_key() / _uses_scopes(), a slots=True uniemożliwia dynamiczne modyfikowanie instancji) — jest to nieudokumentowane wewnętrzne API, do którego ten przewodnik nigdy się nie odwołuje, a więc ta sama kategoria co zmiana router.routes w wersji 0.137.0; ponadto oficjalna dokumentacja FastAPI domyślnie korzysta teraz z projektów uv zamiast pip/venv w 30 plikach, w tym README, index.md, virtual-environments.md oraz na stronach Docker/wdrażania (PR #16032, scalony 21 lipca). Zmiana w dokumentacji nie wpływa na kod, jednak przewodnik konsekwentnie używa pip install -r requirements.txt, przez co odbiega teraz od podstawowej ścieżki prezentowanej w dokumentacji źródłowej — to przyszła decyzja redakcyjna, celowo niepodjęta podczas tej aktualizacji. |
27 |
| 2026-07-24 | htmx 4.0.0-beta6 zastępuje beta5 jako znacznik npm next (opublikowano 23 lipca 2026; wydanie GitHub tego samego dnia). Najważniejsze elementy wersji beta: nowe rozszerzenie hx-multipart (strumieniowane odpowiedzi multipart/mixed/multipart/parallel z nagłówkami akcji HX-* dla każdej części), przywracanie pozycji przewijania z historii za pomocą Navigation API z mechanizmem zapasowym dla Firefoksa, wewnętrzna dla wersji beta zmiana nazwy zdarzenia htmx:swap:finally → htmx:finally:swap, wyzwalanie zdarzeń z nagłówka odpowiedzi HX-Trigger po wymianie, niestandardowe metody żądań oraz przepisana obsługa hx-ws z przekazywaniem protocols. Zalecenie pozostaje bez zmian — w środowisku produkcyjnym należy pozostać przy HTMX 2.x (latest = 2.0.10) aż do stabilnego wydania 4.0; zmiana nazwy powoduje niezgodność wyłącznie w obrębie linii beta 4.0. Zaktualizowano uwagę dotyczącą wersji beta oraz 21. Potwierdzono brak zmian w FastAPI 0.139.2, Uvicorn 0.51.0, Alpine.js 3.15.12, Starlette 1.3.1 i Jinja2 3.1.6; w tym okresie nie opublikowano żadnych ostrzeżeń dotyczących bezpieczeństwa. |
|
| 2026-07-17 | FastAPI 0.139.1 + 0.139.2 (16 lipca): poprawiono obsługę ścieżek zawierających kropki w mechanizmach zapasowych app.frontend() (/users/john.doe, PR #16011) oraz zapewniono bezpieczeństwo wątkowe podczas budowania tras routera w testach wielowątkowych (PR #16013) — bez zmian w API widocznych dla aplikacji. Uvicorn 0.49.0 → 0.51.0: starsza implementacja websockets jest przestarzała, a opcja auto domyślnie wybiera teraz websockets-sansio (0.50.0); domyślna implementacja wymaga websockets>=13.0 (0.50.2), natomiast wersja 0.51.0 (8 lipca) dodaje ponowne uruchamianie procesów roboczych sygnałem SIGHUP z nakładaniem czasu ich działania, co pozwala przeładowywać usługę niemal bez przestojów; repozytorium znajduje się teraz pod adresem Kludex/uvicorn. Potwierdzono brak zmian w HTMX (2.0.10 / 4.0.0-beta5 next), Alpine.js 3.15.12, Starlette 1.3.1, Pydantic 2.13.4, SQLAlchemy 2.0.51 i Bootstrap 5.3.8; w tym okresie nie opublikowano żadnych ostrzeżeń dotyczących bezpieczeństwa. |
|
| 2026-07-07 | htmx 4.0.0-beta5 jest teraz znacznikiem npm next (opublikowano 26 czerwca 2026), zastępując beta4; odpowiednio zaktualizowano uwagę dotyczącą wersji beta HTMX 4.0 oraz [^22]. Zalecenie pozostaje bez zmian — w środowisku produkcyjnym należy pozostać przy HTMX 2.x (latest = 2.0.10) aż do stabilnego wydania 4.0. Zweryfikowano na podstawie znaczników dystrybucji npm htmx.org. |
|
| 2026-07-02 | FastAPI 0.139.0 (1 lipca). app.frontend() obsługuje teraz zależności — na przykład automatyczne uwierzytelnianie za pomocą plików cookie dla obsługiwanego frontendu (PR #15908) — rozszerzając montowanie statycznego frontendu z wersji 0.138.0 o standardowy mechanizm Depends(); nadal jest to niezależne od prezentowanego w przewodniku podejścia z renderowaniem po stronie serwera, co zaznaczono w tym samym akapicie porównawczym. Bez innych zmian w stosie: HTMX 2.0.10, Alpine.js 3.15.12, Bootstrap 5.3.8 i SQLAlchemy 2.0.51 pozostają bez zmian. |
26 |
| 2026-06-22 | FastAPI 0.138.0 + 0.137.2. Wersja 0.138.0 (20 czerwca) dodaje app.frontend("/", directory="dist") / router.frontend(...) do obsługi skompilowanego statycznego frontendu (wynik SPA w katalogu dist/) — rozwiązanie niezależne od prezentowanego w przewodniku podejścia bez kompilacji, z renderowaniem po stronie serwera, wskazane dla porównania w sekcji poświęconej wzorcom async. Wersja 0.137.2 (18 czerwca) dodaje iter_route_contexts() jako obsługiwany sposób wyliczania tras, odkąd router.routes jest elementem wewnętrznym (od wersji 0.137.0). Obie wersje dodają funkcje i nie wprowadzają zmian niezgodnych wstecz; Starlette (1.3.1), Pydantic (2.13.4), HTMX (2.0.10), Alpine.js (3.15.12), Bootstrap (5.3.8) i SQLAlchemy (2.0.51) pozostają bez zmian. |
25 |
| 2026-06-16 | FastAPI 0.137.0/0.137.1 + Starlette 1.0→1.3.1. Wersja FastAPI 0.137.0 (14 czerwca) refaktoryzuje wewnętrzne elementy routera: router.routes jest teraz wewnętrznym drzewem, a nie płaską listą APIRoute (co powoduje niezgodność ze wszystkim, co po niej iteruje), jednocześnie umożliwiając dodawanie tras po wywołaniu include_router() i wprowadzając nowe mechanizmy APIRouter.matches()/.handle(); wersja 0.137.1 (15 czerwca) poprawia typowanie APIRoute oraz routery z pustą ścieżką i bez prefiksu. Pierwszą stabilną wersję Starlette 1.0 wydano 22 marca, a obecnie dostępna jest wersja 1.3.1 (12 czerwca), która usuwa przestarzałe mechanizmy on_event/on_startup/on_shutdown oraz dekoratory @app.route()/@app.websocket_route() — jedynymi rozwiązaniami pozostają lifespan i Route/WebSocketRoute. (Pierwotnie w tym wpisie podano, że FastAPI 0.137.0 przypina Starlette do wersji 1.3.1 — poprawiono 2026-07-25: tak nie jest; wymaganiem uruchomieniowym jest starlette>=0.46.0 bez górnego limitu). Do sekcji poświęconej wzorcom async dodano uwagę dotyczącą cyklu życia aplikacji i routera. SQLAlchemy 2.0.51 (15 czerwca) zawiera wyłącznie poprawki błędów. |
24 |
| 2026-06-08 | Zmiana instalacji async w SQLAlchemy 2.0.50. Od SQLAlchemy 2.0.50 zależność greenlet stosu async nie jest już domyślnie instalowana — należy zainstalować wariant sqlalchemy[asyncio] (w przeciwnym razie pierwsze użycie await z silnikiem zakończy się błędem braku greenlet). Wersja 2.0.50 wymaga także Python 3.10 lub nowszego (zrezygnowano z wersji 3.7–3.9) i dodaje pakiety wheel dla pozbawionej GIL wersji 3.13t. Do sekcji dotyczącej SQLAlchemy 2.0 Async dodano uwagę o instalacji. Bez zmian w treści dotyczącej pozostałej części stosu: najnowszą wersją FastAPI nadal jest 0.136.3 (2026-05-23, brak wydania czerwcowego), stabilną wersją htmx pozostaje 2.0.10 (4.0.0-beta4 „The Fetchening” jest w fazie beta, a stabilne wydanie planowane jest wstępnie na początek 2027 roku, dlatego nie jest jeszcze zalecane do zastosowań produkcyjnych), Alpine.js pozostaje w wersji 3.15.12, a Bootstrap w linii 5.3.x. Zalecenie produkcyjne pozostaje bez zmian: HTMX 2.x aż do stabilnego wydania 4.0.23 |
|
| 2026-05-24 | Kontrola konserwacyjna: lokalny spis treści nadal obejmuje 210 wpisów na blogu, 11 głównych przewodników, 48 studiów projektowych oraz 10 obsługiwanych ustawień regionalnych, w tym angielski. Najnowszą wersją FastAPI jest 0.136.3 (2026-05-23); jedyna refaktoryzacja widoczna dla aplikacji, wymieniona w informacjach o wydaniu, dotyczy bardziej rygorystycznej obsługi podkreśleń w nagłówkach przy convert_underscores=True, natomiast wersja 0.136.2 waliduje pola zdarzeń Server-Sent Event, aby zapobiegać uszkodzeniu danych zdarzeń. Stabilną wersją htmx pozostaje 2.0.10, podczas gdy npm next i dokumentacja 4.0 wskazują teraz wersję 4.0.0-beta4; najnowszą wersją SQLAlchemy 2.0 jest 2.0.50, a Pydantic — nadal 2.13.4. Zalecenie produkcyjne pozostaje bez zmian: należy używać HTMX 2.x, dopóki wersja 4.0 nie stanie się stabilna.122 |
|
| 2026-05-18 | Aktualizacja spisu witryny: lokalny spis treści obejmuje teraz 210 wpisów na blogu, 11 głównych przewodników, 48 studiów projektowych oraz 10 obsługiwanych ustawień regionalnych, w tym angielski. Najnowszą wersją FastAPI pozostaje 0.136.1; stabilną wersją htmx nadal jest 2.0.10, a npm next wskazuje 4.0.0-beta3; najnowszą wersją npm Alpine.js pozostaje 3.15.12. Zalecenie produkcyjne pozostaje bez zmian: należy używać HTMX 2.x, dopóki wersja 4.0 nie stanie się stabilna.12021 |
|
| 2026-05-15 | Kontrola konserwacyjna: najnowszą wersją FastAPI pozostaje 0.136.1; w lokalnym środowisku tej witryny importowane są FastAPI 0.128.0 i Starlette 0.50.0; stabilną wersją htmx nadal jest 2.0.10, a npm next wskazuje obecnie 4.0.0-beta3; najnowszą wersją npm Alpine.js jest 3.15.12; najnowszą wersją Bootstrap jest 5.3.8; najnowszą wersją SQLAlchemy 2.0 jest 2.0.49; najnowszą wersją Pydantic jest 2.13.4. Zalecenie produkcyjne pozostaje bez zmian: należy używać HTMX 2.x, dopóki wersja 4.0 nie stanie się stabilna.2021 |
|
| 2026-05-09 | Śledzenie htmx 4.0.0-beta3 (8 maja 2026): htmx 4.0.0-beta3 jest dostępny pod znacznikiem npm next oraz w dokumentacji 4.0, podczas gdy npm latest nadal wskazuje wersję 2.0.10. Elementy warte obserwowania przed wydaniem GA: nowe rozszerzenie hx-live (reaktywne wyrażenia DOM), nowe rozszerzenie hx-nonce (ochrona atrybutów htmx za pomocą nonce CSP) oraz zmiany w przewodniku migracji dotyczące konfiguracji, historii, zdarzeń i podstawowych funkcji pomocniczych JavaScript. Zalecenie produkcyjne pozostaje bez zmian: htmx 2.x nadal jest wskazywany przez najnowszy znacznik npm i pozostaje zalecaną wersją aż do wydania 4.0 GA.21 |
|
| 2026-05-07 | Kontrola konserwacyjna: najnowszą wersją FastAPI pozostaje 0.136.1; stabilną wersją htmx jest 2.0.10, a wersja 4 nadal znajduje się w fazie beta z planowanym terminem wydania latem 2026; najnowszą wersją npm Alpine.js jest 3.15.12; najnowszą wersją Bootstrap jest 5.3.8; najnowszą wersją SQLAlchemy 2.0 jest 2.0.49; najnowszą wersją Pydantic jest 2.13.4. Lokalne wskaźniki witryny zaktualizowano do 182 wpisów na blogu, 11 przewodników, 10 obsługiwanych ustawień regionalnych i 17 wymagań Python. Zalecenia migracyjne pozostają bez zmian: w środowisku produkcyjnym należy używać HTMX 2.x, dopóki wersja 4.0 nie stanie się stabilna.20 | |
| 2026-04-25 | FastAPI 0.136.1 (23 kwietnia 2026): uporządkowanie elementów przestarzałych w Pydantic v2 (bez zmian zachowania kodu aplikacji). Śledzenie harmonogramu HTMX 4.0: wydano htmx 4.0.0-beta1 (6 kwietnia) i 4.0.0-beta2 (14 kwietnia). Zalecenia migracyjne pozostają bez zmian — htmx 2.x pozostaje pod najnowszym znacznikiem npm aż do ustabilizowania wersji 4.0; poprawki bezpieczeństwa są nadal publikowane, więc nie ma presji na aktualizację. Najważniejsze zmiany w wersji 4.0, które warto już teraz uwzględnić w projekcie: (1) fetch() zastępuje XMLHttpRequest jako podstawową infrastrukturę ajax, (2) dziedziczenie atrybutów jest domyślnie jawne, (3) obsługa historii wysyła żądanie sieciowe w celu pobrania przywracanej treści (bez lokalnej migawki DOM). Wersja FastAPI 0.135.4 (16 kwietnia) usunęła primaaprilisowy dekorator @app.vibe(), dodany w wersji 0.135.3. |
|
| 2026-04-16 | Dodano wzmiankę o wersji HTMX 4.0-beta (z myślą o przyszłości). Odnotowano obsługę pozbawionych GIL kompilacji Python 3.14t w FastAPI 0.136.0. Funkcje Pydantic 2.13.x (fabryki wartości domyślnych prywatnych atrybutów z dostępem do zwalidowanych danych modelu, przestrzeń nazw pydantic.v1 zaktualizowana do wersji 1.10.26 i obsługująca 3.14). Poprawki w Alpine.js 3.15.11: modyfikator x-anchor.noflip, ostrzeżenie o wielu elementach głównych w x-for, poprawka regresji morfowania $refs. |
|
| 2026-03-24 | Pierwsza publikacja |
Źródła
Ten przewodnik opisuje kompletny system używany do budowy serwisu blakecrosley.com. Artykuł Manifest bez budowania przedstawia argumentację stojącą za tym podejściem. Wpis Idealny wynik Lighthouse opisuje proces optymalizacji wydajności. Artykuł Vibe Coding a inżynieria analizuje miejsce programowania wspomaganego przez AI w tym przepływie pracy.
-
Metryki produkcyjne blakecrosley.com według stanu na 18 maja 2026 roku. Serwis zawiera 210 wpisów na blogu, interaktywne komponenty JavaScript, 11 głównych przewodników, 48 studiów projektowych, wersję angielską oraz 9 przetłumaczonych wersji językowych, minimalną liczbę zależności Python i żadnych narzędzi do budowania. Dane zweryfikowano na podstawie lokalnego spisu treści,
app/i18n/config.pyorazrequirements.txt. ↩↩↩↩↩ -
Google PageSpeed Insights (pagespeed.web.dev) przeprowadza audyty Lighthouse dla dowolnego publicznego adresu URL. Według stanu na marzec 2026 roku blakecrosley.com uzyskuje wyniki 100/100/100/100 (wydajność, dostępność, sprawdzone metody, SEO). Wyniki można zweryfikować publicznie. Pełny opis procesu optymalizacji znajduje się w artykule Od 76 do 100: jak uzyskać idealny wynik Lighthouse. ↩↩↩
-
Świeże użycie
npx create-next-app@latest(Next.js 15, testowano w lutym 2026 roku) instaluje wnode_modules/311 pakietów zajmujących łącznie 187 MB. Projekty produkcyjne z dodatkowymi zależnościami zwykle zajmują więcej. Wyniki poszczególnych projektów mogą się różnić. Źródło: testy autora opisane w artykule Manifest bez budowania. ↩ -
Dokumentacja wydajności Next.js firmy Vercel zaleca konkretne optymalizacje (optymalizację obrazów, ładowanie fontów i dzielenie kodu), aby osiągnąć wyniki powyżej 90. Zob. nextjs.org/docs/app/building-your-application/optimizing. Zakres 70–90 odzwierciedla ustawienia domyślne przed zastosowaniem tych optymalizacji. ↩↩
-
Pełną listę zależności zweryfikowano na podstawie pliku
requirements.txtserwisu blakecrosley.com według stanu na maj 2026 roku. Plik zawiera obecnie 17 wpisów wymagań Python i żadnych narzędzi do budowania, kompilatorów ani narzędzi pakujących. ↩ -
Z doświadczenia autora w utrzymywaniu projektów Next.js (2021–2024) wynika, że w aktywnych projektach ekosystem JavaScript generuje miesięcznie 15–25 zgłoszeń PR od Dependabot, z których większość aktualizuje zależności przechodnie, nigdy bezpośrednio niezaimportowane przez programistę. ↩
-
Tim Berners-Lee sformułował zgodność wsteczną jako zasadę projektowania sieci: „przeglądarka powinna zachowywać zgodność wsteczną”. Strona z 1996 roku jest poprawnie wyświetlana w Chrome 2026. Zob. w3.org/DesignIssues/Principles. ↩
-
OWASP zaleca wyłączenie punktów końcowych dokumentacji API w środowisku produkcyjnym, aby ograniczyć powierzchnię ataku. Punkt końcowy
/openapi.jsonujawnia definicje wszystkich tras, parametrów i modeli odpowiedzi. ↩ -
Dokumentacja FastAPI dotycząca asynchronicznych i synchronicznych procedur obsługi: fastapi.tiangolo.com/async/. Łączenie
awaitz wywołaniami blokującymi w funkcjachasyncblokuje pętlę zdarzeń. ↩ -
nh3 to oparty na Rust mechanizm oczyszczania HTML, będący następcą biblioteki Bleach. Jest utrzymywany przez projekt PyO3 i oczyszcza HTML na podstawie listy dozwolonych elementów. Zob. github.com/messense/nh3. ↩
-
Nagłówek
Varyzdefiniowano w sekcji 12.5.5 dokumentu RFC 9110. Nakazuje on pamięciom podręcznym przechowywanie osobnych odpowiedzi na podstawie wartości wskazanych nagłówków żądania. BezVary: HX-RequestCDN mógłby zwrócić fragment HTMX jako odpowiedź zawierającą pełną stronę. Zob. httpwg.org/specs/rfc9110.html#field.vary. ↩↩ -
Właściwości niestandardowe CSS (zmienne CSS) są obsługiwane przez ponad 97% przeglądarek na świecie. Podlegają kaskadowaniu i dziedziczeniu oraz reagują w czasie działania na zapytania medialne — takich możliwości nie mają zmienne preprocesorów. Źródło: caniuse.com/css-variables. ↩
-
Dokumentacja Google dotycząca hreflang: developers.google.com/search/docs/specialty/international/localized-versions. Wartość
x-defaultwskazuje stronę zastępczą dla użytkowników, których języka nie ma na liście hreflang. ↩ -
Alpine.js wymaga użycia
'unsafe-eval'w Content Security Policy na potrzeby mechanizmu oceny wyrażeń. Kompilacja zgodna z CSP (@alpinejs/csp) nie ma tego wymagania, lecz podlega pewnym ograniczeniom. Zob. alpinejs.dev/advanced/csp. ↩ -
Tokeny CSRF oparte na HMAC wykorzystują wzorzec „Signed Double-Submit Cookie” opisany w OWASP CSRF Prevention Cheat Sheet.
hmac.compare_digeststosuje porównanie w stałym czasie, aby zapobiegać atakom wykorzystującym czas wykonania jako kanał boczny. Zob. cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html. ↩ -
WebP zapewnia pliki o 25–35% mniejsze niż JPEG przy porównywalnej jakości obrazu. Badanie WebP firmy Google: developers.google.com/speed/webp/docs/webp_study. ↩
-
Mechanizm 103 Early Hints umożliwia serwerowi (lub CDN) wysłanie wstępnej odpowiedzi ze wskazówkami dotyczącymi wstępnego ładowania, zanim gotowa będzie odpowiedź końcowa. Cloudflare obsługuje Early Hints dla nagłówków
Linkzrel=preload. Zob. developer.chrome.com/blog/early-hints. ↩ -
React 18 wraz z ReactDOM zajmuje około 42 KB po minifikacji i kompresji gzip. Po dodaniu routera, biblioteki do zarządzania stanem i środowiska uruchomieniowego frameworka do budowania typowe aplikacje React dostarczają 100–300 KB kodu JavaScript frameworka. Źródło: bundlephobia.com/package/react-dom@18.2.0. ↩↩
-
Zasady wersjonowania HTMX i zobowiązanie do zachowania zgodności wstecznej opisano na stronie htmx.org/migration-guide-htmx-1/. Carson Gross przedstawił zasadę zgodności wstecznej w książce Hypermedia Systems (2023) autorstwa Grossa, Stepinskiego i Cottera: hypermedia.systems. ↩
-
Kontrola utrzymaniowa z 15 maja 2026 roku. Strona FastAPI w PyPI oraz informacje o wydaniach wskazują wersję 0.136.1; lokalna weryfikacja importu zwróciła FastAPI 0.128.0 i Starlette 0.50.0 dla środowiska tego serwisu; witryna htmx.org wskazuje wersję 2.0.10 w sekcji szybkiego startu; polecenie
npm view htmx.org version dist-tagszwróciłolatest=2.0.10inext=4.0.0-beta3; polecenianpm view alpinejs versionoraznpm view @alpinejs/csp versionzwróciły3.15.12; oficjalny blog Bootstrap i metadane pakietu npm wskazują wersję 5.3.8; strona SQLAlchemy w PyPI i dokumentacja wskazują wersję 2.0.49; strona Pydantic w PyPI wskazuje wersję 2.13.4. ↩↩↩↩ -
htmx 4.0.0-beta6 jest bieżącą wersją oznaczoną tagiem npm
next(opublikowaną 23 lipca 2026 roku; seria beta przeszła od beta3 8 maja 2026 roku przez beta4 i beta5 do beta6), natomiast tag npmlatestnadal wskazuje wersję 2.0.10. Dokumentacja wersji 4.0 na stronie four.htmx.org odpowiada kompilacjinext, indeks rozszerzeń wersji 4.0 wymieniahx-liveihx-nonce, a przewodnik migracji do wersji 4.0 opisuje zmiany, które należy przeanalizować przed migracją aplikacji produkcyjnych z wersji 2.x. Zweryfikowano 24 lipca 2026 roku na podstawie tagów dystrybucyjnych npm pakietuhtmx.org. ↩↩↩↩↩↩ -
Kontrola utrzymaniowa z 24 maja 2026 roku. Polecenia sprawdzające lokalny spis zwróciły 210 wpisów blogowych w formacie Markdown, 11 plików przewodników najwyższego poziomu i 48 plików studiów projektowych. Informacje o wydaniach FastAPI wskazują wersję 0.136.3 z 2026-05-23, wprowadzającą bardziej rygorystyczną obsługę podkreśleń w nagłówkach przy
convert_underscores=True; wersja 0.136.2 weryfikuje pola Server-Sent Event. Poleceniepython3 -m pip index versions fastapizwróciło jako najnowszą wersję0.136.3;python3 -m pip index versions sqlalchemyzwróciło2.0.50;python3 -m pip index versions pydanticzwróciło2.13.4. Polecenienpm view htmx.org dist-tags version time.modified --jsonzwróciłolatest=2.0.10,next=4.0.0-beta4itime.modified=2026-05-22T15:56:21.948Z; dokumentacja instalacji four.htmx.org wskazujehtmx.org@4.0.0-beta4. ↩↩ -
Dziennik zmian SQLAlchemy 2.0.50 oraz wpis o wydaniu, opublikowane 2026-05-24. Zależność
greenletdla asyncio nie jest już domyślnie instalowana; aby ją pobrać, trzeba teraz użyć celu instalacyjnegosqlalchemy[asyncio]. Wersja 2.0.50 rezygnuje również z obsługi Python 3.7/3.8/3.9 (wymagana jest teraz wersja 3.10 lub nowsza), dodaje pakiety wheel Python dla środowisk free-threaded oraz parametr ramki oknaover(..., exclude=...). Według stanu na 2026-06-08 potwierdzono w PyPI, że jest to najnowsza wersja. htmx 4.0.0-beta4 („The Fetchening”, 2026-05-22) pozostaje wersją beta, a wydanie stabilne jest planowane na początek 2027 roku; FastAPI 0.136.3 (2026-05-23), Alpine.js 3.15.12 i Bootstrap 5.3.x nie zmieniły się w tym okresie. ↩↩↩ -
Informacje o wydaniach FastAPI: wersja 0.137.0 (2026-06-14) przebudowuje wewnętrzną strukturę routera, przez co
router.routesnie jest już płaską listą obiektówAPIRoute, lecz drzewem obiektów pośrednich (należy traktować ją jako element wewnętrzny); umożliwia również dodawanie tras po wywołaniuinclude_router(), w tym dołączanie podrzędnego routera przed zdefiniowaniem jego tras, eliminuje kopiowanie tras oraz dodajeAPIRouter.matches()/.handle(). Wersja ta nie przypina Starlette do wersji 1.x: wymaganiem wykonawczym FastAPI jeststarlette>=0.46.0— dolna granica bez górnego limitu — identycznie w wersjach 0.136.3, 0.137.0, 0.138.0, 0.139.2 i 0.140.0, co zweryfikowano 2026-07-25 na podstawie metadanychrequires_distw API JSON PyPI. Wiersz „bump starlette from 1.1.0 to 1.2.1” (PR #15722) w informacjach o wersji 0.137.0 jest aktualizacją Dependabot w sekcji Wewnętrzne, która dotyczy wyłącznie testowego pliku blokadyuv.lockw repozytorium. (Górna granica istniała wcześniej — wersje 0.120.4 i 0.121.0 zawierałystarlette<0.50.0,>=0.40.0— lecz usunięto ją do czasu wydania 0.136.3). Korektę zastosowano 2026-07-25; poprzednie brzmienie tego przypisu i stwierdzenie w treści były błędne. Wersja 0.137.1 (2026-06-15) poprawia typowanie APIRoute i pustą ścieżkę w routerze bez prefiksu. Informacje o wydaniach Starlette: wersja 1.0.0 (2026-03-22), pierwsze stabilne wydanie od około 8 lat, usunęłaon_startup/on_shutdown/on_event()oraz dekoratory@app.route()/@app.websocket_route()(należy używaćlifespaniRoute/WebSocketRoute); najnowsza wersja to 1.3.1 (2026-06-12). SQLAlchemy 2.0.51 (dziennik zmian, 2026-06-15) zawiera wyłącznie poprawki błędów, bez wpływu na async ani instalację. Zweryfikowano 2026-06-16 na podstawie PyPI i oficjalnych informacji o wydaniach. ↩↩↩ -
Informacje o wydaniach FastAPI: wersja 0.138.0 (2026-06-20) dodaje
app.frontend("/", directory="dist")irouter.frontend("/", directory="dist")do udostępniania zbudowanego statycznego frontendu (PR #15800; dokumentacja frontendu) — jest to funkcja udostępniania statycznej aplikacji SPA z katalogudist/, a nie wzorzec renderowania po stronie serwera; nie wprowadza zmian niezgodnych wstecznie. Wersja 0.137.2 (2026-06-18) dodajeiter_route_contexts()do zaawansowanych zastosowań, które wcześniej wymagały przechodzenia porouter.routes(element wewnętrzny od wersji 0.137.0); nie wprowadza zmian niezgodnych wstecznie. Według stanu na 2026-06-22 nie opublikowano wersji nowszej niż 0.138.0. Starlette 1.3.1, Pydantic 2.13.4, Uvicorn 0.49.0, SQLAlchemy 2.0.51, HTMX 2.0.10, Alpine.js 3.15.12 i Bootstrap 5.3.8 pozostały bez zmian. Zweryfikowano 2026-06-22 na podstawie PyPI i oficjalnych informacji o wydaniach. ↩↩ -
Informacje o wydaniu 0.139.0 FastAPI, 1 lipca 2026 roku: „Obsługa zależności w
app.frontend(), na przykład w celu automatycznego uwierzytelniania frontendu za pomocą plików cookie” (PR #15908). Pozostała część wydania obejmuje tłumaczenia, dokumentację i aktualizacje zależności; nie ma zmian niezgodnych wstecznie. Weryfikacja w bieżącej sesji z 2 lipca 2026 roku (PST): wersja 0.139.0 jest najnowszym wydaniem na stronie wydań GitHub. ↩↩ -
Informacje o wydaniu 0.140.0 FastAPI, opublikowanym 24 lipca 2026 roku o 21:16 UTC (
upload_time_iso_8601w PyPI:2026-07-24T21:16:42Z). Jedyny wpis dotyczący refaktoryzacji brzmi: „⚡️ Zmniejszenie zużycia pamięci przez zależności. PR #16049” (scalono 2026-07-24T21:07:52Z). Regresję wprowadził PR #14262 (scalony 2025-11-03), który tego samego dnia trafił do wersji 0.121.0 i dodałfunctools.cached_propertydoDependant.cache_key; w wersji 0.139.2 klasa zawierała dziesięć definicji@cached_property. W wersji 0.140.0 plikfastapi/dependencies/models.pydeklaruje@dataclass(slots=True) class Dependant, a logikę przeniesiono do funkcji modułowych_get_cache_key(),_get_oauth_scopes(),_uses_scopes()i_is_security_scheme()— kod źródłowy zweryfikowano w tagu 0.140.0. Bot CodSpeed w scalonym PR raportuje dla testu pamięcitest_dependency_graphwynik 17,5 MB (wersja bazowa) → 1,1 MB (wersja docelowa), czyli „16-krotną poprawę wydajności”; wersja 0.140.0 dodaje również test pamięci w CI (PR #16046), aby zapobiec ponownemu wystąpieniu regresji. Pierwotne zgłoszenie znajduje się w dyskusji #14742, w której wersja 0.120.4 zużywała mniej niż około 400 MB, natomiast wersja 0.121.3 kończyła się błędem OOM w środowisku produkcyjnym. Uwaga dla autorów narzędzi:Dependant.oauth_scopes,.cache_key,._uses_scopesi._is_security_schemenie istnieją już jako atrybuty, aslots=Trueuniemożliwia dynamiczne modyfikowanie instancji — jest to nieudokumentowany, wewnętrzny element API, którego ten przewodnik nie używa, należący do tej samej kategorii co zmianarouter.routesw wersji 0.137.0. Wszystkie informacje ponownie zweryfikowano 2026-07-25 na podstawie PyPI, API GitHub i oznaczonego tagiem kodu źródłowego. ↩↩↩ -
FastAPI 0.141.0 (2026-07-29, 14:47 UTC) dodał
app.frontend(check_dir="auto")do lokalnego tworzenia oprogramowania za pomocąfastapi dev(PR #16102). FastAPI 0.141.1 (2026-07-29, 17:17 UTC) poprawił obsługę zadań w tle i nagłówków pochodzących z zależności wapp.frontend()(PR #16105) oraz udokumentowałFASTAPI_ENVw przewodniku CLI dla FastAPI (PR #16104). Autorem obu zmian jest @tiangolo. Potwierdzono w PyPI, że 2026-07-29 najnowszą wersją była 0.141.1. ↩↩ -
Wydania FastAPI od 0.140.1 do 0.140.7, wszystkie opublikowane 2026-07-27 między 12:07 a 17:34 UTC (
upload_time_iso_8601w PyPI: 0.140.112:07:51Z, 0.140.214:15:38Z, 0.140.315:30:52Z, 0.140.415:46:49Z, 0.140.516:02:53Z, 0.140.616:31:48Z, 0.140.717:34:47Z). Opis każdego wydania zawiera jeden wpis w sekcji refaktoryzacji: 0.140.1 „Zwiększenie limitu lru_cache dla zależności w celu obsługi dużych aplikacji” (PR #16062); 0.140.2 „Zaprzestanie przechowywania spłaszczonych drzew zależności” (PR #16065); 0.140.3 „Unikanie wielokrotnego spłaszczania zależności w OpenAPI” (PR #16067); 0.140.4 „Pomijanie zbędnego ewidencjonowania powtórzeń zależności” (PR #16069); 0.140.5 „Unikanie spłaszczania zależności dla pól treści żądania” (PR #16071); 0.140.6 „Unikanie spłaszczania zależności dla parametrów żądania, przede wszystkim na potrzeby OpenAPI” (PR #16073); 0.140.7 „Unikanie spłaszczania zależności na potrzeby OpenAPI” (PR #16076). Wartość pamięci podręcznej pochodzi z różnic w #16062, gdzie trzy dekoratory@lru_cache(maxsize=1024)wfastapi/dependencies/models.pyzastąpiono dekoratorami@lru_cache(maxsize=_CALLABLE_CLASSIFICATION_CACHE_SIZE), a pliktests/test_dependency_models.pyzaktualizowano tak, aby sprawdzałcache_info.maxsize == 4096; opis PR stwierdza: „Niektórzy użytkownicy zgłosili liczbę zależności przekraczającą 1024; ta zmiana powinna umożliwić obsługę większych aplikacji”. Wersja 0.140.2 dodaje również test pamięci (PR #16064), a wersja 0.140.7 dodaje testy zależności OpenAPI (PR #16075), zatem pokrycie testami wydajności powstało już po większości wydań z tej serii. Zweryfikowano 2026-07-27 na podstawie strony wydań API GitHub, różnic w PR oraz PyPI; w chwili pisania tekstu wersja 0.140.7 była najnowszym wydaniem. ↩↩