Продвинутый Jekyll

в 15:38, , рубрики: github, github pages, html, javascript, jekyll, open source, static site, web, web-разработка, Программирование, Разработка веб-сайтов

Продвинутый Jekyll - 1

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

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

Автор: Храбров Дмитрий

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js