Подробности про новый сайт

Как известно, с 2021 года я перестал работать с MODX. На самом деле даже раньше, но понял я это только в конце 2020.

Сайт bezumkin.ru в своё время стал довольно популярным именно потому, что на нём разрабатывались и обкатывались популярные дополнения для MODX, такие как pdoTools и Tickets. Собственно, нынешний https://modx.pro и выделился из bezumkin.ru, когда встал вопрос, почему это вся активность сообщества проходит в чьём-то личном блоге.

За 2021 я перенёс все свои активные проекты с MODX на Vesp, оставался только личный сайт. Вот теперь дошли руки и до него.

Перенос данных

Во-первых, с нуля были спроектированы все модели для хранения данных. К MODX привязок больше нет, поэтому можно сделать отдельные таблицы для заметок и их разделов. Раньше это всё было ресурсами MODX которые вкладывлись друг в друга, что вызывало разные недобства. Например, у заметки могут быть комментарии, поэтому есть их счётчик и флаг closed для отключения. У раздела комментариев нет, и такие колонки ему просто не нужны.

Дальше нужно было перенести данные из старых таблиц в новые. Если бы перенос был один в один, то проблем никаких. Но я сразу решил, что новый сайт будет хранить контент в формате Markdown вместо HTML. Работать с таким текстом гораздо удобнее и проще. Но сначала нужно сконвертировать накопленные за много лет богатства, для чего скрипт импорта пришлось переписывать много раз.

Я использовал league/html-to-markdown и помимо отличной работы словил заодно множество проблем, в основном из-за бардака с моих старых текстах. Поэтому, например, пришлось блоки с кодом обрабатывать самостоятельно и прятать от конвертора, а потом подменять обратно.

В итоге функция приведения текста в порядок выглядела вот так:

protected static function normalizeContent(string $content): string
{

    // Убираем экранирование всяких тегов MODX
    $content = html_entity_decode($content);

    // Заменяем теги с кодом на плейсхолдеры, чтобы конвертор их не трогал
    $content = str_replace(
        ["'<pre>'", "'</pre>'", '"<pre>"', '"</pre>"'],
        ["'&lt;pre&gt;'", "'&lt;/pre&gt;'", '"&lt;pre&gt;"', '"&lt;/pre&gt;"'],
        $content
    );
    $blocks = [];
    preg_match_all('#<(?:pre|code).*?>(.*?)</(?:pre|code)>#ms', $content, $matches);
    if (!empty($matches[1])) {
        foreach ($matches[1] as $idx => $match) {
            if ($match) {
                $blocks[$idx] = $match;
                $content = str_replace($matches[0][$idx], "\n<!--code-$idx-code-->\n", $content);
            }
        }
    }
    
    // Меняем старые теги на понятные конвертору
    $content = str_replace(
        ['<b>', '</b>', '<i>', '</i>', '<s>', '</s>'],
        ['<strong>', '</strong>', '<em>', '</em>', '~~', '~~'],
        $content
    );
    // Тут тоже меняем старое барахло и приводим в порядок переносы и пустые строки
    $content = preg_replace('#<cut\s?/>#m', "\n<!--cut-->", $content);
    $content = preg_replace('#<(br.*?|br)>#m', "\n", $content);
    $content = preg_replace("#[\n\r]{2,}#m", '<br><br>', $content);
    $content = preg_replace('#<video>(.*?)</video>#m', '<a href="$1">$1</a>', $content);

    // Самое главное - конвертирование из HTML в Markdown
    $converter = new HtmlConverter([
        'strip_tags' => false,
        'hard_break' => true,
        'use_autolinks' => false,
        'preserve_comments' => true,
    ]);
    $converter->getEnvironment()->addConverter(new TableConverter());
    try {
        $content = $converter->convert($content);
    } catch (Exception $e) {
        // Если конвертор офигел от содержимого - смотрим, что там
        echo $content;
        print_r($blocks);
    }

    // Заменяем плейсхолдеры обратно на блоки с кодом
    if (!empty($blocks)) {
        foreach ($blocks as $idx => $block) {
            $content = str_replace(
                ["\<!--code-$idx-code-->", "<!--code-$idx-code-->"],
                "\n```\n$block\n```\n",
                $content
            );
        }
    }

    // Повторно декодируем всё экранированное, вдруг что осталось
    $content = html_entity_decode($content);

    // А это уже остаётся от комментариев после конвертора - тоже убираем
    return str_replace('\<!--', '<!--', $content);
}

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

