Goblog: Самодельный статический движок для блога на Go

Я люблю писать тексты, люблю отлаживать примеры, пробовать, анализировать. Чего я не люблю, так это возиться с форматированием, закачиванием картинок, проверкой верстки и т.д.

По причине лени я начал использовать Блогспот. Тут тебе и море шаблонов, виджеты всякие, мгновенная индексация Гуглом, статистика разная, с какого-то времени даже комментарии стали древовидные, и прочие свистелки. Ну все бы хорошо, но, увы, не предназначен редактор Блогспота для создания программистских постов. Когда надо вставлять код или таблицы разные, начинаются мучения. Например, для своего другого блога, не про программирование, Яйца всмятку, сэр!, “возможностей” Блогспота вполне хватает.

Еще мне хочется хранить оригиналы постов в нормальном, не в обгаженном HTML’ем виде. Получалось, что материалы по блогу раскиданы по компьютеру там и сям в нескольких копиях. Сначала ты просто пишешь текст в редакторе, только разбивая на абзацы, без ссылок и картинок, и в конце сохраняешь почти готовый документ. Потом начинается верстка в HTML, в процессе которой, помимо, собственно, HTML’я, делаются поправки в оригинальном тексте. При этом обновлять оригинальный файл уже лень, и по сути, он остается в “сыром” виде. А в “сухом” виде остается только HTML’ная помойка. Но это еще не конец истории. Часто уже после публикации замечаешь опечатку, лезешь в Блогспот и правишь прямо на странице. Опять, самый первый оригинал и его локальная об’HTML’ная версия остаются неисправленными. В итоге: актуальные версии постов находятся только на самом Блогспоте. Конечно, можно делать автоматизированный бэкап всего блога, но опять таки – все будет уже только в HTML’е.

Некоторое время назад я начал использовать ReST. Тут жизнь хоть как-то полегчала. ReST позволяет писать текст в уже более менее предсказуемой разметке (абзацы, ссылки, код), и затем из него генерируется HTML, который вставляется (опять таки вручную) в Блогспот. Попытки автоматизировать предварительный просмотр поста через googlecl фактически провалились. Опять оставалась проблема, когда после исправления опечатки на странице оригинальный документ в ReST устаревал. Кроме того, ReST не решал проблему картинок. Их надо было куда-то заранее выкладывать, чтобы можно было полностью сделать preview.

Не могу объяснить почему, но идея динамических движков типа Wordpress’а меня как-то пугала. Сама идея держать посты в базе данных мне кажется перебором.

Я почти уже было остановился на промежуточном решении – Doku Wiki, например как на vak.ru. Тут движок хоть и динамический, но содержимое страниц хранится в файлах, и есть версионность. Doku можно использовать как движок всего сайта, не только блога. Хоть и дизайн неказистый, зато картинки и произвольные аттачменты поддерживаются системой.

Был еще вариант, на который я тоже почти подписался – блог на основе TiddlyWiki. TiddlyWiki – это мой любимый инструмент на Windows для ведения записей. Я про это уже писал. Почему только на Windows? Потому что на Маке я просто делаю записи в простых текстовых файлах, располагая их по смыслу в документах или на рабочем столе, а Spotlight, который индексирует все и вся на компьютере, моментально позволяет искать по фрагментам слов. Получается, что в ключевых возможностях TiddlyWiki – мгновенном поиске, уже не особого смысла. Но я отвлекся.

Оказывается, есть фанаты, которые превратили TiddlyWiki в блог-платформу. В эдакий статико-динамический мутант.

Например, вариант блога с таким движком – Rich Signell’s Work Log. Эзотерика, на мой взгляд. Например, не ясно, как прикрутить комментарии, хотя бы тот же Disqus. Но если кому интересно, есть даже публичный хостинг – tiddlyspot.

