Hugo — generator stron statycznych z wbudowanym pipeline'em assetów
Hugo ma esbuild w środku, sam wywołuje Tailwind CLI i Dart Sass, liczy sumy kontrolne z SRI i koduje WebP oraz AVIF bez żadnego bundlera obok. Pokazujemy kompletną konfigurację Tailwinda v4, pułapki czterech edycji Hugo, model uprawnień, w którym Node dostaje `--permission`, i generowanie stron z API Laravela przez content adapters.
Hugo jest generatorem stron statycznych napisanym w Go i opisuje się jako najszybszy framework do budowania stron. Ta część jest znana. Mniej znana jest ta, która decyduje o tym, czy Hugo nadaje się do dzisiejszego projektu: Hugo ma wbudowany pipeline assetów. W środku siedzi esbuild, obok tego kodowanie WebP i AVIF, liczenie sum kontrolnych z nagłówkami SRI, minifikacja i wywoływanie zewnętrznych narzędzi w rodzaju Tailwind CLI czy Dart Sass — bez Vite'a, Webpacka ani drugiego procesu obok.
Konsekwencja praktyczna: projekt, w którym całą warstwę CSS i JS obsługuje jedna binarka, ma jedną zależność do zaktualizowania i jeden plik konfiguracyjny do zrozumienia. Konsekwencja druga, o której trzeba wiedzieć wcześniej niż później: Tailwind i PostCSS to nadal narzędzia Node'owe, więc „bez Node'a” dotyczy tylko części przypadków. Poniżej rozbieramy oba te zdania na konkrety.
Stan projektu
Repozytorium: github.com/gohugoio/hugo. Pierwszy commit w lipcu 2013, licencja Apache 2.0, kod w Go.
- 89 494 gwiazdki, 8 339 forków, 248 otwartych issues,
- ostatnie wydanie
v0.165.0z 12 sierpnia 2026, wcześniejv0.164.0z 6 lipca — wydania minorowe wychodzą co kilka tygodni, patche częściej, - ostatnie zmiany w repozytorium z 20 sierpnia 2026, czyli sprzed kilku dni,
- trzynasty rok rozwoju bez zmiany właściciela i bez przepisywania od zera.
Numeracja 0.x po trzynastu latach bywa myląca — to nie jest projekt w fazie eksperymentu. Warto natomiast traktować poważnie fakt, że w wydaniach minorowych zdarzają się zmiany łamiące zgodność; jedną z nich, z v0.165.0, opisujemy niżej, bo dotyczy Tailwinda.
Cztery edycje i pułapka, która przeżyła swoje uzasadnienie
Hugo jest wydawane w kilku edycjach i to pierwsze miejsce, w którym łatwo podjąć decyzję na podstawie nieaktualnej wiedzy. Aktualna tabela z dokumentacji:
- standard — funkcje podstawowe; dokumentacja mówi wprost: używaj tej edycji, jeśli nie potrzebujesz czegoś więcej,
- deploy (nowa w
v0.159.2) — dodaje wdrożenie prosto do bucketu Google Cloud Storage, S3 albo kontenera Azure Storage przezhugo deploy, - extended — dodaje wbudowanego LibSassa,
- extended/deploy — jedno i drugie.
I teraz rzecz, która wywraca powszechne przekonanie: edycja extended jest dziś potrzebna wyłącznie do LibSassa, a LibSass został oznaczony jako przestarzały w v0.153.0 i zostanie usunięty. Zalecany transpiler to Dart Sass, który działa z każdą edycją — wystarczy, że jest w PATH. Podobnie z obrazami: kodowanie WebP jest w Hugo od v0.155.0, a AVIF od v0.162.0, w edycji standardowej. Reguła „potrzebujesz extended, żeby zapisać WebP” była prawdziwa i przestała być.
Jeśli wybierasz edycję dziś: bierz standard, a Dart Sassa instaluj osobno. Po extended sięgaj tylko wtedy, gdy masz stary projekt na LibSassie, którego nie chcesz jeszcze migrować — i wpisz sobie tę migrację do backlogu, bo termin usunięcia LibSassa już się zbliża.Dart Sass instaluje się managerem pakietów (brew install sass/sass/sass, choco install sass, snap install dart-sass). Wyjątek: pakiet Snap z Hugo ma Dart Sassa w środku. Git jest potrzebny do modułów Hugo, motywów jako submodułów i do informacji z commitów (.GitInfo); Go — do modułów i budowania ze źródeł.
Start projektu
hugo version
hugo new project moja-strona
cd moja-strona
git init
hugo server -DKomenda nazywa się dziś hugo new project; hugo new site działa jako alias. Serwer deweloperski trzyma stronę w pamięci i przebudowuje zmienione fragmenty; -D to skrót od --buildDrafts. Build produkcyjny:
hugo build --minify --gc--minify minifikuje każdy obsługiwany format wyjściowy (HTML, XML), --gc po zakończeniu builda usuwa nieużywane pliki z cache'u — przydatne, gdy zmieniłeś parametry przetwarzania obrazów albo skasowałeś pliki źródłowe. Dochodzą jeszcze --logLevel (debug, info, warn, error) i --printPathWarnings, które wyłapuje kolizje ścieżek docelowych.
Katalogi, które w tym wpisie mają znaczenie: assets/ (pliki wchodzące w pipeline), content/ (treść), layouts/ (szablony Go), static/ (kopiowane bez zmian), resources/_gen/ (cache przetworzonych assetów) i public/ (wynik).
Hugo Pipes: co jest w środku
Pipeline zaczyna się od pobrania zasobu, a potem jest łańcuchem przekształceń łączonych pionową kreską — dokładnie jak w powłoce. Funkcje pobierające:
resources.Get,resources.GetMatch,resources.Match,resources.ByType— pliki zassets/,resources.GetRemote— pobranie po HTTP w trakcie builda,resources.FromString,resources.ExecuteAsTemplate— zasób z łańcucha znaków albo z szablonu (przydatne, gdy CSS ma zawierać wartości z konfiguracji),resources.Copy— kopia pod inną ścieżką.
Przekształcenia:
css.Build— odv0.158.0, oparte o esbuild: rozwijanie@import, transformacje pod starsze przeglądarki, prefiksy producenta, minifikacja, source mapy. Jeśli@importma zapytanie medialne, zapytanie o funkcję albo przypisanie do warstwy, treść zostaje owinięta odpowiednio w@media,@supportsalbo@layer,css.Sass— transpilacja Sassa, z opcjątranspiler(dartsassalbolibsass),includePaths,outputStyleienableSourceMap,css.TailwindCSS— wywołanie Tailwind CLI, o tym osobno niżej,css.PostCSS— PostCSS z konfiguracją z katalogu projektu,css.ChromaStyles— nowość zv0.165.0: generowanie arkusza dla kolorowania składni Chromy, z opcjamitargetPath,style,mode,classDarkiclassLight. Do tej pory ten CSS trzeba było wyprodukować komendą i wrzucić do repozytorium ręcznie,js.Build— esbuild: bundlowanie, TypeScript i JSX, tree shaking, minifikacja, source mapy, formatyiife(domyślny),cjsiesm,js.Batch— bundlowanie grupowe z globalnym code splittingiem, gdy jeden bundle na całą stronę jest za grubym narzędziem,resources.Minify,resources.Concat,resources.Fingerprint,resources.PostProcess.
Dwie rzeczy o cache'owaniu, bez których łatwo wyciągnąć błędne wnioski o wydajności. Pierwsza: klucz cache'u to cały łańcuch przekształceń, więc przetworzenie następuje raz, a wywołanie tego samego łańcucha w szablonie renderowanym tysiąc razy nie kosztuje już nic. Druga: pliki lądują w resources/_gen/, który można zacommitować albo trzymać w cache'u CI — i to jest właściwy moment, żeby zajrzeć do drugiego zdania tego wpisu.
Tailwind CSS v4 od początku do końca
Tu jest najwięcej ruchomych części, więc idziemy po kolei. Wersja css.TailwindCSS obsługuje Tailwinda 4.0 i nowsze.
Krok 1 — zależności. Od v0.161.0 Hugo nie obsługuje już samodzielnej binarki Tailwinda; CLI instalujemy z npm:
npm install --save-dev tailwindcss @tailwindcss/cli @tailwindcss/typographyKrok 2 — konfiguracja projektu. Hugo musi zapisywać statystyki builda i unieważniać cache CSS, gdy się zmienią:
[build]
[build.buildStats]
enable = true
[[build.cachebusters]]
source = 'assets/notwatching/hugo_stats\.json'
target = 'css'
[[build.cachebusters]]
source = '(postcss|tailwind)\.config\.js'
target = 'css'
[module]
[[module.mounts]]
source = 'assets'
target = 'assets'
[[module.mounts]]
disableWatch = true
source = 'hugo_stats.json'
target = 'assets/notwatching/hugo_stats.json'Co tu się dzieje: przy włączonym buildStats Hugo zapisuje przy każdym buildzie plik hugo_stats.json z tablicami klas, identyfikatorów i nazw znaczników użytych w wygenerowanej stronie. Tailwind czyta ten plik i na jego podstawie wie, których klas nie wyrzucić. Montowanie z disableWatch = true plus cachebusters to obejście oczywistej pętli: plik zmienia się przy każdym buildzie, więc obserwowanie go wprost oznaczałoby przebudowę w kółko.
Osobno warto wiedzieć, że statystyki da się odchudzić — disableIDs, disableTags i disableClasses wyłączają poszczególne kategorie. Dokumentacja zaleca trzymać cały blok buildStats w config/production/, bo przy pracy lokalnej i tak nie usuwamy nieużywanego CSS. Ostrzega też o dwóch rzeczach: parser bywa nadmiernie optymistyczny i potrafi uznać za znacznik HTML coś, co nim nie jest (rzadko i bez konsekwencji), a przy serwerze deweloperskim nowe klasy dochodzą, ale stare nie znikają — do wyczyszczenia listy potrzebny jest restart serwera albo hugo build.
Krok 3 — plik wejściowy CSS. I najbardziej podstępna pułapka całej konfiguracji:
/* assets/css/main.css */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@source "hugo_stats.json";Tailwind respektuje .gitignore. Skoro hugo_stats.json praktycznie zawsze jest w .gitignore, Tailwind sam z siebie go pominie — i dostajesz arkusz bez połowy klas, bez żadnego błędu. Dyrektywa @source jest tu obowiązkowa, nie ozdobna.
Krok 4 — partial przetwarzający CSS.
{{ with resources.Get "css/main.css" }}
{{ $opts := dict "minify" (not hugo.IsDevelopment) }}
{{ with . | css.TailwindCSS $opts }}
{{ if hugo.IsDevelopment }}
<link rel="stylesheet" href="{{ .RelPermalink }}">
{{ else }}
{{ with . | fingerprint }}
<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
{{ end }}
{{ end }}
{{ end }}
{{ end }}Krok 5 — wywołanie z szablonu bazowego, z odroczeniem.
<head>
{{ with (templates.Defer (dict "key" "global")) }}
{{ partial "css.html" . }}
{{ end }}
</head>templates.Defer odkłada wykonanie tego fragmentu na moment, w którym wszystkie strony i wszystkie formaty wyjściowe są już wyrenderowane. Bez tego Tailwind zobaczyłby niekompletny hugo_stats.json — czyli klasy z części strony zbudowanej do tej pory. To nie jest optymalizacja, to warunek poprawności.
Opcje css.TailwindCSS: minify (optymalizacja i minifikacja), optimize (optymalizacja bez minifikacji), disableInlineImports (od v0.147.4) i skipInlineImportsNotFound, który pozwala buildowi przejść pomimo nierozwiązanych importów. Jeśli potrzebujesz wstrzyknąć do arkusza zmienne CSS z konfiguracji strony, łączy się to z css.Build i jego opcją vars — z Tailwindem oznaczonym jako externals, żeby esbuild go nie tknął, i disableInlineImports po stronie Tailwinda, bo importy są już rozwinięte.
Zmiana z v0.165.0, która może zatrzymać build
W v0.165.0 tailwindcss został usunięty z domyślnej listy dozwolonych programów zewnętrznych (security.exec.allow). Brzmi groźnie, a w praktyce jest domknięciem porządków po v0.161.0: skoro samodzielna binarka Tailwinda nie jest już obsługiwana, Hugo uruchamia CLI z node_modules przez Node'a. Domyślna lista dozwolonych zawiera ^node$, więc typowa konfiguracja nadal działa. Jeżeli natomiast masz w projekcie własną listę security.exec.allow zbudowaną wokół ^tailwindcss$ i bez ^node$ — build stanie po aktualizacji. Pełna domyślna lista, warta porównania z tym, co masz w konfiguracji:
[security.exec]
allow = ['^(dart-)?sass$', '^go$', '^git$', '^node$', '^postcss$']Drobiazg z tego samego obszaru, ale wart uwagi przy CI: CSS jest podawany Tailwindowi na standardowe wejście (--input=-), a gdy binarki nie ma w ogóle, Hugo nie wywala builda — zwraca informację o niedostępnej funkcji i sięga po gotowe assety z cache'u. Innymi słowy: zacommitowany resources/_gen/ pozwala zbudować stronę na maszynie bez npm install. Wygodne i jednocześnie łatwe do przegapienia, bo build przechodzi, tylko CSS jest z poprzedniego razu.
Sumy kontrolne, SRI i usuwanie nieużywanego CSS
resources.Fingerprint (alias fingerprint) liczy skrót treści zasobu i robi dwie rzeczy naraz: wkłada go do nazwy pliku, więc dostajesz cache busting bez parametru w adresie, i wystawia .Data.Integrity do atrybutu integrity:
{{ with resources.Get "js/main.js" }}
{{ with . | fingerprint "sha256" }}
<script src="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous"></script>
{{ end }}
{{ end }}Wynik to /js/main.62e...df1.js plus integrity="sha256-Yuh...rfE=". Algorytm: md5, sha256 (domyślny), sha384 albo sha512. Działa dla dowolnego typu zasobu, nie tylko CSS i JS.
resources.PostProcess odkłada przekształcenie na moment po zakończeniu builda i istnieje głównie dla jednego zadania: usunięcia nieużywanych reguł CSS. Kolejność jest wtedy taka — PurgeCSS na podstawie hugo_stats.json, Autoprefixer, minifikacja, suma kontrolna. Przy Tailwindzie 4 nie jest to potrzebne, bo Tailwind sam generuje tylko użyte klasy; przy własnym CSS-ie albo starszym stacku z PostCSS-em — jak najbardziej.
Bezpieczeństwo builda, czyli treść jako dane niezaufane
To sekcja, którą przy generatorach stron statycznych zwykle się pomija, a Hugo ma tu przemyślany model i kilka świeżych zmian. Domyślnie dostęp do os/exec, komunikacji sieciowej i zmiennych środowiskowych jest ograniczony listami dozwolonych, a build, który sięga po coś poza listą, kończy się błędem z wyjaśnieniem.
Domyślne wartości warto znać na pamięć, bo tłumaczą większość niespodzianek:
exec.allow— tylkosass/dart-sass,go,git,nodeipostcss,funcs.getenv— tylko zmienne zaczynające się odHUGO_orazCI. Odczytanieos.Getenv "DATABASE_URL"w szablonie po prostu nie przejdzie,http.methods—GETiPOST,http.urlsdlaresources.GetRemote— adresy, których host zaczyna się od litery albo cyfry, z jawnym zablokowaniem hostów wyglądających na adres IP, wszystkiego zlocalhosti adresów z danymi użytkownika w rodzajuhttp://user@…. To zabezpieczenie przed SSRF-em: szablon pobierający zdalny zasób nie sięgnie do usługi w sieci wewnętrznej,allowContent(odv0.162.0) — treść w formacie HTML jest domyślnie odrzucana. Uzasadnienie w kodzie jest jednozdaniowe i trafne: Hugo wypuszcza HTML z katalogucontentdosłownie, więc to gotowe miejsce na wykonanie dowolnego JavaScriptu. Jeśli Twój pipeline wrzuca tam HTML z zewnętrznego CMS-a, tę listę trzeba świadomie poszerzyć.
Od v0.161.0 doszły dwie rzeczy. Pierwsza to model uprawnień Node'a: narzędzia Node'owe Hugo uruchamia z flagą --permission, domyślnie z prawem czytania katalogu roboczego, bez prawa zapisu i z wyjątkami przyznanymi imiennie. Tailwind dostaje trzy: ładowanie natywnych dodatków, wątki robocze i uruchamianie procesów potomnych (bo detect-libc na części Linuksów woła getconf). Warto docenić, co to znaczy: pakiet npm w Twoim buildzie nie ma domyślnie prawa zapisu w systemie plików.
Druga to reguły negatywne — wzorzec poprzedzony wykrzyknikiem i spacją staje się regułą zakazującą, a zakazy mają pierwszeństwo nad zezwoleniami. Lista złożona wyłącznie z zakazów zezwala na wszystko, czego nie zakazuje, a lista pusta odrzuca wszystko. Dzięki temu da się napisać „wszystko poza tym jednym hostem” bez wypisywania świata:
[security.http]
urls = ['.*', '! ^https?://evil\.example\.com']Każdą z tych list można nadpisać zmienną środowiskową, co jest wygodne w CI — HUGO_SECURITY_HTTP_URLS=none odcina pobieranie zdalnych zasobów całkowicie.
Obrazy
Przetwarzanie obrazów jest częścią tego samego pipeline'u: metody Resize, Fit, Fill, Crop, Process i cała rodzina filtrów (images.Text, images.Overlay, images.Dither, a od niedawna nawet images.QR generujący kody QR). Obraz jest przetwarzany na żądanie, wynik trafia do cache'u, a metoda zwraca nowy zasób z gotowymi .Width i .Height — czyli atrybuty wymiarów w HTML-u masz z pierwszej ręki, bez zgadywania i bez przesuwania layoutu.
{{ with .Resources.Get "sunset.jpg" }}
{{ with .Resize "400x webp" }}
<img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" alt="">
{{ end }}
{{ end }}Kodowanie WebP jest wbudowane od v0.155.0, AVIF od v0.162.0, a od v0.163.0 oba mają osobne sekcje konfiguracji z sensownymi domyślnymi: dla AVIF quality 60, compression lossy, encoderSpeed 10 (niższe wartości dają mniejsze pliki, poniżej 5 build zaczyna wyraźnie zwalniać), dla WebP effort 2 i hint photo. Dokumentacja podaje przy okazji przelicznik, którego nigdzie indziej się nie widuje: jakość 60 w AVIF wygląda mniej więcej jak 75 w JPEG-u — wartości nie są porównywalne między formatami.
Trzy uwagi z sekcji o wydajności, które oszczędzają godziny przy pierwszym większym wdrożeniu. Metadane nie są zachowywane przy przekształceniu — jeśli potrzebujesz EXIF-u, czytaj go z oryginału metodą Meta. Zasoby i czas rosną z wymiarami źródła, więc obraz 4032x2268 kosztuje istotnie więcej niż 1920x1080; jeśli publikujesz mniejsze rozmiary, warto przeskalować źródła przed buildem. I trzecia: cache obrazów trzeba w CI utrwalić, inaczej każdy build przelicza wszystko od zera:
[caches]
[caches.images]
dir = ':cacheDir/images'Treść z zewnątrz: content adapters
Najciekawsza rzecz w Hugo z punktu widzenia zespołu, który ma już backend. Content adapter to szablon o nazwie _content.gotmpl leżący w content/, który tworzy strony w trakcie builda — z danych, nie z plików Markdown. Jeden na katalog i język.
content/
├── artykuly/
│ ├── _index.md
│ └── artykul-1.md
└── produkty/
├── _content.gotmpl <-- adapter
└── _index.mdW adapterze dostępne są zwykłe funkcje szablonów plus metody AddPage i AddResource. Strona to po prostu mapa z treścią, rodzajem, ścieżką i tytułem:
{{ $data := resources.GetRemote "https://api.example.com/produkty" | transform.Unmarshal }}
{{ range $data.items }}
{{ $content := dict "mediaType" "text/markdown" "value" .description }}
{{ $page := dict
"content" $content
"kind" "page"
"path" .slug
"title" .name
"params" (dict "price" .price "sku" .sku)
}}
{{ $.AddPage $page }}
{{ end }}To realna alternatywa dla „przepiszmy frontend na SSR”: katalog produktów albo baza wiedzy żyje w Laravelu, endpoint zwraca JSON, a Hugo generuje z tego statyczne strony przy każdym wdrożeniu. Strony są zwykłymi stronami Hugo — mają taksonomie, sekcje, listy, kanał RSS, wyszukiwanie i wszystko inne. Adapter dostaje też AddResource, więc obrazy produktów wchodzą jako zasoby strony i przechodzą normalnym pipeline'em, z przeskalowaniem i konwersją do WebP włącznie.
Jedno ograniczenie zapisane w dokumentacji wprost: obiekt Site w adapterze nie jest w pełni zainicjowany, więc metody zależne od zbudowanych już stron — Site.Pages i podobne — zwrócą błąd. Adapter buduje treść, nie analizuje gotowej strony.
Jak to wygląda przy aplikacji na Laravelu
Nie namawiamy nikogo do zamiany aplikacji na stronę statyczną. Hugo jest natomiast dobrym wyborem na to, co wokół aplikacji: dokumentację, bazę wiedzy, blog, landing pod kampanię, changelog. Wszystko to nie potrzebuje PHP przy każdym żądaniu, a wystawione jako pliki statyczne za CDN-em nie ma czego zepsuć przy wdrożeniu aplikacji.
Praktyczna strona takiego układu:
- Osobne repozytorium albo katalog, wspólne CI. Build to jedna komenda i kilkanaście megabajtów wyniku,
- Przypnij wersję Hugo. Wydania minorowe wychodzą co kilka tygodni i zdarzają się w nich zmiany łamiące zgodność — jak ta z Tailwindem w
v0.165.0. W CI ustawiamy dokładną wersję, a aktualizację robimy świadomie, po przeczytaniu wpisu wydania, - Cache'uj
resources/_gen/i katalog cache'u Hugo między buildami. Przy stronie z obrazami to różnica między buildem liczonym w sekundach i w minutach, - Docker, gdy nie chcesz Hugo na maszynie. Oficjalny
Dockerfilez repozytorium buduje edycjęextended, dokłada Dart Sassa (dziś w wersji 1.79.3) i wystawia port 1313 dla serwera deweloperskiego, - Wdrożenie — albo przez artefakt w CI i wgranie na hosting, albo
hugo deployprosto do S3, GCS czy Azure Storage. Ta druga droga wymaga edycjideploylubextended/deploy; łatwo to przegapić, bo komenda w innych edycjach po prostu nie istnieje.
Warto też uczciwie powiedzieć, co w Hugo boli. Szablony to Go templates — inna składnia i inny model myślenia niż Blade, z potokami, kontekstem . i konstrukcjami w rodzaju {{ with }}, które przy okazji zmieniają kontekst. Pierwszy dzień jest nieprzyjemny, drugi już nie, ale to jest koszt wejścia i nie da się go pominąć. Do tego 0.x z zmianami łamiącymi zgodność w wydaniach minorowych oraz 248 otwartych issues — projekt jest duży i część zgłoszeń leży długo.
Podsumowanie
Hugo jest wart uwagi nie dlatego, że jest szybki — to prawda, ale przy stu stronach nikogo to nie obchodzi. Jest wart uwagi, bo zamyka całą warstwę budowania assetów w jednej binarce, i to bez upraszczania: esbuild, Sass, Tailwind, PostCSS, sumy kontrolne z SRI, WebP, AVIF, source mapy i cache, który naprawdę działa.
Co dostajesz:
- pipeline assetów w środku generatora —
css.Buildijs.Buildna esbuildzie,css.Sass,css.TailwindCSS,css.PostCSS,js.Batchz code splittingiem, minifikacja i konkatenacja, resources.Fingerprintdający jednym wywołaniem cache busting w nazwie pliku i wartość do atrybutuintegrity,- przetwarzanie obrazów z natywnym WebP (od
v0.155.0) i AVIF (odv0.162.0), z konfiguracją jakości per format i wymiarami zwracanymi do szablonu, - content adapters, czyli generowanie stron z zewnętrznego API — dobre spięcie z backendem, który już masz,
- model bezpieczeństwa oparty o listy dozwolonych: pięć programów zewnętrznych, zmienne środowiskowe tylko z prefiksem
HUGO_, blokada SSRF-u wresources.GetRemote, odrzucanie treści HTML z katalogucontenti uruchamianie narzędzi Node'owych z flagą--permission, domyślnie bez prawa zapisu, - cztery edycje, z których standardowa wystarcza w nowym projekcie — LibSass jest przestarzały, a Dart Sass działa wszędzie.
O czym pamiętać: @source "hugo_stats.json" w pliku wejściowym CSS jest obowiązkowe, bo Tailwind respektuje .gitignore i inaczej po cichu wyrzuci połowę klas. templates.Defer przy przetwarzaniu CSS też jest obowiązkowe, bo bez niego statystyki są niekompletne. Zmiana z v0.165.0 wymaga ^node$ na liście dozwolonych programów. A gdy Tailwinda nie ma w środowisku, Hugo nie przerwie builda, tylko użyje wcześniej wygenerowanych assetów — o tym najlepiej wiedzieć zawczasu, zanim spędzisz wieczór, szukając, czemu nowa klasa nie działa na produkcji.
Licencja: Apache 2.0 — permisywna, bez zastrzeżeń przy użyciu komercyjnym i u klienta, z jawnym udzieleniem patentowym. Redystrybucja wymaga zachowania noty licencyjnej i pliku NOTICE; wygenerowana strona jest oczywiście Twoja i licencja generatora jej nie dotyczy. Osobno sprawdzaj licencje motywów — bywają na innych warunkach niż samo Hugo.