Пользователи тоже были импортированы не все, а только те, у кого есть заметки или комментарии. Пароли, ествественно, сохранены быть не могут - поэтому сгенерировал случайные. Если вы когда-то регистрировались на bezumkin.ru, то попробуйте просто сбросить пароль.

В итоге все данные сайта уместились всего в 15 мегабайт и 8 рабочих таблиц:

Несмотря на то, что новый сайт выглядит примерно как старый, вёрстка тем не менее, новая на 100%.

Как работает сайт

Сайт работает на моём фреймворке Vesp, который я использую в том или ином виде с 2018 года, но до сих пор нет времени написать к нему нормальную документацию. Возможно, теперь получится это сделать, тем более что и домен для этого давно куплен.

Тем не менее, основной принцип работы Vesp - это полное разделение фронтенда и бэкенда. При открытии страницы сайта пользователь получает собранный JS и CSS, которые отрисовываются у него в браузере в мой сайт, а затем делают запросы на сервер в API, котороё выдаёт нужные данные в формате JSON. Например, вот адрес для получения последних комментариев - https://bezumkin.ru/api/web/comments/latest.

Скрипт получает этот ответ и меняет содержимое страницы. Затем, когда вы переходите с одной страницы на другую, JS запрашивает новые данные и отрисовывает на странице изменения, без перезагрузки. Это происходит очень быстро и, что самое важное, без нагрузки на мой сервер - ведь он-то ничего не рисует, вы делаете это сами на своём компьютере или телефоне.

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

Плюсом идёт готовое API с которым можно интегрировать другие сервисы, или мобильные приложения. Да что угодно!

Благодаря тому, что весь код хранится в файлах, а не в БД, я могу нормально использовать Git и выкатывать обновления через Gitlab CI/CD.

Редактор

Много времени я угробил на удобный редактор Markdown. Сначала пытался найти готовый. Перепробовал много разных, но везде меня что-то не устраивало, в основном возможности (вернее отсутствие) кастомизации.

В итоге вооружился низкоуровневой библиотекой textarea-editor и написал свой.

Здесь красивые кнопочки от FontAwesome 6, удобные горячие клавиши, живой предпросмотр синхронизированный с прокруткой текста и еще множество мелочей. А самое главное, я могу улучшить и доработать его в любой момент как пожелаю. Например, есть еще некоторые проблемы с работой Tab в блоках с кодом - и я это исправлю!

Ну и самое главное, конечно, что у меня теперь есть свой редактор для других проектов. А вы можете попробовать его в комментариях:

  • Ctrl+B - жирный текст
  • Ctrl+S - сохранить
  • Ctrl+I - курсив
  • Ctrl+F - развернуть и свернуть на весь экран
  • Ctrl+P - вкл\выкл предпросмотр

Подсветка текста работает с блоках с кодам, нужно обязательно указать язык после трёх обратных апострофов, как на Github:

    ```php
    
    echo 'Hello World';
    ```

И получится

    
echo 'Hello World';

Переход на Markdown сделал написание текста гораздо проще, но вот отображение - наоборот. Нужно же оформлять галереи с картинками, вставлять видео с youtube, открывать внешние ссылки в новом окне и еще много чего. А Markdown этого не умеет.

Но ведь Markdown в итоге превращается в HTML, а его уже можно и доработать, тем более, что происходить это будет не на сервере, а у вас в браузере.

Кому интересно, вот код подготовки:

const renderText = (text) => {
  text = marked.parse(text) // Markdown => HTML

  // Вырезаем опасные теги и прочий XSS, если вдруг как-то пролезло
  if (DOMPurify.sanitize) {
    text = DOMPurify.sanitize(text, purifyConfig)
  }

  // Тут идёт замена регулярок для ссылок youtube на iframe
  replaceFrom.forEach((regexp, idx) => {
    text = text.replace(regexp, replaceTo[idx])
  })

  // И поиск галерей внутри ul и ol
  const matches = text.match(/<(?:ul|ol)>.*?<\/(?:ul|ol)>/gms)
  if (matches) {
    matches.forEach((match) => {
      if (match.includes('<img')) {
        text = text.replace(match, match.replace(/<(ul|ol)>/, '<$1 class="thumbnails">'))
      }
    })
  }

  return text.trim()
}

