Обновление сайта, версия 4.0

6 минут

С момента последнего крупного обновления сайта прошло более 10 лет. Пришло время обновиться.

Маленькая историческая справка🔗︎

Первая версия сайта была написана на HTML + PHP в 2009 году, тогда я только учился писать сайты. Выглядела она так (взято и вебархива):

Позже я захотел получить больше функционала и перенёс сайт на движок PHP-Fusion. Потом был переход на хайповый в то время WordPress - движок, на котором этот сайт продолжал работать до текущего момента.

Почему не WordPress🔗︎

Когда я выбирал движок для сайта, мне казалось, что все функции, которые предоставляются WordPress мне понадобятся, но это оказалось не так. Множество функций просто не нужны или даже мешают.

А для меня, как разработчика, количество ненужных функций ещё больше:

  • WYSIWYG редактор хуже, чем Markdown разметка.
  • Функции админки просто не используются.
  • Встроенные в WordPress комментарии не пригодны для использования (никак не защититься от спама).
  • Постоянно находятся дыры безопасности в WordPress, из-за чего его приходится регулярно обновлять.
  • Для работы требуется MySQL и PHP, что усложняет развёртывание и бекапы сайта.

Альтернативы🔗︎

Сейчас для создания домашней странички гораздо больше выбора чем раньше. Например, появились JavaScript технологии вроде Next/Nuxt/SvelteKit, с помощью которых можно используя только JavaScript написать одновременно фронтенд и бэкенд для сайта. Но для личной страницы это тоже перебор - сложной логики на сайте нет и не предвидится.

Лучшим выбором для личной страницы становятся генераторы статических сайтов. Это такие сборщики, которые на базе шаблонов и файлов конфигурации генерируют сайт, который представляет из себя набор html страниц.

Статические сайты имеют следующие преимущества:

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

Но есть и недостатки, например, порог входа достаточно высокий, потому что для создания сайта требуется иметь некоторые технические навыки, вроде HTML, CSS, JS.

Как проходил процесс переноса🔗︎

Посты🔗︎

Постов на моём старом сайте было не много - чуть больше сотни, все посты были отформатированы обычным HTML. Я мог просто перенести весь текст вместе с HTML разметкой, но решил переносить каждую статью вручную и переформатировать из HTML в Markdown руками. Благодаря этому я нашёл и исправил несколько ошибок в форматировании некоторых статей.

Выбор генератора🔗︎

Немного поискав подходящие генераторы, я нашёл 2 хороших варианта - Hugo и Zola, поверхностно изучив оба решения мой выбор пал на Zola - он функционально отстаёт от Hugo, но проще в использовании. Для моих задач функционала Zola достаточно.

Дизайн🔗︎

Задизайнил шаблон самостоятельно, немного подсматривая за существующими аналогичными сайтами. Потом сверстал - на современном CSS с переменными и вложенными стилями это делать одно удовольствие. Для текста использовал шрифт Roboto, для кода Consolas. Ещё для иконок добавил Font Awesome, но сейчас понял, что я использую всего 5 иконок, поэтому правильнее будет заместо этого шрифта использовать обычный SVG.

Ещё, оказалось, что шрифт Consolas по умолчанию есть только на Windows, на Android его нет, из-за чего код на Android'е выглядел ужасно. Пришлось тоже добавить этот шрифт на сайт.

CDN сервисы с шрифтами я не использую из-за возможных падений этих сервисов и из-за возможных блокировок. Всё это на моём опыте уже случалось.

Поиск🔗︎

Zola не реализует поиск по сайту самостоятельно, но предоставляет возможность генерировать данные для поиска для библиотек elasticlunr и fuse. Посмотрев как на существующих сайтах работает поиск с elasticlunr я остановился на fuse, тем более я его уже использовал в другом проекте.

Zola создаёт файл, который записывает данные для поиска в window.searchIndex. Скорость работы Fuse на столько большая, что можно не бояться и обновлять результаты поиска сразу, без всяких debounce.

