HERON

Контракт данных · часть 5

Картинки, файлы и правила

Пятая часть контракта данных: картинки, видео и файлы, папка static/, что тема требует от HERON, секреты сервисов и ограничения, которые нельзя нарушать.

Страница нужна тем, кто кладёт на сайт фотографии и документы, и тем, кто делает тему.

Простыми словами

Кладите фотографии, видео и документы в папку media/ и ссылайтесь на них со страницы. Остальное HERON сделает сам: из каждой фотографии он сделает лёгкие копии нескольких размеров в современных форматах (WebP, AVIF), и телефон скачает маленькую картинку, а большой экран — чёткую. Уменьшать руками ничего не нужно.

Пример. Одна фотография на странице:

![Наша бригада на крыше в Вене](media/examples/photo.jpg)

Из этой строки HERON делает несколько версий картинки и разметку, по которой браузер сам выбирает подходящий размер. Текст в квадратных скобках — описание для тех, кто не видит картинку, и для поисковиков; заполняйте его всегда.

Здесь же описаны ещё две вещи. static/ — для редких файлов, которые должны остаться ровно такими, как есть (например, файл подтверждения от Google). .env хранит секреты для сервера и никогда не попадает ни в папку сайта, ни в git.

Когда нужна эта страница. Когда вы добавляете фото, видео или PDF, когда картинка выглядит обрезанной или когда настраиваете сервер.

Картинки

media/ — одна общая папка на картинки и видео, по языкам не делится. Ссылка и alt живут в md, поэтому для разных языков можно указать разные файлы — например скан документа на каждом из языков.

  • ссылки в md относительные от корня: media/services/design.png;
  • alt обязателен. Картинка без alt — предупреждение сборки;
  • HERON сам делает варианты под размеры экранов, конвертирует в WebP и AVIF, проставляет width, height, srcset, loading="lazy" кроме самой крупной картинки первого экрана (LCP). Оригиналы не трогаются;
  • в сборку попадает только то, на что есть ссылка. Пропорции из images.ratios — это то, что тема может попросить; режется то, что шаблоны попросили через picture() и media_url(). Запасной файл в исходном формате — один на пропорцию, шириной images.fallback_width (по умолчанию 800). Мастер целиком едет в сборку, если на него ссылаются (media_url(src), og:image, картинка из текста страницы), иначе его там нет;
  • og_image не режется и не конвертируется — соцсети ждут JPEG или PNG 1200×630.

Папка ресурсов называется media

Раньше она называлась img. Имя обещало только изображения, поэтому видео класть было некуда — оно оседало в static/ и ехало в сборку мимо всякой обработки. Теперь в media/ лежит и то, и другое.

Переезд — разовый проход по сайту, и пока он не сделан, сайт обязан собираться: ронять чужую работу ради своего переименования HERON не вправе. Поэтому:

  • есть media/ — тихо, это норма;
  • есть только img — собирается как раньше, одно предупреждение о переименовании;
  • есть обе — E018, сборка останавливается. Это не переходное состояние, а развилка: в какой папке правда, HERON не знает, а гадать дороже;
  • есть media/ и при этом static/media — тоже E018: статика копируется в корень, и обе папки метят в одни адреса. Совпадение имён там — тихая перезапись, которую замечают уже в сети.

Пути внутри сайта пишутся от корня сайта: media/examples/photo.jpg. Форма ./media/… тоже принимается.

Где HERON ищет файлы

Картинка узнаётся по значению, а не по имени поля: имена HERON не угадывает, тема вправе назвать своё поле как угодно. Значением считается путь, который начинается с папки медиа и заканчивается известным расширением; строка из прозы под это не попадает, потому что проверяется целиком, а не вхождением.

В media/ лежит любой файл контента, а не только изображение: ролик, памятка в PDF, презентация. Изображения HERON режет под пропорции и конвертирует, всё остальное копирует как есть — список обрабатываемых расширений остаётся явным, но файл не из него не ошибка. Иначе каждый новый тип файла требовал бы правки HERON, а до неё оседал бы в static/ мимо всех проверок.

