Обновление сайта, версия 4.0
С момента последнего крупного обновления сайта прошло более 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
.
Принцип работы реализованного мной поиска:
- При нажатии на кнопку поиска открывается модальное окно и создаётся инстанс
Fuse
:
При повторном открытии окна новый инстанс не создаётся, используется старый.if (fuse === null) { fuse = new Fuse(searchIndex, { includeScore: true, ignoreLocation: true, ignoreFieldNorm: true, keys: ['title', 'body'], }); }
- При вводе, в событии
input
, удаляются старые результаты поиска, выполняется поиск черезFuse
:// удаляем старые результаты поиска 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
и пересобрать сайт, всё подключится автоматически. Подсветка синтаксиса происходит на этапе компиляции, поэтому можно добавлять сколько угодно вариантов языков без какого-либо влияния на производительность конечного сайта.
Для некоторых нужных мне языков синтаксиса не оказалось, поэтому пришлось сделать свои:
- Pawn - сделал на базе синтаксиса C.
- dcpu16 - сделал на базе синтаксиса ассемблера по примеру из своей статьи.
Ещё я добавил:
phpp
- это обычный PHP, стандартная подсветка PHP у меня, почему-то, не работала.console
- для подсветки консольных команд. Это просто синтаксис bash, но без использования#
в качестве комментария.nginx
- подсветка конфигов nginx специально для этой статьи (ссылка на sublime-syntax).
Сборка🔗︎
Сначала я написал всё на JavaScript и CSS, но в Zola не оказалось функционала для минификации чего-то, отличного от HTML. Поэтому я решил добавить этап сборки для фронтенд кода и стилей. Это позволило переписать поиск на TypeScript и оставить в итоговом бандле только используемые стили и файлы шрифтов.
В качестве пакетного менеджера я использую Bun - он очень быстрый, а для сборки я использовал esbuild, он тоже быстрый, плюс включает в себя кучу функционала для настройки процесса. Скрипт для запуска сборки в 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
выглядит достаточно стандартно, getArgs
я скопировал из другого проекта:esbuild.ts
import { parseArgs } from 'node:util';
import esbuild from 'esbuild/lib/main';
import path from 'node:path';
const getArgs = (argv: string[]) => {
const { values: args } = parseArgs({
args: argv,
options: {
mode: {
type: 'string',
},
watch: {
type: 'boolean',
},
},
strict: true,
allowPositionals: true,
});
if (args.mode !== undefined && args.mode !== 'production' && args.mode !== 'development') {
throw new Error('Invalid mode, only "production" or "development" is valid');
}
return args as {
mode?: 'production' | 'development';
watch?: boolean;
};
};
const args = getArgs(process.argv);
const mode = args.mode ?? 'development';
const watch = args.watch ?? false;
esbuild.context({
bundle: true,
platform: 'browser',
minify: mode === 'production',
define: {
'process.env.NODE_ENV': `"${mode}"`,
},
target: 'es2020',
tsconfig: path.resolve(__dirname, './tsconfig.json'),
entryPoints: [
path.resolve(__dirname, './frontend/css/styles.css'),
path.resolve(__dirname, './frontend/src/index.ts'),
],
loader: {
'.woff2': 'file',
'.ttf': 'file',
},
legalComments: 'none',
outdir: path.resolve(__dirname, './static/build/'),
plugins: [
{
name: 'on build message',
setup: (bld) => {
bld.onEnd((result) => {
if (result.errors.length !== 0) {
console.error(`Build \x1b[31mfailed\x1b[0m`);
} else {
console.log(`Build \x1b[32msucceeded\x1b[0m`);
}
});
},
},
],
}).then((ctx) => {
if (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;
listen [::]:80;
server_name ziggi.org;
if ($host = ziggi.org) {
return 301 https://$host$request_uri;
}
return 404;
}
server {
# For older versions of nginx appened http2 to the listen line after ssl and remove `http2 on`
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name ziggi.org;
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;
root /opt/ziggi.org/;
}
}
}
Старые посты располагались по адресу ziggi.org/название_поста
, на новом сайте формат поменялся на ziggi.org/posts/название_поста
, поэтому потребовалось настроить редирект со старого адреса на новый, делается это просто, вот пример одного такого редиректа:
rewrite ^/uinfo-v2-3 /posts/uinfo-v2-3 permanent;
Итог🔗︎
Итогом переноса стала мгновенная загрузка сайта и очень быстрый поиск. Появилась красивая подсветка синтаксиса, хорошие шрифты и никакого лишнего кода и стилей в итоговом бандле. Упростился процесс деплоя и я перестал бояться потери контента, так как сайт хранится в приватном репозитории GitHub.
В общем, если вы - разработчик, то статическая генерация сайтов the only way to go.