shikhalev.org
Скриншот с [официального сайта Jekyll](https://jekyllrb.com/)
Скриншот с официального сайта Jekyll

Итак, я таки отрефакторил и обновил данный сайт. Почему нельзя было сразу делать правильно? Ну, в основном потому, что я впервые имел дело с Jekyll, изрядно подзабыл (а что-то и не знал) базовые приемы верстки… И так далее, и тому подобное.

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

Но на самом деле этот пост не только, и не столько о рефакторинге как таковом, сколько о технической стороне этого сайта в целом. Благо, сразу после выкатки первого варианта я так технический пост и не написал, желая сначала получше разобраться. Вот, сейчас и пишу о том, с чем разобрался, и о процессе этого разбирательства.

Выбор технологий

Бэкенд

Сайт сделан на статическом генераторе Jekyll и размещен на GitHub Pages, чтобы не платить за хостинг. Комментарии — Staticman, размещенный на Heroku также бесплатно. Собственно, бесплатность была для меня необходимым критерием при выборе технологии, поскольку блогом я не зарабатываю.

За все время существования технобложика, гуглореклама, крутящаяся там, не докрутилась до минимально выводимой суммы, даже не приблизилась. Поэтому сейчас при рефакторинге я ее и убрал совсем (хотя в скрытых элементах верстки и оставил — место под нее, но не ее саму — и если вдруг посещаемость взлетит на несколько порядков, могу вернуть). Блок-по­про­шай­ка сработал пока 1 (один) раз за все время. Что приятно, но тоже не позволяет говорить о профите или окупаемости.

Почему не ЖЖ, Blogger, Wordpress и так далее?

Несколько факторов:

Возможности контроля верстки — я хочу, чтоб мои посты выглядели так, как я хочу. В ЖЖ (и DW) эти возможности вообще крайне скудны. В Blogger и Wordpress они достаточно богаты, но требуют серьезных усилий, чтобы навесить свое оформление поверх сложных механизмов движка.

Более того, мне нужно одновременно иметь контроль над версткой и возможность о ней не задумываться при написании текстов, то есть разметку типа Markdown и автоматическую систему раскраски исходного кода. И еще что-нибудь вроде макросов для тех случаев, когда маркдауна не достаточно.

Возможность легкого и быстрого «перезда» на случай, если мои тексты вступят в конфликт с политикой хостера, или его самого заблокируют. Увы, в последнее время угроза бана на любом сервисе вполне реальна, для этого не требуется нарушать законы, достаточно стука со стороны «прогрессивной общественности». Ну, а российские блокировки и вовсе лупят по площадям, как в копеечку…

И хотя упомянутые сервисы и позволяют делать какие-то архивы и бэкапы, развернуть их где-то еще не получится. Исключение — Wordpress, который можно поднять и на своем сервере, но учитывая его сложность и системные требования, быстрым и легким такой переезд точно не назовешь.

Еще важное пожелание — контроль версий. Git, развертывание сайта из репозитория, человекочитаемая история изменений, и так далее, и тому подобное.

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

Почему не свой сервер с самописным бэкендом на рельсах или другом фреймворке?

Тут просто — цена. Причем, кроме затрат на хостинг нуж­но еще учитывать время на разработку и администрирование. Хотя, конечно, сейчас все это не так уж и дорого и, как вариант «в случае чего», я вполне рассматриваю переезд на ненавороченный VPS/VDS, причем именно в текущем виде, со статической генерацией, мне достаточно будет са­мо­го-пре­са­мо­го дешевого.

При всем при этом мне не нужна логика на бэкенде, динамически формируемые страницы, вот это всё. Даже комментарии, в об­щем-то, вторичны. Таким образом, все пути ведут к генераторам статических страниц.

Почему именно Jekyll?

Ключевое его преимущество — интеграция с GitHub Pages. То есть схема работы не «сгенерировал страницы из рабочей копии репозитория и отправил на сервер», а «написал, закоммитил, запушил» — публикация происходит автоматически при фиксации изменений, исходники в репозитории и сайт всегда синхронизированы, то есть не возникает ситуация, когда в репозиторий закоммитил, а опубликовать забыл, или, что еще хуже — опубликовал, не закоммитил, и в историю изменений реально работающая версия не попала. Аналогичным образом можно организовать публикацию и через GitLab (там, впрочем, вариантов побольше, но первичный расчет все же на GitHub, поскольку я им давно активно пользуюсь). На собственном хостинге эти фазы будут разделены, но во-пер­вых, см. выше про первичный расчет, а во-вто­рых, можно и там такое организовать через хуки Git.

Второй важный аспект — это то, что Jekyll является свободным ПО, его код открыт и опубликован под ли­цен­зи­ей MIT. Что, впрочем, характерно и для многих его аналогов с точностью до конкретной лицензии…

Менее существенно, но субъективно тоже приятно, что Jekyll написан на Ruby — одном из моих любимых языков. Что означает реальную, а не гипотетическую возможность воспользоваться преимуществами open source с моей стороны. Реализую ли я ее — это отдельный вопрос, однако мое знакомство с пакетной инфраструктурой Ruby — плагины Jekyll распространяются именно как гемы — явно мне помогло в его установке и настройке.

Что же касается недостатков и подводных камней, далее будет еще много подробностей…

Почему именно Staticman?

Для начала отмечу, что требования к системе комментариев у меня самые минимальные. И не могу сказать, чтобы я уделил этому вопросу много времени. Собственно, ключевым преимуществом Staticman стало простое развертывание на бесплатном аккаунте Heroku вкупе с возможностью также быстро и просто «в случае чего» развернуть его и на своем сервере (в наличии docker-образ).

Кроме того:

  • Комментарии хранятся в том же репозитории, что и сайт. Можно и в другом, факт в том, что это будет а) git-ре­по­зи­то­рий, б) под моим управлением.

  • Прикручивается защита от спама. Есть два варианта: re­CAPT­CHA и A­kis­met, и со вторым я пока не разбирался, надеюсь, руки еще дойдут. О недостатках и проблемах рекапчи мне известно.

  • Визуальное отображение не просто кастомизируется, а вообще полностью находится в зоне ответственности владельца сайта.

  • Исходники открыты, под лицензией MIT.