Это только вывод текста, дальше идёт обработка клика по нему:

const clickText = (e) => {
  // Выбираем ссылки
  const link = e.target.tagName === 'A' ? e.target : e.target.parentNode.tagName === 'A' ? e.target.parentNode : null
  if (link) {
    let href = link.href
    const isImg = /\.(jpe?g|gif|png|webp)$/.test(href)
    // Если это картинка
    if (isImg) {
      const elements = []
      const thumbnails = e.target.closest('ul.thumbnails')
      // И картинка внутри галереи
      if (thumbnails) {
        const links = thumbnails.querySelectorAll('a')
        // Собираем внутрь все картинки блока
        links.forEach((a) => {
          elements.push({href: a.href, type: 'image'})
        })
      } else {
        elements.push({href, type: 'image'})
      }
      // И выводим картинку всплывашкой
      if (elements.length) {
        e.preventDefault()
        app.$lightbox({elements}).open()
      }
    }

    // Если же это обычная ссылка, то проверяем адрес
    // Относительные ссылки приводим к абсолютным
    if (!/:\/\//.test(href) && !/mailto:/.test(href)) {
      href = 'https://bezumkin.ru' + href
    }

    // А потом проверяем адрес и внутренние страницы открываем через роутер Vue
    if (/https?:\/\/bezumkin\.ru/.test(href) && !/\.ru\/api\//.test(href)) {
      const url = new URL(href)
      const route = app.router.resolve(url.pathname + url.search + url.hash)
      if (route.route) {
        e.preventDefault()
        app.router.push(route.route)
      }
    } else {
      // Все остальные ссылки открываем в новом окне
      link.target = '_blank'
    }
  }
}

Понятное дело, что по мере тестирования работы сайта эти функциии будут дорабатываться. Например, проверку ссылки на API я добавил во время написания этого текста!

Планы на будущее

Я сделал новый сайт примерно за 10 дней и, хотя функционал здесь довольно базовый, это всё равно очень быстро для подобного объёма.

Дело в том, что уже второй год я работаю полный день в компании Pixmill, используя именно Vesp. Так что разных заготовок, вроде авторизации со сбросом пароля, или отправки почты с удобным оформлением шаблонов через Fenom у меня уже много. Я могу просто копировать и вставлять их из других проектов в этом, собирая его по кирпичикам. Возможно в будущем придумаю, как это делать более удобным, выпуская готовые расширения.

А пока вот что я планирую сделать в ближайшем будущем:

  • Настроить OpenGraph для красивого расшаривания заметок
  • Редактирование профиля пользователя, включая загрузку аватарок.
  • Поддержку этих аватарок в комментариях
  • Удобную загрузку картинок, по типу Github и Gitlab (когда их просто кидаешь в текст)
  • Старые картинки все нужно проверить и выкачать на свой сервер
  • А еще нужно проверить и отредактировать старые заметки, заодно проверить и убрать мёртвые ссылки
  • Избранные заметки и комментарии
  • Поиск по сайту

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

Никаких рейтингов с плюсами и минусами не планируется.

Более дальние планы:

  • Начать писать заметки по работе с Vesp, которые будут переезжать на https://vesp.pro как документация.
  • Возможно (возможно!) новые платные обучающие курсы по работе с современными технологиями. Но тут как и соцсетями непонятно, нужно ли это кому, учитывая как заспамили всё вокруг всякие geekbox и skillbrains.

Не то, чтобы мне нужны были деньги, но как стимул не забрасывать написание интересного (я ж себя знаю) есть идеи о постоянной монетизации ресурса:

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

Сейчас на сайте уже есть реклама от Google, которая отключается при авторизации, но её не хватит даже на оплату хостинга.

Это, повторюсь, только мысли. Не факт, что мой энтузиазм по реанимации bezumkin.ru не закончится раньше. В общем, всё зависит от вашей активности, дорогие читатели!

