Skip to main content

Импортозамещение своего сайта

О чем название статьи, о том и текст

Введение

Рождение идеи

Алгоритм рождения прогрессивных идей

Пилим с Йунцом проект, и мне в голову приходит мысль, как спустить весь свой отпуск в наименее эффективной форме. И вот идея - переделать свой сайт. В N-ый раз

Что хочу

  • уйти на коробочный server-side
  • баланс и в DX и в UX
  • уйти от сторонних платформ
    • везде свои WYSWYG редакторы и постинг где-либо это всегда болезненный процесс
    • цензура
      • по отношению к другим пользователям
      • по отношению к контенту

Как происходил выбор между платформами в части цензуры

Что импортозамещаем

До старта работ уже случились

  • проблемы со масштабированием и поддержкой старой конфигурации
  • client-side custom: любое «добавить» = разработка
  • Vercel: классная платформа, но по деньгам не сходится
  • Supabase (Postgre внутри): инструмент отличный, но дорогой; после 2022-го платить стало отдельным квестом
  • Google Analytics стал избыточно сложным
  • надоел дизайн 2018-го.

Старый сайт Каса Элвирова

Мой знаменатель — это два классических ограничителя: время и деньги

И сразу понятно, что менять:

  • систему мониторинга
  • коробку для блоггинга
  • хостинг
  • дизайн

Roadmap

План я расписал до деталей — элегантно и чётко. И он... пошёл курить уже к концу первого дня

Основная часть

Мониторинг

Что тут сказать, за последние 10 лет перепробовал много

  • и Google Analytics
  • и Yandex Metrika
  • и Firebase Crashlytics
  • и ряд self-host коробок

Выбор инструмента

Краткие обзор чего касался

СистемаПлюсыМинусыЦенаГде крутится
Google Analytics (GA4)+ мощная экосистема (BigQuery, Ads)
+ куча готовых отчётов
+ интеграции со всем подряд
– сложный интерфейс, нужен отдельный курс
– блокируется AdBlock
– после 2022 у RU-проектов проблемы с доступом
бесплатно (базовый)облако Google (США)
Yandex Metrika+ простая настройка
+ карта кликов и вебвизор
+ интеграция с Yandex Ads/Direct
+ не режется AdBlock
– отчёты проще, чем в GA
– фокус на RU, для глобала бесполезно
бесплатнооблако Яндекса (РФ)
Umami+ open-source, без трекеров
+ минимализм и быстрый UI
+ self-host или любой сервер
– мало “из коробки” (например, нет вебвизора)
– нужен свой хостинг
бесплатно (только хостинг)где сам поставишь
Matomo (бывш. Piwik)+ open-source альтернатива GA
+ отчёты почти как в GA
+ GDPR-friendly
– интерфейс тяжелее, чем у Umami
– требует больше ресурсов сервера
бесплатно (self-host)
есть платный Cloud
self-host или Matomo Cloud (ЕС)
Plausible Analytics+ минимализм
+ privacy-first
+ очень лёгкий код (не режется блокировщиками)
– платный SaaS
– нет вебвизора/тепловых карт
от $9/месEU облако (Германия)

У меня в голове эта таблица визуализируется следующим образом

Взял Umami потому, что мне нужен не набор для приготовления суши, а шаверма за углом: быстро и без рекламы

Когда использовать / когда нет
  • Использовать: если нужен лёгкий self-hosted мониторинг без лишней рекламы
  • Не использовать: если проекту критичны сложные отчёты, реклама, воронки или автоматическая аналитика

Применение

Для начала тест, решил обкатать это решение на своём браузерном расширении (Github Gloc - считает приблизительное количество строк в исходниках на GitHub). Несколько месяцев собирал логи и смотрел на то как растет потребление по базе

Umami - gloc

Скрин с Umami сервиса по части расширения

Как пользователю результатов, мне нравится

  • простота использования
  • разворачивание всех логов в нужные мне отчеты и графики

Umami - tables

Как разработчику мне нравится

  • быстрое и понятное разворачивание
  • нормализация данных. Всего 10 таблиц (нет бесконечного прироста сущностей)
  • скос нагрузки только на 2-х основных таблицах - то есть понятно что мигрировать и что партицировать (я бы сделал по дате отправки лога created_at)

