В прошлой части, я закончил на том, что получил набор HTML файлов. Эти файлы просто лежат у меня на диске и никто их не видит. Теперь надо решить задачу с показом нашёго сайта миру.

И у нас нет с этим проблем, так как это просто набор файлов. Есть множество хостингов, которые предоставляют эту функцию, причем дешевле, чем хостинг с чем-то вроде PHP. Но есть вариант ещё и без использования хостингов. Это же просто набор файлов! Как удачно, что некоторые сервисы предоставляют показывать статику бесплатно. Один из таких - это GitHub Pages. Есть и другие, но надо поискать 😃. Такую функцию предоставлял ещё Dropbox. Ты просто закибывал файлы в папку, у себя на компе и после они сразу появлялось на твоем сайте.

Но я использую GitHub. Из-за того, что я и так зарегистрирован на нём и он относительно надёжен. Что надо сделать для получения своего заветного сайта на их платформе?

  1. Зарегистрироваться
  2. Создать репозиторий с названием USERNAME.github.io (Вместо USERNAME - свой ник)
  3. Все!

После этого, сайт будет доступен по этому адресу, у меня это grishy.github.io. Так же можно купить свой домен второго уровня. А-ля grishy.ru. Насчет его подключения инструкцию можно найти в настройках репозитория USERNAME.github.io.

Так же надо после добавить файл CNAME с именем внутри, в корень репозитория. Это если вы подключили свой домен.

Так вот, насчёт постобработки. Мне не хватило встроенных функций и из-за этого я добавил ещё один уровень, задачи которого:

  1. Запуск локальной копии, для просмотра того, что написал. Он не до конца функционален, из-за того что показывается без нижних пунктов.
  2. Генерация статики, со всем постобработками.
  3. Автозагрузка на github изменений.

Это все оформлено в один index.js файл. Для вызова мне в корне надо написать код ниже. На самом деле script это папка и в ней храниться index.js файл.

Вот список всех команд:

> blog
$ node script help
$ node script
$ node script gen
$ node script deploy

Сделано так, из-за того что я хотел выделить все, что связано с генерацией в одну часть. В этой папке у меня хранятся бинарники Hugo, модули для Node.js и т.д. Теперь более подробно про все команды.

Запуск локальной копии ($ node script)

Это самое простое. Нужно вызвать Hugo из провального бинарника. Для Windows - это hugo.exe, для Linux - hugo, Mac я не рассматривал. После запуска у меня выводится ссылка в консоль, где собственно уже можно все посмотреть в браузере.

Решить проблему с выводом статьи после всех обработок, а не до при такой архитектуре можно сделав саму свой локальный сервер. Можно заранее сгенерировать все, провести обработки и после запустить. Сейчас, если это надо, то можно использовать Python. Запустив маленький локальный сервер на 8000 порту. Но это как-то мне не мешает пока. Тем более, что есть вариант номер два, о котором ниже.

python -m http.server 8000

Генерация статики ($ node script gen)

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

  1. Получить файл
  2. Распарсить
  3. Найти элементы, которые надо изменить
  4. Изменить их!
  5. Записать назад изменения в файл

Очень напоминает работу Gulp из прошлой статьи, где он собирал мне стили для сайта. Но я решил сделать руками, тем более что тут работы не много и мне не хотелось писать плагины для Gulp.

Получение всех файлов

Для начала возьмет файлы, которые надо обработать. В нашём случает это все .html файлы. Воспользуемся модулем glob, для взятия файлов по маске.

glob(CONFIG["public"] + "**/**/*.html", function (err, files) {
  // ...
});

Конструкция вида **/**/*.html говорит, что надо взять файлы с любым именем и любым уровнем вложенности. Дальше нам в функцию будет передана ошибка и список подходящих файлов.

Для каждого файла мы запустим обработку. Тут видно, что файл я начал писать этот скрипт, пока ещё новый стандарт не поддерживался в node.js. Из-за того, что я тут использую "сокращенный синтаксис" записи функций. Хотя там есть ещё отличия, от предыдущего способа, но чаще всего они не важны.

files.forEach((item) => {
  // ...
});

Обработка файла

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

var html = fs.readFileSync(item, "utf8");

var $ = cheerio.load(html, {
  decodeEntities: false,
});

Так как изменения файла будут только внутри маленьких кусочков, то их можно запустить параллельно. Так мы увеличим скорость обработки одной записи. Воспользуется пакетом async.

async.parallel(
  [
    (callback) => {
      // 1
    },
    (callback) => {
      // 2
    },
    // ...
  ],
  function () {
    // 3
  },
);