Принцип работы реализованного мной поиска:

  • При нажатии на кнопку поиска загружается заранее сгенерированный searchIndex, открывается модальное окно и создаётся инстанс Fuse:
    if (!window.searchIndex) {
      await import(`${window.location.origin}/search_index.ru.js`);
    }
    
    const searchIndex = window.searchIndex;
    if (searchIndex === undefined) {
      console.error('searchIndex not found');
    
      return;
    }
    
    if (fuse === null) {
      fuse = new Fuse(searchIndex, {
        includeScore: true,
        ignoreLocation: true,
        ignoreFieldNorm: true,
        keys: ['title', 'body'],
      });
    }
    
    При повторном открытии окна, searchIndex повторно не загружается и новый инстанс Fuse не создаётся.
  • При вводе, в событии input, удаляются старые результаты поиска, выполняется поиск через Fuse:
    // сбрасываем прозцию скролла на самый верх
    searchResultEl.scrollTo({ top: 0 });
    // удаляем старые результаты поиска
    searchResultEl.innerHTML = '';
    
    // ищем
    const inputElement = input.target as HTMLInputElement;
    const searchText = inputElement.value;
    const result = fuse.search(searchText);
    
    // переключаем отображения блока с результатами
    searchResultEl.style.display = result.length === 0 ? 'none' : 'flex';
    
    // создаём результаты
    result.forEach((item) => {
      // не отображаем неподходящие результаты
      if (item.score === undefined || item.score > 0.5) {
        return;
      }
    
      // создаём HTML элемент с резульатом поиска
      const el = createResultElement(item);
    
      searchResultEl.insertAdjacentElement('beforeend', el)
    });
    
  • Функция создания HTML элемента с результатом поиска:
    const createResultElement = (result: FuseResult<SearchIndexItem>) => {
      // ссылка
      const el = document.createElement('a');
      el.classList.add('search-results__item');
      el.href = result.item.url;
    
      // заголовок
      const titleEl = document.createElement('div');
      titleEl.classList.add('search-item__title');
      titleEl.innerHTML = result.item.title;
    
      // начало поста
      const textEl = document.createElement('div');
      textEl.classList.add('search-item__text');
      textEl.innerHTML = result.item.body.slice(0, 100);
    
      // формируем элемент
      el.insertAdjacentElement('afterbegin', textEl);
      el.insertAdjacentElement('afterbegin', titleEl);
    
      return el;
    }
    

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

Мне очень понравилась встроенная в Zola подсветка синтаксиса, с шрифтом Consolas она выглядит шикарно. Zola для подсветки использует формат редактора Sublime Text sublime-syntax. Поэтому, достаточно найти такой файл для своего языка, добавить в каталог syntaxes и пересобрать сайт, всё подключится автоматически. Подсветка синтаксиса происходит на этапе компиляции, поэтому можно добавлять сколько угодно вариантов языков без какого-либо влияния на производительность конечного сайта.

Для некоторых нужных мне языков синтаксиса не оказалось, поэтому пришлось сделать свои:

Ещё я добавил:

  • phpp - это обычный PHP, стандартная подсветка PHP у меня, почему-то, не работала.
  • console - для подсветки консольных команд. Это просто синтаксис bash, но без использования # в качестве комментария.
  • nginx - подсветка конфигов nginx специально для этой статьи (ссылка на sublime-syntax).

Сборка🔗︎

Сначала я написал всё на JavaScript и CSS, но в Zola не оказалось функционала для минификации чего-то, отличного от HTML. Поэтому я решил добавить этап сборки для фронтенд кода и стилей. Это позволило переписать поиск на TypeScript и оставить в итоговом бандле только используемые стили и файлы шрифтов.

В качестве пакетного менеджера я использую Bun - он очень быстрый, а для сборки я использовал esbuild, он тоже быстрый и включает в себя кучу функционала для настройки процесса сборки. В Bun есть встроенный сборщик, но в нём, например, нельзя в файле конфигурации включить watch режим или добавлять файлы шрифтов как внешние файлы (он их бандлит вовнутрь CSS). Скрипт для запуска сборки в package.json выглядит так:

"scripts": {
  "build:frontend": "bun run esbuild.ts --mode=production",
  "build:zola": "zola build",
  "build": "npm-run-all --silent \"build:frontend\" \"build:zola\"",
  "dev:frontend": "bun run esbuild.ts --watch",
  "dev:zola": "zola serve",
  "dev": "npm-run-all --parallel \"dev:frontend\" \"dev:zola\""
}

npm-run-all нужен для управления очерёдностью запуска сборки фронтенда и запуска тестового Zola сервера.

esbuild.ts выглядит достаточно стандартно:

esbuild.ts
import { parseArgs } from 'node:util';
import * as esbuild from 'esbuild';

const { values: args } = parseArgs({
  args: process.argv,
  options: {
    mode: { type: 'string' },
    watch: { type: 'boolean' },
  },
  strict: true,
  allowPositionals: true,
});

esbuild.context({
  bundle: true,
  platform: 'browser',
  minify: args.mode === 'production',
  target: 'es2020',
  tsconfig: './tsconfig.json',
  entryPoints: [
    './frontend/css/styles.css',
    './frontend/src/index.ts',
  ],
  loader: {
    '.woff2': 'file',
  },
  assetNames: 'assets/[name]',
  legalComments: 'none',
  outdir: './static/build/',
}).then((ctx) => {
  if (args.watch) {
    ctx.watch();
  } else {
    ctx.rebuild();
    ctx.dispose();
  }
});

Хостинг🔗︎

Раньше сайт работал на веб-хостинге - это такой хостинг, который не даёт полный доступ к самой системе, а даёт только возможность загрузить свой PHP+MySQL сайт. Сейчас я перенёс его на обычный VPS, поэтому настройка веб-сервера перешла на меня - это создаёт некоторые неудобства, но даёт полный контроль над сайтом.

Для веб-сервера я выбрал Angie - это форк Nginx от его бывших разработчиков. Он полностью совмести с Nginx и имеет некоторы преимущества. Плюс в Angie из коробки поддерживается автоматическая выдача SSL сертификатов - это очень удобно.

Файл конфигурации для сайта ziggi.org выглядит так:

http {
    acme_client ziggi_org https://acme-v02.api.letsencrypt.org/directory;

    # Redirect HTTP to HTTPS
    server {
        listen 80;
        server_name ziggi.org;

        if ($host = ziggi.org) {
            return 301 https://$host$request_uri;
        }

        return 404;
    }

    server {
        listen 443 quic reuseport;
        listen 443 ssl;
        http2 on;

        server_name ziggi.org;

        # ssl
        acme ziggi_org;

        ssl_certificate $acme_cert_ziggi_org;
        ssl_certificate_key $acme_cert_key_ziggi_org;

        # compression
        gzip            on;
        gzip_min_length 1000;
        gzip_proxied    expired no-cache no-store private auth;
        gzip_types      text/plain application/xml;

        location / {
          expires    8h;
          add_header Cache-Control private;
          add_header Alt-Svc 'h3=":443"; ma=86400';

          root /opt/ziggi.org/;
        }
    }
}

Старые посты располагались по адресу ziggi.org/название_поста, на новом сайте формат поменялся на ziggi.org/posts/название_поста, поэтому потребовалось настроить редирект со старого адреса на новый, делается это просто, вот пример одного такого редиректа:

rewrite ^/uinfo-v2-3 /posts/uinfo-v2-3 permanent;

Итог🔗︎

Итогом переноса стала мгновенная загрузка сайта и очень быстрый поиск. Появилась красивая подсветка синтаксиса, хорошие шрифты и никакого лишнего кода и стилей в итоговом бандле. Упростился процесс деплоя и я перестал бояться потери контента, так как сайт хранится в приватном репозитории GitHub.

В общем, если вы - разработчик, то статическая генерация сайтов the only way to go.