Кстати, в какой-то момент я хотел оптимизировать по типу событий (у меня это event_name) пока я не запустил сл скрипт

-- топ событий за последние 120 дней
SELECT event_name,
COUNT(*) AS events,
MIN(created_at) AS first_seen,
MAX(created_at) AS last_seen
FROM website_event
WHERE created_at >= now() - interval '120 days'
GROUP BY event_name
ORDER BY events DESC;

Umami - logs by event type

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

Кстати, обожую в Supabase эту фишку когда можно выборку разложить тут-же на график

Umami - logs by event type

В этом плане Supabase просто прекрасен, лучшая оболочка для работы с БД которую я встречал

  • и работа с данными
  • и анализ нагрузки
  • и анализ объёма
  • и обширная рекомендательная система. Но даже без неё информации столько много и она так детализирована, что ты сразу понимаешь куда копать и что делать (в примере выше вообще нашел FE баг копая БД)

Одновременно с миграцией у меня стало заканчиваться место в Supabase а лишаться 3-х косарей я не хотел и логи мне нужны - люблю собирать куки, а потом читать

В общем моя миграция началась именно с миграции мониторинга. Пересобрал расширение и запушил это в

  • Edge store
  • Chrome store

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

Gloc with malware

В тот день у сотен людей сгорели пердаки, я в какой-то момент стал получать

  • пачки Issue на Github
  • гневные письма
  • сообщения в отзывах в Chrome store и прочих магазах расширений (Opera, Mozilla)

Под капотом использую Umami в таком режиме. Кастомные события не кидаю

    scripts: isProd ? [
{
src: "https://MY_INSTANCE/script.js",
defer: true,
"data-website-id": "LONG_HASH",
},
] : [],

Коннект BE и DB случился только по этому методу

postgresql://USERNAME:PASSNAME:PORT/default_db?sslmode=verify-full

Занимательный факт. Как только подключил логирование сразу отписалось более 10% людей. Отписки продолжаются до сих пор. Пользаки параноят за куки, а ведь это лечится простой наклейкой на вебкамеру

Gloc - chrome users

Сперва, не переживал. Думаю, с 10% отписок можно жить. К текущему моменту цифра составила минус 33%. Кстати если суммарно посчитать то за 2 года лишился 50%. Есть план по возврату аудитори. Но, думаю эффективнее будет вложиться в крипту

Gloc - edge users

Коробка для блоггинга

Выбор инструмента

Пробовал не так много, но каждый из экспериментов был чем-то отличным от предыдущего

ПлатформаПлюсыМинусыГде юзал
WordPress+ куча плагинов
+ легко найти инструкции
– админка в стиле 2000-х
– бесконечные апдейты и дырки в безопасности
– скорость оставляет желать лучшего
для пары корпоративных сайтов
Joomla+ гибкость
+ локальное комьюнити (когда-то)
– UX на уровне “разберись сам”
– ощущение олдскула
учебные проекты лет 10 назад
Custom (Next.js/React)+ полный контроль
+ кастомный дизайн/логика
– время и деньги на поддержку
– легко скатиться в велосипед
мой старый блог на Vercel + Supabase
Docusaurus+ простой деплой
+ работаешь с md/mdx
+ отличная документация
+ комьюнити от Meta
– не блоговая CMS “из коробки”
– сборка может быть тяжёлой
– местами костыль в кастомизации
дока на работе + теперь блог
Diplodoc+ российский разработчик, open-source, разработано не педофилами– насвай от Yandex который даже в имени косплеит Docusaurus. Чистейший аналог японской системы по увеличению членовумозрительно

Выбрал Docusaurus потому, что