И вот реально я возбудился на идее чисто статических движков. Прелесть тут в том, что такой блог хостить можно где угодно. Тут не только база данных не нужна, но и серверное скриптование. Но дальше – больше. GitHub или Heroku позволяют не только хостить статические сайты, но и управлять контентом через git.

Например, есть статический движок Jekyll. В Jekyll посты пишутся с использованием разметки Markdown или Textile. Также можно добавлять в проект произвольные файлы, которые при генерации сайта будут выкладываться без изменений. По сути – это движок сайта, в котором еще можно некоторые файлы оформлять в виде блога.

Комментарии же, как основная “динамика” блога, может реализоваться через, например, Disqus. К слову сказать, есть эстэты статических блогов с высшей степенью дзэна – со статическими комментариями (для меня даже это словосочетание является оксюмороном). Подход тут такой: у поста внизу есть секция со статически выведенными ранее введенными комментариями, и рядом форма для ввода нового. Ты вводишь комментарий, и он отсылается автору блога. Тот его подтверждает (или нет), куда-то кликает, и комментарий помещается в виде файла в статический проект блога, все пересобирается и выкладывается на публику. Понятно, что это никакой ни разу не real-time, а больше похоже на комментарии с пре-модерированием, причем модератор выходит на связь раз в неделю.

Я очень ценю дискуссию, и подобный подход не для меня. И продолжаю использовать Disqus. Кстати, из Disqus можно прекрасно экспортировать базу комментариев, и, например, превратить ее в статические страницы, если вдруг придется с него уходить.

Но вернемся к Jekyll. Например, GitHub Pages напрямую поддерживает Jekyll (его автор и есть сооснователь GitHub) и умеет рендерить проекты Jekyll (хотя можно и рендерить самому локально). Заливаешь через git проект Jekyll, и сайт становится видимым в GitHub Pages.

На Heroku идея немного иная. Heroku хостит Ruby, поэтому статический сайт на Heroku – это сами страницы и программа-вебсервер, которая их отдает. Звучит страшновато, но на Ruby такой сервер выглядит весьма компактно, например так:

require 'bundler/setup'
require 'sinatra/base'

class SinatraStaticServer < Sinatra::Base  

  get(/.+/) do
    send_sinatra_file(request.path) {404}
  end

  def send_sinatra_file(path, &missing_file_block)
    file_path = File.join(File.dirname(__FILE__), 'public',  path)
    file_path = File.join(file_path, 'index.html') unless file_path =~ /\.[a-z]+$/i  
    File.exist?(file_path) ? send_file(file_path) : missing_file_block.call
  end
end

run SinatraStaticServer

Как ни странно, хостинг на Heroku в целом проще, чем на GitHub. Также, на Heroku, git-репозиторий блога остается private, тогда как на GitHub’е он становиться открытым, как и все остальные проекты. Хотя для меня звучит странно держать проект блога (фактически, сайта) закрытым. Он же и так весь доступен через веб.

Да, и GitHub Pages и Heroku позволяют “прикрутить” нормальный домен второго уровня, если у вас есть таковой.

Итак, я выбрал Jekyll c хостингом на Heroku. Увы, если взять чистый Jekyll, то придется самому с нуля разрабатывать стили и макет страниц. Если этим заниматься лень, то можно взять Octopress.

Octopress – это статический движок блога на базе Jekyll, но который укомплектован красивым HTML5 макетом страниц, пачкой удобных плагинов и автоматизированной возможностью выкладывания блога на GitHub Pages и Heroku.

Итак, я взял Octopress, покрутил туда-сюда, попробовал несколько постов, протестировал рендеринг блога локально, повыкладывал на Heroku и GitHub Pages. Все вроде было на мази.

Далее была самая нудная часть марлезонского балета – перетаскивание постов из любимого Блогспота. Фактически приходилось это делать вручную через cut-and-paste. Недели три мучений, и свои несчастные триста постов я обработал.

