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

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

Еще мне хочется хранить оригиналы постов в нормальном, не в обгаженном 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 секунд выросло до двух минут. Основное время тратилось на раскраску кода. Приходили мысли на тему кеша уже раскрашенных фрагментов и прочей ерунде, но после небольшого поиска проблема решилась радикально.

Надо было просто взять колоризатор, написанный на правильном для данной задачи языке. Отыскались две альтернативы: 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. Красота.