Хакер - Как сделать сайт без движка: изучаем Lektor — статический генератор веб-сайтов
nopaywall

Содержание статьи
Как человеку, который застал примитивный веб-дизайн девяностых, мне странно видеть, что теперь для любого сайта ставят полноценный движок. WordPress, Drupal, Joomla и прочие популярные CMS рассчитаны на тысячи страниц и поддержку ресурса силами целой команды. Но зачастую их используют, чтобы сделать персональный блог или даже сайт-визитку. Стоит ли городить огород, если всё, что нам нужно — это публиковать статический контент?
Если избавиться от движка и класть на сервер только HTML, то это значительно снизит потребление ресурсов, сделает сайт быстрее, ну и, конечно, упростит (вернее, упразднит) настройку и обновление серверного софта. Можно будет спокойно переходить на хостинг попроще и экономить деньги, а в случае чего в момент мигрировать на другую площадку. Ну и, конечно, такой сайт невозможно будет взломать, ведь взламывать в нем нечего.
Если ты вдруг подумал, что я предлагаю тебе уподобиться пещерным людям и вручную делать каждый .html, то ты ошибаешься. Современный способ иной: запустить CMS на локальной машине, а на сервер выкладывать только статические файлы. Это оставляет возможность пользоваться шаблонизатором: если тебе понадобится поменять что-то в теме, то править каждую страницу не придется.
Генераторы статических сайтов могут взять на себя и такие вещи, как пагинацию, автоматическое создание главной страницы и страниц рубрик, сортировку, а также многие другие штуки, которые обычно реализуются при помощи динамических движков.
В 2013 году в «Хакере» уже публиковалась подборка статических блогогенераторов, статья называлась «Просто пиши». Однако с тех пор прошло много времени и не все перечисленные проекты до сих пор поддерживаются. К тому же в этом классе программ появились новые лидеры. Среди движков на Node.js популярен Hexo, а поклонникам Ruby рекомендую глянуть на Jekyll.
Здесь же мы рассмотрим работу с движком Lektor, который написан на Python и сочетает в себе простоту, удобство и расширяемость, а также обладает вебовой админкой. Запустив локальный сервер, ты сможешь просматривать и редактировать сайт так, будто ты работаешь с полноценной CMS. Закончив редактировать, ты одной командой сможешь синхронизировать изменения с копией на сервере.
Lektor — это новый проект Армина Ронахера, автора популярного минималистичного веб-фреймворка Flask и языка шаблонов Jinja2. Собственно, в Lektor, как и во Flask, используются шаблоны Jinja, и именно в них — вся сила «Лектора».
Ставим Lektor
Команда установки Lektor написана прямо на главной странице сайта. Если у тебя macOS или Linux, то ты можешь просто скопировать ее в командную строку и запустить:
curl -sf https://www.getlektor.com/install.sh | sh
Аналогичная команда есть и для Windows, она заметно длиннее.
Девиз Lektor: «Реализовать идеи просто, как пожарить яйцо», отсюда и яичница на главной страницеЕщё тебе нужно убедиться, что в системе есть Python 2.x и библиотека Imagemagick. В Ubuntu все зависимости ставятся одной командой:
sudo apt-get install python-dev libssl-dev libffi-dev imagemagick
В Windows и macOS для установки Imagemagick тебе сначала понадобится поставить пакетный менеджер — Chocolatey или Homebrew соответственно. Кстати, для macOS у Lektor есть десктопное приложение, которое само установит тебе утилиты командной строки — нужно лишь выбрать пункт меню Install Shell Command.
Если же ты собираешься модифицировать сам Lektor, то разработчики рекомендуют ставить версию с GitHub и создать виртуальное окружение (в других случаях оно не нужно). Руководство ты найдешь на странице проекта.
Разбираем простой проект
Чтобы не создавать каталоги и основные файлы вручную, воспользуемся функцией создания типового проекта с набором стандартных элементов. Для этого открываем терминал, переходим в папку, где будет лежать проект, и пишем
lektor quickstart
Тебе зададут несколько вопросов: как будет называться проект, как написать твое имя, какой каталог использовать и создать ли шаблон для блога.
В последнем случае к тестовому проекту будет добавлена страница с постами и шаблон для них. Рекомендую согласиться — лишние примеры не повредят.