Все было готово для запуска моего нового статического блога. Но тут меня ждало главное разочарование. Драгоценный Jekyll, написанный на Ruby, рендерил мои несчастные триста постов (внимание!) – 15 минут (на Mac Air). А как сами понимаете, по началу надо было много пробовать, пересобирать, снова пробовать, снова пересобирать и т.д. И такое время полной пересборки не лезло ни в какие ворота.

Методом тыка я нашел узкое место в движке Jekyll/Octopress – львиная доля этих 15 минут уходило на генерацию файла atom.xml, RSS-фида. Почему-то в изначальных шаблонах в этот RSS-файл включалось только последние двадцать постов. Но у меня блог небольшой, поэтому я включил туда все посты, и тогда время генерации этого файла приводилось к пятнадцати минутной сборке всего блога.

Все это показалось мне каким-то абсурдом (при всей моей любви к Ruby). После небольшого размышления (я к тому времени уже более менее понимал внутренности Jekyll) и нежелания корячить Jekyll в попытках его ускорить, я задался вопросом – а не написать ли мне свой статический движок по схожей идее? Ведь это всего-навсего работа с файлами, текстом и, возможно, шаблонами. К тому же, в Jekyll нет многоязычности ни в каком виде, и у меня были планы туда ее добавить, но с собственным движком у меня полностью развязаны руки, и можно сделать все стройно и красиво.

На чем писать? Можно по-мужски: на C++/boost. Будет работать очень быстро, но скучно. Я решил на Go. Нативная, очень быстрая компиляция (фактически, у меня нет фазы компиляции, так как она совмещена с фазой запуска), удобная работа со строками и файловой системой, упрощенная работа с памятью (сборщик мусора), регулярные выражения, массивы, хэши, библиотека шаблонов, библиотека для Markdown. Все, кроме последнего, “из коробки”. Каких-либо проблем с производительностью не должно быть вообще. Тут как раз вышел релиз Go 1, и теперь есть нормальные дистрибутивы под Windows и Mac.

Итак, после трех вечеров родился мой велосипед – Goblog. Весь проект открытый. Сайт и его исходные тексты находятся вместе.

Принцип работы

Есть два основных места: проект и собранный сайт-блог. В первом лежат исходные файлы. В процессе сборки файлы из проекта копируется в собранный сайт с сохранением локальной структуры каталогов. По умолчанию файлы копируются без изменений, как двоичные. Если же какой-то файл имеет расширение html, xml или js, то этот файл прогоняется через систему шаблонов Go. Файлы с расширением markdown дополнительно перед шаблонами обрабатываются библиотекой Markdown.

Каталоги:

  • <root> – Здесь находится собранный сайт, как он видится по адресу http://demin.ws/.
  • <root>/_engine – Это проект, тут лежат исходники и генератор сайта. Технически, этот каталог виден и через web.

Подкаталоги и файлы в каталоге _engine:

  • _includes – Файлы, которые можно подставлять через макрос {{include “filename”}}.

  • _layouts – Файлы-layout’ы (см. ниже).

  • _site – Собственно, каталоги и файлы сайта. Этот каталог является корнем будущего сайта. Файлы из него при сборке перекладываются в собранный сайт. Некоторые обрабатываются шаблонами.

  • _posts – Исходники постов. Эти файлы обрабатываются особо. Помимо шаблонов, они файлы переименовываются по структуре блога, где дата является частью URL: домен/blog/язык/год/месяц/день/название-поста/

Посты – это Markdown-файлы, имеющие особый заголовок и имя. Данные файлы выкладываются в отдельный каталог /blog с подкаталогами-датами. Информация о постах собирается в специальные переменные, которые делаются видимыми из шаблонов. Также по постам строится обратный индекс для поиска.

Layouts

