Почему это стоит прочитать #

С прошлого разбора (v0.160.1, 8 апреля) накопилось пять релизов: 0.161.0, 0.161.1, 0.162.0, 0.163.0 и патчи до 0.163.3 (18 июня). Это не один минор, а целый квартал развития — поэтому разбор догоняющий.

Если сжать до двух заголовков, их два:

  1. AVIF — Hugo научился кодировать и декодировать AVIF прямо в image pipeline. Третий формат рядом с JPEG и WebP, и для фото-блога это самое интересное.
  2. Хардненинг безопасности — Node-инструменты теперь запускаются с --permission, text/html-контент запрещён по умолчанию, ужесточены security.http.urls, плюс транзитом приехали XSS-фиксы из Go 1.26.3.

Остальное — приятные улучшения конфигурации (slice-based permalinks, гибкие идентификаторы в именах файлов, вложенные vars в css.Build) и порция депрекейтов. Breaking changes есть, но точечные — разберём, кого они задевают.

Главное за один взгляд #

v0.161.0 / 0.161.1 (28–29 апреля) — фичи + первый хардненинг:

  • Node-инструменты (PostCSS, Babel, TailwindCSS) запускаются с флагом --permission. Нужен Node ≥ 22, а Tailwind теперь только как npm-пакет (standalone-бинарь больше не поддерживается).
  • Ужесточены дефолты security.http.urls; добавлен security.node.permissions (включая AllowChildProcess).
  • Slice-based permalinks — конфигурация permalinks с target-матчерами (как в cascade).
  • Гибкая схема идентификаторов в именах файлов: _language_en_, _role_, _version_.
  • Вложенные vars в css.Build / css.Sass: @import "hugo:vars/dark".

v0.162.0 / 0.162.1 (26–28 мая) — AVIF + security:

  • 🎯 Поддержка AVIF (encoder + decoder) в image pipeline.
  • text/html-контент запрещён по умолчанию — новая политика security.allowContent.
  • Go 1.26.3: фиксы XSS в html/template (CVE-2026-39826, CVE-2026-39823).
  • Проверка security.http.urls на каждом редирект-хопе resources.GetRemote; отказ от symlink-записей в resources.Get.

v0.163.0 (8 июня) — тюнинг AVIF + депрекейты:

  • AVIF по умолчанию quality = 60 (≈ JPEG/WebP q75), content-aware chroma subsampling через hint, качество настраивается per-format.
  • Депрекейт глобального imaging.quality и Imaging.Compression (переезжают в per-format конфиги).
  • Добавлен .IsBranch, депрекейт .IsNode; депрекейт baseUrl в jsconfig.

v0.163.1 / 0.163.2 / 0.163.3 (11–18 июня) — патчи:

  • Security: bump golang.org/x/image, нормализация integer-IPv4 в http.urls, отказ от symlink в os.ReadDir/ReadFile/Stat/FileExists.
  • Escape lang в дефолтном рендере код-блоков; сохранение non-ASCII пробела после summary-разделителя; поддержка вариантов конфигов babel/postcss; фикс регрессии коллизии имён page/section.

Дальше — что это значит на практике.

AVIF: третий формат в pipeline 🖼️ #