Из недостатков стоит отметить:

  • Привязка к GitHub/GitLab. Это касается, конечно, не хостинга (Pages), а репозитория — Staticman для добавления комментариев использует API этих сервисов, а не пуши Git. Если и там, и там забанят, придется искать другой сервис комментариев. Впрочем, для своего хостинга их полно, да и собственный пишется на коленке легко и просто. А уже имеющиеся комментарии никуда не денутся — они в репозитории.

  • Нет авторизации. Кто угодно может писать под каким угодно именем.

  • Уведомления сделаны через Mailgun, который бесплатных тарифных планов не имеет (есть триальный, но это ж временно). Я не стал подключать. Вместо этого я в новой версии сайта добавил отображение последних (глобально) трех комментариев в отдельной панели на всех1 страницах.

В целом, вопрос идеальной системы комментариев остается открытым… Но все более-менее интересные варианты, с авторизацией через соцсети и т.д. — платные. Опять же, нельзя сказать, чтоб они были дороги, но сайт-то полностью непрофитный. В случае же переезда на платный хостинг, имеет смысл вернуться к этому вопросу именно в плане свободного софта, неподходящего для бесплатных хостингов, но подходящего для VPS/VDS.

Фронтенд

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

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

  • Верстать по старинным стандартам, что му́ка само по себе и, кроме того, получается крайне несемантично, то есть недружественно к современным поисковикам.

  • Использовать фронтенд-фреймворки и переложить верстку на JavaScript, густо обмазавшись слоями совместимости. Ма­ло­ос­мыс­лен­но по трудозатратам и непредсказуемо по реакции поисковиков (они, конечно, сейчас умеют выполнять скрипты, но неизвестно, где их переклинит).

  • Использовать простую современную семантическую верстку и позволить старым браузерам спокойно деградировать. В результате 1–2% посетителей получат не совсем то визуальное представление, что задумывалось, но спокойно смогут прочитать тексты и посмотреть картинки. А большинство увидит именно то, что и нужно, сразу адекватное их устройству, без скачивания (и исполнения) тонн ненужных скриптов совместимости.