Он запустит параллельно функции 1 и 2. После того, как они все закончили работу, вызовется функция номер 3. Кол-во функций типа 1 и 2 может быть не ограничено, но у меня их 3. После того, как функция типа 1 и т.д. закончили свою заботу, они должны вызвать callback. Так async поймет, что эта функция закончила. Кажется не самым удобным и почему он сам не поймёт? Тут немного надо углубиться в JavaScript, делать я это конечно же не буду. Гуглите Event Loop. Просто в JS все работает асинхронно, не так как в Python, C++ и других.

Время

Как выглядит у меня до обработки:

Надо найти элемент, в котором записано время и преобразовать. Стандартно у меня время записано в формате Unix. Определяется как количество секунд, прошедших с полуночи 1 января 1970 года. Как я и сказал, вызываем callback().

$(".Unix-time").each((i, el) => {
  var block = $(el);
  var timeF = moment.unix(block.text());
  block.text(timeF.format("DD MMMM YYYY").toUpperCase());
});
callback();

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

Для подсветки кода, используется highlight.js. Мы находим элементы с кодом, отдаем в hl.js и что он вернул, вставляем на тоже место.

$("pre code").each((i, el) => {
  var block = $(el);
  var cls = block.attr("class");
  var code = block.text();

  if (cls == undefined) {
    block.html(hljs.highlightAuto(code).value);
  } else if (cls == "language-nohighlight") {
    // 'Есть сказали ничего не делать, не делаем :) '
  } else {
    // [0] - вся строка (по умолчанию в регулярках)
    // [1] - язык
    var lang = /(?:language-)(.*)/g.exec(cls)[1];
    if (lang != null) {
      var hlHTML = hljs.highlight(lang, code).value;
      block.attr("class", lang + " hljs");
      // <<span class="hljs-name">path</span>
      // Одна из скобок удаляется.
      let escHTML = us_s.escapeHTML(hlHTML);
      block.html(escHTML);
    }
  }
});

Думаю объяснять почти не надо. Если я не написал стандартно, какой язык использовать, то надо хоть как-то подсветить. Если сказал, что ничего делать не надо, пропускаем. Иначе взять из класса элемента атрибут class и вытащить из него сам язык. После преобразовать и вставить на прошлое место. Тут ещё код преобразуется в эскейп последовательности, чтобы браузер не убрал из кода символы, которые ему не нравятся. Они могут совпадать с заранее заданными.

escapeHTML("<div>Blah blah blah</div>");
// => "&lt;div&gt;Blah blah blah&lt;/div&gt;"

Как видите, все угловые скобки были заменены. Визуально это никак в браузере не измениться, зато теперь я спокоен, что браузер не начнёт их как то преобразовывать.

Математические формулы - MathJax

Найти все формулы в тексте, и заменить их на сгенерированные SVG картинки. Знаете, я сейчас не посмотрел на код, и мне не понятно, почему я в таком стиле написал. Зачем я использовал async.every?

async.every(
  $("span.mathjax"),
  function (el, cb) {
    let block = $(el);
    let math = block.html();

    mjAPI.typeset(
      {
        math: math,
        format: "TeX",
        svg: true,
      },
      function (data) {
        block.html(data.svg);
        cb(null, true);
      },
    );
  },
  () => {
    callback();
  },
);

Теперь, если я напишу в тексте:

{ {< tex >} }
    \sigma = \sqrt{ \frac{1}{N} \sum_{i=1}^N (x_i -\mu)^2}
{ {< /tex >} }

То вы увидите: {{< tex >}} \sigma = \sqrt{ \frac{1}{N} \sum_{i=1}^N (x_i -\mu)^2} {{< /tex >}}

В этом примере, как раз видно, когда вызов callback помогает. Я не знаю когда закончится обработка, но когда она закончиться, async сразу об этом узнает (последние строки).

Вот и все, после этого я сохраняю результат в HTML, перезаписывая прошлый файл.

Загрузка на GitHub ($ node script deploy)

После того, как все сгенерировалось, оно лежит в папке под названием public, вместе с блогом. Теперь надо закоммитить эту папку. Я копирую blog/public в grishy.github.io. У меня разделены данные доя блога и сам блог. Но сохраняются они одновременно. Т.е. когда я пишу node script deploy, то все изменения в обеих папках запоминаются и отправляются. Перед этим я ещё генерирую название для коммита, которое начинается со смайлика и дальше дата.

Для загрузки я использую готовый node.js модуль - simple-git. После этого, в течении 1 минуты все изменения будут видны в блоге.

Как-то много вышло, писал сразу, не особо думаю, что будет дальше 😄. Наверное будет 3 часть, где будет рассмотрена производительность, что улучшить, какие баги и подводные камни.


https://github.com/grishy/blog/blob/hugo/content/post/about-the-blog-2.md