WWW
Сайт getlektor.com — отличное пособие по Lektor сразу в двух смыслах: во-первых, там есть документация, во-вторых, этот сайт сам сделан на Lektor и в нем использованы некоторые неочевидные трюки. Так что в его исходниках всегда можно что-нибудь подсмотреть.
Чтобы глянуть, на то, что у тебя вышло, пиши lektor server. Запустится локальный сервер, при помощи которого можно тестировать сайты. Обычно он доступен по адресу http://127.0.0.1:5000/.
Вот, как будет выглядеть тестовая страница.
Обрати внимание на карандаш в правом верхнем углу — это ссылка на редактирование страницы в админке.
Теперь посмотрим, что творится в папке с проектом.
На первый взгляд для такого скромного сайта файлов многовато, но разобраться со всем этим несложно. Пройдемся по порядку.
- assets — папка для статического контента, общего для всего проекта (стили, значки и т.п.);
- content — основная директория для сайта. Каждый подкаталог в ней соответствует одной странице. Обрати внимание: картинки к отдельным страницам размещаются в них же, а не в
assets. - models — папка с описаниями моделей данных в виде файлов .ini.
- templates — папка с шаблонами HTML в формате Jinja2. Названия файлов соответствуют названиям файлов с моделями.
Формат ini тебе наверняка знаком по Windows — это список пар ключ = значение, которые обычно разбиты на секции. Файл с расширением .lektorproject, который лежит в корневом каталоге проекта — это, считай, тот же ini. Пока что в нем записано только название проекта, но позже мы впишем сюда адрес сервера, куда Lektor будет загружать сайт.
Файлы .lr — это тоже простой текст, в них храниться все содержимое сайта (так что сразу закрепляй это расширение за удобным редактором). На каждую страницу — по своей папке со своим contents.lr. Можно сказать, что их формат аналогичен Markdown, но на самом деле всё чуть сложнее: в .lr хранится содержимое переменных, описанных в модели, и один из форматов этих переменных — Markdown.
Звучит замысловато? Просто открой contents.lr из корня папки content и загляни внутрь. Ты увидишь, что там задано две переменные — title и body (переменные отделяются друг от друга тремя черточками на отдельной строке). Первая содержит заголовок текста и имеет формат «строка», вторая — текст страницы, записанный в виде Markdown. Если страницы будут иметь ещё какие-то характерные элементы, то ты сможешь создать переменные для них и легко менять из админки.
Чтобы узнать, как выглядят модели, открой файл page.ini из папки models. Внутри ты увидишь три секции: model, fields.title и fields.body. Первая — это название нашей модели и заголовок страницы в админке. Он будет соответствовать названию страницы (заметь, что тут используется формат шаблона).
Каждая секция fields.* описывает одно поле на странице редактирования поста. Сейчас ты видишь описание двух полей: title в формате string и body в формате Markdown. Помимо этого для каждого из них задано текстовое название (label), которое будет выводиться во время редактирования страницы.
К примеру, если каждый из твоих постов будет сопровождаться картинкой, то можно добавить вот такое поле:
[fields.picture] label = Main Picture name = string
Впрочем, конкретно в этом случае проще подхватывать заглавные картинки автоматически — для этого нужно было бы придумать им одинаковое название и задать его в шаблоне. Кстати, заглянем напоследок в папку с шаблонами. В ней ты найдешь три файла: layout.html, page.html и pagination.html в подкаталоге macros. Если ты уже работал с шаблонизатором Django или другими похожими шаблонами, то язык Jinja2, который здесь используется, будет понятен тебе с первого взгляда.
Коротко о шаблонах
Если шаблоны для тебя темный лес, то попытаюсь в двух словах объяснить их устройство. В нашем примере файл page.html (шаблон страницы) расширяет layout.html (общий шаблон верстки — шапка, навигация и футер). На практике это означает, что содержимое page вставляется в тех местах, где написано {% block title %} и {% block body %}. Собственно, page.html содержит определение этих блоков и описывает, как подставлять переменные title и body, которые мы задали в модели.
Помимо того, что один шаблон можно включать в другой, важно знать ещё о двух механизмах: циклах (for..endfor) и условиях (if..endif). Циклы нужны на случай, если, к примеру, нужно обернуть элементы массива в одинаковые кусочки HTML. Условия — когда какой-то элемент может присутствовать или не присутствовать на странице в зависимости от чего-либо.
Например, в файле layout.html с помощью двух этих конструкций реализован вывод навигации.
{% for href, title in [ ['/blog', 'Blog'], ['/projects', 'Projects'], ['/about', 'About'] ] %} <li{% if this.is_child_of(href) %} class="active"{% endif %}> <a href=''>{{ title }}</a></li> {% endfor %}
Шаблонизатор проходится по массиву из названий страниц и их адресов, и для каждого подставляет в итоговый HTML элемент <li>. Если оказывается, что текущая страница совпадает с урлом из списка (то есть функция this.is_child_of(href) возвращает True), то <li> получит свойство class="active".
Все ещё осталась куча непонятного? Увы, мене не остается ничего иного, кроме как отправить тебя к документации шаблонов Jinja — иначе пришлось бы перевести ее полностью. Она содержит много волшебных секретов, и если ты собираешься делать что-то сложносочиненное, то тебе стоит с ней внимательно ознакомиться. Если же нет, то разобраться методом научного тыка тоже вполне реально.
Добавляем свои поля в модель
Представим, что ты делаешь сайт, на котором собираешься публиковать рассказы собственного сочинения. Каждый рассказ будет отдельной страницей, и ты бы хотел, чтобы стиль подзаголовков немного отличался: где-то они будут по центру, где-то — с выравниванием по левому краю, а ещё неплохо бы отдельно задавать полужирное и курсивное начертание. Получается девять разных вариантов. Неужели делать по шаблону на каждый?
Именно для таких случаев у Lektor есть возможность настраивать админку и добавлять кастомные поля в модель. В документации ты найдешь страницу, где перечислены все возможные типы полей. Мы используем тип select, чтобы выбирать выравнивание и checkboxes — чтобы создать поле с чекбоксами «Жирный» и «Курсив».
Клонируем модель page.ini, обозвав, скажем, text.ini. Внутри нового файла меняем name на Text и добавляем две секции:
[fields.align] label = Выключка подзаголовкoв type = select choices = left, centered choice_labels = Слева, По центру [fields.style] label = Стиль подзаголовков type = checkboxes choices = bold, italic choice_labels = Жирный, Курсив
Теперь прописываем в CSS те стили, которые нужно включать в соответствии с выбором опций. Называем соответствующие классы center, left, bold и italic и делаем так, чтобы они применялись только к тегу <h2> (чтобы прописывать их не каждому подзаголовку, а всему контейнеру с текстом).
Почти всё готово. Осталось как-то прокинуть мостик между интерфейсом и включением стилей CSS на странице. Это делается при помощи шаблонов.
Сначала создаем для страницы рассказа новый шаблон. Копируем page.html, называем text.html (в соответствии с моделью) и заменяем описание block body на следующее:
{% block body %} <h1>{{ this.title }}</h1> <div class="{{ this.align }} {% for style in this.style %} {{style}}{% endfor %}"> {{ this.body }} </div> {% endblock %}
Как видишь, выбранные пункты вставляются в качестве параметров свойства classконтейнера с текстом. Пункт списка (align) можно добавлять как есть, а вот названия чекбоксов приходят в виде массива — мы его перебираем и вставляем в шаблон каждый элемент.
Чтобы проверить, как всё сработало, открываем админку и создаем новую страницу (Add Page). Нам предложат задать название и выбрать модель — выбираем Text. Ура, появились элементы интерфейса, которые мы описывали в модели.
Задаем что-нибудь, сохраняем и смотрим в файл contents.lr. В нем всё выглядит примерно так же, как и в админке. В принципе, ничто не мешало создать страницу вручную.
Если заглянешь в HTML, открыв в браузере код собранной страницы, то увидишь, что там стоит class="center bold italic". Ну и, конечно, подзаголовок должен приобрести выбранное нами начертание.
Делаем галерею
Это был очень простой пример, так что сейчас я покажу кое-что поинтереснее. Давай сделаем страницу, на которую будут автоматически вставляться картинки из папки. Закидываешь картинку, и она сама появляется в галерее на странице.
Берем новый шаблонный проект и проводим подготовительную работу.
- Клонируем папку с любой страницей — например,
about. Называем ееgallery. - Клонируем модель
page.iniи называемgallery-page.ini. Править ее не придется. - Открываем
gallery/contents.lrи добавляем в начало_model: gallery-pageи---на следующей строке. - Открываем
layout.htmlи добавляем в массив с названиями и адресами страниц (см. врезку) строку['/gallery', 'Gallery']. - Клонируем
page.htmlи называемgallery-page.html.
Теперь у нас есть страница для галереи, а также модель и шаблон для нее, но внутри нет картинок. Добавить их поможет объект attachments, в который Lektor собирает все мультимедийные файлы, найденные в папке со страницей. К примеру, метаданные картинок лежат в this.attachments.images. Lektor может даже сгенерировать превью, если вызывать для картинки метод .thumbnail(n), где n — новая ширина.
В результате код галереи будет выглядеть примерно следующий образом. Вставлять его нужно внутрь block body в нашем gallery-page.html.
{% for image in this.attachments.images %} <a href="{{ image|url }}"> <img src="{{ image.thumbnail(320)|url }}"> </a> {% endfor %}
Результат будет выглядеть примерно как на скриншоте.
Что делать, если на странице с галереей есть какие-то картинки, которые не должны в нее войти? Мне приходят на ум как минимум два решения: первое — отфильтровать их по какому-то префиксу, добавив в шаблон блок if..endif и проверяя наличие подстроки в названии файла с картинкой; второе — создать фиктивную страницу (к примеру, gallery-images), скинуть все картинки в ее папку, а в шаблоне галереи вместо картинок к текущей папке (this.attachments.images) брать их из другой: site.get('/gallery-images').attachments.images.
Как видишь, у Lektor и Jinja2 огромный запас гибкости, которого хватит, даже если ты захочешь сделать что-то нестандартное.
Деплоим сайт
Предположим, что ты закончил первую версию сайта и ее пора заливать на сервер. Lektor с этим отлично справится, но сначала тебе нужно решить, каким способом деплоить. Lektor нативно поддерживает GitHub Pages, а для Amazon S3 есть готовый плагин. Но в большинстве случаев выбор будет между FTP и rsync. Если у тебя есть доступ к серверу по SSH, то rsync — для тебя, если нет, то придется заливать по старинке через FTP.
Предположим, что у нас есть шелл и на сервере установлен rsync (к примеру, в Ubuntu он стоит по умолчанию). Открывай файл .lektorproject в корне проекта и дописывай туда вот такую секцию.
[servers.production] name = Production enabled = yes default = yes target = rsync://имя_пользователя@айпишник_сервера/var/www/папка_проекта/
Теперь все, что нужно для деплоя — это написать lektor deploy и ввести пароль. Lektor при помощи rsync посмотрит, какие файлы требуют обновления, и загрузит новую версию. Теперь можешь зайти на сервер и посмотреть, как выглядит сайт в сборе.
Фичи на будущее
- Наследование моделей. Если у тебя будут сложные модели, то ты можешь применить к ним тот же принцип, что и к шаблонам — не копировать одно и то же каждый раз, а наследовать одну модель от другой.
- Объединение страниц. Если захочешь, чтобы части текста лежали в разных файлах, но на сайте отображались одной страницей с оглавлением, то это несложно организовать при помощи шаблона. Очень полезно, если будешь использовать Lektor, чтобы публиковать документацию (а это, кстати, одно из главных его применений).
- Альтернативы — возможность создавать разные версии одной и той же страницы. Это нужно в первую очередь для перевода сайта на другие языки.
- Flow — механизм, который позволяет заранее сверстать несколько видов блоков, а потом через админку добавлять на страницу блоки разных типов (к примеру, текст, галерея, таблица определенного вида и так далее). Полезно для одностраничных сайтов, лэндингов, статей со сложной версткой или для главной страницы.
- Data Bags. В папку
databagsможно сложить массивы данных в виде INI или JSON. К элементам потом можно обращаться из шаблонов через функциюbag('название файла'). Она возвращает объект, структура которого повторяет твой «мешок данных». - Плагины. Их не так много, но есть кое-что полезное. Одни позволяют легко подключить популярные сервисы вроде Google Analytics, «Яндекс Метрики» и Disqus, в других реализованы разные фичи для сайта — к примеру, подсветка кода (если ты собираешься публиковать код), теги и фиды RSS.
Пределы возможностей
Как видишь, сделать на Lektor можно если не что угодно, то очень многое.
Да, от динамики придется отказаться, но, по-хорошему, действительно нужна она только в одном случае — если пользователи сохраняют что-то на сайте перманентно. Те же комментарии, аналитику и поиск можно (и зачастую лучше) подключить сторонние, а это, наверное, 80-90% случаев, когда без серверных скриптов никак. К тому же современный JavaScript позволяет делать локально самые разные фокусы.
Второй большой недостаток статического генератора — это невозможность редактировать сайт силами нескольких человек. Но даже это ограничение при желании можно обойти, сложив проект в репозиторий.
Заметь, я ни в коем случае не утверждаю, что динамические движки не нужны, но хотел показать тебе, как легко можно сделать простой сайт, обойдясь без них и ничего не потеряв. И даже выиграв!
Читайте ещё больше платных статей бесплатно: https://t.me/nopaywall