Мы пойдем третьим путем и будем использовать: HTML5 ради семантических тегов, CSS3 ради удобной верстки посредством flex и grid, а так­же JavaScript по ми­ни­муму2.

Внешние ресурсы

Из таковых у меня подгружаются шрифты с Google Fonts — это свободно распространяемые шрифты Paratype — семейство PT Sans и PT Mo­no. На то есть две причины: во-пер­вых, на разных устройствах под разными операционными системами имеющиеся шрифты сильно разные, а во-вто­рых зачастую ужасные и не согласованные между собой. Мне же хочется, чтобы, скажем, примеры кода (моноширинным шрифтом) не выбивались по общему виду — метрикам, насыщенности и т.д. из основного текста. Аналогично согласованность желательна и для шрифтов заголовков с одной стороны, и сносок — с другой.

Еще что касается шрифтов: также как веб-шрифт я подгружаю Font Awesome Free — уже не снаружи, а копию на сайте. Этим шрифтом сделаны монохромные иконки в оформлении сайта. Цветные в основном тексте — это уже картинки (по возможности SVG), набранные из разных источников3.

Кроме того, из внешних источников подгружаются следующие, условно говоря, модули:

Они, конечно, тянут за собой много всякого, но все-таки полезные.

Разработка

Совсем пошагово описывать процесс я не буду. Дело это достаточно скучное, пройдусь по тем моментам, которые чем-то интересны, или вызвали у меня затруднения.

Jekyll

Во-первых, я при этом рефакторинге отказался от готовой темы. Изначально я использовал дефолтную для Jekyll тему minima, обвешивая ее уже ка­ки­ми-то своими дополнениями. Это, в принципе, правильный путь — взять готовое, причем максимально простое, и дорабатывать, но итоговая верстка достигалась грязными хаками и для адаптации под мобильные годилась мало (хотя сама тема вполне адаптирована).

Могу заметить, что никаких проблем с неиспользованием темы не обнаружилось. Собственно, движку без разницы, что обрабатывать.

Во-вторых, язык шаблонов Liquid — это что-то с чем-то. И мне, честно говоря, не понятно, почему в Jekyll используется именно он, а не, например, ERB, идущий в комплекте с Ruby, на котором сам Jekyll написан… Возможно, дело в том, что ERB слишком богат — по сути это полноценный Ruby, встраиваемый в шаблоны. А у Ruby с точки зрения встраивания куда-либо есть один недостаток: невозможность сделать полностью изолированную песочницу. Хотя, на мой взгляд, имеющихся уровней изоляции должно быть достаточно… Так вот, Liquid — система очень ограниченная.

В языке есть, в принципе, объекты или структуры, но создать их нельзя. Есть массивы, но чтобы создать массив, нужно пропустить пустую строку, через фильтр split5. Вообще всё делается через фильтры и выглядит это несколько странно. Точнее, в выражении подстановки {{ ... }} это выглядит хорошо, а вот если мы хотим присвоить значение переменной — несколько избыточно, особенно в инструкции {% assign ... %}.

Например, мы можем сделать так:

<span class="{{ include.class }}">{{ include.title }}</span>

Но если мы захотим передать такую составную строку как параметр в какой-то другой {% include ... %}, то вынуждены сначала присвоить ее переменной. И выглядеть это будет так:

{% assign var = '<span class="' | append: include.class
                                | append: '">'
                                | append: include.title
                                | append: '</span>' %}

Впрочем, не все так страшно — можно сделать так:

{% capture var %}
<span class="{{ include.class }}">{{ include.title }}</span>
{% endcapture %}

Но выглядит довольно непривычно.

Еще один момент — и это уже хорошо прикрытые грабли — то, что нет областей видимости и, соответственно, локальных переменных. Единственный способ организовать нечто подобное — это те самые параметры {% include ... %}, с ними можно даже использовать рекурсию6, других-то подпрограмм у нас нет. Тот факт, что при общей глобальности всех переменных, параметры include не перезатирают друг друга при вложенных вызовах, оказался для меня приятным сюрпризом и большим удобством.

В-третьих, непонятки случаются и с объектами, предоставляемыми Jekyll. Например, объект page почему-то не предоставляет свойство name, хотя оно есть в документации (а еще лучше было бы basename), поэтому для страниц рубрик, то есть «категорий» в терминологии Jekyll, приходится задавать вручную свойство category_id, хотя поначалу хотелось обойтись просто именованием файлов.

И, last but not least, принцип Jekyll: один входной файл дает один выходной, сгенерировать несколько представлений для одной страницы мы не можем. Есть, правда, объект paginator, но он работает только со всеми постами сразу, без категорий, или какой-то еще фильтрации. Отсюда и такое мое решение, когда последние посты показываются, как принято в блогах, до ката, а дальше одна страница с хронологией и только заголовками.

Вообще, конечно, Jekyll отлично расширяется, даже в части тегов Liquid, посредством плагинов, написанных на Ruby. И всё бы было хорошо, если б не ограничения GitHub Pages — там допустимы только плагины из заранее определенного списка (и еще версия Jekyll не самая распоследняя7). Приходится довольствоваться малым. Или, как вариант, переходить на ло­каль­ную генерацию, отправляя на Pa­ges только финальный результат.

Markdown

Нетрудно догадаться, что маркдаун у меня активно перемежается с разной более сложной версткой. И в первую очередь, это картинки, обтекаемые текстом. На этой итерации я вынес их формирование в отдельный include и обернул в тег <figure> для семантичности. При этом столкнулся с таким моментом: маркдаун упорно пытается обернуть теги <img> внутрь тегов <p>, да еще и лажает с последовательностью закрывающих тегов (получается что-то типа <figure><p>...</figure></p>) и вся верстка рушится. Вообще, по умолчанию такого бы не происходило, но у меня вручную в _config.yml8 выставлен параметр kram­down.​parse_​block_​html в true, поскольку как правило парсинг внутри блочных тегов мне нужен. Однако, его можно запретить и локально, указав в самом теге «атрибут» markdown="0". При этом, как выяснилось, внутри во вложенных тегах, это поведение можно и скорректировать, так что в <figcaption> я выставил markdown="span" (то есть обрабатывать только inline-разметку, не добавляя никаких <p>) и доволен, как слон9.

Оглавление

Очень полезный модуль allejo/jekyll-toc, я даже в футере на него решил ссылку сделать, если вы читаете эту страницу на достаточно большом экране, обратите внимание на соответствующий блок в правом верхнем углу (куда его вписать на узких экранах, я пока не решил). Также рекомендую обратить внимание на его параметры, в частности h_max и skip_no_ids. Последний позволяет игнорировать заголовки без атрибута id (или с пустым), чего можно добиться в mark­down указав {: id=""} непосредственно перед строкой заголовка.

Верстка

Grid & Flex

Это настолько удобные и правильные способы верстки, что совершенно не понятно, почему до сих пор не все ими пользуются. Особенно сейчас, когда реально требуется поддержка разных устройств. Выбор между ними прост: flex для однородных элементов, grid — для разнородных.