Идея layouts унаследована из Jekyll. Если пост или страница имеет в заголовке атрибут layout (например), то для ее рендеринга загружается указанный шаблон-layout (из каталога _layouts), тело поста или страницы вставляется в определенное место layout’а (у меня это плейсхолдер Page.child), и затем все рендерится вместе. Это позволяет единообразно оформлять группы схожих страниц (например, постов). Layout’ы могут быть вложенные.

Генератор

И теперь, собственно, генератор – main.go.

Все, что я делаю для сборки (в каталоге _engine), это:

make

Выводится примерно следующее:

_engine$ make
gofmt -w=true -tabs=false -tabwidth=2 main.go
go run main.go 
Go static blog generator  Copyright (C) 2012 by Alexander Demin
Words in russian index: 18452
Words in english index: 3563
15.672979s
Processed 344 posts.

Если все хорошо, то в корне проекта (в каталоге .. относительно _engine) образуются файлы, готовые для выкладки. На моем Mac Air сборка занимает 15 секунд (привет, Jekyll/Octopress, и до свидания). Tак как все находится под git, то всегда четко видно, где и какие файлы появились, исчезли или изменились.

Далее можно проверить сайт локально (см. ниже).

Если все готово, можно добавить измененные файлы (как исходники из _site/, так и собранные файлы) в локальный репозиторий:

git add ../*
git commit -m "New post about ..."

И выложить на GitHub Pages:

git push

Практически сразу после push файлы появляются на demin.ws.

В Makefile несколько дополнительных команд для облегчения жизни.

Локальное тестирование

Чтобы запустить сайт локально, я временно добавляю 127.0.0.1 demin.ws в /etc/hosts и запускаю мини web-сервер. Помните, как он выглядел на Ruby? Маленький, правда? А теперь версия на Go (server.go):

package main
import "net/http"
func main() {
  panic(http.ListenAndServe(":80", http.FileServer(http.Dir(".."))))
}

Итак:

go run server.go&

И можно тестировать сайт локально (возможно придется запустить через sudo, чтобы “сесть” на 80-й порт).

В принципе, можно и не трогать /etc/hosts и использовать адрес localhost:80, но RSS-фид файл atom.xml содержит абсолютные ссылки c доменом, поэтому для если надо тестировать RSS, то без подмены адреса не обойтись.

Подсветка синтаксиса

В качестве расширения Markdown у меня есть специальный тег для вставки блоков кода:

{% codeblock lang:xxx %}
...
{% endcodeblock %}

Я унаследовал этот тег из Octopress’a. Markdown уже имеет синтаксис для кода:

``` xxx
...
```

где xxx – язык.

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

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

Первое, что пришло в голову – pygments. Все бы хорошо, но благодаря Питону, работает крайне медленно. Время полной сборки сайта с 15 секунд выросло до двух минут. Основное время тратилось на раскраску кода. Приходили мысли на тему кеша уже раскрашенных фрагментов и прочей ерунде, но после небольшого поиска проблема решилась радикально.

Надо было просто взять колоризатор, написанный на правильном для данной задачи языке. Отыскались две альтернативы: GNU Source-highlight и Highlight. Обе написаны на C++, поэтому работают практически мгновенно.

Например, вот тут человек сравнивал производительность pygments и syntax-highlight.

Мне больше понравился Highlight. В нем языков больше поддерживается (например, в GNU’шном даже Go нет). После перехода на Highlight время полной сборки вернулось к ~15-16 секундам, и я удовлетворился.

Вызов колоризатора сделан через обратный вызов в регулярном выражении, которое обрабатывает тег {% codeblock %} (функция highlight()).

Редакторы для Markdown

Полно редакторов с preview для Markdown. Я использую MarkdownPad под Windows, и Marked на Маке.

Теги (категории) постов

Я решил не делать теги вообще. Основываясь на собственном опыте, я понял, что никогда не пользуюсь тегами ни в своем блоге, ни в чужих. К тому же со временем взгляды на логику категоризации информации меняются, и порой приходится просто для совместимости с прошлым расставлять теги, в которых уже не видишь смысла. Какой, например, смысл в теге c++ в моем блоге? Кто-нибудь когда-нибудь его использовал?

Но минимализм – это не путь к усложнению жизни. Наоборот. Лично я постоянно что-то ищу у себя в блоге в старых постах. На Блогспоте я просто заходил на главную страницу, жал ⌘-F (ой, простите, CTRL-F) и искал по фрагментам слов в заголовках. Именно для этого я с некоторого в правой колонке стал выводить ссылки практически на все информативные посты.

В новом блоге все “работает” точно также прямо на первой странице с каталогом постов. При переносе постов я изменил заголовки некоторых, сделав их более информативными и пригодными для поиска.

Но! Все это уже не важно, так как теперь в блоге работает полнофункциональный контекстный поиск.

Проверки

Одним из досадных неудобств Jekyll – это отстуствие каких-либо проверок чего-либо. А я прошел через это в полной мере в процессе перетаскивания постов из Блогспота. Битые ссылки, неверные даты, забытые кавычки, непроставленные языки и прочие атрибуты постов и многое другое. Поэтому Goblog везде где только можно проверяет все – форматы, ссылки, семантику и т.д. Если где-то ошибка, сборка останавливается. Когда я добавил функцию check_links(), которая проверяет все локальные ссылки по всем файлам в уже собранном сайте, я выловил изрядное количество “дохлых” ссылок.

Два языка

Была еще проблема, которую, как мне кажется, удалось решить весьма элегантно: двуязычность. Мне нужен блог и сайт на двух языках. Но хардкодить “прозрачную” поддержку русского и английского как-то не хотелось, к тому же версии на разных языках могу радикально отличаться, и мне не сложно поддерживать их шаблоны независимо. В итоге, у меня есть просто понятие языка у каждого обрабатываемого файла (или поста), заданное в заголовке. Goblog не знает о языках. Он просто делает информацию о языке файла или поста доступной через шаблоны. А я уж сам решаю, где лежат какие файлы. Например, все русское лежит, начиная с корня сайта, а все английское имеет префикс /english.

Например, русская титульная страница и английская титульная страница.

Чем я таки не доволен

Я не люблю web-программирование: javascript, css, html, или web-дизайн, чего вообще делать не умею. Но тут мне таки пришлось покопаться в этом (с Octopress’ом было проще). Я за основу взял сайт автора Jekyll. Сделал все минималистично просто. К тому же все равно большинство людей читают через RSS и ходят на сайт только если хотят оставить комментарий. Следовательно, надо чтобы работал RSS и страничка поста была удобной (что для меня значит простой, без изощренных шрифтов и странного форматирования) для чтения.

Мораль

Вы думаете, я сейчас буду убеждать использовать мой движок? Совсем нет. Хоть я старался сделать движок максимально гибким и непривязанным конкретно к моему блогу, но мне пришлось переносить старые посты и их комментарии, поддержать два языка и т.д. В итоге в коде есть куски, “заточенные” конкретно под мой блог (особенно в области Disqus-ссылок на комментарии к старым постам).

Только могу порекомендовать, это что статический движок персонального сайта/блога можно написать самому. Почему? А потому, что эта задача решается за несколько вечеров (раз), и в нем будет только то, что вам реально нужно (остальное вам будет лень программировать) (два). Уверен, что все можно было сделать и на Руби, и на Питоне, PHP и т.д. Но было глупо упускать возможность поупражняться на новом языке с реальной задачей.

Ссылки по теме:

P.S. Этот писался почти неделю, урывками. Параллельно я писал поиск. Внезапно я осознал, как все-таки это нереально удобно с git’ом работать с блогом. Пишешь в бэкграунде пост – работаешь в одной ветке, дописываешь функционал – другая ветка. Когда что-то готово, сливает в master и push на GitHub. Красота.


Disclaimer

Комментарии