ХарактеристикаПлюсыМинусы
Лицензия / модельOpen Source (MIT)
Поддерживается Meta (Facebook)
Зависимость от экосистемы React
Roadmap под контролем Meta
ПростотаРабота с обычными .md и .mdx файлами
Лёгкая структура документации
Готовые плагины для блога, i18n, поиска
Сборка может быть тяжёлой (особенно при большом проекте)
Иногда требует костылей для кастомизации
ФункциональностьАвтоматическая генерация документации
Поддержка версионирования
Много тем и плагинов
Отличный DX для разработчиков
Нет “админки” из коробки (не для ноукодеров)
Не полноценная CMS
РазвёртываниеЛегко хостить где угодно (Vercel, Netlify, Timeweb, GitHub Pages, Docker)
Простая CI/CD интеграция
При больших проектах сборка может упираться в ресурсы (RAM/CPU)
ИнтеграцияХорошо встраивается с React-компонентами
Поддерживает плагины для аналитики, поиска, темизации
Сложнее встраивать сторонние виджеты, чем в WordPress/Notion
Стоимость0 ₽ (только за хостинг)При использовании платных плагинов или CDN расходы растут
Когда использовать / когда нет
  • Использовать: если нужен блог/дока для разработчиков, любишь работать с Markdown и React
  • Не использовать: если хочется drag&drop редактора или бизнес-CMS для маркетологов

Применение

  • добавил проект из Github
  • прописал скрипт сборку и всё

Вцелом с этим сервисом (Docusaurus) у меня большой опыт, на нескольких проектах всю документацию поднимал именно на нём, так что тут экспериментов и сложноестей не было. Простая работа с md файлами

Инфра/хостер

Выбор инструмента

В первую очередь хотелось

  • не иметь проблем с оплатой
  • хороший UX (не C panel)
  • ну и по бабкам, чтобы не было накладно
ПлатформаПлюсыМинусыЦена (базовая)Где крутится
Yandex Cloud+ дата-центры в РФ (нет проблем с оплатой)
+ богатый набор сервисов (Compute, Object Storage, DB, AI)
+ нормальный UX, API, Terraform
– иногда тормозит UI
– ценник ближе к AWS, чем к “бюджетному”
– документация местами тяжеловата
от ~500–700 ₽/мес за VMРФ
Bluehost+ один из старейших хостеров
+ удобная интеграция с WordPress
+ 24/7 саппорт
– заточен под WP, слаб для кастомных стэков
– cPanel (олдскул, неудобно)
– оплата в $ (после 2022 боль)
от $5/месСША
Vercel+ шикарный DX для фронтов
+ автоматический CI/CD
+ serverless функции
+ бесплатный тариф (ограниченный)
– цены быстро растут при нагрузке
– Postgre / DB надо выносить отдельно
– оплата в $ (сложности для RU)
free → от $20/месСША / глобально
Timeweb Cloud+ российский хостинг
+ дешёвые тарифы (1–2 GB RAM VM за копейки)
+ нормальный UI (не cPanel)
+ можно крутить Docker
– нет богатства сервисов как у Yandex Cloud
– саппорт/стабильность похуже чем у hyperscale
– не для highload проектов
от 350–400 ₽/мес за VMРФ
  • Vercel — как Tesla: удобно, но дорого и в долларах
  • Yandex Cloud — как Сбер: много сервисов, иногда тяжело разобраться. Это, определённо, сегмент крупного бизнеса
  • Bluehost — привет из 2000-х. Хотя был у меня сервис на Wordpress с DAU в 40к и ему хоть бы хуй - нагрузки держит как не в себя
  • А Timeweb Cloud — ларёк у дома: дёшево, понятно и рядом. Не понятно только когда уходит в pending по ряду операций

💡 Лайфхаки по экономии на облаках которые мне известны

Данила Козловский - бабки gif

ПлатформаЛайфхакЭкономия
Yandex CloudБери минимальные VM и выноси статику в Object Storage (он дешёвый, а трафик CDN покрывает многое).
Используй preemptible VM (временные, в 3 раза дешевле).
30–70% от тарифа
BluehostОплачивай не помесячно, а на 1–3 года вперёд — у них жёсткий дисконт.
Лови купоны/акции (часто 60–70% скидка).
до 50%
VercelОптимизируй билды: реже деплой, меньше функций.
Вынеси тяжёлые API на сторонний сервер (иначе платить за серверлес как за космос).
20–40%
Timeweb CloudСобирать проект на VM с 2 GB RAM (чтобы прошла сборка), а потом переключать на 1 GB (runtime живёт).
Для пет-проектов хватит тарифов за 350 ₽.
50%