Единственная закавыка, которую я могу упомянуть — это невозможность рисовать границы сквозь gap, в результате чего у меня в основном макете вместо простого указания этого свойства между основным текстом и правой панелью введена дополнительная пустая фиксированная колонка. Если бы я не изгалялся с забрасыванием на средней ширине блока поиска в заголовок, можно было б так не заморачиваться, наверное… А может быть, я че­го-то просто не знаю.

Картинки и фреймы

Когда мы заворачиваем <img> или <iframe> в <figure>, важно поставить им dis­play:​block;​width:​100%;, а фрейму еще и подходящее значение as­pect-ra­tio.

Причем проблема с тоненькой полосой между нижней границей фрейма или картинки и нижней границей оборачивающего блока, судя по результатам гугленья, возникает у многих, но отвечают им в основном банальности про отступы (которые, конечно, контролировать нужно, но это первое, что человек проверяет), а про display:block; не пишут. Тогда как реально эта тонкая полоска — расстояние от baseline до предела выносных элементов шрифта, и можно, конечно, вылечить ее выставлением правильного vertical-align, но не нужно, изменение display тут более адекватно.

Что касается width:100%; (или max-width) тут все достаточно очевидно — это свойство нужно, чтобы можно было указывать конкретную ширину только один раз — для окружающего блока. Альтернативой могло бы быть использование изображений с уже подогнанными размерами… Но, как можно видеть, например, в посте «О ресайзе PNG на примерах», в некоторых случаях (и не редких — скриншоты в формате PNG) уменьшенное изображение весит больше, и существенно, чем исходное, при этом уменьшение средствами браузера не сказать, чтоб уступает предварительной подготовке.

Ну, а уж для фреймов, скажем, гуглокарт10 выставление dis­play:​block;​width:​100%;​as­pect-ra­tio:​1/1; просто единственный способ получить адекватную адаптивную верстку.

Иконки

Они у меня имеются в двух видах:

Шрифтовые

посредством Font Awesome — для маркировки категорий и прочей служебности. Главный плюс — легко стилизуются, в частности по цвету. Главный минус — ограниченный набор.

Картинками

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

Если шрифтовые прекрасно работают как через псевдоэлемент ::before для тегов <a> и <span>, так и ::marker для элементов списков, то картинки в качестве маркера списка отображаться у меня отказались. Вероятно, я чего-то недопонимаю.

Единственный способ сделать так, чтобы иконки не отрывались от собственно элемента, которому они предшествуют — задать этому элементу, всему, white-space:nowrap;. А если он длинный и его нужно переносить внутри? Приходится делать дополнительный внутренний <span>, совершенно не семантичный. Это, вероятно, как-то связано с тем, что свойство content у них пустое, а картинка грузится через background-image, т.е. в качестве фона, а иначе не хочет корректно масштабироваться. Отсюда и еще один косяк — по умолчанию их не видно на печати, нужно ставить галочку в диалоге «Печатать фон» (а кто это будет делать?).

Еще один момент: некоторые SVG-иконки некорректно отображаются браузерами (как Firefox, так и Chrome) — например, найденные мной на Википедии иконки Dark­tab­le и Lu­mi­nan­ce HDR (сама Википедия традиционно использует рендеринг SVG в PNG нескольких размеров, ей не мешает).

Чтобы упростить создание стилей для иконок я воспользовался структурами map и циклами SASS/SCSS, что можно посмотреть в исходниках в файле icons.scss. Вообще, я был раньше знаком с SASS/SCSS, но как-то недооценивал, насколько это удобно и экономит усилия, особенно при рефакторинге.

Стихи

Собственно, все, что относится к верстке стихов, расположено в одном файле verse.scss, и он небольшой. А ключевая часть выглядит так:

.verse {
  display: table;
  margin: 0px auto;
  clear: both;

  p {
    text-align: left;
    hyphens: manual;
    white-space: pre;
  }
}

Печать

Тут понадобилось обратное действие — разрешить переносы в фрагментах кода, тогда как на экране слишком широкий <pre> имеет горизонтальную прокрутку11.

