Jekyll — генератор статических сайтов. Это означает, что на вход ему даётся какая-либо информация, а на выходе получается набор HTML-страничек. Всё отлично когда сайт простой или даже одностраничный. Но что насчёт более сложных сайтов? Справится ли Jekyll? Будет ли удобно?


Данная публикация — попытка обобщить знания, полученные при создании нескольких веб-сайтов. Поэтому оставляю ссылки и на рабочие примеры, и на их полные исходники на GitHub. Уровень материала идёт от простого к сложному.


На хабре уже было несколько публикаций по Jekyll: Практическое руководство по Jekyll, Блог на Jekyll и Github, Jekyll 2 надвигается на Github!. Однако все они описывают в основном базовые возможности Jekyll. Кроме того, на GitHub Pages уже используется Jekyll 3, что привнесло новых плюшек. Итак, начнём!


Статический HTML


Сервис Github Pages использует Jekyll как генератор сайтов. Может ли GitHub Pages использован самостоятельно, без Jekyll? Да! Можно использовать GitHub Pages как простой HTML-хостинг (бесплатно!). Особенно, если сайт уже был где-то сгенерирован.


Веб-демо Winter Novel


Например, Веб-демо Winter Novel было сгенерировано в С-приложении. Это просто пачка простых HTML-страниц, расположенных на GitHub Pages. Исходники здесь.


Пользовательские домены


Как было видно выше, существует возможность использовать своё доменное имя вместо *.github.io. И сделать это достаточно просто: нужно создать файл CNAME с единственной строкой — доменом:


winternovel.dexp.in

После коммита установленный домен так же можно видеть в настройках репозитория:


GitHub Pages: настройки репозитория


Можно использовать домены любого уровня, а не только третьего. Например, dexp.in тоже хостится на GitHub'e.


CloudFlare


Другой отличный бесплатный сервис — CloudFlare. Ведь в любом случае нужен менеджер DNS для домена. А CloudFlare предоставляет не только инструменты для управления DNS, но и CDN-сервис. Это значит, что CloudFlare может кэшировать страницы и отображать их даже если GitHub Pages будет недоступен.


dexp.in на CloidFlare


Главный домен напрямую по IP привязан к GitHub. Субдомены реализованы через cname-альясы на dexp.github.io.


Markdown


Jekyll поддерживает и HTML и Markdown. HTML даёт больше возможностей, но не слишком удобен для написания человеком. Markdown — очень крутая штука для написания текстов.


Пример куска текста на HTML:


<p>Creating a website in Manga Maker Comipo is almost like creating it in Photoshop. That means the program is only responsible for creating the image of the future design. Everything else must be done in other programs and requires skills and knowledge of HTML and CSS.</p>
<p>Let's see an example shown below:</p>

<p class="centered"><img src="{{ page.linkadd }}pic/tutorials/site/OneMangaDay-site-comipo.png" alt="Website in Comipo" class="imgshad"></p>

<p>You can see "Layer List" panel. It already has a stack of layers. You just need to implement this stack in HTML! The most convenient way is exporting each layer as a separate image. Export can be accessed from the menu "File - Export Image File". Also you can press F2. Export options:</p>
<p class="centered"><img src="{{ page.linkadd }}pic/tutorials/site/OneMangaDay-branch-export.png" alt="Export parameters for website miking in Comipo"></p>

Как минимум, нужно добавлять и <p> и </p>… И пример куска текста на Markdown:


Visual novels creating is not such a difficult thing, as it might seem. And RenPy engine will help us: [http://renpy.org](http://renpy.org){:target="_blank"}. On the one hand, the engine is simple and understandable even for beginners. On the other hand, the engine is quite powerful and allows you to create really cool games. You need to download engine and install it. Nothing complicated in this process is not present, the default settings are good. Here is the RenPy main window:

![RenPy main window]({{ page.picdir }}RenPy-main-en.png){:.imgshad}

There is a list of projects on left. And active project options on the right (the active project is highlighted with blue in projects list ). To create your game you need to click "Add New Project" under the list of projects. Further, the engine will ask a few simple questions. Remember the name of the game should be in English (do not use international/unicode symbols).

Markdown-код выглядит намного лучше. Просто попробуйте, не пожалеете.


SASS / SCSS


Sass — это язык поверх CSS. Добавлено много хорошей функциональности: переменные, блоки кода, вложенные правила, include и т.д. Пример SCSS кода с примесями и переменными:


@mixin border-radius($radius,$border,$color) {
  -webkit-border-radius: $radius;
     -moz-border-radius: $radius;
      -ms-border-radius: $radius;
          border-radius: $radius;
    border:$border solid $color
}
.box { @include border-radius(10px,1px,red); }

Код будет транслирован в:


.box {
   -webkit-border-radius: 10px; 
      -moz-border-radius: 10px; 
       -ms-border-radius: 10px; 
           border-radius: 10px; 
   border: 1px solid red; 
}

Полезные ссылки: Sass Basics, Sass Tutorial


Разметка


Ok, чистый HTML это уже хорошо. Но может в Jekyll есть что-то типа include? Тогда можно будет построить свои страницы в стиле:


include header.html
{ моё содержимое }
include footer.html

Да, в Jekyll есть include. Но лучше использовать эту директиву для других целей. Для внешнего вида же лучше использовать шаблоны: Layout.


Пускай исходный код страницы выглядит следующим образом:


---
layout: default
---
{ моё содержимое }

И файл _layouts/default.html:


<html>...
{{ content }}
...</html>

Страница генерирует какой-то код и сохраняет его в переменную content. Jekyll из заголовка видит, что следующим обрабатываемым файлом будет _layouts/default.html. Далее нужно просто вывести содержимое этой переменной в любом месте кода.


Конечно, в реальности код получается более сложным. Например, главная страница One Manga Day и layout для неё.


Переменные


Мы уже затронули тему переменных. Рассмотрим более подробно заголовок главной страницы One Manga Day:


---
layout: default
curlang: en
title: Home page
addcss: badges
---

Переменная layout говорит Jekyll, какой следующий файл будет обрабатываться. Остальные переменные используются в коде layout и созданы для удобства:


<!DOCTYPE html>
<html>
    <head>
        <title>{{ site.name }} | {{ page.title }}</title>

Переменная page.title установлена в заголовке страницы, site.name — в _config.yml. Результатом выполнения этой строки в моём случае будет: One Manga Day | Home page


Коллекции


Предыдущие возможности Jekyll были не очень продвинутыми, даже скорее базовыми. Но коллекции позволяют делать по-настоящему мощные вещи. Например, галереи. И как частный пример — страница с мангой.


One Manga Day: Manga page


Небольшая вырезка из _data/galleries.yml:


- id: screenshots
  description: One Manga Day screenshots 
  imagefolder: pic/scr
  images:
  - name: OMD-s0001.png
    thumb: thumb-1.jpg
    text: Main menu
  - name: OMD-s0002.png
    thumb: thumb-2.jpg
    text: Instructor

В этом коде создаётся коллекция site.data.galleries. Первый знак минуса обозначает создание элемента коллекции. images — субколлекция, каждый элемент который имеет поля name, thumb и text.


И пример, как работать с данной коллекцией, _includes/mangascript.html:


<script type="text/javascript">
var imageGallery = [
{% assign gallery = site.data.galleries | where:"id",page.gId | first %}
{% for image in gallery.images %}
  "{{ page.linkadd }}{{ gallery.imagefolder }}/{{ image.name }}",
{% endfor %}
];
...
</script>

Изначально коллекция site.data.galleries фильтруется по полю id (значение должно быть равно page.gId). Результатом фильтра where может быть несколько значений, поэтому берётся только первое (это позволительно, т.к. id коллекции в данном случае подразумевается уникальным). Результат сохраняется в переменную gallery.


Далее просто проходим всю галерею в цикле for.


Результат исполнения будет примерно таким:


var imageGallery = [
  "pic/manga/OneMangaDay_000_001.png",
  "pic/manga/OneMangaDay_000_002.png", 
  ...
  "pic/manga/OneMangaDay_999.png"
];

Полезные ссылки: Jekyll (Liquid) reference, "Advanced Liquid: Where".


Галереи, выстроенные в страницу


Ещё одно хорошая возможность для галерей — встраивание их коллекций прямо в код страницы. Например, страница игры:


---
layout: page
title:  "One Manga Day"
gallery:
    - image_url: omd/OMD-s0001.jpg
    - image_url: omd/OMD-s0002.jpg
    - image_url: omd/OMD-s0003.jpg
...
buttons:
    - caption: "Website"
      url: "http://onemangaday.dexp.in/"
      class: "warning"
    - caption: "Steam"
      url: "http://store.steampowered.com/app/365070/"
      class: "info"
...
---
Manga are... 
{% include gallery %}
...
{% include buttons %}

В эту страницу встроены сразу две коллекции: gallery и buttons. Переменные page.gallery и page.buttons используются в _includes/gallery и _includes/buttons соответственно.


Пример галереи


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


Наборы фильтров


Мы уже затронули тему фильтров. Теперь можно взять и более продвинутую задачу. Я хочу увидеть все мои "Linux" посты, сгруппированные по году, только за 3 года.


{% assign cp = site.tags.linux | sort | reverse %}
{% assign byYear = cp | group_by_exp:"post", "post.date | date: '%Y'" %}

{% for yearItem in byYear limit:3 %}
  <h4>{{ yearItem.name }}</h4>
  <ul>
    {% for post in yearItem.items %}
    <li><a href="{{ post.url }}">{{ post.title }}</a></li>
    {% endfor %}
  </ul>
{% endfor %}

Сначала нужно взять все посты по тэгу "Linux": site.tags.linux. Следующая строка группирует по дате. Можно выбрать любое поле или формат для группировки. Ну и последнее условие выполняется через limit у цикла for. Вывод:


Посты, сгруппированные по году


Реально это используется у меня в фотоальбоме (исходник).


Запись переменных


Пускай нужно не выводить сразу переменную {{ content }}, а как-либо её модифицировать, а только потом вывести.


Обычное присвоение:


{% assign someString = "value" %}

Не работает, т.к. нужны не сырые (raw) строки, а предобработанные с помощью Jekyll.


Решением является директива capture. И для примера будет рассмотрен хак для обхода бага в compress.html. Пускай изначальный код выглядит следующим образом:


{% highlight AnyLanguage linenos %}
Some code
{% endhighlight %}

Изменим его для использования capture:


{% capture _code %}{% highlight AnyLanguage linenos %}
Some code
{% endhighlight %}{% endcapture %}{% include fixlinenos.html %}
{{ _code }}

Теперь весь подсвеченный код находится в переменной _code. Далее происходит его обработка в _include/fixlinenos.html:


{% if _code contains '<pre class="lineno">' %}
    {% assign _code = _code | replace: "<pre><code", "<code" %}
    {% assign _code = _code | replace: "</code></pre>", "</code>" %}
{% endif %}

Код проверяет на вхождение подстроки <pre class="lineno">. Если такая найдена, то у нас в наличии бажный HTML-код, неправильные куски которого просто заменяются на правильные.


Полезная ссылка: Jekyll (Liquid) string filters


Сжатие кода


Можно ужать весь SASS-код всего одной строкой в _config.yml:


sass:
    style: :compressed

Если нужно ужать SASS-код на лету, то можно воспользоваться фильтром scssify (стиль сжатия из конфига будет применён и для фильтра):


{{ some_scss | scssify }}

В Jekyll не предусмотрено стандартных методов для сжатия HTML. Можно воспользоваться сторонним решением — compress.html. Нужно добавить всего одну строку в свой layout верхнего уровня:


---
layout: compress
---

Компрессия будет происходить уже после всей генерации кода. В конечном счёте HTML-код страницы будет выглядеть примерно так:


Сжатый в одну строку HTML


Своя подсветка синтаксиса


Пускай нужно подсветить синтаксис какого-нибудь экзотического языка программирования, о котором не знает Jekyll. Это можно сделать программно.


Я сделал customhighlight.html для подсветки RenPy кода (базируется на Python). Идея проста и тоже базируется на фильтре replace:


{% assign _customtag = "image side hide play show scene" | split: " " %}

{% for _element in _customtag %}
  {% capture _from %}<span class="n">{{ _element }}{% endcapture %}
  {% capture _to %}<span class="k">{{ _element }}{% endcapture %}
  {% assign _code = _code | replace: _from, _to %}
{% endfor %}

Здесь формируется массив тэгов, потом поиск-замена для каждого элемента в строке кода. Единственная новая вещь — это разделение строки в массив. Пример подсвеченного кода:


Код RenPy


Пример использования здесь. Также подсветка встроена в код оригинальной статьи на английском.


Облако тэгов


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


Облако тэгов


Основные вычисления происходят в файле _includes/tagcloud.html:


<ul id="cloud">
  <li style="font-size: 150%"><a href="index.html">All</a></li>
{% for tag in site.tags %}
  {% assign curTag = tag | first | slugize %}
  {% assign langtag = landat.tags | where:"slug",curTag | first %} 
  <li style="font-size: {{ tag | last | size | times: 100 | divided_by: site.tags.size | plus: 20 }}%">
    <a href="{{ curTag }}.html">{{ langtag.name }}</a>
  </li>
{% endfor %}
</ul>

Переменная site.tags хранит все использованные тэги из всех статей. Переменная langtag — это текущий тэг в человеческом формате, описанный в _data/lang.yml.


На каждой итерации будет переменная tag, взятая из коллекции site.tags. Переменная tag содержит список всех статей с данным тэгом. Так что можно просто взять размер, умножить, поделить и пр. Самый маленький тэг был слишком маленьким для меня, так что я добавил ещё 20%.


Полезная ссылка: Jekyll (Liquid) array filters


Мультиязычные сайты


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


Также можно использовать поддомен для каждого языка. Но в таком случае для 2 языков будет существовать ровно 2 сайта. И 10 сайтов для 10 языков, что не всегда удобно.


One Manga Day in Polish


На сайте One Manga Day для каждого языка выделена своя папка. Идея проста. Например, существует английская страница cat/page.html. Если у этой страницы есть русский вариант, то URL у неё будет ru/cat/page.html. Если страниц немного, то можно все их создавать вручную. Но если страниц много, то необходимо проверять наличие страницы на том или ином языке. Однако в Jekyll вообще не существует файловых функций в целях безопасности серверов GitHub.


Для проверки существования файлов можно воспользоваться моим _includes/CHECKEXISTS.html. Там просто пробегаются все страницы и посты сайты:


{% assign curUrlExists = false %}
{% assign curFUrl = curUrl | remove: ".html" %}

{% for curFile in site.pages %}
    {% assign cFile = curFile.url | remove: ".html" %}
    {% if cFile == curFUrl %}
        {% assign curUrlExists = true %}
    {% endif %}
{% endfor %}

{% for curFile in site.posts %}
    {% assign cFile = curFile.url | remove: ".html" %}
    {% if cFile == curFUrl %}
        {% assign curUrlExists = true %}
    {% endif %}
{% endfor %}

Если файл на языке существует, то можно добавлять ссылку на него на своей странице.


Система комментариев


Первое приходящее на ум решение — Disqus или подобная система комментариев на базе AJAX. В страницу просто встраивается небольшой JS-код и всё работает хорошо.


One Manga Day Disqus comments


Но лично мне гораздо больше понравился Staticman — система комментариев, основанная на пулл-реквестах в репозиторий сайта. Таким образом комментарии также являются Jekyll-коллекцией!


Код для отображения комментариев к странице:


{% capture post_slug %}{{ page.url | slugify }}{% endcapture %}
{% if site.data.comments[post_slug] %}
  {% assign comments = site.data.comments[post_slug] | sort %}

  {% for comment in comments %}
    {% assign email = comment[1].email %}
    {% assign name = comment[1].name %}
    {% assign url = comment[1].url %}
    {% assign date = comment[1].date %}
    {% assign message = comment[1].message %}

    {% include _post-comment.html index=forloop.index 
       email=email name=name url=url date=date message=message %}
  {% endfor %}
{% endif %}

Форма отправки комментария также достаточно проста:


Форма отправки комментария


Модерирование комментариев реализовано через подтверждение пулл-реквеста. Также можно просто попросить Staticman класть комментарии напрямую, минуя подтверждение реквеста. После принятия реквеста весь сайт будет перегенерирован и комментарий появится на сайте.


Заключение


Jekyll — очень крутая штука для маленьких сайтов и блогов. Просто попробуйте, вы его полюбите!


Jekyll logo


Полезные ссылки



Комментарии (18)


  1. FirsofMaxim
    26.08.2017 19:29

    Спасибо, держу блог на gihub.io, сейчас думаю над подключением комментариев, получается что Staticman требует от комментатора github аккаунта?


    1. DeXPeriX Автор
      26.08.2017 19:32

      Нет, не требует. Данные POST'ом отправляются роботу, который соавтор в репозитории. Он и делает коммит. Для пользователей всё абсолютно прозрачно, формочку можно на том же Ajax сделать.


    1. andreysmind
      27.08.2017 10:52

      А чего DISQUS не подошел? Он вообще тремя строчками встраивается.


      1. DeXPeriX Автор
        27.08.2017 11:46

        Ну как минимум, это зависимость от внешних сервисов. Если на сайте есть реклама, то Disqus будет или платным, или показывать свою рекламу. И чисто психологический аспект: я не контролирую свои данные (комментарии), неизвестно что с ними может там стать.
        Ну и лично у меня ещё стояла задача переноса старых комментариев. Как это сделать в Disqus мне не очень понятно. Зато со Staticman нужно было просто создать соответствующие файлы, и комментарии 2007 года снова на сайте.


      1. FirsofMaxim
        27.08.2017 13:58

        Буду пробовать оба, где быстрее будет работать, тот вариант и выберу.


    1. masai
      27.08.2017 16:52

      Я для комментариев использую isso. Отличная вещь, но нужен свой сервер. Лично для меня это преимущество.


  1. gimntut
    26.08.2017 20:25
    +1

    GitHub Pages + CloudFlare лучше использовать netlify. Во-первых снимаются многие ограничения, вроде упомянутых файловых операций. А во-вторых добавляется множество плюшек, вроде URLRewrite и https по-умолчанию.


    1. DeXPeriX Автор
      26.08.2017 20:49

      Ну плюшек и здесь хватает. Jekyll делался как движок для программистов — его приятно использовать. Кроме того, open source. Как следствие получаем независимость от серверов GitHub, т.к. при желании можно перенести куда угодно. Https CloudFlare даёт по умолчанию. Для доменов *.github.io GitHub даже форсит https.
      Мне не совсем понятно, зачем для статических сайтов нужен URLRewrite, но в Jekyll он есть: permalink в заголовке страницы (пример).
      Ограничения, конечно, есть везде. Но если использовать Jekyll по предназначению, то в них редко упираешься. А если и упёрся — то один раз делается решение, выносится в отдельный файл и просто подключается.
      Да, Jekyll может далеко не всё. Мне так и не удалось узнать в Jekyll размер файла. Т.е. не получилось автоматом сделать ссылки в стиле "скачать (3 Мб)". Лично мне этот функционал в итоге так и не понадобился. Если же нужен часто, то по-моему лучше использовать более специализированные решения вроде netlify.
      Аналогично, если нужно очень гибкое облако тэгов. В предложенном решении нужно всегда помнить, какие тэги вообще есть в наличии. Если создаётся новый тэг, то его нужно прописать сразу в несколько мест, что не всегда удобно. Если тэгов с (пару-тройку) десятков и создать их заранее, то решение вполне удобное и имеет право жить. Если для каждой публикации куча тэгов, часто новые, и публикуетесь часто — то опять же лучше использовать что-то другое.


      1. gimntut
        26.08.2017 21:56

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


        Мне не совсем понятно, зачем для статических сайтов нужен URLRewrite.

        Я тоже не думал, что такая экзотическая вещь мне может понадобится, но когда понадобилось оказалось, что хостинг такую функцию предоставляет.
        В моём случае, javascript отображает одну и ту же страницу по разному в зависимости от url. Страница одна, а адресов несколько.


        1. DeXPeriX Автор
          26.08.2017 22:04

          Прошу прощения, насчёт Netlify был не прав. По поводу отсутствия ограничений на плагины — это классно! Иногда вместо извращений с кодом можно сразу взять готовый плагин и просто его включить…
          По поводу вместо двух инструментов один — не согласен. Если домен свой, то им всё-равно нужно как-то управлять. Например, где создать поддомен? Можно воспользоваться чем-то типа FreeDNS, а можно сразу напрямую использовать CloudFlare.


          1. gimntut
            26.08.2017 22:23
            +1

            В этом смысле netlify не отличается от CloudFlare. Просто потому, что netlify — это в первую очередь cdn. Но остальное в нём тоже сделано очень хорошо.


    1. rhamdeew
      27.08.2017 00:57

      Есть еще очень хороший вариант — surge.sh


  1. KvanTTT
    27.08.2017 00:26
    +2

    [http://renpy.org](http://renpy.org)


    Для такого, кстати, можно использовать более короткий синтаксис: <http://renpy.org>


  1. mifistor
    27.08.2017 23:05

    Я когда выбирал статический генератор сайтов выбрал всё-таки Hugo. Написан на go, легче (субъективное мнение) в настройке и он адски быстрый, даже «пухлый» сайт генерится за секунды. Советую, однозначно.


    1. mgremlin
      28.08.2017 19:15
      +1

      Полностью согласен.


  1. DeLuxis
    28.08.2017 07:08

    Как там обстоят дела со спамом в комментариях?


    1. DeXPeriX Автор
      28.08.2017 09:53

      Вроде бы нормально. Staticman внутри себя использует Akismet для проверки комментариев на спам.


      1. DeLuxis
        28.08.2017 10:11

        Благодарю за ответ.