Визуализирую так

Timeweb cloud

Когда использовать / когда нет

Timeweb Cloud — для пет-проектов и блогов — идеально. Для Enterprise-нагрузки — лучше смотреть на Яндекс, или AWS

Применение инструмента

Плюсы

Я рассмотрю их как с точки зрения BE так и FE

Так выглядит главная страница FE сервиса Timeweb Cloud - главная страница FE сервиса

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

Timeweb Cloud - главная страница FE сервиса - память

логи дэплоя находятся отдельно. Нигде такого не встречал, разве что в Vercel

Timeweb Cloud - логи дэплоя

тут же могу выбрать на какую версию приложения откатитсья. Откат, обычно, происходит моментально и незаметно для пользователя - очень похоже на подмену пода в Kubernetes

Timeweb Cloud - дэплой уже существующей сборки

Удобная работа с логами. Ничего лишнего Timeweb Cloud - дэплой уже существующей сборки

Тут-же консоль Timeweb Cloud - консоль сервиса

Немаловажная часть работы для тех кто работает с хостерами - это домены. Они и тут сделали всё удобно

Timeweb Cloud - настройка домена

и особенно круты настройки домена - добавление записей. Кто сталкивался с линковкой своих сервисов к доменам меня поймёт. На Vercel этот участок для меня был всегда самым противным. А скорость обновления была непредсказуемой. Тут я обновил настройки DNS, переключился в соседний таб с абсолютно другим сервисом и всё подтянулось

Timeweb Cloud - настройка записей домена

Теперь обзор всех моих сервисов и доменов. Всё чётко и понятно

Timeweb Cloud - мои сервисы

Также есть возможность обзора в режиме мэш

Timeweb Cloud - мои сервисы

По итогу - потрясающий UX и его начинка логикой и данными. Интерфейс похож на рабочую панель звездолёта где находится всё самое нужное. Я не задумываюсь о UI когда с ним работаю. Чувствуется многолетний опыт работы команды


Теперь работа с базой. Моя любимая главная панель и здесь всё самое важное Timeweb Cloud - главная панель сервиса с базой данных

и нагрузки по конектам и транзакциям Timeweb Cloud - главная панель сервиса с базой данных и виджеты нагрузки по конектам и транзакциям

И морда для работы с базой. Ну, это точно не Supabase. Это Adminer Timeweb Cloud - Adminer и главная страница базы

вот структура таблички

Timeweb Cloud - Adminer и структура таблицы

а вот сами данные

Timeweb Cloud - Adminer и данные таблицы

и напоследок - выполнение SQL запросов. Никаких автокомплитов - только суть как она есть. В Supabase intellisence на каждый чих Timeweb Cloud - Adminer и выбор данных

Минусы

Timeweb Cloud умер

Не без них. Сайт написал пару месяцев назад, но написать статью руки дошли только сейчас. И вот захожу в панель Timeweb а мой сервис Umami лежит. Перезапускаю и логи деплоя не появляются по несколько часов. Лечил каждый раз по разному

  • на конфигурации в 1GB по RAM не проходил. В итоге отключил минификацию и взял план на 2GB и сборка прошла со свистом
  • затем все сервисы пизданулись сохраняя по показателям стабильность и не присылая уведомлений
  • конфигурацию поднял до 2GB
  • второй этап повторился
  • конфигурацию поднял до 4GB

Лайфхак

  • можно разворачивать сервис на профиле в 2GB
  • а потом переключать на 1GB. Runtime он держит
  • в итоге экономия в 2 раза. Но делать так не стал. Лень

Timeweb Cloud логи

И в какой-то момент произошло еще вот что

Timeweb Cloud упал

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

  • сервис лежал и я не получил уведомления
  • аптайм непресказуемый
  • не происходил сбор логов с другого приложения на сервис мониторинга который лежит здесь (то есть вся моя микроэкосистема умерла)
  • само приложение ни логирование/мониторинг не работали. В панеле все было зеленым цветом