Следующая заметка →
Работа с картинками
Комментарии (9)
CoraloReefВячеслав Сергееевич
09.02.2022 15:27

Как всегда всё интересно написано! Продолжай!

Реактивность сайту к лицу, очень приятно пользоваться.

Когда у нас в компании возникла необходимость отказа от хранения html в БД, решили хранить контент в json-ах. А в качестве редактора отлично зашел editor.js. Текст редактируется примерно как в Notion. В прочем, у нас просто задачи иные, MD - тоже отличное решение.

bezumkinВасилий Наумкин
09.02.2022 17:02

Да, я работал с Editor.js - хорошая штука. Даже написал его аналог на Vue для https://brandboard.app.

Но тут такое мне показалось излишним. Во-первых, для комментариев всё равно нужно что-то попроще, без блоков. А во-вторых, Markdown удобен тем, что заметки можно писать в любых редакторах, хоть на телефоне, а потом просто вставлять здесь.

Да и вообще, текущее решение как-то ближе целевой аудитории, потому что очень похоже на редактор Github. Editor.js же делался для проектов Комитета (vc.ru, dtf), там публика совсем другая.

inetloverАлександр Наумов
04.12.2022 11:56

Василий добрый день!

Да, я работал с Editor.js - хорошая штука. Даже написал его аналог на Vue для https://brandboard.app.

Скажи, пожалуйста, а ты использовал тип данных MySQL JSON для хранения?

bezumkinВасилий Наумкин
04.12.2022 11:57

Да, конечно.

inetloverАлександр Наумов
04.12.2022 12:12

Понятно, спасибо!

А то в интернете пишут о минусах разных я и сомневаться стал.

gvozdbПавел Гвоздь
13.02.2022 06:14

Даёшь больше статей и заметок про Vesp + Vue!

А касательно закрытого раздела и возможности всем на этом зарабатывать – ну очень далеко идущие планы, даже не особо верится в это.😉

bezumkinВасилий Наумкин
13.02.2022 08:57

Сначала заметки, а там - посмотрим!

Алексей
18.03.2022 15:19

Как начать пользоваться модулями? в планах разработка сайта с пользователями, с онлайн-оплатой подписки. делать на MODX уже желания нету, встаёт выбор laravel или твоё комплексное решение VESP. Возможно ли в "сыром" виде "пощупать" заготовки для авторизации и смены пароля пользователем, или пока рано выкладывать внутренний код таких наработок в общий доступ?

Так что разных заготовок, вроде авторизации со сбросом пароля, или отправки почты с удобным оформлением шаблонов через Fenom у меня уже много. Я могу просто копировать и вставлять их из других проектов в этом, собирая его по кирпичикам. Возможно в будущем придумаю, как это делать более удобным, выпуская готовые расширения.

bezumkinВасилий Наумкин
18.03.2022 15:35

Авторизация есть из коробки, для входа в базовую админку. Можно установить через composer и собрать фронтенд.

А вот более сложные штуки пока придётся писать самостоятельно, тут Laravel будет выгоднее. Там и готовых решений вагон и большое сообщество, которое поможет.

У меня тут проект пока строго для энтузиастов, которые не хотят рендерить HTML через PHP.

bezumkin
Василий Наумкин
09.04.2024 01:45
Ошибка 500 Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи. Во...
futuris
Futuris
04.04.2024 05:56
Я просто немного запутался. Когда в абзаце &quot;Vesp/Core&quot; ты пишешь про &quot;новый trait Fil...
bezumkin
Василий Наумкин
20.03.2024 18:21
Volledig!
Андрей
14.03.2024 10:47
Василий! Как всегда очень круто! Моё почтение!
russelgal
russel gal
09.03.2024 17:17
А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал ...
inetlover
Александр Наумов
27.01.2024 00:06
Василий, спасибо! Извини, тупанул.
bezumkin
Василий Наумкин
22.01.2024 04:43
Давай-давай!
bezumkin
Василий Наумкин
24.12.2023 11:26
Спасибо!
bezumkin
Василий Наумкин
27.11.2023 02:43
Ура!
bezumkin
Василий Наумкин
25.11.2023 08:30
Vesp тянет 2 зависимости: vesp-frontent для фронта и vesp-core для бэкенда. Их можно обновлять, но э...