Главная фича квартала. В 0.162.0 Hugo получил полноценную обработку AVIF — и кодер, и декодер (issue #7837, висел с 2020 года). AVIF при том же визуальном качестве заметно легче JPEG и обычно легче WebP, плюс умеет HDR (у Hugo есть демо-сайт, который имеет смысл смотреть на HDR-экране вроде Retina).

В 0.163.0 фичу довели до ума:

  • Дефолтный quality для AVIF опустили до 60 — оказалось, JPEG/WebP с q75 сопоставимы с AVIF q60. Не гоните качество вверх по привычке.
  • Появился hint (как у WEBP): для lossy фото по умолчанию используется YUV420 chroma subsampling, а для текста/иконок/схем — 444. Это резко снижает память на кодирование больших картинок.
  • Качество можно задавать на формат:
[imaging]
  # глобальный quality теперь deprecated
[imaging.webp]
  quality = 80
[imaging.avif]
  quality = 60
  hint    = "photo"

В шаблоне AVIF получается так же, как WebP — через .Process / метод ресайза:

{{ $img := resources.Get "img/cover.jpg" }}
{{ $avif := $img.Process "resize 1600x avif" }}
<picture>
  <source srcset="{{ $avif.RelPermalink }}" type="image/avif">
  <source srcset="{{ ($img.Process "resize 1600x webp").RelPermalink }}" type="image/webp">
  <img src="{{ ($img.Resize "1600x").RelPermalink }}" alt="…">
</picture>

Что это значит для нашего блога. Раздел /photos уже генерит 800/1600/2400 px в JPEG + WebP и отдаёт через <picture>/srcset. Добавить AVIF — это правка шаблона, который собирает <source>-ы (плюс per-format quality в hugo.toml): первым <source type="image/avif">, дальше WebP, в конце JPEG-фолбэк. Браузеры берут первый поддерживаемый. На фотографиях с retina это даст ощутимую экономию трафика при том же качестве. В этот заход не делаю — но это очевидный следующий шаг для фото-ленты.

Хардненинг безопасности 🔒 #

Самая «breaking» часть. Изменения копились через все релизы:

Node-инструменты под --permission (0.161.0). PostCSS, Babel и TailwindCSS теперь запускаются с Node permission-флагом и правами из security.node.permissions. Следствия:

  • нужен Node ≥ 22;
  • css.TailwindCSS требует Tailwind CLI как npm-пакет — standalone-бинарь больше не годится.

Если вы собираете CSS через Hugo Pipes с Tailwind/PostCSS — это первое, что стоит проверить перед обновлением.

Запрет text/html-контента по умолчанию (0.162.0). Новая политика security.allowContent гейтит, какие media-типы допустимы для страниц в /content. text/html теперь запрещён по умолчанию. Сайты, где есть руками написанные .html-файлы в content/ (или их эмитит content adapter), вернут доступ явно:

[security]
  allowContent = ['.*']

Прочее (0.162.0 → 0.163.3):

  • security.http.urls перепроверяется на каждом редирект-хопе в resources.GetRemote — мимо валидации больше не проскочить через 30x.
  • Symlink-записи отклоняются в resources.Get, а в 0.163.1 — ещё и в os.ReadDir/ReadFile/Stat/FileExists.
  • Go подняли до 1.26.3 с фиксами XSS в html/template (CVE-2026-39826, CVE-2026-39823) — это касается любого сайта, рендерящего недоверенный ввод в шаблонах.

Любопытная ремарка от мейнтейнера в release notes 0.163.1: всплеск security-репортов в последнее время — это не «Hugo стал менее безопасным», а работа новых AI-инструментов, которые берут security-модель Hugo за baseline и методично её простукивают.

В 0.161.0 конфигурация permalinks стала гибче (старая map-форма продолжает работать). Теперь это список с теми же target-матчерами, что и в cascade:

permalinks:
  - target:
      kind: page
      path: "/books/**"
    pattern: /books/:year/:slug/
  - target:
      kind: page
    pattern: /other/:slug/

Удобно, когда для разных секций нужны разные схемы URL без дублирования настроек. Нашему блогу с плоской структурой posts/ это пока избыточно, но знать про механизм полезно — особенно если когда-нибудь заведём разделы со своими правилами URL.

Гибкие идентификаторы в именах файлов 🏷️ #

Тоже из 0.161.0. Раньше язык кодировался как mypost.en.md. Теперь доступна расширяемая схема префиксов в имени файла:

ПрефиксЧто задаётГде применимо
_language_ЯзыкКонтент и layout-файлы
_role_РольКонтент и layout-файлы
_version_ВерсияКонтент и layout-файлы
_outputformat_Output formatLayout-файлы
_kind_Page kindLayout-файлы

content/mypost._language_en_.md эквивалентно content/mypost.en.md, но открывает дорогу к версиям и ролям контента. Для одноязычного блога — мимо, для документации с версиями — заметное упрощение.

css.Build: вложенные vars 🎨 #

Развитие фичи из 0.160. К @import "hugo:vars" добавились вложенные наборы — @import "hugo:vars/<name>". Классический сценарий — палитры под prefers-color-scheme:

[params.style]
  primary    = "#000000"
  background = "#ffffff"
  [params.style.dark]
    primary    = "#ffffff"
    background = "#000000"
@import "hugo:vars";
@import "hugo:vars/dark" (prefers-color-scheme: dark);

Аккуратно, но не для нас: переключатель дня/ночи на блоге работает через JS и localStorage (явный выбор пользователя, без авто), а не через prefers-color-scheme. Палитра живёт в :root. Так что фича в копилку «знать», но не «внедрять».

Депрекейты и мелочи #

  • .IsNode.IsBranch (0.163.0): если в шаблонах есть .IsNode, мигрируйте на .IsBranch (issue #11574).
  • Глобальный imaging.quality (0.163.0): помечен deprecated в пользу per-format ([imaging.webp], [imaging.avif], …). Старая форма пока работает и даже не всегда ругается — но направление задано.
  • jsconfig baseUrl убран (был deprecated).
  • Сохранение non-ASCII пробела после summary-разделителя (0.163.3) — мелочь, но именно для кириллических текстов с `

` это правильное поведение.

  • Фикс коллизии имён page/section (0.163.3, #15046) — регрессия, всплывшая в 0.163.x; ещё одна причина брать самый свежий патч, а не 0.163.0.

Что это значит для нашего блога #

Прогнал блог на 0.163.3 — собирается чисто, 90 страниц, 0 ошибок, 52 изображения обработаны без проблем. Конкретика:

  • Запрет text/html нас не задел — в content/ нет руками-сверстанных .html, всё в Markdown. security.allowContent трогать не нужно.
  • Node-permission мимо — Tailwind/PostCSS через Pipes не используем, Pagefind живёт отдельным npm-инструментом.
  • languageCodelocale: попутно почистил deprecation в hugo.toml (languageCode = 'ru-RU'locale = 'ru-RU'). Этот warning ушёл. Оставшиеся три (.Site.LanguageCode, .Language.LanguageCode, .Language.Direction) приходят из шаблонов темы typo — уйдут, когда обновится тема.
  • AVIF — главная зацепка на будущее: добавить третий <source> в фото-pipeline и срезать вес ленты /photos.

Чек-лист обновления ✅ #

  1. Прыгайте сразу на 0.163.3, а не на 0.163.0 — патчи .1/.2/.3 закрывают security-дыры и регрессию коллизии имён.

  2. Если собираете CSS через Tailwind/PostCSS/Babel в Pipes — поставьте Node ≥ 22 и Tailwind как npm-пакет; проверьте security.node.permissions.

  3. Если в content/ есть .html-файлы — добавьте security.allowContent = ['.*'], иначе страницы выпадут из сборки.

  4. Почистите депрекейты:

    # languageCode → locale в конфиге
    # .IsNode → .IsBranch в шаблонах
    grep -rn '\.IsNode' layouts/ themes/
  5. Если работаете с изображениями — присмотритесь к AVIF и per-format quality; глобальный imaging.quality уводите в [imaging.<format>].

  6. Стандартный прогон и сверка warning’ов до/после:

    hugo --minify --gc

Итог 🧠 #

0.161–0.163 — это квартал «инфраструктурного взросления» Hugo: новый формат изображений (AVIF), методичный security-хардненинг и более гибкая конфигурация (permalinks, идентификаторы файлов, вложенные CSS-vars). Громкая фича ровно одна — AVIF; всё остальное либо про безопасность, либо про удобство.

Breaking changes реальны, но адресные: они бьют по сайтам с Tailwind-в-Pipes и с HTML-контентом в content/. Если вы (как наш блог) пишете на чистом Markdown и не гоняете Node-сборку CSS — переход бесшовный, достаточно поднять версию и пробежаться по депрекейтам.

Если сидите на 0.160 или раньше — есть смысл прыгать сразу на 0.163.3, мимо промежуточных: так вы заодно получите все security-фиксы одним движением.

Источники #