Umami 13 дней без логов из-за упавшего сервиса на Timeweb Cloud

Дни без статистики (13 дней)

Дизайн

Палитра/концепт

Мне хотелось вложить на этот раз в работу некоторый символизм и дизайн-систему развивать от него. Спустя какое-то время у меня родилась мысль которую я быстро набросал во flyvi (кстати, неплохой аналог Canva)

Kas Elvirov logo

Заглавная анимация

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

  • подогнать внешний круг под элементы для которых они предназначены
  • выровнить татуху. Покрути код и сделай её вертикальной. Если получится, то с меня обед
import React from "react";

import { KasLogoProps } from "./KasLogo.types";

/**
* Логотип с прорезями на двух кругах:
* - напротив левого глаза (angleLeft)
* - напротив правого глаза (angleRight)
* - напротив тату на горле (angleNeck)
*
* @author Elvirov Kas
*/
export const KasLogo: React.FC<KasLogoProps> = ({
size,
strokeColor = "#E62323",
strokeWidth = 8,
step = 0.18,
innerRingRadius = 180,
angleLeft = 170,
angleRight = 25,
angleNeck = 277,
gapPct = 0.04, // ~6% окружности
}) => {
const d = (i: number) => `${(i * step).toFixed(3)}s`;

// helper: перевод угла в оффсет [0..1] для stroke-dashoffset (0° = 3 часа)
const toOffset = (deg: number) => ((deg % 360) + 360) % 360 / 360;

const gaps = [
{ id: "gLeft", offset: toOffset(angleLeft) },
{ id: "gRight", offset: toOffset(angleRight) },
{ id: "gNeck", offset: toOffset(angleNeck) },
];

return (
<div style={{ justifyItems: "center" }}>
<svg
width={size}
height={size}
viewBox="0 0 512 512"
role="img"
aria-label="Изоморфический логотип Каса Элвирова с прорезями"
style={{ overflow: "visible", display: "block" }}
>
<defs>
<linearGradient id="isoStroke" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor={strokeColor} />
<stop offset="100%" stopColor={strokeColor} />
</linearGradient>

<filter id="isoEmboss" x="-100%" y="-100%" width="300%" height="300%">
<feConvolveMatrix
order="3"
kernelMatrix="-2 -1 0 -1 1 1 0 1 2"
divisor="1" bias="0" edgeMode="duplicate"
/>
<feComposite in2="SourceGraphic" operator="atop" />
</filter>

<style>{`
.isoLine {
fill: none;
stroke: url(#isoStroke);
stroke-width: ${strokeWidth}px;
stroke-linecap: round;
stroke-linejoin: round;
vector-effect: non-scaling-stroke;
stroke-dasharray: 100;
stroke-dashoffset: 100;
animation: draw 1s ease forwards;
}
@keyframes draw { to { stroke-dashoffset: 0; } }
`}</style>

{/* Маска для вырезов на внешнем кольце */}
<mask id="outerMask">
{/* <!-- белый фон = всё видно --> */}
<rect x="0" y="0" width="512" height="512" fill="white" />
{/* <!-- три "чёрных" коротких дуги = невидимые участки --> */}
{gaps.map((g, i) => (
<circle
key={i}
cx="256" cy="256" r="230"
fill="none"
stroke="black"
strokeWidth={strokeWidth * 1.6}
pathLength={1}
strokeDasharray={`${gapPct} ${1 - gapPct}`}
strokeDashoffset={g.offset}
/>
))}
</mask>

{/* Маска для вырезов на внутреннем кольце */}
<mask id="innerMask">
<rect x="0" y="0" width="512" height="512" fill="white" />
{gaps.map((g, i) => (
<circle
key={i}
cx="256" cy="256" r={innerRingRadius}
fill="none"
stroke="black"
strokeWidth={strokeWidth * 1.6}
pathLength={1}
strokeDasharray={`${gapPct} ${1 - gapPct}`}
strokeDashoffset={g.offset}
/>
))}
</mask>
</defs>

<g filter="url(#isoEmboss)">
{/* Внешний круг с прорезями */}
<circle
className="isoLine"
pathLength={100}
cx={256}
cy={256}
r={230}
style={{ animationDelay: d(0) }}
mask="url(#outerMask)"
/>

{/* Внутренний круг-ореол с прорезями */}
{/* <circle
className="isoLine"
pathLength={100}
cx={256}
cy={256}
r={innerRingRadius}
style={{ animationDelay: d(0.7) }}
mask="url(#innerMask)"
/> */}

{/* Контур головы (как было) */}
<path
className="isoLine"
pathLength={100}
d="M256,116c-70,0-112,50-112,130s42,130,112,130s112-50,112-130S326,116,256,116z"
style={{ animationDelay: d(1.4) }}
/>

{/* Шея */}
<path className="isoLine" pathLength={100} d="M192,356c0,30-12,46-28,58" style={{ animationDelay: d(2.0) }} />
<path className="isoLine" pathLength={100} d="M320,356c0,30,12,46,28,58" style={{ animationDelay: d(2.2) }} />

{/* Глаза */}
<line className="isoLine" pathLength={100} x1={200} y1={240} x2={240} y2={250} style={{ animationDelay: d(2.6) }} />
<line className="isoLine" pathLength={100} x1={272} y1={250} x2={312} y2={240} style={{ animationDelay: d(2.8) }} />

{/* Вертикальная тату. Должна быть, но не такая */}
<line className="isoLine" pathLength={100} x1={256} y1={400} x2={257} y2={450} style={{ animationDelay: d(3.0) }} />
</g>
</svg>
</div>
);
};