pre {
  overflow-x: auto;

  @media print {
    overflow-x: visible;
    white-space: pre-wrap;
  }
}

Еще для печати получилось по рецепту из интернетов12 сделать watermark, хотя это, конечно, чистые понты. И не получилось найти рабочий рецепт для нумерации страниц. Вообще, такое ощущение, что стилизация печати настолько никому не нужна, что никто ей и не занимается… Так что, если печатными версиями статей заморачиваться всерьез, стоит генерировать из них PDF не через HTML, а через LaTeX посредством какого-нибудь pandoc.

Разное

Для адекватного отображения на устройствах с разной плотностью пикселей нужно не забывать про метатег viewport.

<meta name="viewport" content="width=device-width, initial-scale=1">

Для переноса длинных URL (или других длинных последовательностей букв, где нежелательно использовать символ мягкого перноса &shy;), можно использовать пробел нулевой ширины — &#x200B; (HTML-мнемоники он, к сожалению, не имеет). Что до меня, то я его настроил на клавиатуре как [L3]+[B], см. пост «Ввод «типографских» символов с клавиатуры (ed. 2021)», а мягкий перенос — как [L3]+[N]. Буквы выбраны просто из со­об­ра­же­ний удобства, без ка­ко­го-ли­бо смысла или мне­мо­ни­чес­ко­го правила.


В CSS есть интересный псевдокласс :target, который позволяет выделить элемент, на который ведет ссылка (#id). Для примера посмотрите на блок подписок. Выше по тексту есть еще ссылки на другие блоки.


Сделал нынче таки кнопку возврата к началу страницы, см. topper.js, topper.scss и default.html:86-87. Очень уж не хватает на мобильном кнопки [Home], да и на компьютере бывает удобнее мышкой ткнуть, когда просто читаешь что-то и колесиком скроллишь.

В связи с этим познакомился с position:sticky;.


На этом, наверное, все. Напомню, что исходный код сайта открыт и доступен для изучения на GitHub. Если что-то в коде непонятно, комментарии к этому посту — подходящее место для вопросов.

  1. На самом деле — не на всех… В разделе «Тексты» я этот блок отключил как отвлекающий. Но это такой очень специальный раздел. 

  2. Сейчас у меня JS используется в двух местах: это обслуживание комментариев (там без него не получится, или получится крайне неадекватно, организовать ответы друг другу, и плюс попытка подтягивать обновленные комментарии в фоне) и кнопка «», то есть «Вернуться в начало страницы». См. comments.js и topper.js соответственно. 

  3. Впрочем, один из источников стоит особой благодарности — https://devicon.dev/

  4. Бывшие Ян­декс.День­ги. 

  5. Пример такого создания пустого массива см. в last_comments.liquid:3 — дело в том, что комментарии хранятся сгруппированные по постам, тогда как в данном месте мне нужно получить их общий список. Возможно, имеет смысл переделать саму схему хранения, но пока так. 

  6. Пример рекурсии см. в chain.liquid:3 — там я рекурсивно получаю цепочку родительских категорий от текущей к корню. 

  7. Очень желательно эту самую версию зафиксировать и для локальной разработки через Gemfile, и/или добавить там зависимость от гема github-pages, который зафиксирует и версию Jekyll, и версии используемых плагинов. Иначе может получиться так, что прекрасно работающий локально сайт свалится на деплое. 

  8. См. файл в репозитории

  9. См. исходники image.liquid:39-50

  10. Пример внедрения гуглокарты см. в посте «Мини-экспедиция». 

  11. См. исходники code.scss:10-17

  12. Сейчас не могу найти, откуда срисовывал, но практически то же самое тут: https://csslayout.io/patterns/watermark/. Как это выглядит у меня можно посмотреть в файлах: watermark.scss — стили, и, великом и ужасном default.html:25-31 — разметку.