В сборку попадают только объявленные файлы. Лежащее в media/, но никем не упомянутое, никуда не едет и попадает в отчёт строкой «файлы в media, которых никто не ждёт»: это не ошибка — мастер часто кладут раньше, чем пишут страницу, — но и не повод копить в папке старые версии годами.

Файл тяжелее 10 МБ даёт предупреждение с именем страницы, независимо от типа. Это не ошибка: бывает и репортаж на двадцать минут. Но узнать об этом человек должен на сборке, а не от посетителя с мобильным интернетом.

Ищутся файлы везде, где тема может их объявить:

  • поля image и og_image;
  • любое дополнительное поле фронтматтера, на любой глубине: строка, карта, список, список карт, карта со списками внутри. Раскладывать пути и подписи в два параллельных списка, связанных только порядком, больше не нужно;
  • тело страницы — обычные ссылки на картинки в markdown;
  • справочники data/: сквозные блоки живут там, и картинка оттуда такая же настоящая. В сообщении об ошибке владельцем назовётся файл справочника.

Глубина обхода ограничена: YAML-якоря дают общие объекты, и предел дешевле, чем однажды ловить зациклившийся обход.

Значение с собственной схемой (https://…, //…) картинкой сайта не считается — это чужой файл, и искать его на своём диске HERON не идёт.

Опечатка в пути даёт E007 с именем страницы — и во вложенном поле тоже. Раньше такая картинка терялась молча: ни ошибки, ни предупреждения, просто пустое место на странице.

Что не надо резать

Тема вправе сказать, что файлы по такому-то пути трогать не нужно:

images:
  ratios: ["1:1", "16:9", "4:3"]
  widths: [400, 800, 1200, 1600]
  formats: [avif, webp]
  as_is: [media/docs/]

Такой файл копируется в сборку как есть: ни вариантов, ни конвертации. Скан документа не иллюстрация — его открывают целиком и в одном виде, адаптивные размеры ему не нужны. Три соотношения на четыре ширины в двух форматах — это два десятка файлов на каждый скан, которые никто никогда не запросит, и минуты сборки на каждую дюжину.

Путь пишется началом, как и crop. Проверка существования, E007 и предупреждение о весе работают как для любого другого объявленного файла.

Адрес файла для темы: media_url

Раньше тема умела получить от HERON только готовый элемент picture. Адрес файла как такового было не получить — а он нужен постоянно: ссылка «открыть скан», href модального окна, адрес для скачивания, картинка в разметке для соцсетей, ссылка на документ из текста. Без него файл дублировали в static/: два места, которые надо не забыть поправить вместе.

{{ media_url('media/examples/scan.webp') }}
{{ media_url('media/examples/photo.jpg', ratio='4:3', width=1200) }}
{{ 'media/examples/leaflet.pdf'|media_url }}
  • без параметров — адрес самого файла в сборке. Мастер копируется в сборку рядом с вариантами именно ради этого;
  • с ratio — адрес нарезанного варианта; width берёт ближайший размер не меньше запрошенного, а если таких нет — самый крупный: отдать крупнее и дать браузеру сжать честнее, чем показать мыло;
  • format выбирает формат варианта; без него — исходный формат мастера, потому что это адрес для ссылки и скачивания, а не для picture;
  • запрошенного варианта нет — предупреждение с путём и адрес исходника. Молча отдавать пустую строку нельзя: именно так теряются картинки, и замечают это глазами через неделю после выката;
  • файла нет на диске — та же ошибка E007, что и для любого объявленного файла;
  • работает и для файлов, которые HERON не обрабатывает: PDF, ролик, архив.

Видео

video: media/examples/tour.mp4
video_poster: media/examples/tour-poster.jpg
video_title: "Обзор мастерской"

В шаблон это приходит готовым объектом page.video с полями src, poster, title и external. Тема не разбирает схемы адресов сама: сегодня это свой файл, завтра сторонний хостинг, и каждая тема училась бы этому заново.

  • значением video может быть внешняя ссылка — различается по схеме адреса. Локальный путь отдаётся файлом, полный адрес — вставкой стороннего плеера. Список площадок не зашит;
  • video_poster обязателен при локальном файле, иначе E019. Без постера браузер тянет первый кадр, то есть грузит видео до того, как посетитель нажал «играть»; на мобильном интернете это десятки мегабайт за просмотр страницы, которую пролистали дальше. У стороннего плеера свой постер — там ничего не требуется;
  • видео не перекодируется и не режется, копируется как есть. Проверяется наличие файла и наличие постера;
  • нет поля video — page.video пустой, и блок не рисуется;
  • секция {#video} в контенте даёт подпись и описание, сам файл берётся из фронтматтера.

Иллюстрация вписывается, фотография кадрируется

В репозитории лежит один файл на сущность — мастер любого соотношения с прозрачным фоном. HERON вписывает его в каждое соотношение, которое просит тема, и добивает прозрачными полями до нужного размера:

4:3    мастер, лежит в media/ — соотношение любое
16:9   карточка в каталоге и на главной — вписывается, поля прозрачные
1:1    квадратный слот — вписывается, поля прозрачные

Кадрировать иллюстрации запрещено. Фотографии — портреты, интерьеры, работы — режутся центральным кропом, как раньше: у них нет альфа-канала, и поля им взять неоткуда. Признак один: есть альфа — вписываем, нет — кадрируем. Тема вправе назвать исключения явно.

Предупреждения «объект упирается в край мастера» больше нет: при вписывании это перестало быть проблемой.

Почему так. Квадратный мастер с запасом по краям — требование, которое иллюстраторы не выполняют: рисунок почти всегда доходит до границы кадра, и любой центральный кроп его срезает. Перерисовывать файлы под каждую пропорцию дороже, чем вписывать, а вписывание даёт тот же результат без запаса по краям.

Прозрачность и темы

Иллюстрации хранятся с альфа-каналом (PNG или WebP) и кладутся на подложку, которую даёт тема. Следствия:

  • JPEG для иллюстраций запрещён — нет альфы, белая плашка останется намертво и в тёмной теме будет светиться прямоугольником. JPEG остаётся только для фотографий;
  • заливать фон в цвет темы нельзя — это тот же прямоугольник, просто в цвет;
  • белый фон допустим только как промежуточный этап генерации: модели проще нарисовать чистый контур на плоском фоне, чем сразу выдать корректную альфу. В репозиторий уходит вырезанное;
  • второй файл под тёмную тему не делается. Одно изображение работает в обеих, если тело объекта светлое, а контур средней светлоты: тёмный контур на тёмном фоне даёт около 2:1 по контрасту, и силуэт расплывается. Конкретные тона задаёт документ темы, а не этот;
  • подложка под иллюстрацией всегда нейтральная. Карточка в цвет объекта съедает силуэт;
  • фотографии в тёмной теме не инвертируются и не затемняются фильтром.

static — запасной выход, а не вторая папка для картинок

static/ копируется в dist/ байт в байт по тому же пути: без обработки, без проверок и без упоминания в контенте. Это её единственное назначение и единственная причина существования.

Она нужна файлам, которые обязаны лежать по точному адресу и которые HERON трогать не должен:

  • подтверждение прав вебмастера;
  • .well-known/security.txt;
  • ads.txt, проверочные файлы платёжных систем;
  • site.webmanifest, favicon.

Картинкам, видео и документам там не место — их дом media/, где они получают проверку на существование, варианты под размеры экранов и внятную ошибку при опечатке. Оформлению сайта — логотипу, иконкам интерфейса, шрифтам — тоже не место: их дом theme/assets/, потому что это часть темы, а не контента.

Правило держится не на честном слове: heron check называет файл с расширением картинки или видео, найденный в static/, и предлагает перенести его в media/. Без этого через полгода там снова окажутся фотографии — положить файл в обход обработки всегда быстрее, чем объявить его в контенте.

Пример. Google просит подтвердить права на сайт файлом в корне. Положите его в static/, и после сборки он отвечает по тому же адресу:

static/google1a2b3c4d.html          →  https://example.com/google1a2b3c4d.html
static/.well-known/security.txt     →  https://example.com/.well-known/security.txt

Пустая static/ — нормальное состояние сайта. У большинства сайтов там не лежит ничего.

Чего тема ждёт от HERON

Тема живёт в репозитории сайта и обновляется отдельно от HERON. Поэтому она объявляет свои требования к нему прямо в theme.yaml:

heron: ">=0.1"          # версия HERON, на которую рассчитана тема
fields: [nav_title]     # поля фронтматтера, к которым обращаются шаблоны

HERON проверяет это на первом этапе, до обхода контента. Версия не та — E012; в fields поле, которого HERON не знает, — E023 со списком полей, а не по ошибке на каждой странице с бесполезной подсказкой «опечатка во фронтматтере».

fields — только поля самого HERON (таблица фронтматтера). Свои поля темы (topic, eyebrow) туда не пишут: HERON их не знает и знать не должен. В шаблоне они доступны как page.meta.get('topic').

Оба ключа необязательны. Тема, не объявившая ничего, собирается как раньше — до первого расхождения, которое тогда вылезет при отрисовке.

.env

Сборке секреты не нужны ни одного. Она идёт без сети и читает только файлы сайта. .env принадлежит сервисам — heron-relay, позже админке — и в контейнер сборки не монтируется никогда.

Файл закладывается сразу, чтобы потом не переделывать.

SMTP_HOST=
SMTP_PORT=
SMTP_USER=
SMTP_PASSWORD=
TELEGRAM_BOT_TOKEN=
ADMIN_USER=
ADMIN_PASSWORD_HASH=
SESSION_SECRET=
DB_PATH=

Правила обращения:

  • .env в .gitignore и в .dockerignore. В репозитории лежит .env.example с пустыми значениями;
  • монтируется файлом, не через environment: и не через --env-file: переменные окружения видны в docker inspect любому с доступом к сокету, смонтированный файл — нет;
  • читается в момент использования, не при старте;
  • монтируется только в тот контейнер, которому нужен. nginx про почту не знает;
  • пароль администратора хранится хешем, не открытым текстом;
  • секреты никогда не попадают в логи. Самая частая утечка — не взлом, а трейсбек при ошибке подключения.

Отдельно: для отправки почты брать API-ключ почтового сервиса, а не SMTP-пароль от ящика. Утечка ключа — спам с домена. Утечка пароля от ящика — доступ ко всей переписке владельца.

Что генерируется, а что лежит руками

Генерируется HERON в dist/, в репозитории отсутствует:

sitemap.xml (на многоязычном сайте — индекс и по файлу на язык) · robots.txt · llms.txt (на многоязычном — и по файлу на язык) · 404.html · redirects.map для nginx · JSON-LD внутрь страниц · hreflang и канонические адреса · feed.xml, когда появятся статьи.

Лежит руками в static/: favicon и его варианты · site.webmanifest · .well-known/security.txt.

robots.txt, положенный руками, через полгода начинает врать: адреса меняются, а файл — нет. С картой сайта, которую строит сторонний плагин, происходит то же самое.

Ограничения, которые нельзя нарушать

  • Никакого JavaScript для отображения контента. Скрипты обслуживают только тему, куки-баннер, мобильное меню и аккордеон FAQ. Числа и текст обязаны быть в HTML: счётчик, который рисует скрипт, для робота стоит нулём;
  • одна страница — один H1, иерархия заголовков без пропусков уровней;
  • пустой блок не выводится. Заголовок без содержимого хуже отсутствующего;
  • факты не выдумываются. Данные, которых заказчик не подтвердил, остаются отсутствующими и попадают в сводку долгов, а не заполняются правдоподобным текстом. Пустой блок лучше вымысла;
  • целевое действие доступно с любой страницы — это требование темы, и тема отвечает за то, чтобы оно выполнялось на каждом шаблоне.

Источник

Эта страница повторяет разделы 7–10 файла docs/spec/20-data-contract.md из ветки prod. Репозиторий обновляется первым: открыть файл на GitHub.