В результате и получилась данная работа

Kas Elvirov logo at main page

Лайфхак

Перейди на главную, кликни на лого и не отводи мышку

Итого

Импортозамещение

Борат

Тут я воспользовался старым добрым паттерном

info

Взять чужое -> Положить в карман -> Сказать, что это наше

Список чужого

ЧтоКак называетсяГражданство технологии
База данных для мониторингаPostgreАСАШАЙ (Беркли)
Скрипты для мониторингаNext.jsАСАШАЙ (Vercel)
Коробка для мониторингаUmamiАСАШАЙ (Umami software)
Коробка для блоггингаDocusaurusАСАШАЙ (Meta). Принадлежит компании Meta, признанной экстремистской и запрещённой на территории РФ
Скрипты на клиентеReactАСАШАЙ (Meta). Принадлежит компании Meta, признанной экстремистской и запрещённой на территории РФ
Разметка на клиентеMDАСАШАЙ . Блогер Джон Грубер

Архитектура

Слово громкое, но она тут есть

Риски

Timeweb как хостер - около 10 лет пользуюсь их услугами. Я помню какие были лимиты раньше и как все работало. Чуть MAU на Wordpress сайте скакнет и ты срзу уходишь в за пиковые значения. И потом тебе пишут мол "Ваша популярность не вписывается в текущий план". Сейчас это уже абсолютно другая компания которая одна из первых несет такие мощные и простые в использовании инструменты. Я им искренне желаю не останавливаться и преодолеть эту ИИ трансформацию

Деньги

Статья расходовМесячная ценаГодовая ценаКомментарий
Домен .com/.ru~200 ₽~2400 ₽регистратор Webnames
Timeweb Cloud (2 GB RAM)~1500 ₽~18000 ₽хватает для докухи + умами
База данных (Postgres)включеновключенов тарифе Timeweb
Umami (self-host)0 ₽0 ₽open-source, только хостинг
Timeweb Cloud CI/CD0 ₽0 ₽в тарифе Timeweb
Итого~1700 ₽~20 400 ₽меньше, чем один ужин в Москве

Немало конечно, но ничего - часть затрат я отобью вэбкамом

“Раньше только Supabase сожрал бы 25$, а теперь весь блог (DB для мониторинга для нескольких проектов, сервис мониторинга для нескльких проектов, сервис со статикой для блога и домен) живёт за 1500 рублей в месяц. Импортозамещение...”

Вынесенные уроки

Тратить отпуск на инженерную возьню — иногда самое лучшее вложение